Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Structural directive autocompletion #40032

Closed
wants to merge 8 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {absoluteFrom, absoluteFromSourceFile, dirname, FileSystem, LogicalFileSy
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../../src/ngtsc/scope';
import {DecoratorHandler} from '../../../src/ngtsc/transform';
import {NgccReflectionHost} from '../host/ngcc_host';
import {Migration} from '../migrations/migration';
Expand Down Expand Up @@ -91,11 +91,13 @@ export class DecorationAnalyzer {
importGraph = new ImportGraph(this.moduleResolver);
cycleAnalyzer = new CycleAnalyzer(this.importGraph);
injectableRegistry = new InjectableClassRegistry(this.reflectionHost);
typeCheckScopeRegistry = new TypeCheckScopeRegistry(this.scopeRegistry, this.fullMetaReader);
handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
new ComponentDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader,
this.scopeRegistry, this.scopeRegistry, new ResourceRegistry(), this.isCore,
this.resourceManager, this.rootDirs, !!this.compilerOptions.preserveWhitespaces,
this.scopeRegistry, this.scopeRegistry, this.typeCheckScopeRegistry, new ResourceRegistry(),
this.isCore, this.resourceManager, this.rootDirs,
!!this.compilerOptions.preserveWhitespaces,
/* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat,
/* usePoisonedData */ false,
/* i18nNormalizeLineEndingsInICUs */ false, this.moduleResolver, this.cycleAnalyzer,
Expand Down
35 changes: 17 additions & 18 deletions packages/compiler-cli/src/ngtsc/annotations/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {IndexingContext} from '../../indexer';
import {ClassPropertyMapping, ComponentResources, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, Resource, ResourceRegistry} from '../../metadata';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope';
import {ComponentScopeReader, LocalModuleScopeRegistry, TypeCheckScopeRegistry} from '../../scope';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform';
import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck/api';
import {getTemplateId, makeTemplateDiagnostic} from '../../typecheck/diagnostics';
Expand All @@ -30,7 +30,6 @@ import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagno
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {TypeCheckScopes} from './typecheck_scopes';
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';

const EMPTY_MAP = new Map<string, Expression>();
Expand Down Expand Up @@ -87,6 +86,7 @@ export class ComponentDecoratorHandler implements
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private metaReader: MetadataReader,
private scopeReader: ComponentScopeReader, private scopeRegistry: LocalModuleScopeRegistry,
private typeCheckScopeRegistry: TypeCheckScopeRegistry,
private resourceRegistry: ResourceRegistry, private isCore: boolean,
private resourceLoader: ResourceLoader, private rootDirs: ReadonlyArray<string>,
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean,
Expand All @@ -100,7 +100,6 @@ export class ComponentDecoratorHandler implements

private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
private elementSchemaRegistry = new DomElementSchemaRegistry();
private typeCheckScopes = new TypeCheckScopes(this.scopeReader, this.metaReader);

/**
* During the asynchronous preanalyze phase, it's necessary to parse the template to extract
Expand Down Expand Up @@ -397,6 +396,7 @@ export class ComponentDecoratorHandler implements
baseClass: analysis.baseClass,
...analysis.typeCheckMeta,
isPoisoned: analysis.isPoisoned,
isStructural: false,
});

this.resourceRegistry.registerResources(analysis.resources, node);
Expand Down Expand Up @@ -440,15 +440,14 @@ export class ComponentDecoratorHandler implements

typeCheck(ctx: TypeCheckContext, node: ClassDeclaration, meta: Readonly<ComponentAnalysisData>):
void {
if (!ts.isClassDeclaration(node)) {
if (this.typeCheckScopeRegistry === null || !ts.isClassDeclaration(node)) {
return;
}

if (meta.isPoisoned && !this.usePoisonedData) {
return;
}

const scope = this.typeCheckScopes.getTypeCheckScope(node);
const scope = this.typeCheckScopeRegistry.getTypeCheckScope(node);
if (scope.isPoisoned && !this.usePoisonedData) {
// Don't type-check components that had errors in their scopes, unless requested.
return;
Expand Down Expand Up @@ -492,17 +491,17 @@ export class ComponentDecoratorHandler implements
// Determining this is challenging, because the TemplateDefinitionBuilder is responsible for
// matching directives and pipes in the template; however, that doesn't run until the actual
// compile() step. It's not possible to run template compilation sooner as it requires the
// ConstantPool for the overall file being compiled (which isn't available until the transform
// step).
// ConstantPool for the overall file being compiled (which isn't available until the
// transform step).
//
// Instead, directives/pipes are matched independently here, using the R3TargetBinder. This is
// an alternative implementation of template matching which is used for template type-checking
// and will eventually replace matching in the TemplateDefinitionBuilder.
// Instead, directives/pipes are matched independently here, using the R3TargetBinder. This
// is an alternative implementation of template matching which is used for template
// type-checking and will eventually replace matching in the TemplateDefinitionBuilder.


// Set up the R3TargetBinder, as well as a 'directives' array and a 'pipes' map that are later
// fed to the TemplateDefinitionBuilder. First, a SelectorMatcher is constructed to match
// directives that are in scope.
// Set up the R3TargetBinder, as well as a 'directives' array and a 'pipes' map that are
// later fed to the TemplateDefinitionBuilder. First, a SelectorMatcher is constructed to
// match directives that are in scope.
type MatchedDirective = DirectiveMeta&{selector: string};
const matcher = new SelectorMatcher<MatchedDirective>();

Expand Down Expand Up @@ -563,8 +562,8 @@ export class ComponentDecoratorHandler implements
}

// Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures.
// This is required if any directive/pipe reference is to a declaration in the same file but
// declared after this component.
// This is required if any directive/pipe reference is to a declaration in the same file
// but declared after this component.
const wrapDirectivesAndPipesInClosure =
usedDirectives.some(
dir => isExpressionForwardReference(dir.type, node.name, context)) ||
Expand Down Expand Up @@ -890,8 +889,8 @@ export class ComponentDecoratorHandler implements
// 2. By default, the template parser strips leading trivia characters (like spaces, tabs, and
// newlines). This also destroys source mapping information.
//
// In order to guarantee the correctness of diagnostics, templates are parsed a second time with
// the above options set to preserve source mappings.
// In order to guarantee the correctness of diagnostics, templates are parsed a second time
// with the above options set to preserve source mappings.

const {nodes: diagNodes} = parseTemplate(templateStr, templateUrl, {
preserveWhitespaces: true,
Expand Down
18 changes: 17 additions & 1 deletion packages/compiler-cli/src/ngtsc/annotations/src/directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, Expression, Identifiers, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3DependencyMetadata, R3DirectiveDef, R3DirectiveMetadata, R3FactoryTarget, R3QueryMetadata, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler';
import {compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, Expression, ExternalExpr, Identifiers, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3DependencyMetadata, R3DirectiveDef, R3DirectiveMetadata, R3FactoryTarget, R3QueryMetadata, R3ResolvedDependencyType, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';

import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
Expand Down Expand Up @@ -42,6 +42,7 @@ export interface DirectiveHandlerData {
inputs: ClassPropertyMapping;
outputs: ClassPropertyMapping;
isPoisoned: boolean;
isStructural: boolean;
}

export class DirectiveDecoratorHandler implements
Expand Down Expand Up @@ -109,6 +110,7 @@ export class DirectiveDecoratorHandler implements
typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector),
providersRequiringFactory,
isPoisoned: false,
isStructural: directiveResult.isStructural,
}
};
}
Expand All @@ -129,6 +131,7 @@ export class DirectiveDecoratorHandler implements
baseClass: analysis.baseClass,
...analysis.typeCheckMeta,
isPoisoned: analysis.isPoisoned,
isStructural: analysis.isStructural,
});

this.injectableRegistry.registerInjectable(node);
Expand Down Expand Up @@ -226,6 +229,7 @@ export function extractDirectiveMetadata(
metadata: R3DirectiveMetadata,
inputs: ClassPropertyMapping,
outputs: ClassPropertyMapping,
isStructural: boolean;
}|undefined {
let directive: Map<string, ts.Expression>;
if (decorator === null || decorator.args === null || decorator.args.length === 0) {
Expand Down Expand Up @@ -352,6 +356,17 @@ export function extractDirectiveMetadata(
ctorDeps = unwrapConstructorDependencies(rawCtorDeps);
}

const isStructural = ctorDeps !== null && ctorDeps !== 'invalid' && ctorDeps.some(dep => {
if (dep.resolved !== R3ResolvedDependencyType.Token || !(dep.token instanceof ExternalExpr)) {
return false;
}
if (dep.token.value.moduleName !== '@angular/core' || dep.token.value.name !== 'TemplateRef') {
return false;
}

return true;
});

// Detect if the component inherits from another class
const usesInheritance = reflector.hasBaseClass(clazz);
const type = wrapTypeReference(reflector, clazz);
Expand Down Expand Up @@ -386,6 +401,7 @@ export function extractDirectiveMetadata(
metadata,
inputs,
outputs,
isStructural,
};
}

Expand Down
33 changes: 26 additions & 7 deletions packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';

import {CycleAnalyzer, ImportGraph} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {absoluteFrom} from '../../file_system';
Expand All @@ -13,7 +16,7 @@ import {ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '..
import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../scope';
import {getDeclaration, makeProgram} from '../../testing';
import {ResourceLoader} from '../src/api';
import {ComponentDecoratorHandler} from '../src/component';
Expand Down Expand Up @@ -48,17 +51,33 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil
const refEmitter = new ReferenceEmitter([]);
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
const resourceRegistry = new ResourceRegistry();
const typeCheckScopeRegistry = new TypeCheckScopeRegistry(scopeRegistry, metaReader);

const handler = new ComponentDecoratorHandler(
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry,
reflectionHost,
evaluator,
metaRegistry,
metaReader,
scopeRegistry,
scopeRegistry,
typeCheckScopeRegistry,
resourceRegistry,
/* isCore */ false, new StubResourceLoader(), /* rootDirs */['/'],
/* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
/* isCore */ false,
new StubResourceLoader(),
/* rootDirs */['/'],
/* defaultPreserveWhitespaces */ false,
/* i18nUseExternalIds */ true,
/* enableI18nLegacyMessageIdFormat */ false,
/* usePoisonedData */ false,
/* i18nNormalizeLineEndingsInICUs */ undefined, moduleResolver, cycleAnalyzer, refEmitter,
NOOP_DEFAULT_IMPORT_RECORDER, /* depTracker */ null, injectableRegistry,
/* annotateForClosureCompiler */ false);
/* i18nNormalizeLineEndingsInICUs */ undefined,
moduleResolver,
cycleAnalyzer,
refEmitter,
NOOP_DEFAULT_IMPORT_RECORDER,
/* depTracker */ null,
injectableRegistry,
/* annotateForClosureCompiler */ false,
);
return {reflectionHost, handler};
}

