From 48aec63ee48fe9d20c1f1565b044ec359100736d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 16 Feb 2024 13:33:06 +0100 Subject: [PATCH] fix(compiler-cli): identify aliased initializer functions (#54480) 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 #54480 --- .../annotations/component/src/handler.ts | 9 +- .../component/test/component_spec.ts | 4 +- .../annotations/directive/src/handler.ts | 7 +- .../directive/src/initializer_functions.ts | 162 ++++++----- .../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 | 267 ++++++++++++++++++ .../src/ngtsc/core/src/compiler.ts | 6 +- .../compiler-cli/src/ngtsc/imports/index.ts | 1 + .../imports/src/imported_symbols_tracker.ts | 104 +++++++ .../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, 562 insertions(+), 131 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 18c0be10357..f4e9536c237 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 13c8d0f38ab..e9eee3850e9 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 4402f2f6396..57f3858d0d0 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 044c5faaaeb..34efb1da26a 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)) { - 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; - } - - // Find if the `target` matches one of the expected APIs are are looking for. - apiName = fnNames.find(n => n === target!.text); - - // Ensure the call refers to the real API function from Angular core. - if (apiName === undefined || - !isReferenceToInitializerApiFunction(apiName, target, isCore, reflector)) { + // 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; } return { - apiName, call, - isRequired: true, + isRequired: staticResult.isRequired, + apiName: resolvedImport.name as InitializerApiFunction, }; } /** - * 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 an initializer function, + * e.g. `prop = input()`. 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 parseTopLevelCall( + call: ts.CallExpression, fnNames: InitializerApiFunction[], + importTracker: ImportedSymbolsTracker): StaticInitializerData|null { + const node = call.expression; + + if (!ts.isIdentifier(node)) { + return null; } - return null; + + return fnNames.some( + name => importTracker.isPotentialReferenceToNamedImport(node, name, CORE_MODULE)) ? + {node, isRequired: false} : + 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 required initializer, + * e.g. `prop = 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 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 targetImport.name === functionName && targetImport.from === CORE_MODULE; + const expression = node.expression; + const matchesCoreApi = fnNames.some( + name => importTracker.isPotentialReferenceToNamedImport(expression, name, CORE_MODULE)); + + return matchesCoreApi ? {node: expression, isRequired: true} : null; +} + + +/** + * 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 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 {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 c6c682732dc..a8a4452c2ed 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 b0f9376e026..2b4e332e8f5 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 d0163569a5c..d4593f13978 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 e51f78a6fd0..b855f4c4a67 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 b485366684f..ec80e4d87bf 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( @@ -750,13 +751,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( @@ -838,8 +839,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; @@ -850,6 +852,7 @@ function parseInputFields( member, evaluator, reflector, + importTracker, isCore, refEmitter, compilationMode, @@ -1028,8 +1031,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[], } { @@ -1046,7 +1049,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( @@ -1106,14 +1109,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 fe7b3ad6d2e..437fe1136e3 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 00000000000..add4b401d92 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/test/initializer_functions_spec.ts @@ -0,0 +1,267 @@ +/*! + * @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); + }); + }); +}); + + +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/@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 3326e9559ed..31d23a8c68c 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 5d0d1333421..cd1fdc93324 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 00000000000..9d830e6eb12 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/imports/src/imported_symbols_tracker.ts @@ -0,0 +1,104 @@ +/*! + * @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'; + +/** Mapping between modules and the named imports consumed by them in a file. */ +type NamedImportsMap = { + [moduleName: string]: { + // The key is the symbol's original name, while the set + // includes all the local names it is vailable under. + [exportedName: string]: Set, + } +}; + +type NamespaceImportsMap = { + // The key is a set of names the namespace is available under within a file. + [moduleName: string]: Set +}; + +/** + * 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[moduleName] ?? null; + const symbolImports = moduleImports?.[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[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 = {}; + const namespaceImports: NamespaceImportsMap = {}; + 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' + namespaceImports[moduleName] ??= new Set(); + namespaceImports[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; + namedImports[moduleName] ??= {}; + namedImports[moduleName][exportedName] ??= new Set(); + namedImports[moduleName][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 f7f128d0fbd..b03c8d7490e 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 0e68663f28b..0d258d443d9 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 02ab8d29531..ad7c197bd15 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 568df8ea3cc..5453e8ec4ad 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 94d9467f1b4..605679d01cf 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 80358a2ab4c..cf2a30eb10b 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 9b01f2a9f1c..bad537e6acb 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 d604db8d17a..cd0e19b318e 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 d161ed0af2a..acfde0f1fc9 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 693d36009b5..e5ccc24cd7d 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 c4195cf0ac1..9d2adf9ffa1 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 17267423773..45dd8510261 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)}`),