From f5c566c0793eacf9ca146c8a6b8da15b0e8f4c4d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 26 Feb 2024 18:28:21 +0100 Subject: [PATCH] fix(compiler-cli): identify aliased initializer functions (#54609) Fixes that initializer functions weren't being recognized if they are aliased (e.g. `import {model as alias} from '@angular/core';`). To do this efficiently, I had to introduce the `ImportedSymbolsTracker` which scans the top-level imports of a file and allows them to be checked quickly, without having to go through the type checker. It will be useful in the future when verifying that that initializer APIs aren't used in unexpected places. I've also introduced tests specifically for the `tryParseInitializerApiMember` function so that we can test it in isolation instead of going through the various functions that call into it. PR Close #54609 --- .../annotations/component/src/handler.ts | 9 +- .../component/test/component_spec.ts | 4 +- .../annotations/directive/src/handler.ts | 7 +- .../directive/src/initializer_functions.ts | 158 ++++++---- .../directive/src/input_function.ts | 8 +- .../directive/src/model_function.ts | 5 +- .../directive/src/output_function.ts | 7 +- .../directive/src/query_functions.ts | 10 +- .../ngtsc/annotations/directive/src/shared.ts | 45 +-- .../directive/test/directive_spec.ts | 6 +- .../test/initializer_functions_spec.ts | 294 ++++++++++++++++++ .../src/ngtsc/core/src/compiler.ts | 6 +- .../compiler-cli/src/ngtsc/imports/index.ts | 1 + .../imports/src/imported_symbols_tracker.ts | 109 +++++++ .../src/transformers/jit_transforms/index.ts | 7 +- .../input_function.ts | 6 +- .../model_function.ts | 3 +- .../output_function.ts | 3 +- .../query_functions.ts | 3 +- .../initializer_api_transforms/transform.ts | 7 +- .../transform_api.ts | 8 +- packages/compiler-cli/test/BUILD.bazel | 1 + .../test/initializer_api_transforms_spec.ts | 4 +- .../signal_queries_metadata_transform_spec.ts | 4 +- .../test/acceptance/authoring/BUILD.bazel | 1 + .../authoring/authoring_test_compiler.ts | 5 +- 26 files changed, 592 insertions(+), 129 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/annotations/directive/test/initializer_functions_spec.ts create mode 100644 packages/compiler-cli/src/ngtsc/imports/src/imported_symbols_tracker.ts diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index 18c0be1035789..f4e9536c237f5 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -12,7 +12,7 @@ import ts from 'typescript'; import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../../cycles'; import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics'; import {absoluteFrom, relative} from '../../../file_system'; -import {assertSuccessfulReferenceEmit, DeferredSymbolTracker, ImportedFile, LocalCompilationExtraImportsTracker, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; +import {assertSuccessfulReferenceEmit, DeferredSymbolTracker, ImportedFile, ImportedSymbolsTracker, LocalCompilationExtraImportsTracker, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; import {DependencyTracker} from '../../../incremental/api'; import {extractSemanticTypeParameters, SemanticDepGraphUpdater} from '../../../incremental/semantic_graph'; import {IndexingContext} from '../../../indexer'; @@ -83,7 +83,8 @@ export class ComponentDecoratorHandler implements private injectableRegistry: InjectableClassRegistry, private semanticDepGraphUpdater: SemanticDepGraphUpdater|null, private annotateForClosureCompiler: boolean, private perf: PerfRecorder, - private hostDirectivesResolver: HostDirectivesResolver, private includeClassMetadata: boolean, + private hostDirectivesResolver: HostDirectivesResolver, + private importTracker: ImportedSymbolsTracker, private includeClassMetadata: boolean, private readonly compilationMode: CompilationMode, private readonly deferredSymbolTracker: DeferredSymbolTracker, private readonly forbidOrphanRendering: boolean, private readonly enableBlockSyntax: boolean, @@ -225,8 +226,8 @@ export class ComponentDecoratorHandler implements // @Component inherits @Directive, so begin by extracting the @Directive metadata and building // on it. const directiveResult = extractDirectiveMetadata( - node, decorator, this.reflector, this.evaluator, this.refEmitter, this.referencesRegistry, - this.isCore, this.annotateForClosureCompiler, this.compilationMode, + node, decorator, this.reflector, this.importTracker, this.evaluator, this.refEmitter, + this.referencesRegistry, this.isCore, this.annotateForClosureCompiler, this.compilationMode, this.elementSchemaRegistry.getDefaultComponentElementName(), this.useTemplatePipeline); if (directiveResult === undefined) { // `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts index 13c8d0f38ab67..e9eee3850e964 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts @@ -13,7 +13,7 @@ import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../../cycles import {ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../../diagnostics'; import {absoluteFrom} from '../../../file_system'; import {runInEachFileSystem} from '../../../file_system/testing'; -import {DeferredSymbolTracker, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; +import {DeferredSymbolTracker, ImportedSymbolsTracker, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; import {CompoundMetadataReader, DtsMetadataReader, HostDirectivesResolver, LocalMetadataRegistry, ResourceRegistry} from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; import {NOOP_PERF_RECORDER} from '../../../perf'; @@ -68,6 +68,7 @@ function setup( const typeCheckScopeRegistry = new TypeCheckScopeRegistry(scopeRegistry, metaReader, hostDirectivesResolver); const resourceLoader = new StubResourceLoader(); + const importTracker = new ImportedSymbolsTracker(); const handler = new ComponentDecoratorHandler( reflectionHost, @@ -99,6 +100,7 @@ function setup( /* annotateForClosureCompiler */ false, NOOP_PERF_RECORDER, hostDirectivesResolver, + importTracker, true, compilationMode, new DeferredSymbolTracker(checker, /* onlyExplicitDeferDependencyImports */ false), diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts index 4402f2f639693..57f3858d0d060 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts @@ -9,7 +9,7 @@ import {compileClassMetadata, compileDeclareClassMetadata, compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, FactoryTarget, makeBindingParser, R3ClassMetadata, R3DirectiveMetadata, WrappedNodeExpr} from '@angular/compiler'; import ts from 'typescript'; -import {Reference, ReferenceEmitter} from '../../../imports'; +import {ImportedSymbolsTracker, Reference, ReferenceEmitter} from '../../../imports'; import {extractSemanticTypeParameters, SemanticDepGraphUpdater} from '../../../incremental/semantic_graph'; import {ClassPropertyMapping, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, HostDirectiveMeta, InputMapping, MatchSource, MetadataReader, MetadataRegistry, MetaKind} from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; @@ -62,6 +62,7 @@ export class DirectiveDecoratorHandler implements private semanticDepGraphUpdater: SemanticDepGraphUpdater|null, private annotateForClosureCompiler: boolean, private perf: PerfRecorder, + private importTracker: ImportedSymbolsTracker, private includeClassMetadata: boolean, private readonly compilationMode: CompilationMode, private readonly useTemplatePipeline: boolean, @@ -104,8 +105,8 @@ export class DirectiveDecoratorHandler implements this.perf.eventCount(PerfEvent.AnalyzeDirective); const directiveResult = extractDirectiveMetadata( - node, decorator, this.reflector, this.evaluator, this.refEmitter, this.referencesRegistry, - this.isCore, this.annotateForClosureCompiler, this.compilationMode, + node, decorator, this.reflector, this.importTracker, this.evaluator, this.refEmitter, + this.referencesRegistry, this.isCore, this.annotateForClosureCompiler, this.compilationMode, /* defaultSelector */ null, this.useTemplatePipeline); if (directiveResult === undefined) { return {}; diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_functions.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_functions.ts index 044c5faaaebc7..34efb1da26a4e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_functions.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_functions.ts @@ -8,6 +8,7 @@ import ts from 'typescript'; +import {ImportedSymbolsTracker} from '../../../imports'; import {ClassMember, ReflectionHost} from '../../../reflection'; import {CORE_MODULE} from '../../common'; @@ -39,6 +40,18 @@ interface InitializerFunctionMetadata { isRequired: boolean; } +/** + * Metadata that can be inferred from an initializer + * statically without going through the type checker. + */ +interface StaticInitializerData { + /** Identifier in the initializer that refers to the Angular API. */ + node: ts.Identifier; + + /** Whether the call is required. */ + isRequired: boolean; +} + /** * Attempts to identify an Angular class member that is declared via * its initializer referring to a given initializer API function. @@ -48,97 +61,110 @@ interface InitializerFunctionMetadata { */ export function tryParseInitializerApiMember( fnNames: FnNames, member: Pick, reflector: ReflectionHost, - isCore: boolean): InitializerFunctionMetadata&{apiName: FnNames[number]}|null { + importTracker: ImportedSymbolsTracker): InitializerFunctionMetadata|null { if (member.value === null || !ts.isCallExpression(member.value)) { return null; } + const call = member.value; + const staticResult = parseTopLevelCall(call, fnNames, importTracker) || + parseTopLevelRequiredCall(call, fnNames, importTracker) || + parseTopLevelCallFromNamespace(call, fnNames, importTracker); - // Extract target. Either: - // - `[input]` - // - `core.[input]` - // - `input.[required]` - // - `core.input.[required]`. - let target = extractPropertyTarget(call.expression); - if (target === null) { + if (staticResult === null) { return null; } - // Find if the `target` matches one of the expected APIs we are looking for. - // e.g. `input`, or `viewChild`. - let apiName = fnNames.find(n => n === target!.text); - - // Case 1: API is directly called. e.g. `input` - // If no API name was matched, continue looking for `input.required`. - if (apiName !== undefined) { - if (!isReferenceToInitializerApiFunction(apiName, target, isCore, reflector)) { - return null; - } - return {apiName, call, isRequired: false}; - } - - // Case 2: API is the `.required` - // Ensure there is a property access to `[input].required` or `[core.input].required`. - if (target.text !== 'required' || !ts.isPropertyAccessExpression(call.expression)) { + // Once we've statically determined that the initializer is one of the APIs we're looking for, we + // need to verify it using the type checker which accounts for things like shadowed variables. + // This should be done as the absolute last step since using the type check can be expensive. + const resolvedImport = reflector.getImportOfIdentifier(staticResult.node); + if (resolvedImport === null || !(fnNames as string[]).includes(resolvedImport.name)) { return null; } - // e.g. `[input.required]` (the full property access is this) - const apiPropertyAccess = call.expression; - // e.g. `[input].required` (we now extract the left side of the access). - target = extractPropertyTarget(apiPropertyAccess.expression); - if (target === null) { - return null; - } + return { + call, + isRequired: staticResult.isRequired, + apiName: resolvedImport.name as InitializerApiFunction, + }; +} - // Find if the `target` matches one of the expected APIs are are looking for. - apiName = fnNames.find(n => n === target!.text); +/** + * Attempts to parse a top-level call to an initializer function, + * e.g. `prop = input()`. Returns null if it can't be parsed. + */ +function parseTopLevelCall( + call: ts.CallExpression, fnNames: InitializerApiFunction[], + importTracker: ImportedSymbolsTracker): StaticInitializerData|null { + const node = call.expression; - // Ensure the call refers to the real API function from Angular core. - if (apiName === undefined || - !isReferenceToInitializerApiFunction(apiName, target, isCore, reflector)) { + if (!ts.isIdentifier(node)) { return null; } - return { - apiName, - call, - isRequired: true, - }; + return fnNames.some( + name => importTracker.isPotentialReferenceToNamedImport(node, name, CORE_MODULE)) ? + {node, isRequired: false} : + null; } /** - * Extracts the identifier property target of a expression, supporting - * one level deep property accesses. - * - * e.g. `input.required` will return `required`. - * e.g. `input` will return `input`. - * + * Attempts to parse a top-level call to a required initializer, + * e.g. `prop = input.required()`. Returns null if it can't be parsed. */ -function extractPropertyTarget(node: ts.Expression): ts.Identifier|null { - if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) { - return node.name; - } else if (ts.isIdentifier(node)) { - return node; +function parseTopLevelRequiredCall( + call: ts.CallExpression, fnNames: InitializerApiFunction[], + importTracker: ImportedSymbolsTracker): StaticInitializerData|null { + const node = call.expression; + + if (!ts.isPropertyAccessExpression(node) || !ts.isIdentifier(node.expression) || + node.name.text !== 'required') { + return null; } - return null; + + const expression = node.expression; + const matchesCoreApi = fnNames.some( + name => importTracker.isPotentialReferenceToNamedImport(expression, name, CORE_MODULE)); + + return matchesCoreApi ? {node: expression, isRequired: true} : null; } + /** - * Verifies that the given identifier resolves to the given initializer API - * function expression from Angular core. + * Attempts to parse a top-level call to a function referenced via a namespace import, + * e.g. `prop = core.input.required()`. Returns null if it can't be parsed. */ -function isReferenceToInitializerApiFunction( - functionName: InitializerApiFunction, target: ts.Identifier, isCore: boolean, - reflector: ReflectionHost): boolean { - let targetImport: {name: string, from: string}|null = reflector.getImportOfIdentifier(target); - if (targetImport === null) { - if (!isCore) { - return false; - } - // We are compiling the core module, where no import can be present. - targetImport = {name: target.text, from: CORE_MODULE}; +function parseTopLevelCallFromNamespace( + call: ts.CallExpression, fnNames: InitializerApiFunction[], + importTracker: ImportedSymbolsTracker): StaticInitializerData|null { + const node = call.expression; + + if (!ts.isPropertyAccessExpression(node)) { + return null; + } + + let apiReference: ts.Identifier|null = null; + let isRequired = false; + + // `prop = core.input()` + if (ts.isIdentifier(node.expression) && ts.isIdentifier(node.name) && + importTracker.isPotentialReferenceToNamespaceImport(node.expression, CORE_MODULE)) { + apiReference = node.name; + } else if ( + // `prop = core.input.required()` + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && ts.isIdentifier(node.expression.name) && + importTracker.isPotentialReferenceToNamespaceImport( + node.expression.expression, CORE_MODULE) && + node.name.text === 'required') { + apiReference = node.expression.name; + isRequired = true; + } + + if (apiReference === null || !(fnNames as string[]).includes(apiReference.text)) { + return null; } - return targetImport.name === functionName && targetImport.from === CORE_MODULE; + return {node: apiReference, isRequired}; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_function.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_function.ts index c6c682732dcba..a8a4452c2edc6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_function.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_function.ts @@ -8,9 +8,9 @@ import ts from 'typescript'; -import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics'; +import {ImportedSymbolsTracker} from '../../../imports'; import {InputMapping} from '../../../metadata'; -import {ClassMember, ReflectionHost, reflectObjectLiteral} from '../../../reflection'; +import {ClassMember, ReflectionHost} from '../../../reflection'; import {tryParseInitializerApiMember} from './initializer_functions'; import {parseAndValidateInputAndOutputOptions} from './input_output_parse_options'; @@ -21,8 +21,8 @@ import {parseAndValidateInputAndOutputOptions} from './input_output_parse_option */ export function tryParseSignalInputMapping( member: Pick, reflector: ReflectionHost, - isCore: boolean): InputMapping|null { - const signalInput = tryParseInitializerApiMember(['input'], member, reflector, isCore); + importTracker: ImportedSymbolsTracker): InputMapping|null { + const signalInput = tryParseInitializerApiMember(['input'], member, reflector, importTracker); if (signalInput === null) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/model_function.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/model_function.ts index b0f9376e026ac..2b4e332e8f583 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/model_function.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/model_function.ts @@ -8,6 +8,7 @@ import ts from 'typescript'; +import {ImportedSymbolsTracker} from '../../../imports'; import {ModelMapping} from '../../../metadata'; import {ClassMember, ReflectionHost} from '../../../reflection'; @@ -19,8 +20,8 @@ import {parseAndValidateInputAndOutputOptions} from './input_output_parse_option */ export function tryParseSignalModelMapping( member: Pick, reflector: ReflectionHost, - isCore: boolean): ModelMapping|null { - const model = tryParseInitializerApiMember(['model'], member, reflector, isCore); + importTracker: ImportedSymbolsTracker): ModelMapping|null { + const model = tryParseInitializerApiMember(['model'], member, reflector, importTracker); if (model === null) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/output_function.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/output_function.ts index d0163569a5c27..d4593f139785c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/output_function.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/output_function.ts @@ -9,6 +9,7 @@ import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics'; +import {ImportedSymbolsTracker} from '../../../imports'; import {InputOrOutput} from '../../../metadata'; import {ClassMember, ReflectionHost} from '../../../reflection'; @@ -21,8 +22,10 @@ import {parseAndValidateInputAndOutputOptions} from './input_output_parse_option */ export function tryParseInitializerBasedOutput( member: Pick, reflector: ReflectionHost, - isCore: boolean): {call: ts.CallExpression, metadata: InputOrOutput}|null { - const output = tryParseInitializerApiMember(['output', 'ɵoutput'], member, reflector, isCore); + importTracker: ImportedSymbolsTracker): {call: ts.CallExpression, metadata: InputOrOutput}| + null { + const output = + tryParseInitializerApiMember(['output', 'ɵoutput'], member, reflector, importTracker); if (output === null) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/query_functions.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/query_functions.ts index e51f78a6fd001..b855f4c4a67eb 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/query_functions.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/query_functions.ts @@ -11,6 +11,7 @@ import {createMayBeForwardRefExpression, ForwardRefHandling, MaybeForwardRefExpr import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics'; +import {ImportedSymbolsTracker} from '../../../imports'; import {ClassMember, ReflectionHost, reflectObjectLiteral} from '../../../reflection'; import {tryUnwrapForwardRef} from '../../common'; @@ -35,9 +36,10 @@ const defaultDescendantsValue = (type: QueryFunctionName) => type !== 'contentCh * @returns Resolved query metadata, or null if no query is declared. */ export function tryParseSignalQueryFromInitializer( - member: Pick, reflector: ReflectionHost, isCore: boolean): + member: Pick, reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker): {name: QueryFunctionName, metadata: R3QueryMetadata, call: ts.CallExpression}|null { - const query = tryParseInitializerApiMember(queryFunctionNames, member, reflector, isCore); + const query = tryParseInitializerApiMember(queryFunctionNames, member, reflector, importTracker); if (query === null) { return null; } @@ -58,10 +60,10 @@ export function tryParseSignalQueryFromInitializer( const read = options?.has('read') ? parseReadOption(options.get('read')!) : null; const descendants = options?.has('descendants') ? parseDescendantsOption(options.get('descendants')!) : - defaultDescendantsValue(query.apiName); + defaultDescendantsValue(query.apiName as QueryFunctionName); return { - name: query.apiName, + name: query.apiName as QueryFunctionName, call: query.call, metadata: { isSignal: true, diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts index e021d5a81300e..b4b8b70ce08fb 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts @@ -10,7 +10,7 @@ import {createMayBeForwardRefExpression, emitDistinctChangesOnlyDefaultValue, Ex import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError, makeRelatedInformation} from '../../../diagnostics'; -import {assertSuccessfulReferenceEmit, ImportFlags, Reference, ReferenceEmitter} from '../../../imports'; +import {assertSuccessfulReferenceEmit, ImportedSymbolsTracker, ImportFlags, Reference, ReferenceEmitter} from '../../../imports'; import {ClassPropertyMapping, DecoratorInputTransform, HostDirectiveMeta, InputMapping, InputOrOutput, isHostDirectiveMetaForGlobalMode} from '../../../metadata'; import {DynamicValue, EnumValue, PartialEvaluator, ResolvedValue, traceDynamicValue} from '../../../partial_evaluator'; import {AmbientImport, ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection'; @@ -38,9 +38,10 @@ const QUERY_TYPES = new Set(queryDecoratorNames); */ export function extractDirectiveMetadata( clazz: ClassDeclaration, decorator: Readonly, reflector: ReflectionHost, - evaluator: PartialEvaluator, refEmitter: ReferenceEmitter, - referencesRegistry: ReferencesRegistry, isCore: boolean, annotateForClosureCompiler: boolean, - compilationMode: CompilationMode, defaultSelector: string|null, useTemplatePipeline: boolean): { + importTracker: ImportedSymbolsTracker, evaluator: PartialEvaluator, + refEmitter: ReferenceEmitter, referencesRegistry: ReferencesRegistry, isCore: boolean, + annotateForClosureCompiler: boolean, compilationMode: CompilationMode, + defaultSelector: string|null, useTemplatePipeline: boolean): { decorator: Map, metadata: R3DirectiveMetadata, inputs: ClassPropertyMapping, @@ -84,19 +85,19 @@ export function extractDirectiveMetadata( const inputsFromMeta = parseInputsArray(clazz, directive, evaluator, reflector, refEmitter, compilationMode); const inputsFromFields = parseInputFields( - clazz, members, evaluator, reflector, refEmitter, isCore, compilationMode, inputsFromMeta, - decorator); + clazz, members, evaluator, reflector, importTracker, refEmitter, isCore, compilationMode, + inputsFromMeta, decorator); const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields}); // And outputs. const outputsFromMeta = parseOutputsArray(directive, evaluator); - const outputsFromFields = - parseOutputFields(clazz, decorator, members, isCore, reflector, evaluator, outputsFromMeta); + const outputsFromFields = parseOutputFields( + clazz, decorator, members, isCore, reflector, importTracker, evaluator, outputsFromMeta); const outputs = ClassPropertyMapping.fromMappedObject({...outputsFromMeta, ...outputsFromFields}); // Parse queries of fields. const {viewQueries, contentQueries} = - parseQueriesOfClassFields(members, reflector, evaluator, isCore); + parseQueriesOfClassFields(members, reflector, importTracker, evaluator, isCore); if (directive.has('queries')) { const signalQueryFields = new Set( @@ -759,13 +760,13 @@ function tryGetDecoratorOnMember( function tryParseInputFieldMapping( clazz: ClassDeclaration, member: ClassMember, evaluator: PartialEvaluator, - reflector: ReflectionHost, isCore: boolean, refEmitter: ReferenceEmitter, - compilationMode: CompilationMode): InputMapping|null { + reflector: ReflectionHost, importTracker: ImportedSymbolsTracker, isCore: boolean, + refEmitter: ReferenceEmitter, compilationMode: CompilationMode): InputMapping|null { const classPropertyName = member.name; const decorator = tryGetDecoratorOnMember(member, 'Input', isCore); - const signalInputMapping = tryParseSignalInputMapping(member, reflector, isCore); - const modelInputMapping = tryParseSignalModelMapping(member, reflector, isCore); + const signalInputMapping = tryParseSignalInputMapping(member, reflector, importTracker); + const modelInputMapping = tryParseSignalModelMapping(member, reflector, importTracker); if (decorator !== null && signalInputMapping !== null) { throw new FatalDiagnosticError( @@ -847,8 +848,9 @@ function tryParseInputFieldMapping( /** Parses the class members that declare inputs (via decorator or initializer). */ function parseInputFields( clazz: ClassDeclaration, members: ClassMember[], evaluator: PartialEvaluator, - reflector: ReflectionHost, refEmitter: ReferenceEmitter, isCore: boolean, - compilationMode: CompilationMode, inputsFromClassDecorator: Record, + reflector: ReflectionHost, importTracker: ImportedSymbolsTracker, refEmitter: ReferenceEmitter, + isCore: boolean, compilationMode: CompilationMode, + inputsFromClassDecorator: Record, classDecorator: Decorator): Record { const inputs = {} as Record; @@ -859,6 +861,7 @@ function parseInputFields( member, evaluator, reflector, + importTracker, isCore, refEmitter, compilationMode, @@ -1037,8 +1040,8 @@ function assertEmittableInputType( * initializers for signal-based queries. */ function parseQueriesOfClassFields( - members: ClassMember[], reflector: ReflectionHost, evaluator: PartialEvaluator, - isCore: boolean): { + members: ClassMember[], reflector: ReflectionHost, importTracker: ImportedSymbolsTracker, + evaluator: PartialEvaluator, isCore: boolean): { viewQueries: R3QueryMetadata[], contentQueries: R3QueryMetadata[], } { @@ -1055,7 +1058,7 @@ function parseQueriesOfClassFields( for (const member of members) { const decoratorQuery = tryGetQueryFromFieldDecorator(member, reflector, evaluator, isCore); - const signalQuery = tryParseSignalQueryFromInitializer(member, reflector, isCore); + const signalQuery = tryParseSignalQueryFromInitializer(member, reflector, importTracker); if (decoratorQuery !== null && signalQuery !== null) { throw new FatalDiagnosticError( @@ -1115,14 +1118,14 @@ function parseOutputsArray( /** Parses the class members that are outputs. */ function parseOutputFields( clazz: ClassDeclaration, classDecorator: Decorator, members: ClassMember[], isCore: boolean, - reflector: ReflectionHost, evaluator: PartialEvaluator, + reflector: ReflectionHost, importTracker: ImportedSymbolsTracker, evaluator: PartialEvaluator, outputsFromMeta: Record): Record { const outputs = {} as Record; for (const member of members) { const decoratorOutput = tryParseDecoratorOutput(member, evaluator, isCore); - const initializerOutput = tryParseInitializerBasedOutput(member, reflector, isCore); - const modelMapping = tryParseSignalModelMapping(member, reflector, isCore); + const initializerOutput = tryParseInitializerBasedOutput(member, reflector, importTracker); + const modelMapping = tryParseSignalModelMapping(member, reflector, importTracker); if (decoratorOutput !== null && initializerOutput !== null) { throw new FatalDiagnosticError( diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts index fe7b3ad6d2ed6..437fe1136e396 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts @@ -10,7 +10,7 @@ import ts from 'typescript'; import {absoluteFrom} from '../../../file_system'; import {runInEachFileSystem} from '../../../file_system/testing'; -import {ReferenceEmitter} from '../../../imports'; +import {ImportedSymbolsTracker, ReferenceEmitter} from '../../../imports'; import {CompoundMetadataReader, DtsMetadataReader, LocalMetadataRegistry} from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; import {NOOP_PERF_RECORDER} from '../../../perf'; @@ -174,13 +174,15 @@ runInEachFileSystem(() => { metaReader, new CompoundMetadataReader([metaReader, dtsReader]), new MetadataDtsModuleScopeResolver(dtsReader, null), refEmitter, null); const injectableRegistry = new InjectableClassRegistry(reflectionHost, /* isCore */ false); + const importTracker = new ImportedSymbolsTracker(); const handler = new DirectiveDecoratorHandler( reflectionHost, evaluator, scopeRegistry, scopeRegistry, metaReader, injectableRegistry, refEmitter, referenceRegistry, /*isCore*/ false, /*strictCtorDeps*/ false, /*semanticDepGraphUpdater*/ null, - /*annotateForClosureCompiler*/ false, NOOP_PERF_RECORDER, /*includeClassMetadata*/ true, + /*annotateForClosureCompiler*/ false, NOOP_PERF_RECORDER, importTracker, + /*includeClassMetadata*/ true, /*compilationMode */ CompilationMode.FULL, /* useTemplatePipeline */ true, /*generateExtraImportsInLocalMode*/ false); diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/test/initializer_functions_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/test/initializer_functions_spec.ts new file mode 100644 index 0000000000000..287f37484479c --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/test/initializer_functions_spec.ts @@ -0,0 +1,294 @@ +/*! + * @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 ts from 'typescript'; + +import {absoluteFrom} from '../../../file_system'; +import {runInEachFileSystem} from '../../../file_system/testing'; +import {ImportedSymbolsTracker} from '../../../imports'; +import {ClassMember, TypeScriptReflectionHost} from '../../../reflection'; +import {makeProgram} from '../../../testing'; +import {tryParseInitializerApiMember} from '../src/initializer_functions'; + + +runInEachFileSystem(() => { + describe('initializer function detection', () => { + it('should identify a non-required function that is imported directly', () => { + const {member, reflector, importTracker} = setup(` + import {Directive, model} from '@angular/core'; + + @Directive() + export class Dir { + test = model(1); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + + expect(result).toEqual({ + apiName: 'model', + isRequired: false, + call: jasmine.objectContaining({kind: ts.SyntaxKind.CallExpression}), + }); + }); + + it('should identify a required function that is imported directly', () => { + const {member, reflector, importTracker} = setup(` + import {Directive, model} from '@angular/core'; + + @Directive() + export class Dir { + test = model.required(); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + + expect(result).toEqual({ + apiName: 'model', + isRequired: true, + call: jasmine.objectContaining({kind: ts.SyntaxKind.CallExpression}), + }); + }); + + it('should identify a non-required function that is aliased', () => { + const {member, reflector, importTracker} = setup(` + import {Directive, model as alias} from '@angular/core'; + + @Directive() + export class Dir { + test = alias(1); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + + expect(result).toEqual({ + apiName: 'model', + isRequired: false, + call: jasmine.objectContaining({kind: ts.SyntaxKind.CallExpression}), + }); + }); + + it('should identify a required function that is aliased', () => { + const {member, reflector, importTracker} = setup(` + import {Directive, model as alias} from '@angular/core'; + + @Directive() + export class Dir { + test = alias.required(); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + + expect(result).toEqual({ + apiName: 'model', + isRequired: true, + call: jasmine.objectContaining({kind: ts.SyntaxKind.CallExpression}), + }); + }); + + it('should identify a non-required function that is imported via namespace import', () => { + const {member, reflector, importTracker} = setup(` + import * as core from '@angular/core'; + + @core.Directive() + export class Dir { + test = core.model(1); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + + expect(result).toEqual({ + apiName: 'model', + isRequired: false, + call: jasmine.objectContaining({kind: ts.SyntaxKind.CallExpression}), + }); + }); + + it('should identify a required function that is imported via namespace import', () => { + const {member, reflector, importTracker} = setup(` + import * as core from '@angular/core'; + + @core.Directive() + export class Dir { + test = core.model.required(); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + + expect(result).toEqual({ + apiName: 'model', + isRequired: true, + call: jasmine.objectContaining({kind: ts.SyntaxKind.CallExpression}), + }); + }); + + it('should not identify a valid core function that is not being checked for', () => { + const {member, reflector, importTracker} = setup(` + import {Directive, input} from '@angular/core'; + + @Directive() + export class Dir { + test = input(1); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + expect(result).toBe(null); + }); + + it('should not identify a function coming from a different module', () => { + const {member, reflector, importTracker} = setup(` + import {Directive} from '@angular/core'; + import {model} from '@not-angular/core'; + + @Directive() + export class Dir { + test = model(1); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + expect(result).toBe(null); + }); + + it('should not identify an invalid call on a core function', () => { + const {member, reflector, importTracker} = setup(` + import {Directive, model} from '@angular/core'; + + @Directive() + export class Dir { + test = model.unknown(); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + expect(result).toBe(null); + }); + + it('should not identify an invalid call on a core function through a namespace import', () => { + const {member, reflector, importTracker} = setup(` + import {Directive} from '@angular/core'; + import * as core from '@angular/core'; + + @Directive() + export class Dir { + test = core.model.unknown(); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + expect(result).toBe(null); + }); + + it('should identify shadowed declarations', () => { + const {member, reflector, importTracker} = setup(` + import {Directive, model} from '@angular/core'; + + function wrapper() { + function model(value: number): any {} + + @Directive() + class Dir { + test = model(1); + } + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + expect(result).toBe(null); + }); + }); + + it('should identify an initializer function in a file containing an import whose name overlaps with an object prototype member', + () => { + const {member, reflector, importTracker} = setup(` + import {Directive, model} from '@angular/core'; + import {toString} from '@unknown/utils'; + + @Directive() + export class Dir { + test = model(1); + } + `); + + const result = tryParseInitializerApiMember(['model'], member, reflector, importTracker); + + expect(result).toEqual({ + apiName: 'model', + isRequired: false, + call: jasmine.objectContaining({kind: ts.SyntaxKind.CallExpression}), + }); + }); +}); + + +function setup(contents: string) { + const fileName = absoluteFrom('/test.ts'); + const {program} = makeProgram([ + { + name: absoluteFrom('/node_modules/@angular/core/index.d.ts'), + contents: ` + export const Directive: any; + + export interface InitializerFunction { + (initialValue: any): any; + required(): any; + unknown(): any; + } + + export const input: InitializerFunction; + export const model: InitializerFunction; + `, + }, + { + name: absoluteFrom('/node_modules/@unknown/utils/index.d.ts'), + contents: ` + export declare function toString(value: any): string; + `, + }, + { + name: absoluteFrom('/node_modules/@not-angular/core/index.d.ts'), + contents: ` + export interface InitializerFunction { + (initialValue: any): any; + required(): any; + } + export const model: InitializerFunction; + `, + }, + {name: fileName, contents} + ]); + const sourceFile = program.getSourceFile(fileName); + const importTracker = new ImportedSymbolsTracker(); + const reflector = new TypeScriptReflectionHost(program.getTypeChecker()); + + if (sourceFile === undefined) { + throw new Error(`Cannot resolve test file ${fileName}`); + } + + let member: Pick|null = null; + + (function walk(node: ts.Node) { + if (ts.isPropertyDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === 'test') { + member = {value: node.initializer ?? null}; + } else { + ts.forEachChild(node, walk); + } + })(sourceFile); + + if (member === null) { + throw new Error(`Could not resolve a class property with a name of "test" in the test file`); + } + + return {member, reflector, importTracker}; +} diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index 623e8b6fe6668..f222d9b9fa901 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -15,7 +15,7 @@ import {COMPILER_ERRORS_WITH_GUIDES, ERROR_DETAILS_PAGE_BASE_URL, ErrorCode, isF import {DocEntry, DocsExtractor} from '../../docs'; import {checkForPrivateExports, ReferenceGraph} from '../../entry_point'; import {absoluteFromSourceFile, AbsoluteFsPath, LogicalFileSystem, resolve} from '../../file_system'; -import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, DeferredSymbolTracker, ImportRewriter, LocalCompilationExtraImportsTracker, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports'; +import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, DeferredSymbolTracker, ImportedSymbolsTracker, ImportRewriter, LocalCompilationExtraImportsTracker, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports'; import {IncrementalBuildStrategy, IncrementalCompilation, IncrementalState} from '../../incremental'; import {SemanticSymbol} from '../../incremental/semantic_graph'; import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer'; @@ -1101,6 +1101,7 @@ export class NgCompiler { const injectableRegistry = new InjectableClassRegistry(reflector, isCore); const hostDirectivesResolver = new HostDirectivesResolver(metaReader); const exportedProviderStatusResolver = new ExportedProviderStatusResolver(metaReader); + const importTracker = new ImportedSymbolsTracker(); const typeCheckScopeRegistry = new TypeCheckScopeRegistry(scopeReader, metaReader, hostDirectivesResolver); @@ -1174,7 +1175,7 @@ export class NgCompiler { this.cycleAnalyzer, cycleHandlingStrategy, refEmitter, referencesRegistry, this.incrementalCompilation.depGraph, injectableRegistry, semanticDepGraphUpdater, this.closureCompilerEnabled, this.delegatingPerfRecorder, hostDirectivesResolver, - supportTestBed, compilationMode, deferredSymbolsTracker, + importTracker, supportTestBed, compilationMode, deferredSymbolsTracker, !!this.options.forbidOrphanComponents, this.enableBlockSyntax, this.options.useTemplatePipeline ?? SHOULD_USE_TEMPLATE_PIPELINE, localCompilationExtraImportsTracker), @@ -1187,6 +1188,7 @@ export class NgCompiler { injectableRegistry, refEmitter, referencesRegistry, isCore, strictCtorDeps, semanticDepGraphUpdater, this.closureCompilerEnabled, this.delegatingPerfRecorder, + importTracker, supportTestBed, compilationMode, this.options.useTemplatePipeline ?? SHOULD_USE_TEMPLATE_PIPELINE, !!this.options.generateExtraImportsInLocalMode, diff --git a/packages/compiler-cli/src/ngtsc/imports/index.ts b/packages/compiler-cli/src/ngtsc/imports/index.ts index 5d0d1333421e2..cd1fdc93324ce 100644 --- a/packages/compiler-cli/src/ngtsc/imports/index.ts +++ b/packages/compiler-cli/src/ngtsc/imports/index.ts @@ -11,6 +11,7 @@ export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAnd export {DefaultImportTracker} from './src/default'; export {DeferredSymbolTracker} from './src/deferred_symbol_tracker'; export {AbsoluteModuleStrategy, assertSuccessfulReferenceEmit, EmittedReference, FailedEmitResult, ImportedFile, ImportFlags, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitKind, ReferenceEmitResult, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesStrategy} from './src/emitter'; +export {ImportedSymbolsTracker} from './src/imported_symbols_tracker'; export {LocalCompilationExtraImportsTracker} from './src/local_compilation_extra_imports_tracker'; export {isAliasImportDeclaration, loadIsReferencedAliasDeclarationPatch} from './src/patch_alias_reference_resolution'; export {Reexport} from './src/reexport'; diff --git a/packages/compiler-cli/src/ngtsc/imports/src/imported_symbols_tracker.ts b/packages/compiler-cli/src/ngtsc/imports/src/imported_symbols_tracker.ts new file mode 100644 index 0000000000000..76a91856cd093 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/imports/src/imported_symbols_tracker.ts @@ -0,0 +1,109 @@ +/*! + * @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 ts from 'typescript'; + +/** + * A map of imported symbols to local names under which the symbols are available within a file. + */ +type LocalNamesMap = Map>; + +/** Mapping between modules and the named imports consumed by them in a file. */ +type NamedImportsMap = Map; + +/** + * Tracks which symbols are imported in specific files and under what names. Allows for efficient + * querying for references to those symbols without having to consult the type checker early in the + * process. + * + * Note that the tracker doesn't account for variable shadowing so a final verification with the + * type checker may be necessary, depending on the context. Also does not track dynamic imports. + */ +export class ImportedSymbolsTracker { + private fileToNamedImports = new WeakMap(); + private fileToNamespaceImports = new WeakMap(); + + /** + * Checks if an identifier is a potential reference to a specific named import within the same + * file. + * @param node Identifier to be checked. + * @param exportedName Name of the exported symbol that is being searched for. + * @param moduleName Module from which the symbol should be imported. + */ + isPotentialReferenceToNamedImport(node: ts.Identifier, exportedName: string, moduleName: string): + boolean { + const sourceFile = node.getSourceFile(); + this.scanImports(sourceFile); + const fileImports = this.fileToNamedImports.get(sourceFile)!; + const moduleImports = fileImports.get(moduleName); + const symbolImports = moduleImports?.get(exportedName); + return symbolImports !== undefined && symbolImports.has(node.text); + } + + /** + * Checks if an identifier is a potential reference to a specific namespace import within the same + * file. + * @param node Identifier to be checked. + * @param moduleName Module from which the namespace is imported. + */ + isPotentialReferenceToNamespaceImport(node: ts.Identifier, moduleName: string): boolean { + const sourceFile = node.getSourceFile(); + this.scanImports(sourceFile); + const namespaces = this.fileToNamespaceImports.get(sourceFile)!; + return namespaces.get(moduleName)?.has(node.text) ?? false; + } + + /** Scans a `SourceFile` for import statements and caches them for later use. */ + private scanImports(sourceFile: ts.SourceFile): void { + if (this.fileToNamedImports.has(sourceFile) && this.fileToNamespaceImports.has(sourceFile)) { + return; + } + + const namedImports: NamedImportsMap = new Map(); + const namespaceImports: LocalNamesMap = new Map(); + this.fileToNamedImports.set(sourceFile, namedImports); + this.fileToNamespaceImports.set(sourceFile, namespaceImports); + + // Only check top-level imports. + for (const stmt of sourceFile.statements) { + if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteralLike(stmt.moduleSpecifier) || + stmt.importClause?.namedBindings === undefined) { + continue; + } + + const moduleName = stmt.moduleSpecifier.text; + + if (ts.isNamespaceImport(stmt.importClause.namedBindings)) { + // import * as foo from 'module' + if (!namespaceImports.has(moduleName)) { + namespaceImports.set(moduleName, new Set()); + } + namespaceImports.get(moduleName)!.add(stmt.importClause.namedBindings.name.text); + } else { + // import {foo, bar as alias} from 'module' + for (const element of stmt.importClause.namedBindings.elements) { + const localName = element.name.text; + const exportedName = + element.propertyName === undefined ? localName : element.propertyName.text; + + if (!namedImports.has(moduleName)) { + namedImports.set(moduleName, new Map()); + } + + const localNames = namedImports.get(moduleName)!; + + if (!localNames.has(exportedName)) { + localNames.set(exportedName, new Set()); + } + + localNames.get(exportedName)?.add(localName); + } + } + } + } +} diff --git a/packages/compiler-cli/src/transformers/jit_transforms/index.ts b/packages/compiler-cli/src/transformers/jit_transforms/index.ts index f7f128d0fbd2f..b03c8d7490e2a 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/index.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/index.ts @@ -8,7 +8,7 @@ import ts from 'typescript'; -import {PartialEvaluator} from '../../ngtsc/partial_evaluator'; +import {ImportedSymbolsTracker} from '../../ngtsc/imports'; import {TypeScriptReflectionHost} from '../../ngtsc/reflection'; import {getDownlevelDecoratorsTransform} from './downlevel_decorators_transform'; @@ -39,13 +39,14 @@ export function angularJitApplicationTransform( program: ts.Program, isCore = false): ts.TransformerFactory { const typeChecker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(typeChecker); - const evaluator = new PartialEvaluator(reflectionHost, typeChecker, null); + const importTracker = new ImportedSymbolsTracker(); const downlevelDecoratorTransform = getDownlevelDecoratorsTransform( typeChecker, reflectionHost, [], isCore, /* enableClosureCompiler */ false); - const initializerApisJitTransform = getInitializerApiJitTransform(reflectionHost, isCore); + const initializerApisJitTransform = + getInitializerApiJitTransform(reflectionHost, importTracker, isCore); return (ctx) => { return (sourceFile) => { diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/input_function.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/input_function.ts index 0e68663f28bb5..0d258d443d932 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/input_function.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/input_function.ts @@ -28,6 +28,7 @@ export const signalInputsTransform: PropertyTransform = ( member, host, factory, + importTracker, importManager, classDecorator, isCore, @@ -38,10 +39,7 @@ export const signalInputsTransform: PropertyTransform = ( } const inputMapping = tryParseSignalInputMapping( - {name: member.name.text, value: member.initializer ?? null}, - host, - isCore, - ); + {name: member.name.text, value: member.initializer ?? null}, host, importTracker); if (inputMapping === null) { return member; } diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/model_function.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/model_function.ts index 02ab8d29531e9..ad7c197bd1511 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/model_function.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/model_function.ts @@ -21,6 +21,7 @@ export const signalModelTransform: PropertyTransform = ( member, host, factory, + importTracker, importManager, decorator, isCore, @@ -34,7 +35,7 @@ export const signalModelTransform: PropertyTransform = ( const modelMapping = tryParseSignalModelMapping( {name: member.name.text, value: member.initializer ?? null}, host, - isCore, + importTracker, ); if (modelMapping === null) { diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/output_function.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/output_function.ts index 568df8ea3ccb6..5453e8ec4adf6 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/output_function.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/output_function.ts @@ -24,6 +24,7 @@ export const initializerApiOutputTransform: PropertyTransform = ( member, host, factory, + importTracker, importManager, classDecorator, isCore, @@ -36,7 +37,7 @@ export const initializerApiOutputTransform: PropertyTransform = ( const output = tryParseInitializerBasedOutput( {name: member.name.text, value: member.initializer ?? null}, host, - isCore, + importTracker, ); if (output === null) { return member; diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/query_functions.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/query_functions.ts index 94d9467f1b40c..605679d01cfd3 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/query_functions.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/query_functions.ts @@ -33,6 +33,7 @@ export const queryFunctionsTransforms: PropertyTransform = ( member, host, factory, + importTracker, importManager, classDecorator, isCore, @@ -49,7 +50,7 @@ export const queryFunctionsTransforms: PropertyTransform = ( const queryDefinition = tryParseSignalQueryFromInitializer( {name: member.name.text, value: member.initializer ?? null}, host, - isCore, + importTracker, ); if (queryDefinition === null) { return member; diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform.ts index 80358a2ab4c6f..cf2a30eb10bbb 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform.ts @@ -9,6 +9,7 @@ import ts from 'typescript'; import {isAngularDecorator} from '../../../ngtsc/annotations'; +import {ImportedSymbolsTracker} from '../../../ngtsc/imports'; import {ReflectionHost} from '../../../ngtsc/reflection'; import {addImports} from '../../../ngtsc/transform'; import {ImportManager} from '../../../ngtsc/translator'; @@ -42,6 +43,7 @@ const propertyTransforms: PropertyTransform[] = [ */ export function getInitializerApiJitTransform( host: ReflectionHost, + importTracker: ImportedSymbolsTracker, isCore: boolean, ): ts.TransformerFactory { return ctx => { @@ -50,7 +52,7 @@ export function getInitializerApiJitTransform( sourceFile = ts.visitNode( sourceFile, - createTransformVisitor(ctx, host, importManager, isCore), + createTransformVisitor(ctx, host, importManager, importTracker, isCore), ts.isSourceFile, ); @@ -68,6 +70,7 @@ function createTransformVisitor( ctx: ts.TransformationContext, host: ReflectionHost, importManager: ImportManager, + importTracker: ImportedSymbolsTracker, isCore: boolean, ): ts.Visitor { const visitor: ts.Visitor = (node: ts.Node): ts.Node => { @@ -90,7 +93,7 @@ function createTransformVisitor( for (const transform of propertyTransforms) { const newNode = transform( member as ts.PropertyDeclaration & {name: ts.Identifier | ts.StringLiteralLike}, - host, ctx.factory, importManager, angularDecorator, isCore); + host, ctx.factory, importTracker, importManager, angularDecorator, isCore); if (newNode !== member) { hasChanged = true; diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform_api.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform_api.ts index 9b01f2a9f1c10..bad537e6acbfd 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform_api.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform_api.ts @@ -8,14 +8,16 @@ import ts from 'typescript'; -import {Decorator, ReflectionHost} from '../../..//ngtsc/reflection'; +import {ImportedSymbolsTracker} from '../../../ngtsc/imports'; +import {Decorator, ReflectionHost} from '../../../ngtsc/reflection'; import {ImportManager} from '../../../ngtsc/translator'; /** Function that can be used to transform class properties. */ export type PropertyTransform = (node: ts.PropertyDeclaration&{name: ts.Identifier | ts.StringLiteralLike}, - host: ReflectionHost, factory: ts.NodeFactory, importManager: ImportManager, - classDecorator: Decorator, isCore: boolean) => ts.PropertyDeclaration; + host: ReflectionHost, factory: ts.NodeFactory, importTracker: ImportedSymbolsTracker, + importManager: ImportManager, classDecorator: Decorator, isCore: boolean) => + ts.PropertyDeclaration; /** * Creates an import and access for a given Angular core import while diff --git a/packages/compiler-cli/test/BUILD.bazel b/packages/compiler-cli/test/BUILD.bazel index d604db8d17a32..cd0e19b318eb0 100644 --- a/packages/compiler-cli/test/BUILD.bazel +++ b/packages/compiler-cli/test/BUILD.bazel @@ -62,6 +62,7 @@ ts_library( ], deps = [ ":test_utils", + "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/transformers/jit_transforms", diff --git a/packages/compiler-cli/test/initializer_api_transforms_spec.ts b/packages/compiler-cli/test/initializer_api_transforms_spec.ts index d161ed0af2a0a..acfde0f1fc9f0 100644 --- a/packages/compiler-cli/test/initializer_api_transforms_spec.ts +++ b/packages/compiler-cli/test/initializer_api_transforms_spec.ts @@ -8,6 +8,7 @@ import ts from 'typescript'; +import {ImportedSymbolsTracker} from '../src/ngtsc/imports'; import {TypeScriptReflectionHost} from '../src/ngtsc/reflection'; import {getDownlevelDecoratorsTransform, getInitializerApiJitTransform} from '../src/transformers/jit_transforms'; @@ -51,9 +52,10 @@ describe('initializer API metadata transform', () => { const testFile = program.getSourceFile(TEST_FILE_INPUT); const typeChecker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(typeChecker); + const importTracker = new ImportedSymbolsTracker(); const transformers: ts.CustomTransformers = { before: [ - getInitializerApiJitTransform(reflectionHost, /* isCore */ false), + getInitializerApiJitTransform(reflectionHost, importTracker, /* isCore */ false), ] }; diff --git a/packages/compiler-cli/test/signal_queries_metadata_transform_spec.ts b/packages/compiler-cli/test/signal_queries_metadata_transform_spec.ts index 693d36009b5db..e5ccc24cd7d16 100644 --- a/packages/compiler-cli/test/signal_queries_metadata_transform_spec.ts +++ b/packages/compiler-cli/test/signal_queries_metadata_transform_spec.ts @@ -8,6 +8,7 @@ import ts from 'typescript'; +import {ImportedSymbolsTracker} from '../src/ngtsc/imports'; import {TypeScriptReflectionHost} from '../src/ngtsc/reflection'; import {getDownlevelDecoratorsTransform, getInitializerApiJitTransform} from '../src/transformers/jit_transforms'; @@ -58,9 +59,10 @@ describe('signal queries metadata transform', () => { const testFile = program.getSourceFile(TEST_FILE_INPUT); const typeChecker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(typeChecker); + const importTracker = new ImportedSymbolsTracker(); const transformers: ts.CustomTransformers = { before: [ - getInitializerApiJitTransform(reflectionHost, /* isCore */ false), + getInitializerApiJitTransform(reflectionHost, importTracker, /* isCore */ false), ] }; diff --git a/packages/core/test/acceptance/authoring/BUILD.bazel b/packages/core/test/acceptance/authoring/BUILD.bazel index c4195cf0ac1f5..9d2adf9ffa156 100644 --- a/packages/core/test/acceptance/authoring/BUILD.bazel +++ b/packages/core/test/acceptance/authoring/BUILD.bazel @@ -19,6 +19,7 @@ ts_library( srcs = ["authoring_test_compiler.ts"], deps = [ "//packages/compiler-cli", + "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/transformers/jit_transforms", diff --git a/packages/core/test/acceptance/authoring/authoring_test_compiler.ts b/packages/core/test/acceptance/authoring/authoring_test_compiler.ts index 1726742377321..45dd8510261c2 100644 --- a/packages/core/test/acceptance/authoring/authoring_test_compiler.ts +++ b/packages/core/test/acceptance/authoring/authoring_test_compiler.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ImportedSymbolsTracker} from '@angular/compiler-cli/src/ngtsc/imports'; import {TypeScriptReflectionHost} from '@angular/compiler-cli/src/ngtsc/reflection'; import {getInitializerApiJitTransform} from '@angular/compiler-cli/src/transformers/jit_transforms'; import fs from 'fs'; @@ -28,11 +29,13 @@ async function main() { }); const host = new TypeScriptReflectionHost(program.getTypeChecker()); + const importTracker = new ImportedSymbolsTracker(); for (const inputFileExecpath of inputFileExecpaths) { const outputFile = ts.transform( program.getSourceFile(inputFileExecpath)!, - [getInitializerApiJitTransform(host, /* isCore */ false)], program.getCompilerOptions()); + [getInitializerApiJitTransform(host, importTracker, /* isCore */ false)], + program.getCompilerOptions()); await fs.promises.writeFile( path.join(outputDirExecPath, `transformed_${path.basename(inputFileExecpath)}`),