From c0ab43f3c8fdf3815329f8333f6ee054da089e81 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Fri, 4 Dec 2020 14:43:26 -0800 Subject: [PATCH] refactor(compiler-cli): introduce APIs to support directive autocompletion (#40032) This commit adds two new APIs to the `TemplateTypeChecker`: `getPotentialDomBindings` and `getDirectiveMetadata`. Together, these will support the Language Service in performing autocompletion of directive inputs/outputs. PR Close #40032 --- .../src/ngtsc/core/src/compiler.ts | 2 +- .../src/ngtsc/typecheck/api/checker.ts | 21 ++++++++++++-- .../src/ngtsc/typecheck/src/checker.ts | 28 ++++++++++++++---- .../src/ngtsc/typecheck/test/test_utils.ts | 9 ++++-- ...ecker__get_symbol_of_template_node_spec.ts | 29 +++++++++++++++---- .../src/schema/dom_element_schema_registry.ts | 13 +++++++++ 6 files changed, 84 insertions(+), 18 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index a7ebb7c143..0e8b772a47 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -805,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, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts index f87d6662ca..69dcaebb4e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts @@ -6,14 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, MethodCall, ParseError, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstNode, TmplAstTemplate} from '@angular/compiler'; +import {AST, MethodCall, ParseError, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler'; import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; import * as ts from 'typescript'; -import {FullTemplateMapping} from './api'; +import {FullTemplateMapping, TypeCheckableDirectiveMeta} from './api'; import {GlobalCompletion} from './completion'; import {DirectiveInScope, PipeInScope} from './scope'; -import {ShimLocation, Symbol} from './symbols'; +import {DirectiveSymbol, ElementSymbol, ShimLocation, Symbol} from './symbols'; /** * Interface to the Angular Template Type Checker to extract diagnostics and intelligence from the @@ -110,6 +110,7 @@ export interface TemplateTypeChecker { * * @see Symbol */ + getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null; getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null; /** @@ -149,6 +150,20 @@ export interface TemplateTypeChecker { * the DOM schema. */ getPotentialElementTags(component: ts.ClassDeclaration): Map; + + /** + * Retrieve any potential DOM bindings for the given element. + * + * This returns an array of objects which list both the attribute and property names of each + * binding, which are usually identical but can vary if the HTML attribute name is for example a + * reserved keyword in JS, like the `for` attribute which corresponds to the `htmlFor` property. + */ + getPotentialDomBindings(tagName: string): {attribute: string, property: string}[]; + + /** + * Retrieve the type checking engine's metadata for the given directive class, if available. + */ + getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null; } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 22acdfa999..ba92ab7e07 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -6,17 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, CssSelector, DomElementSchemaRegistry, MethodCall, ParseError, parseTemplate, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler'; +import {AST, CssSelector, DomElementSchemaRegistry, MethodCall, ParseError, parseTemplate, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler'; import * as ts from 'typescript'; import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; -import {ReferenceEmitter} from '../../imports'; +import {Reference, ReferenceEmitter} from '../../imports'; import {IncrementalBuild} from '../../incremental/api'; import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../reflection'; -import {ComponentScopeReader} from '../../scope'; +import {ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope'; import {isShim} from '../../shims'; import {getSourceFileOrNull} from '../../util/src/typescript'; -import {DirectiveInScope, FullTemplateMapping, GlobalCompletion, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, ShimLocation, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api'; +import {DirectiveInScope, ElementSymbol, FullTemplateMapping, GlobalCompletion, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, ShimLocation, Symbol, TemplateId, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api'; import {TemplateDiagnostic} from '../diagnostics'; import {CompletionEngine} from './completion'; @@ -83,7 +83,8 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { private refEmitter: ReferenceEmitter, private reflector: ReflectionHost, private compilerHost: Pick, private priorBuild: IncrementalBuild, - private readonly componentScopeReader: ComponentScopeReader) {} + private readonly componentScopeReader: ComponentScopeReader, + private readonly typeCheckScopeRegistry: TypeCheckScopeRegistry) {} resetOverrides(): void { for (const fileRecord of this.state.values()) { @@ -471,7 +472,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } return this.state.get(path)!; } - + getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null; getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null { const builder = this.getOrCreateSymbolBuilder(component); if (builder === null) { @@ -513,6 +514,13 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return data.pipes; } + getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null { + if (!isNamedClassDeclaration(dir)) { + return null; + } + return this.typeCheckScopeRegistry.getTypeCheckDirectiveMetadata(new Reference(dir)); + } + getPotentialElementTags(component: ts.ClassDeclaration): Map { if (this.elementTagCache.has(component)) { return this.elementTagCache.get(component)!; @@ -543,6 +551,14 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return tagMap; } + getPotentialDomBindings(tagName: string): {attribute: string, property: string}[] { + const attributes = REGISTRY.allKnownAttributesOfElement(tagName); + return attributes.map(attribute => ({ + attribute, + property: REGISTRY.getMappedPropName(attribute), + })); + } + private getScopeData(component: ts.ClassDeclaration): ScopeData|null { if (this.scopeCache.has(component)) { return this.scopeCache.get(component)!; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts index aab925cfd0..320efaf75b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts @@ -13,9 +13,9 @@ import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError, LogicalFileSystem} f import {TestFile} from '../../file_system/testing'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reexport, Reference, ReferenceEmitter} from '../../imports'; import {NOOP_INCREMENTAL_BUILD} from '../../incremental'; -import {ClassPropertyMapping} from '../../metadata'; +import {ClassPropertyMapping, CompoundMetadataReader} from '../../metadata'; import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; -import {ComponentScopeReader, LocalModuleScope, ScopeData} from '../../scope'; +import {ComponentScopeReader, LocalModuleScope, ScopeData, TypeCheckScopeRegistry} from '../../scope'; import {makeProgram} from '../../testing'; import {getRootDirs} from '../../util/src/typescript'; import {ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckContext} from '../api'; @@ -461,9 +461,12 @@ export function setup(targets: TypeCheckingTarget[], overrides: { } }; + const typeCheckScopeRegistry = + new TypeCheckScopeRegistry(fakeScopeReader, new CompoundMetadataReader([])); + const templateTypeChecker = new TemplateTypeCheckerImpl( program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host, - NOOP_INCREMENTAL_BUILD, fakeScopeReader); + NOOP_INCREMENTAL_BUILD, fakeScopeReader, typeCheckScopeRegistry); return { templateTypeChecker, program, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts index effba31830..6d64f68774 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts @@ -7,6 +7,7 @@ */ import {ASTWithSource, Binary, BindingPipe, Conditional, Interpolation, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate} from '@angular/compiler'; +import {AST, LiteralArray, LiteralMap} from '@angular/compiler/src/compiler'; import * as ts from 'typescript'; import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; @@ -693,7 +694,7 @@ runInEachFileSystem(() => { }); it('literal array', () => { - const literalArray = interpolation.expressions[0]; + const literalArray = interpolation.expressions[0] as LiteralArray; const symbol = templateTypeChecker.getSymbolOfNode(literalArray, cmp)!; assertExpressionSymbol(symbol); expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('Array'); @@ -701,7 +702,7 @@ runInEachFileSystem(() => { }); it('literal map', () => { - const literalMap = interpolation.expressions[1]; + const literalMap = interpolation.expressions[1] as LiteralMap; const symbol = templateTypeChecker.getSymbolOfNode(literalMap, cmp)!; assertExpressionSymbol(symbol); expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('__object'); @@ -762,7 +763,7 @@ runInEachFileSystem(() => { it('should get symbol for pipe, checkTypeOfPipes: false', () => { setupPipesTest(false); - const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!; + const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)! as PipeSymbol; assertPipeSymbol(pipeSymbol); expect(pipeSymbol.tsSymbol).toBeNull(); expect(program.getTypeChecker().typeToString(pipeSymbol.tsType!)).toEqual('any'); @@ -772,6 +773,24 @@ runInEachFileSystem(() => { .toEqual('TestPipe'); }); + it('should get symbols for pipe expression and args', () => { + setupPipesTest(false); + const aSymbol = templateTypeChecker.getSymbolOfNode(binding.exp, cmp)!; + assertExpressionSymbol(aSymbol); + expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toEqual('a'); + expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string'); + + const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0] as AST, cmp)!; + assertExpressionSymbol(bSymbol); + expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toEqual('b'); + expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number'); + + const cSymbol = templateTypeChecker.getSymbolOfNode(binding.args[1] as AST, cmp)!; + assertExpressionSymbol(cSymbol); + expect(program.getTypeChecker().symbolToString(cSymbol.tsSymbol!)).toEqual('c'); + expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean'); + }); + for (const checkTypeOfPipes of [true, false]) { describe(`checkTypeOfPipes: ${checkTypeOfPipes}`, () => { // Because the args are property reads, we still need information about them. @@ -782,12 +801,12 @@ runInEachFileSystem(() => { expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toEqual('a'); expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string'); - const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0], cmp)!; + const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0] as AST, cmp)!; assertExpressionSymbol(bSymbol); expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toEqual('b'); expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number'); - const cSymbol = templateTypeChecker.getSymbolOfNode(binding.args[1], cmp)!; + const cSymbol = templateTypeChecker.getSymbolOfNode(binding.args[1] as AST, cmp)!; assertExpressionSymbol(cSymbol); expect(program.getTypeChecker().symbolToString(cSymbol.tsSymbol!)).toEqual('c'); expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean'); diff --git a/packages/compiler/src/schema/dom_element_schema_registry.ts b/packages/compiler/src/schema/dom_element_schema_registry.ts index 69b9cb9785..5e11a656c4 100644 --- a/packages/compiler/src/schema/dom_element_schema_registry.ts +++ b/packages/compiler/src/schema/dom_element_schema_registry.ts @@ -240,6 +240,13 @@ const _ATTR_TO_PROP: {[name: string]: string} = { 'tabindex': 'tabIndex', }; +// Invert _ATTR_TO_PROP. +const _PROP_TO_ATTR: {[name: string]: string} = + Object.keys(_ATTR_TO_PROP).reduce((inverted, attr) => { + inverted[_ATTR_TO_PROP[attr]] = attr; + return inverted; + }, {} as {[prop: string]: string}); + export class DomElementSchemaRegistry extends ElementSchemaRegistry { private _schema: {[element: string]: {[property: string]: string}} = {}; @@ -386,6 +393,12 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry { return Object.keys(this._schema); } + allKnownAttributesOfElement(tagName: string): string[] { + const elementProperties = this._schema[tagName.toLowerCase()] || this._schema['unknown']; + // Convert properties to attributes. + return Object.keys(elementProperties).map(prop => _PROP_TO_ATTR[prop] ?? prop); + } + normalizeAnimationStyleProperty(propName: string): string { return dashCaseToCamelCase(propName); }