Merge remote-tracking branch 'origin/angular.ossez.com'

# Conflicts:
#	.husky/commit-msg
#	.husky/pre-commit
#	.husky/prepare-commit-msg
#	package.json
This commit is contained in:
YuCheng Hu
2021-04-05 12:22:49 -04:00
781 changed files with 69166 additions and 48 deletions
-43
View File
@@ -1,43 +0,0 @@
## PR Checklist
Please check if your PR fulfills the following requirements:
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)
## PR Type
What kind of change does this PR introduce?
<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Documentation content changes
- [ ] angular.io application / infrastructure changes
- [ ] Other... Please describe:
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
## Does this PR introduce a breaking change?
- [ ] Yes
- [ ] No
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information
+22590
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -104,6 +104,7 @@
"@angular/service-worker": "11.2.3",
"@bazel/bazelisk": "^1.7.5",
"@webcomponents/custom-elements": "1.2.1",
"husky": "^6.0.0",
"rxjs": "^6.5.3",
"tslib": "^2.1.0",
"zone.js": "~0.11.4"
@@ -127,7 +128,7 @@
"codelyzer": "^6.0.0",
"cross-spawn": "^5.1.0",
"css-selector-parser": "^1.3.0",
"dgeni": "^0.4.13",
"dgeni": "^0.4.14",
"dgeni-packages": "^0.28.4",
"entities": "^1.1.1",
"esbuild": "^0.9.0",
@@ -154,9 +155,9 @@
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"light-server": "^2.6.2",
"lighthouse": "^7.2.0",
"lighthouse": "^7.3.0",
"lighthouse-logger": "^1.2.0",
"lodash": "^4.17.4",
"lodash": "^4.17.21",
"lunr": "^2.1.0",
"npm-run-all": "^4.1.5",
"protractor": "~7.0.0",
+2 -2
View File
@@ -102,10 +102,10 @@ assertSucceeded "Expected 'ngcc' to log 'Compiling'."
# Did it generate a base factory call for synthesized constructors correctly?
grep "/\*@__PURE__\*/ function () { let ɵMatTable_BaseFactory; return function MatTable_Factory(t) { return (ɵMatTable_BaseFactory || (ɵMatTable_BaseFactory = ɵngcc0.ɵɵgetInheritedFactory(MatTable)))(t || MatTable); }; }();" node_modules/@angular/material/esm2015/table/table.js
grep "const ɵMatTable_BaseFactory = /\*@__PURE__\*/ ɵngcc0.ɵɵgetInheritedFactory(MatTable);" node_modules/@angular/material/esm2015/table/table.js
assertSucceeded "Expected 'ngcc' to generate a base factory for 'MatTable' in '@angular/material' (esm2015)."
grep "/\*@__PURE__\*/ function () { var ɵMatTable_BaseFactory; return function MatTable_Factory(t) { return (ɵMatTable_BaseFactory || (ɵMatTable_BaseFactory = ɵngcc0.ɵɵgetInheritedFactory(MatTable)))(t || MatTable); }; }();" node_modules/@angular/material/esm5/table/table.js
grep "var ɵMatTable_BaseFactory = /\*@__PURE__\*/ ɵngcc0.ɵɵgetInheritedFactory(MatTable);" node_modules/@angular/material/esm5/table/table.js
assertSucceeded "Expected 'ngcc' to generate a base factory for 'MatTable' in '@angular/material' (esm5)."
@@ -0,0 +1,41 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {fakeAsync} from '@angular/core/testing';
import {NoopAnimationPlayer} from '../src/animations';
import {AnimationGroupPlayer} from '../src/players/animation_group_player';
describe('AnimationGroupPlayer', () => {
it('should getPosition of an empty group', fakeAsync(() => {
const players: NoopAnimationPlayer[] = [];
const groupPlayer = new AnimationGroupPlayer(players);
expect(groupPlayer.getPosition()).toBe(0);
}));
it('should getPosition of a single player in a group', fakeAsync(() => {
const player = new NoopAnimationPlayer(5, 5);
player.setPosition(0.2);
const players = [player];
const groupPlayer = new AnimationGroupPlayer(players);
expect(groupPlayer.getPosition()).toBe(0.2);
}));
it('should getPosition based on the longest player in the group', fakeAsync(() => {
const longestPlayer = new NoopAnimationPlayer(5, 5);
longestPlayer.setPosition(0.2);
const players = [
new NoopAnimationPlayer(1, 4),
new NoopAnimationPlayer(4, 1),
new NoopAnimationPlayer(7, 0),
longestPlayer,
new NoopAnimationPlayer(1, 1),
];
const groupPlayer = new AnimationGroupPlayer(players);
expect(groupPlayer.getPosition()).toBe(0.2);
}));
});
+20
View File
@@ -0,0 +1,20 @@
# Copyright Google LLC All Rights Reserved.
#
# Use of this source code is governed by an MIT-style license that can be
# found in the LICENSE file at https://angular.io/license
load(":ng_module.bzl", "NgPerfInfo")
def _ng_perf_flag_impl(ctx):
return NgPerfInfo(enable_perf_logging = ctx.build_setting_value)
# `ng_perf_flag` is a special `build_setting` rule which ultimately enables a command-line boolean
# flag to control whether the `ng_module` rule produces performance tracing JSON files (in Ivy mode)
# as declared outputs.
#
# It does this via the `NgPerfInfo` provider and the `perf_flag` attriubute on `ng_module`. For more
# details, see: https://docs.bazel.build/versions/master/skylark/config.html
ng_perf_flag = rule(
implementation = _ng_perf_flag_impl,
build_setting = config.bool(flag = True),
)
@@ -0,0 +1,45 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ng_module", "ts_library")
ts_library(
name = "ng_module_ivy_test_lib",
testonly = True,
srcs = ["ng_module_ivy_test.ts"],
tags = ["ivy-only"],
)
# `ng_module` with `compilation_mode` explicitly set to `partial`.
ng_module(
name = "test_module_partial_compilation",
srcs = ["test_module_partial_compilation.ts"],
compilation_mode = "partial",
tags = ["ivy-only"],
deps = ["//packages/core"],
)
# `ng_module` with `compilation_mode` explicitly set to `full`.
ng_module(
name = "test_module_full_compilation",
srcs = ["test_module_full_compilation.ts"],
compilation_mode = "full",
tags = ["ivy-only"],
deps = ["//packages/core"],
)
# `ng_module` with no specific `compilation_mode` attribute specified.
ng_module(
name = "test_module_default_compilation",
srcs = ["test_module_default_compilation.ts"],
tags = ["ivy-only"],
deps = ["//packages/core"],
)
jasmine_node_test(
name = "ng_module_ivy_test",
srcs = [":ng_module_ivy_test_lib"],
data = [
":test_module_default_compilation",
":test_module_full_compilation",
":test_module_partial_compilation",
],
tags = ["ivy-only"],
)
@@ -0,0 +1,41 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {readFileSync} from 'fs';
/** Runfiles helper from bazel to resolve file name paths. */
const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']!);
describe('ng_module with ivy enabled', () => {
describe('default compilation mode', () => {
it('should generate definitions', () => {
const outputFile = runfiles.resolveWorkspaceRelative(
'packages/bazel/test/ngc-wrapped/ivy_enabled/test_module_default_compilation.js');
const fileContent = readFileSync(outputFile, 'utf8');
expect(fileContent).toContain(`TestComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent(`);
});
});
describe('full compilation mode', () => {
it('should generate definitions', () => {
const outputFile = runfiles.resolveWorkspaceRelative(
'packages/bazel/test/ngc-wrapped/ivy_enabled/test_module_full_compilation.js');
const fileContent = readFileSync(outputFile, 'utf8');
expect(fileContent).toContain(`TestComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent(`);
});
});
describe('partial compilation mode', () => {
it('should generate declarations', () => {
const outputFile = runfiles.resolveWorkspaceRelative(
'packages/bazel/test/ngc-wrapped/ivy_enabled/test_module_partial_compilation.js');
const fileContent = readFileSync(outputFile, 'utf8');
expect(fileContent).toContain(`TestComponent.ɵcmp = i0.ɵɵngDeclareComponent(`);
});
});
});
@@ -0,0 +1,15 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component} from '@angular/core';
@Component({
template: 'Hello',
})
export class TestComponent {
}
@@ -0,0 +1,15 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component} from '@angular/core';
@Component({
template: 'Hello',
})
export class TestComponent {
}
@@ -0,0 +1,15 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component} from '@angular/core';
@Component({
template: 'Hello',
})
export class TestComponent {
}
+99
View File
@@ -0,0 +1,99 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* A token used to manipulate and access values stored in `HttpContext`.
*
* @publicApi
*/
export class HttpContextToken<T> {
constructor(public readonly defaultValue: () => T) {}
}
/**
* Http context stores arbitrary user defined values and ensures type safety without
* actually knowing the types. It is backed by a `Map` and guarantees that keys do not clash.
*
* This context is mutable and is shared between cloned requests unless explicitly specified.
*
* @usageNotes
*
* ### Usage Example
*
* ```typescript
* // inside cache.interceptors.ts
* export const IS_CACHE_ENABLED = new HttpContextToken<boolean>(() => false);
*
* export class CacheInterceptor implements HttpInterceptor {
*
* intercept(req: HttpRequest<any>, delegate: HttpHandler): Observable<HttpEvent<any>> {
* if (req.context.get(IS_CACHE_ENABLED) === true) {
* return ...;
* }
* return delegate.handle(req);
* }
* }
*
* // inside a service
*
* this.httpClient.get('/api/weather', {
* context: new HttpContext().set(IS_CACHE_ENABLED, true)
* }).subscribe(...);
* ```
*
* @publicApi
*/
export class HttpContext {
private readonly map = new Map<HttpContextToken<unknown>, unknown>();
/**
* Store a value in the context. If a value is already present it will be overwritten.
*
* @param token The reference to an instance of `HttpContextToken`.
* @param value The value to store.
*
* @returns A reference to itself for easy chaining.
*/
set<T>(token: HttpContextToken<T>, value: T): HttpContext {
this.map.set(token, value);
return this;
}
/**
* Retrieve the value associated with the given token.
*
* @param token The reference to an instance of `HttpContextToken`.
*
* @returns The stored value or default if one is defined.
*/
get<T>(token: HttpContextToken<T>): T {
if (!this.map.has(token)) {
this.map.set(token, token.defaultValue());
}
return this.map.get(token) as T;
}
/**
* Delete the value associated with the given token.
*
* @param token The reference to an instance of `HttpContextToken`.
*
* @returns A reference to itself for easy chaining.
*/
delete(token: HttpContextToken<unknown>): HttpContext {
this.map.delete(token);
return this;
}
/**
* @returns a list of tokens currently stored in the context.
*/
keys(): IterableIterator<HttpContextToken<unknown>> {
return this.map.keys();
}
}
+59
View File
@@ -0,0 +1,59 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {HttpContext, HttpContextToken} from '../src/context';
const IS_ENABLED = new HttpContextToken<boolean>(() => false);
const CACHE_OPTION =
new HttpContextToken<{cache: boolean, expiresIn?: number}>(() => ({cache: false}));
describe('HttpContext', () => {
let context: HttpContext;
beforeEach(() => {
context = new HttpContext();
});
describe('with basic value', () => {
it('should test public api', () => {
expect(context.get(IS_ENABLED)).toBe(false);
expect([...context.keys()]).toEqual([
IS_ENABLED
]); // value from factory function is stored in the map upon access
context.set(IS_ENABLED, true);
expect(context.get(IS_ENABLED)).toBe(true);
expect([...context.keys()]).toEqual([IS_ENABLED]);
context.delete(IS_ENABLED);
expect([...context.keys()]).toEqual([]);
});
});
describe('with complex value', () => {
it('should test public api', () => {
expect(context.get(CACHE_OPTION)).toEqual({cache: false});
expect([...context.keys()]).toEqual([CACHE_OPTION]);
const value = {cache: true, expiresIn: 30};
context.set(CACHE_OPTION, value);
expect(context.get(CACHE_OPTION)).toBe(value);
expect([...context.keys()]).toEqual([CACHE_OPTION]);
context.delete(CACHE_OPTION);
expect([...context.keys()]).toEqual([]);
});
it('should ensure that same reference is returned for default value between multiple accesses',
() => {
const value = context.get(CACHE_OPTION); // will get default value
expect(value).toEqual({cache: false});
expect(context.get(CACHE_OPTION)).toBe(value);
});
});
});
+16
View File
@@ -0,0 +1,16 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* A wrapper around the `XMLHttpRequest` constructor.
*
* @publicApi
*/
export abstract class XhrFactory {
abstract build(): XMLHttpRequest;
}
@@ -0,0 +1,22 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {LinkerOptions} from '../..';
import {ReadonlyFileSystem} from '../../../src/ngtsc/file_system';
import {Logger} from '../../../src/ngtsc/logging';
export interface LinkerPluginOptions extends Partial<LinkerOptions> {
/**
* File-system, used to load up the input source-map and content.
*/
fileSystem: ReadonlyFileSystem;
/**
* Logger used by the linker.
*/
logger: Logger;
}
@@ -0,0 +1,36 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {SourceFile, SourceFileLoader} from '../../../src/ngtsc/sourcemaps';
/**
* A function that will return a `SourceFile` object (or null) for the current file being linked.
*/
export type GetSourceFileFn = () => SourceFile|null;
/**
* Create a `GetSourceFileFn` that will return the `SourceFile` being linked or `null`, if not
* available.
*/
export function createGetSourceFile(
sourceUrl: AbsoluteFsPath, code: string, loader: SourceFileLoader|null): GetSourceFileFn {
if (loader === null) {
// No source-mapping so just return a function that always returns `null`.
return () => null;
} else {
// Source-mapping is available so return a function that will load (and cache) the `SourceFile`.
let sourceFile: SourceFile|null|undefined = undefined;
return () => {
if (sourceFile === undefined) {
sourceFile = loader.loadSourceFile(sourceUrl, code);
}
return sourceFile;
};
}
}
@@ -0,0 +1,28 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {declarationFunctions} from './partial_linkers/partial_linker_selector';
/**
* Determines if the provided source file may need to be processed by the linker, i.e. whether it
* potentially contains any declarations. If true is returned, then the source file should be
* processed by the linker as it may contain declarations that need to be fully compiled. If false
* is returned, parsing and processing of the source file can safely be skipped to improve
* performance.
*
* This function may return true even for source files that don't actually contain any declarations
* that need to be compiled.
*
* @param path the absolute path of the source file for which to determine whether linking may be
* needed.
* @param source the source file content as a string.
* @returns whether the source file may contain declarations that need to be linked.
*/
export function needsLinking(path: string, source: string): boolean {
return declarationFunctions.some(fn => source.includes(fn));
}
@@ -0,0 +1,88 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {compileFactoryFunction, ConstantPool, FactoryTarget, R3DeclareDependencyMetadata, R3DeclareFactoryMetadata, R3DependencyMetadata, R3FactoryMetadata, R3PartialDeclaration} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstObject} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
import {PartialLinker} from './partial_linker';
import {parseEnum, wrapReference} from './util';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareFactory()` call expressions.
*/
export class PartialFactoryLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
linkPartialDeclaration(
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
const meta = toR3FactoryMeta(metaObj);
const def = compileFactoryFunction(meta);
return def.expression;
}
}
/**
* Derives the `R3FactoryMetadata` structure from the AST object.
*/
export function toR3FactoryMeta<TExpression>(
metaObj: AstObject<R3DeclareFactoryMetadata, TExpression>): R3FactoryMetadata {
const typeExpr = metaObj.getValue('type');
const typeName = typeExpr.getSymbolName();
if (typeName === null) {
throw new FatalLinkerError(
typeExpr.expression, 'Unsupported type, its name could not be determined');
}
return {
name: typeName,
type: wrapReference(typeExpr.getOpaque()),
internalType: metaObj.getOpaque('type'),
typeArgumentCount: 0,
target: parseEnum(metaObj.getValue('target'), FactoryTarget),
deps: getDeps(metaObj, 'deps'),
};
}
function getDeps<TExpression>(
metaObj: AstObject<R3DeclareFactoryMetadata, TExpression>,
propName: keyof R3DeclareFactoryMetadata): R3DependencyMetadata[]|null|'invalid' {
if (!metaObj.has(propName)) {
return null;
}
const deps = metaObj.getValue(propName);
if (deps.isArray()) {
return deps.getArray().map(dep => getDep(dep.getObject()));
}
if (deps.isString()) {
return 'invalid';
}
return null;
}
function getDep<TExpression>(depObj: AstObject<R3DeclareDependencyMetadata, TExpression>):
R3DependencyMetadata {
const isAttribute = depObj.has('attribute') && depObj.getBoolean('attribute');
const token = depObj.getOpaque('token');
// Normally `attribute` is a string literal and so its `attributeNameType` is the same string
// literal. If the `attribute` is some other expression, the `attributeNameType` would be the
// `unknown` type. It is not possible to generate this when linking, since it only deals with JS
// and not typings. When linking the existence of the `attributeNameType` only acts as a marker to
// change the injection instruction that is generated, so we just pass the literal string
// `"unknown"`.
const attributeNameType = isAttribute ? o.literal('unknown') : null;
const dep: R3DependencyMetadata = {
token,
attributeNameType,
host: depObj.has('host') && depObj.getBoolean('host'),
optional: depObj.has('optional') && depObj.getBoolean('optional'),
self: depObj.has('self') && depObj.getBoolean('self'),
skipSelf: depObj.has('skipSelf') && depObj.getBoolean('skipSelf'),
};
return dep;
}
@@ -0,0 +1,49 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {compileInjector, ConstantPool, R3DeclareInjectorMetadata, R3InjectorMetadata, R3PartialDeclaration} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstObject} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
import {PartialLinker} from './partial_linker';
import {wrapReference} from './util';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareInjector()` call expressions.
*/
export class PartialInjectorLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
linkPartialDeclaration(
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
const meta = toR3InjectorMeta(metaObj);
const def = compileInjector(meta);
return def.expression;
}
}
/**
* Derives the `R3InjectorMetadata` structure from the AST object.
*/
export function toR3InjectorMeta<TExpression>(
metaObj: AstObject<R3DeclareInjectorMetadata, TExpression>): R3InjectorMetadata {
const typeExpr = metaObj.getValue('type');
const typeName = typeExpr.getSymbolName();
if (typeName === null) {
throw new FatalLinkerError(
typeExpr.expression, 'Unsupported type, its name could not be determined');
}
return {
name: typeName,
type: wrapReference(typeExpr.getOpaque()),
internalType: metaObj.getOpaque('type'),
providers: metaObj.has('providers') ? metaObj.getOpaque('providers') : null,
imports: metaObj.has('imports') ? metaObj.getArray('imports').map(i => i.getOpaque()) : [],
};
}
@@ -0,0 +1,128 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {compileNgModule, ConstantPool, R3DeclareNgModuleMetadata, R3NgModuleMetadata, R3PartialDeclaration, R3Reference} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstObject, AstValue} from '../../ast/ast_value';
import {PartialLinker} from './partial_linker';
import {wrapReference} from './util';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareNgModule()` call expressions.
*/
export class PartialNgModuleLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
constructor(
/**
* If true then emit the additional declarations, imports, exports, etc in the NgModule
* definition. These are only used by JIT compilation.
*/
private emitInline: boolean) {}
linkPartialDeclaration(
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
const meta = toR3NgModuleMeta(metaObj, this.emitInline);
const def = compileNgModule(meta);
return def.expression;
}
}
/**
* Derives the `R3NgModuleMetadata` structure from the AST object.
*/
export function toR3NgModuleMeta<TExpression>(
metaObj: AstObject<R3DeclareNgModuleMetadata, TExpression>,
emitInline: boolean): R3NgModuleMetadata {
const wrappedType = metaObj.getOpaque('type');
const meta: R3NgModuleMetadata = {
type: wrapReference(wrappedType),
internalType: wrappedType,
adjacentType: wrappedType,
bootstrap: [],
declarations: [],
imports: [],
exports: [],
emitInline,
containsForwardDecls: false,
schemas: [],
id: metaObj.has('id') ? metaObj.getOpaque('id') : null,
};
// Each of `bootstrap`, `declarations`, `imports` and `exports` are normally an array. But if any
// of the references are not yet declared, then the arrays must be wrapped in a function to
// prevent errors at runtime when accessing the values.
// The following blocks of code will unwrap the arrays from such functions, because
// `R3NgModuleMetadata` expects arrays of `R3Reference` objects.
// Further, since the `ɵɵdefineNgModule()` will also suffer from the forward declaration problem,
// we must update the `containsForwardDecls` property if a function wrapper was found.
if (metaObj.has('bootstrap')) {
const bootstrap: AstValue<unknown, TExpression> = metaObj.getValue('bootstrap');
if (bootstrap.isFunction()) {
meta.containsForwardDecls = true;
meta.bootstrap = wrapReferences(unwrapForwardRefs(bootstrap));
} else
meta.bootstrap = wrapReferences(bootstrap);
}
if (metaObj.has('declarations')) {
const declarations: AstValue<unknown, TExpression> = metaObj.getValue('declarations');
if (declarations.isFunction()) {
meta.containsForwardDecls = true;
meta.declarations = wrapReferences(unwrapForwardRefs(declarations));
} else
meta.declarations = wrapReferences(declarations);
}
if (metaObj.has('imports')) {
const imports: AstValue<unknown, TExpression> = metaObj.getValue('imports');
if (imports.isFunction()) {
meta.containsForwardDecls = true;
meta.imports = wrapReferences(unwrapForwardRefs(imports));
} else
meta.imports = wrapReferences(imports);
}
if (metaObj.has('exports')) {
const exports: AstValue<unknown, TExpression> = metaObj.getValue('exports');
if (exports.isFunction()) {
meta.containsForwardDecls = true;
meta.exports = wrapReferences(unwrapForwardRefs(exports));
} else
meta.exports = wrapReferences(exports);
}
if (metaObj.has('schemas')) {
const schemas: AstValue<unknown, TExpression> = metaObj.getValue('schemas');
meta.schemas = wrapReferences(schemas);
}
return meta;
}
/**
* Extract an array from the body of the function.
*
* If `field` is `function() { return [exp1, exp2, exp3]; }` then we return `[exp1, exp2, exp3]`.
*
*/
function unwrapForwardRefs<TExpression>(field: AstValue<unknown, TExpression>):
AstValue<TExpression[], TExpression> {
return (field as AstValue<Function, TExpression>).getFunctionReturnValue();
}
/**
* Wrap the array of expressions into an array of R3 references.
*/
function wrapReferences<TExpression>(values: AstValue<TExpression[], TExpression>): R3Reference[] {
return values.getArray().map(i => wrapReference(i.getOpaque()));
}
@@ -0,0 +1,55 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {compilePipeFromMetadata, ConstantPool, R3DeclarePipeMetadata, R3PartialDeclaration, R3PipeMetadata} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstObject} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
import {PartialLinker} from './partial_linker';
import {wrapReference} from './util';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclarePipe()` call expressions.
*/
export class PartialPipeLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
constructor() {}
linkPartialDeclaration(
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
const meta = toR3PipeMeta(metaObj);
const def = compilePipeFromMetadata(meta);
return def.expression;
}
}
/**
* Derives the `R3PipeMetadata` structure from the AST object.
*/
export function toR3PipeMeta<TExpression>(metaObj: AstObject<R3DeclarePipeMetadata, TExpression>):
R3PipeMetadata {
const typeExpr = metaObj.getValue('type');
const typeName = typeExpr.getSymbolName();
if (typeName === null) {
throw new FatalLinkerError(
typeExpr.expression, 'Unsupported type, its name could not be determined');
}
const pure = metaObj.has('pure') ? metaObj.getBoolean('pure') : true;
return {
name: typeName,
type: wrapReference(typeExpr.getOpaque()),
internalType: metaObj.getOpaque('type'),
typeArgumentCount: 0,
deps: null,
pipeName: metaObj.getString('name'),
pure,
};
}
@@ -0,0 +1,31 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {R3Reference} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstValue} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
export function wrapReference<TExpression>(wrapped: o.WrappedNodeExpr<TExpression>): R3Reference {
return {value: wrapped, type: wrapped};
}
/**
* Parses the value of an enum from the AST value's symbol name.
*/
export function parseEnum<TExpression, TEnum>(
value: AstValue<unknown, TExpression>, Enum: TEnum): TEnum[keyof TEnum] {
const symbolName = value.getSymbolName();
if (symbolName === null) {
throw new FatalLinkerError(value.expression, 'Expected value to have a symbol name');
}
const enumValue = Enum[symbolName as keyof typeof Enum];
if (enumValue === undefined) {
throw new FatalLinkerError(value.expression, `Unsupported enum value for ${Enum}`);
}
return enumValue;
}
@@ -0,0 +1,67 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {needsLinking} from '../../src/file_linker/needs_linking';
describe('needsLinking', () => {
it('should return true for directive declarations', () => {
expect(needsLinking('file.js', `
export class Dir {
ɵdir = ɵɵngDeclareDirective({type: Dir});
}
`)).toBeTrue();
});
it('should return true for namespaced directive declarations', () => {
expect(needsLinking('file.js', `
export class Dir {
ɵdir = ng.ɵɵngDeclareDirective({type: Dir});
}
`)).toBeTrue();
});
it('should return true for unrelated usages of ɵɵngDeclareDirective', () => {
expect(needsLinking('file.js', `
const fnName = 'ɵɵngDeclareDirective';
`)).toBeTrue();
});
it('should return false when the file does not contain ɵɵngDeclareDirective', () => {
expect(needsLinking('file.js', `
const foo = ngDeclareDirective;
`)).toBeFalse();
});
it('should return true for component declarations', () => {
expect(needsLinking('file.js', `
export class Cmp {
ɵdir = ɵɵngDeclareComponent({type: Cmp});
}
`)).toBeTrue();
});
it('should return true for namespaced component declarations', () => {
expect(needsLinking('file.js', `
export class Cmp {
ɵdir = ng.ɵɵngDeclareComponent({type: Cmp});
}
`)).toBeTrue();
});
it('should return true for unrelated usages of ɵɵngDeclareComponent', () => {
expect(needsLinking('file.js', `
const fnName = 'ɵɵngDeclareComponent';
`)).toBeTrue();
});
it('should return false when the file does not contain ɵɵngDeclareComponent', () => {
expect(needsLinking('file.js', `
const foo = ngDeclareComponent;
`)).toBeFalse();
});
});
@@ -0,0 +1,16 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// This file exists as a target for g3 patches which change the Angular compiler's behavior.
// Separating the patched code in a separate file eliminates the possibility of conflicts with the
// patch diffs when making changes to the rest of the compiler codebase.
// In ngtsc we no longer want to compile undecorated classes with Angular features.
// Migrations for these patterns ran as part of `ng update` and we want to ensure
// that projects do not regress. See https://hackmd.io/@alx/ryfYYuvzH for more details.
export const compileUndecoratedClassesWithAngularFeatures = false;
@@ -0,0 +1,16 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "semantic_graph",
srcs = ["index.ts"] + glob([
"src/**/*.ts",
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/reflection",
"@npm//typescript",
],
)
@@ -0,0 +1,12 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export {SemanticReference, SemanticSymbol} from './src/api';
export {SemanticDepGraph, SemanticDepGraphUpdater} from './src/graph';
export {areTypeParametersEqual, extractSemanticTypeParameters, SemanticTypeParameter} from './src/type_parameters';
export {isArrayEqual, isReferenceEqual, isSetEqual, isSymbolEqual} from './src/util';
@@ -0,0 +1,127 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../../file_system';
import {ClassDeclaration} from '../../../reflection';
/**
* Represents a symbol that is recognizable across incremental rebuilds, which enables the captured
* metadata to be compared to the prior compilation. This allows for semantic understanding of
* the changes that have been made in a rebuild, which potentially enables more reuse of work
* from the prior compilation.
*/
export abstract class SemanticSymbol {
/**
* The path of the file that declares this symbol.
*/
public readonly path: AbsoluteFsPath;
/**
* The identifier of this symbol, or null if no identifier could be determined. It should
* uniquely identify the symbol relative to `file`. This is typically just the name of a
* top-level class declaration, as that uniquely identifies the class within the file.
*
* If the identifier is null, then this symbol cannot be recognized across rebuilds. In that
* case, the symbol is always assumed to have semantically changed to guarantee a proper
* rebuild.
*/
public readonly identifier: string|null;
constructor(
/**
* The declaration for this symbol.
*/
public readonly decl: ClassDeclaration,
) {
this.path = absoluteFromSourceFile(decl.getSourceFile());
this.identifier = getSymbolIdentifier(decl);
}
/**
* Allows the symbol to be compared to the equivalent symbol in the previous compilation. The
* return value indicates whether the symbol has been changed in a way such that its public API
* is affected.
*
* This method determines whether a change to _this_ symbol require the symbols that
* use to this symbol to be re-emitted.
*
* Note: `previousSymbol` is obtained from the most recently succeeded compilation. Symbols of
* failed compilations are never provided.
*
* @param previousSymbol The symbol from a prior compilation.
*/
abstract isPublicApiAffected(previousSymbol: SemanticSymbol): boolean;
/**
* Allows the symbol to determine whether its emit is affected. The equivalent symbol from a prior
* build is given, in addition to the set of symbols of which the public API has changed.
*
* This method determines whether a change to _other_ symbols, i.e. those present in
* `publicApiAffected`, should cause _this_ symbol to be re-emitted.
*
* @param previousSymbol The equivalent symbol from a prior compilation. Note that it may be a
* different type of symbol, if e.g. a Component was changed into a Directive with the same name.
* @param publicApiAffected The set of symbols of which the public API has changed.
*/
isEmitAffected?(previousSymbol: SemanticSymbol, publicApiAffected: Set<SemanticSymbol>): boolean;
/**
* Similar to `isPublicApiAffected`, but here equivalent symbol from a prior compilation needs
* to be compared to see if the type-check block of components that use this symbol is affected.
*
* This method determines whether a change to _this_ symbol require the symbols that
* use to this symbol to have their type-check block regenerated.
*
* Note: `previousSymbol` is obtained from the most recently succeeded compilation. Symbols of
* failed compilations are never provided.
*
* @param previousSymbol The symbol from a prior compilation.
*/
abstract isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean;
/**
* Similar to `isEmitAffected`, but focused on the type-check block of this symbol. This method
* determines whether a change to _other_ symbols, i.e. those present in `typeCheckApiAffected`,
* should cause _this_ symbol's type-check block to be regenerated.
*
* @param previousSymbol The equivalent symbol from a prior compilation. Note that it may be a
* different type of symbol, if e.g. a Component was changed into a Directive with the same name.
* @param typeCheckApiAffected The set of symbols of which the type-check API has changed.
*/
isTypeCheckBlockAffected?
(previousSymbol: SemanticSymbol, typeCheckApiAffected: Set<SemanticSymbol>): boolean;
}
/**
* Represents a reference to a semantic symbol that has been emitted into a source file. The
* reference may refer to the symbol using a different name than the semantic symbol's declared
* name, e.g. in case a re-export under a different name was chosen by a reference emitter.
* Consequently, to know that an emitted reference is still valid not only requires that the
* semantic symbol is still valid, but also that the path by which the symbol is imported has not
* changed.
*/
export interface SemanticReference {
symbol: SemanticSymbol;
/**
* The path by which the symbol has been referenced.
*/
importPath: string|null;
}
function getSymbolIdentifier(decl: ClassDeclaration): string|null {
if (!ts.isSourceFile(decl.parent)) {
return null;
}
// If this is a top-level class declaration, the class name is used as unique identifier.
// Other scenarios are currently not supported and causes the symbol not to be identified
// across rebuilds, unless the declaration node has not changed.
return decl.name.text;
}
@@ -0,0 +1,281 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Expression, ExternalExpr} from '@angular/compiler';
import {AbsoluteFsPath} from '../../../file_system';
import {ClassDeclaration} from '../../../reflection';
import {SemanticReference, SemanticSymbol} from './api';
export interface SemanticDependencyResult {
/**
* The files that need to be re-emitted.
*/
needsEmit: Set<AbsoluteFsPath>;
/**
* The files for which the type-check block should be regenerated.
*/
needsTypeCheckEmit: Set<AbsoluteFsPath>;
/**
* The newly built graph that represents the current compilation.
*/
newGraph: SemanticDepGraph;
}
/**
* Represents a declaration for which no semantic symbol has been registered. For example,
* declarations from external dependencies have not been explicitly registered and are represented
* by this symbol. This allows the unresolved symbol to still be compared to a symbol from a prior
* compilation.
*/
class OpaqueSymbol extends SemanticSymbol {
isPublicApiAffected(): false {
return false;
}
isTypeCheckApiAffected(): false {
return false;
}
}
/**
* The semantic dependency graph of a single compilation.
*/
export class SemanticDepGraph {
readonly files = new Map<AbsoluteFsPath, Map<string, SemanticSymbol>>();
readonly symbolByDecl = new Map<ClassDeclaration, SemanticSymbol>();
/**
* Registers a symbol in the graph. The symbol is given a unique identifier if possible, such that
* its equivalent symbol can be obtained from a prior graph even if its declaration node has
* changed across rebuilds. Symbols without an identifier are only able to find themselves in a
* prior graph if their declaration node is identical.
*/
registerSymbol(symbol: SemanticSymbol): void {
this.symbolByDecl.set(symbol.decl, symbol);
if (symbol.identifier !== null) {
// If the symbol has a unique identifier, record it in the file that declares it. This enables
// the symbol to be requested by its unique name.
if (!this.files.has(symbol.path)) {
this.files.set(symbol.path, new Map<string, SemanticSymbol>());
}
this.files.get(symbol.path)!.set(symbol.identifier, symbol);
}
}
/**
* Attempts to resolve a symbol in this graph that represents the given symbol from another graph.
* If no matching symbol could be found, null is returned.
*
* @param symbol The symbol from another graph for which its equivalent in this graph should be
* found.
*/
getEquivalentSymbol(symbol: SemanticSymbol): SemanticSymbol|null {
// First lookup the symbol by its declaration. It is typical for the declaration to not have
// changed across rebuilds, so this is likely to find the symbol. Using the declaration also
// allows to diff symbols for which no unique identifier could be determined.
let previousSymbol = this.getSymbolByDecl(symbol.decl);
if (previousSymbol === null && symbol.identifier !== null) {
// The declaration could not be resolved to a symbol in a prior compilation, which may
// happen because the file containing the declaration has changed. In that case we want to
// lookup the symbol based on its unique identifier, as that allows us to still compare the
// changed declaration to the prior compilation.
previousSymbol = this.getSymbolByName(symbol.path, symbol.identifier);
}
return previousSymbol;
}
/**
* Attempts to find the symbol by its identifier.
*/
private getSymbolByName(path: AbsoluteFsPath, identifier: string): SemanticSymbol|null {
if (!this.files.has(path)) {
return null;
}
const file = this.files.get(path)!;
if (!file.has(identifier)) {
return null;
}
return file.get(identifier)!;
}
/**
* Attempts to resolve the declaration to its semantic symbol.
*/
getSymbolByDecl(decl: ClassDeclaration): SemanticSymbol|null {
if (!this.symbolByDecl.has(decl)) {
return null;
}
return this.symbolByDecl.get(decl)!;
}
}
/**
* Implements the logic to go from a previous dependency graph to a new one, along with information
* on which files have been affected.
*/
export class SemanticDepGraphUpdater {
private readonly newGraph = new SemanticDepGraph();
/**
* Contains opaque symbols that were created for declarations for which there was no symbol
* registered, which happens for e.g. external declarations.
*/
private readonly opaqueSymbols = new Map<ClassDeclaration, OpaqueSymbol>();
constructor(
/**
* The semantic dependency graph of the most recently succeeded compilation, or null if this
* is the initial build.
*/
private priorGraph: SemanticDepGraph|null) {}
/**
* Registers the symbol in the new graph that is being created.
*/
registerSymbol(symbol: SemanticSymbol): void {
this.newGraph.registerSymbol(symbol);
}
/**
* Takes all facts that have been gathered to create a new semantic dependency graph. In this
* process, the semantic impact of the changes is determined which results in a set of files that
* need to be emitted and/or type-checked.
*/
finalize(): SemanticDependencyResult {
if (this.priorGraph === null) {
// If no prior dependency graph is available then this was the initial build, in which case
// we don't need to determine the semantic impact as everything is already considered
// logically changed.
return {
needsEmit: new Set<AbsoluteFsPath>(),
needsTypeCheckEmit: new Set<AbsoluteFsPath>(),
newGraph: this.newGraph,
};
}
const needsEmit = this.determineInvalidatedFiles(this.priorGraph);
const needsTypeCheckEmit = this.determineInvalidatedTypeCheckFiles(this.priorGraph);
return {
needsEmit,
needsTypeCheckEmit,
newGraph: this.newGraph,
};
}
private determineInvalidatedFiles(priorGraph: SemanticDepGraph): Set<AbsoluteFsPath> {
const isPublicApiAffected = new Set<SemanticSymbol>();
// The first phase is to collect all symbols which have their public API affected. Any symbols
// that cannot be matched up with a symbol from the prior graph are considered affected.
for (const symbol of this.newGraph.symbolByDecl.values()) {
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null || symbol.isPublicApiAffected(previousSymbol)) {
isPublicApiAffected.add(symbol);
}
}
// The second phase is to find all symbols for which the emit result is affected, either because
// their used declarations have changed or any of those used declarations has had its public API
// affected as determined in the first phase.
const needsEmit = new Set<AbsoluteFsPath>();
for (const symbol of this.newGraph.symbolByDecl.values()) {
if (symbol.isEmitAffected === undefined) {
continue;
}
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null || symbol.isEmitAffected(previousSymbol, isPublicApiAffected)) {
needsEmit.add(symbol.path);
}
}
return needsEmit;
}
private determineInvalidatedTypeCheckFiles(priorGraph: SemanticDepGraph): Set<AbsoluteFsPath> {
const isTypeCheckApiAffected = new Set<SemanticSymbol>();
// The first phase is to collect all symbols which have their public API affected. Any symbols
// that cannot be matched up with a symbol from the prior graph are considered affected.
for (const symbol of this.newGraph.symbolByDecl.values()) {
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null || symbol.isTypeCheckApiAffected(previousSymbol)) {
isTypeCheckApiAffected.add(symbol);
}
}
// The second phase is to find all symbols for which the emit result is affected, either because
// their used declarations have changed or any of those used declarations has had its public API
// affected as determined in the first phase.
const needsTypeCheckEmit = new Set<AbsoluteFsPath>();
for (const symbol of this.newGraph.symbolByDecl.values()) {
if (symbol.isTypeCheckBlockAffected === undefined) {
continue;
}
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null ||
symbol.isTypeCheckBlockAffected(previousSymbol, isTypeCheckApiAffected)) {
needsTypeCheckEmit.add(symbol.path);
}
}
return needsTypeCheckEmit;
}
/**
* Creates a `SemanticReference` for the reference to `decl` using the expression `expr`. See
* the documentation of `SemanticReference` for details.
*/
getSemanticReference(decl: ClassDeclaration, expr: Expression): SemanticReference {
return {
symbol: this.getSymbol(decl),
importPath: getImportPath(expr),
};
}
/**
* Gets the `SemanticSymbol` that was registered for `decl` during the current compilation, or
* returns an opaque symbol that represents `decl`.
*/
getSymbol(decl: ClassDeclaration): SemanticSymbol {
const symbol = this.newGraph.getSymbolByDecl(decl);
if (symbol === null) {
// No symbol has been recorded for the provided declaration, which would be the case if the
// declaration is external. Return an opaque symbol in that case, to allow the external
// declaration to be compared to a prior compilation.
return this.getOpaqueSymbol(decl);
}
return symbol;
}
/**
* Gets or creates an `OpaqueSymbol` for the provided class declaration.
*/
private getOpaqueSymbol(decl: ClassDeclaration): OpaqueSymbol {
if (this.opaqueSymbols.has(decl)) {
return this.opaqueSymbols.get(decl)!;
}
const symbol = new OpaqueSymbol(decl);
this.opaqueSymbols.set(decl, symbol);
return symbol;
}
}
function getImportPath(expr: Expression): string|null {
if (expr instanceof ExternalExpr) {
return `${expr.value.moduleName}\$${expr.value.name}`;
} else {
return null;
}
}
@@ -0,0 +1,70 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ClassDeclaration} from '../../../reflection';
import {isArrayEqual} from './util';
/**
* Describes a generic type parameter of a semantic symbol. A class declaration with type parameters
* needs special consideration in certain contexts. For example, template type-check blocks may
* contain type constructors of used directives which include the type parameters of the directive.
* As a consequence, if a change is made that affects the type parameters of said directive, any
* template type-check blocks that use the directive need to be regenerated.
*
* This type represents a single generic type parameter. It currently only tracks whether the
* type parameter has a constraint, i.e. has an `extends` clause. When a constraint is present, we
* currently assume that the type parameter is affected in each incremental rebuild; proving that
* a type parameter with constraint is not affected is non-trivial as it requires full semantic
* understanding of the type constraint.
*/
export interface SemanticTypeParameter {
/**
* Whether a type constraint, i.e. an `extends` clause is present on the type parameter.
*/
hasGenericTypeBound: boolean;
}
/**
* Converts the type parameters of the given class into their semantic representation. If the class
* does not have any type parameters, then `null` is returned.
*/
export function extractSemanticTypeParameters(node: ClassDeclaration): SemanticTypeParameter[]|
null {
if (!ts.isClassDeclaration(node) || node.typeParameters === undefined) {
return null;
}
return node.typeParameters.map(
typeParam => ({hasGenericTypeBound: typeParam.constraint !== undefined}));
}
/**
* Compares the list of type parameters to determine if they can be considered equal.
*/
export function areTypeParametersEqual(
current: SemanticTypeParameter[]|null, previous: SemanticTypeParameter[]|null): boolean {
// First compare all type parameters one-to-one; any differences mean that the list of type
// parameters has changed.
if (!isArrayEqual(current, previous, isTypeParameterEqual)) {
return false;
}
// If there is a current list of type parameters and if any of them has a generic type constraint,
// then the meaning of that type parameter may have changed without us being aware; as such we
// have to assume that the type parameters have in fact changed.
if (current !== null && current.some(typeParam => typeParam.hasGenericTypeBound)) {
return false;
}
return true;
}
function isTypeParameterEqual(a: SemanticTypeParameter, b: SemanticTypeParameter): boolean {
return a.hasGenericTypeBound === b.hasGenericTypeBound;
}
@@ -0,0 +1,93 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SemanticReference, SemanticSymbol} from './api';
/**
* Determines whether the provided symbols represent the same declaration.
*/
export function isSymbolEqual(a: SemanticSymbol, b: SemanticSymbol): boolean {
if (a.decl === b.decl) {
// If the declaration is identical then it must represent the same symbol.
return true;
}
if (a.identifier === null || b.identifier === null) {
// Unidentifiable symbols are assumed to be different.
return false;
}
return a.path === b.path && a.identifier === b.identifier;
}
/**
* Determines whether the provided references to a semantic symbol are still equal, i.e. represent
* the same symbol and are imported by the same path.
*/
export function isReferenceEqual(a: SemanticReference, b: SemanticReference): boolean {
if (!isSymbolEqual(a.symbol, b.symbol)) {
// If the reference's target symbols are different, the reference itself is different.
return false;
}
// The reference still corresponds with the same symbol, now check that the path by which it is
// imported has not changed.
return a.importPath === b.importPath;
}
export function referenceEquality<T>(a: T, b: T): boolean {
return a === b;
}
/**
* Determines if the provided arrays are equal to each other, using the provided equality tester
* that is called for all entries in the array.
*/
export function isArrayEqual<T>(
a: readonly T[]|null, b: readonly T[]|null,
equalityTester: (a: T, b: T) => boolean = referenceEquality): boolean {
if (a === null || b === null) {
return a === b;
}
if (a.length !== b.length) {
return false;
}
return !a.some((item, index) => !equalityTester(item, b[index]));
}
/**
* Determines if the provided sets are equal to each other, using the provided equality tester.
* Sets that only differ in ordering are considered equal.
*/
export function isSetEqual<T>(
a: ReadonlySet<T>|null, b: ReadonlySet<T>|null,
equalityTester: (a: T, b: T) => boolean = referenceEquality): boolean {
if (a === null || b === null) {
return a === b;
}
if (a.size !== b.size) {
return false;
}
for (const itemA of a) {
let found = false;
for (const itemB of b) {
if (equalityTester(itemA, itemB)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}

Some files were not shown because too many files have changed in this diff Show More