8000 Initial language service support for selectorless by crisbeto · Pull Request #61240 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

Initial language service support for selectorless #61240

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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
PropertyRead,
SafePropertyRead,
TemplateEntity,
TmplAstComponent,
TmplAstDirective,
TmplAstElement,
TmplAstHostElement,
TmplAstNode,
Expand All @@ -30,7 +32,14 @@ import {ClassDeclaration} from '../../reflection';
import {FullSourceMapping, NgTemplateDiagnostic, TypeCheckableDirectiveMeta} from './api';
import {GlobalCompletion} from './completion';
import {PotentialDirective, PotentialImport, PotentialImportMode, PotentialPipe} from './scope';
import {ElementSymbol, Symbol, TcbLocation, TemplateSymbol} from './symbols';
import {
ElementSymbol,
SelectorlessComponentSymbol,
SelectorlessDirectiveSymbol,
Symbol,
TcbLocation,
TemplateSymbol,
} from './symbols';

/**
* Interface to the Angular Template Type Checker to extract diagnostics and intelligence from the
Expand Down Expand Up @@ -119,6 +128,14 @@ export interface TemplateTypeChecker {
*/
getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol | null;
getSymbolOfNode(node: TmplAstTemplate, component: ts.ClassDeclaration): TemplateSymbol | null;
getSymbolOfNode(
node: TmplAstComponent,
component: ts.ClassDeclaration,
): SelectorlessComponentSymbol | null;
getSymbolOfNode(
node: TmplAstDirective,
component: ts.ClassDeclaration,
): SelectorlessDirectiveSymbol | null;
getSymbolOfNode(node: AST | TmplAstNode, component: ts.ClassDeclaration): Symbol | null;

/**
Expand Down
54 changes: 53 additions & 1 deletion packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/

import {
TmplAstComponent,
TmplAstDirective,
TmplAstElement,
TmplAstLetDeclaration,
TmplAstReference,
Expand All @@ -33,6 +35,8 @@ export enum SymbolKind {
DomBinding,
Pipe,
LetDeclaration,
SelectorlessComponent,
SelectorlessDirective,
}

/**
Expand All @@ -49,7 +53,9 @@ export type Symbol =
| TemplateSymbol
| DomBindingSymbol
| PipeSymbol
| LetDeclarationSymbol;
| LetDeclarationSymbol
| SelectorlessComponentSymbol
| SelectorlessDirectiveSymbol;

/**
* A `Symbol` which declares a new named entity in the template scope.
Expand Down Expand Up @@ -305,6 +311,52 @@ export interface TemplateSymbol {
templateNode: TmplAstTemplate;
}

/** A representation of a selectorless component reference in a template. */
export interface SelectorlessComponentSymbol {
kind: SymbolKind.SelectorlessComponent;

/** The `ts.Type` for the component class. */
tsType: ts.Type;

/** The `ts.Symbol` for the component class. */
tsSymbol: ts.Symbol | null;

/**
* Includes the component class itself and any host directives
* that may have been applied as a side-effect of it.
*/
directives: DirectiveSymbol[];

/** The location in the shim file for the variable that holds the type of the component. */
tcbLocation: TcbLocation;

/** Template AST node defining the component. */
templateNode: TmplAstComponent;
}

/** A representation of a selectorless directive reference in a template. */
export interface SelectorlessDirectiveSymbol {
kind: SymbolKind.SelectorlessDirective;

/** The `ts.Type` for the directive class. */
tsType: ts.Type;

/** The `ts.Symbol` for the directive class. */
tsSymbol: ts.Symbol | null;

/**
* Includes the directive class itself and any host directives
* that may have been applied as a side-effect of it.
*/
directives: DirectiveSymbol[];

/** The location in the shim file for the variable that holds the type of the directive. */
tcbLocation: TcbLocation;

/** Template AST node defining the directive. */
templateNode: TmplAstDirective;
}