Expand Down
25 changes: 25 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ runInEachFileSystem(() => {
isComponent: false,
name: 'Dir',
selector: '[dir]',
isStructural: false,
};
matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta);

Expand All @@ -118,6 +119,30 @@ runInEachFileSystem(() => {
// and field names.
expect(propBindingConsumer).toBe(dirMeta);
});

it('should identify a structural directive', () => {
const src = `
import {Directive, TemplateRef} from '@angular/core';

@Directive({selector: 'test-dir'})
export class TestDir {
constructor(private ref: TemplateRef) {}
}
`;
const {program} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Directive: any; export declare class TemplateRef {}',
},
{
name: _('/entry.ts'),
contents: src,
},
]);

const analysis = analyzeDirective(program, 'TestDir');
expect(analysis.isStructural).toBeTrue();
});
});

// Helpers
Expand Down
12 changes: 8 additions & 4 deletions packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {NOOP_PERF_RECORDER, PerfRecorder} from '../../perf';
import {DeclarationNode, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {AdapterResourceLoader} from '../../resource';
import {entryPointKeyFor, NgModuleRouteAnalyzer} from '../../routing';
import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../scope';
import {generatedFactoryTransform} from '../../shims';
import {ivySwitchTransform} from '../../switch';
import {aliasTransformFactory, CompilationMode, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform';
Expand All @@ -44,6 +44,7 @@ interface LazyCompilationState {
reflector: TypeScriptReflectionHost;
metaReader: MetadataReader;
scopeRegistry: LocalModuleScopeRegistry;
typeCheckScopeRegistry: TypeCheckScopeRegistry;
exportReferenceGraph: ReferenceGraph|null;
routeAnalyzer: NgModuleRouteAnalyzer;
dtsTransforms: DtsTransformRegistry;
Expand Down Expand Up @@ -732,6 +733,7 @@ export class NgCompiler {
const injectableRegistry = new InjectableClassRegistry(reflector);

const metaReader = new CompoundMetadataReader([localMetaReader, dtsReader]);
const typeCheckScopeRegistry = new TypeCheckScopeRegistry(scopeReader, metaReader);


// If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in
Expand Down Expand Up @@ -761,8 +763,9 @@ export class NgCompiler {
const handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
new ComponentDecoratorHandler(
reflector, evaluator, metaRegistry, metaReader, scopeReader, scopeRegistry,
resourceRegistry, isCore, this.resourceManager, this.adapter.rootDirs,
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false,
typeCheckScopeRegistry, resourceRegistry, isCore, this.resourceManager,
this.adapter.rootDirs, this.options.preserveWhitespaces || false,
this.options.i18nUseExternalIds !== false,
this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData,
this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer,
refEmitter, defaultImportTracker, this.incrementalDriver.depGraph, injectableRegistry,
Expand Down Expand Up @@ -802,7 +805,7 @@ export class NgCompiler {
const templateTypeChecker = new TemplateTypeCheckerImpl(
this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler,
this.getTypeCheckingConfig(), refEmitter, reflector, this.adapter, this.incrementalDriver,
scopeRegistry);
scopeRegistry, typeCheckScopeRegistry);

return {
isCore,
Expand All @@ -814,6 +817,7 @@ export class NgCompiler {
routeAnalyzer,
mwpScanner,
metaReader,
typeCheckScopeRegistry,
defaultImportTracker,
aliasingHost,
refEmitter,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/indexer/test/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function getBoundTemplate(
inputs: ClassPropertyMapping.fromMappedObject({}),
outputs: ClassPropertyMapping.fromMappedObject({}),
exportAs: null,
isStructural: false,
});
});
const binder = new R3TargetBinder(matcher);
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler-cli/src/ngtsc/metadata/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
* and reliable metadata.
*/
isPoisoned: boolean;

/**
* Whether the directive is likely a structural directive (injects `TemplateRef`).
*/
isStructural: boolean;
}

/**
Expand Down