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)}`),