/** Interface shared between host and non-host directives. */
interface DirectiveSymbolBase extends PotentialDirective {
kind: SymbolKind.Directive;
Expand Down
12 changes: 12 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
PropertyRead,
SafePropertyRead,
TemplateEntity,
TmplAstComponent,
TmplAstDirective,
TmplAstElement,
TmplAstHostElement,
TmplAstNode,
Expand Down Expand Up @@ -68,6 +70,8 @@ import {
PotentialImportMode,
PotentialPipe,
ProgramTypeCheckAdapter,
SelectorlessComponentSymbol,
SelectorlessDirectiveSymbol,
Symbol,
TcbLocation,
TemplateDiagnostic,
Expand Down Expand Up @@ -670,6 +674,14 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
}
getSymbolOfNode(node: TmplAstTemplate, component: ts.ClassDeclaration): TemplateSymbol | null;
getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol | null;
getSymbolOfNode(
node: TmplAstComponent,
component: ts.ClassDeclaration,
): SelectorlessComponentSymbol | null;
getSymbolOfNode(
node: TmplAstDirective,
component: ts.ClassDeclaration,
): SelectorlessDirectiveSymbol | null;
getSymbolOfNode(node: AST | TmplAstNode, component: ts.ClassDeclaration): Symbol | null {
const builder = this.getOrCreateSymbolBuilder(component);
if (builder === null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
SafePropertyRead,
TmplAstBoundAttribute,
TmplAstBoundEvent,
TmplAstComponent,
TmplAstDirective,
TmplAstElement,
TmplAstLetDeclaration,
TmplAstNode,
Expand Down Expand Up @@ -45,6 +47,8 @@ import {
OutputBindingSymbol,
PipeSymbol,
ReferenceSymbol,
SelectorlessComponentSymbol,
SelectorlessDirectiveSymbol,
Symbol,
SymbolKind,
TcbLocation,
Expand Down Expand Up @@ -88,6 +92,8 @@ export class SymbolBuilder {
getSymbol(
node: TmplAstReference | TmplAstVariable | TmplAstLetDeclaration,
): ReferenceSymbol | VariableSymbol | LetDeclarationSymbol | null;
getSymbol(node: TmplAstComponent): SelectorlessComponentSymbol | null;
getSymbol(node: TmplAstDirective): SelectorlessDirectiveSymbol | null;
getSymbol(node: AST | TmplAstNode): Symbol | null;
getSymbol(node: AST | TmplAstNode): Symbol | null {
if (this.symbolCache.has(node)) {
Expand All @@ -103,6 +109,10 @@ export class SymbolBuilder {
symbol = this.getSymbolOfBoundEvent(node);
} else if (node instanceof TmplAstElement) {
symbol = this.getSymbolOfElement(node);
} else if (node instanceof TmplAstComponent) {
symbol = this.getSymbolOfSelectorlessComponent(node);
} else if (node instanceof TmplAstDirective) {
symbol = this.getSymbolOfSelectorlessDirective(node);
} else if (node instanceof TmplAstTemplate) {
symbol = this.getSymbolOfAstTemplate(node);
} else if (node instanceof TmplAstVariable) {
Expand Down Expand Up @@ -156,8 +166,52 @@ export class SymbolBuilder {
};
}

private getDirectivesOfNode(element: TmplAstElement | TmplAstTemplate): DirectiveSymbol[] {
const elementSourceSpan = element.startSourceSpan ?? element.sourceSpan;
private getSymbolOfSelectorlessComponent(
node: TmplAstComponent,
): SelectorlessComponentSymbol | null {
const directives = this.getDirectivesOfNode(node);
const primaryDirective =
directives.find((dir) => !dir.isHostDirective && dir.isComponent) ?? null;

if (primaryDirective === null) {
return null;
}

return {
tsType: primaryDirective.tsType,
tsSymbol: primaryDirective.tsSymbol,
tcbLocation: primaryDirective.tcbLocation,
kind: SymbolKind.SelectorlessComponent,
directives,
templateNode: node,
};
}

private getSymbolOfSelectorlessDirective(
node: TmplAstDirective,
): SelectorlessDirectiveSymbol | null {
const directives = this.getDirectivesOfNode(node);
const primaryDirective =
directives.find((dir) => !dir.isHostDirective && !dir.isComponent) ?? null;

if (primaryDirective === null) {
return null;
}

return {
tsType: primaryDirective.tsType,
tsSymbol: primaryDirective.tsSymbol,
tcbLocation: primaryDirective.tcbLocation,
kind: SymbolKind.SelectorlessDirective,
directives,
templateNode: node,
};
}

private getDirectivesOfNode(
templateNode: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
): DirectiveSymbol[] {
const elementSourceSpan = templateNode.startSourceSpan ?? templateNode.sourceSpan;
const tcbSourceFile = this.typeCheckBlock.getSourceFile();
// directives could be either:
// - var _t1: TestDir /*T:D*/ = null! as TestDir;
Expand All @@ -172,6 +226,7 @@ export class SymbolBuilder {
filter: isDirectiveDeclaration,
});
const symbols: DirectiveSymbol[] = [];
const seenDirectives = new Set<ts.ClassDeclaration>();

for (const node of nodes) {
const symbol = this.getSymbolOfTsNode(node.parent);
Expand All @@ -183,13 +238,16 @@ export class SymbolBuilder {
continue;
}

const meta = this.getDirectiveMeta(element, symbol.tsSymbol.valueDeclaration);
const declaration = symbol.tsSymbol.valueDeclaration;
const meta = this.getDirectiveMeta(templateNode, declaration);

if (meta !== null && meta.selector !== null) {
const ref = new Reference<ClassDeclaration>(symbol.tsSymbol.valueDeclaration as any);
// Host directives will be added as identifiers with the same offset as the host
// which means that they'll get added twice. De-duplicate them to avoid confusion.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of curiosity, is this new or why is this surfacing now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was always like that, we just didn't have any tests with host directives so we never noticed it. I think that also the language service integration was de-duping the information produced as a result at some point so it didn't affect users either.

if (meta !== null && !seenDirectives.has(declaration)) {
const ref = new Reference<ClassDeclaration>(declaration as ClassDeclaration);

if (meta.hostDirectives !== null) {
this.addHostDirectiveSymbols(element, meta.hostDirectives, symbols);
this.addHostDirectiveSymbols(templateNode, meta.hostDirectives, symbols, seenDirectives);
}

const directiveSymbol: DirectiveSymbol = {
Expand All @@ -198,40 +256,44 @@ export class SymbolBuilder {
tsSymbol: symbol.tsSymbol,
selector: meta.selector,
isComponent: meta.isComponent,
ngModule: this.getDirectiveModule(symbol.tsSymbol.valueDeclaration),
ngModule: this.getDirectiveModule(declaration),
kind: SymbolKind.Directive,
isStructural: meta.isStructural,
isInScope: true,
isHostDirective: false,
};

symbols.push(directiveSymbol);
seenDirectives.add(declaration);
}
}

return symbols;
}

private addHostDirectiveSymbols(
host: TmplAstTemplate | TmplAstElement,
host: TmplAstTemplate | TmplAstElement | TmplAstComponent | TmplAstDirective,
hostDirectives: HostDirectiveMeta[],
symbols: DirectiveSymbol[],
seenDirectives: Set<ts.ClassDeclaration>,
): void {
for (const current of hostDirectives) {
if (!isHostDirectiveMetaForGlobalMode(current)) {
throw new Error('Impossible state: typecheck code path in local compilation mode.');
}

if (!ts.isClassDeclaration(current.directive.node)) {
const node = current.directive.node;

if (!ts.isClassDeclaration(node) || seenDirectives.has(node)) {
continue;
}

const symbol = this.getSymbolOfTsNode(current.directive.node);
const meta = this.getDirectiveMeta(host, current.directive.node);
const symbol = this.getSymbolOfTsNode(node);
const meta = this.getDirectiveMeta(host, node);

if (meta !== null && symbol !== null && isSymbolWithValueDeclaration(symbol.tsSymbol)) {
if (meta.hostDirectives !== null) {
this.addHostDirectiveSymbols(host, meta.hostDirectives, symbols);
this.addHostDirectiveSymbols(host, meta.hostDirectives, symbols, seenDirectives);
}

const directiveSymbol: DirectiveSymbol = {
Expand All @@ -243,36 +305,41 @@ export class SymbolBuilder {
exposedOutputs: current.outputs,
selector: meta.selector,
isComponent: meta.isComponent,
ngModule: this.getDirectiveModule(current.directive.node),
ngModule: this.getDirectiveModule(node),
kind: SymbolKind.Directive,
isStructural: meta.isStructural,
isInScope: true,
};

symbols.push(directiveSymbol);
seenDirectives.add(node);
}
}
}

private getDirectiveMeta(
host: TmplAstTemplate | TmplAstElement,
host: TmplAstTemplate | TmplAstElement | TmplAstComponent | TmplAstDirective,
directiveDeclaration: ts.ClassDeclaration,
): TypeCheckableDirectiveMeta | null {
let directives = this.typeCheckData.boundTarget.getDirectivesOfNode(host);

// `getDirectivesOfNode` will not return the directives intended for an element
// on a microsyntax template, for example `<div *ngFor="let user of users;" dir>`,
// the `dir` will be skipped, but it's needed in language service.
const firstChild = host.children[0];
if (firstChild instanceof TmplAstElement) {
const isMicrosyntaxTemplate =
host instanceof TmplAstTemplate && sourceSpanEqual(firstChild.sourceSpan, host.sourceSpan);
if (isMicrosyntaxTemplate) {
const firstChildDirectives = this.typeCheckData.boundTarget.getDirectivesOfNode(firstChild);
if (firstChildDirectives !== null && directives !== null) {
directives = directives.concat(firstChildDirectives);
} else {
directives = directives ?? firstChildDirectives;
if (!(host instanceof TmplAstDirective)) {
const firstChild = host.children[0];
if (firstChild instanceof TmplAstElement) {
const isMicrosyntaxTemplate =
host instanceof TmplAstTemplate &&
sourceSpanEqual(firstChild.sourceSpan, host.sourceSpan);
if (isMicrosyntaxTemplate) {
const firstChildDirectives =
this.typeCheckData.boundTarget.getDirectivesOfNode(firstChild);
if (firstChildDirectives !== null && directives !== null) {
directives = directives.concat(firstChildDirectives);
} else {
directives = directives ?? firstChildDirectives;
}
}
}
}
Expand Down Expand Up @@ -513,11 +580,7 @@ export class SymbolBuilder {
): DirectiveSymbol | null {
// In all cases, `_t1["index"]` or `_t1.index`, `node.expression` is _t1.
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(fieldAccessExpr.expression);
if (
tsSymbol?.declarations === undefined ||
tsSymbol.declarations.length === 0 ||
selector === null
) {
if (tsSymbol?.declarations === undefined || tsSymbol.declarations.length === 0) {
return null;
}

Expand Down
Loading
0