8000 Type checking support for host bindings by crisbeto · Pull Request #60267 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

Type checking support for host bindings #60267

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 8 commits into from
Prev Previous commit
Next Next commit
refactor(compiler-cli): track host binding information
Sets up the infrastructure to track the host bindings of directives for type checking purposes.
  • Loading branch information
crisbeto committed Mar 17, 2025
commit 82ffb0e0151d15f55256b1c68d291ee6ad315d50
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import {
TypeCheckableDirectiveMeta,
TypeCheckContext,
TemplateContext,
HostBindingsContext,
} from '../../../typecheck/api';
import {ExtendedTemplateChecker} from '../../../typecheck/extended/api';
import {TemplateSemanticsChecker} from '../../../typecheck/template_semantics/api/api';
Expand Down Expand Up @@ -155,7 +156,11 @@ import {
validateHostDirectives,
wrapFunctionExpressionsInParens,
} from '../../common';
import {extractDirectiveMetadata, parseDirectiveStyles} from '../../directive';
import {
extractDirectiveMetadata,
extractHostBindingResources,
parseDirectiveStyles,
} from '../../directive';
import {createModuleWithProvidersResolver, NgModuleSymbol} from '../../ng_module';

import {checkCustomElementSelectorForErrors, makeCyclicImportInfo} from './diagnostics';
Expand Down Expand Up @@ -742,11 +747,11 @@ export class ComponentDecoratorHandler
}
}
}
const templateResource = template.declaration.isInline
? {path: null, expression: component.get('template')!}
const templateResource: Resource = template.declaration.isInline
? {path: null, node: component.get('template')!}
: {
path: absoluteFrom(template.declaration.resolvedTemplateUrl),
expression: template.sourceMapping.node,
node: template.sourceMapping.node,
};
const relativeTemplatePath = getProjectRelativePath(
templateResource.path ?? ts.getOriginalNode(node).getSourceFile().fileName,
Expand All @@ -760,6 +765,7 @@ export class ComponentDecoratorHandler
let styles: string[] = [];
const externalStyles: string[] = [];

const hostBindingResources = extractHostBindingResources(directiveResult.hostBindingNodes);
const styleResources = extractInlineStyleResources(component);
const styleUrls: StyleUrlMeta[] = [
...extractComponentStyleUrls(this.evaluator, component),
Expand All @@ -781,7 +787,7 @@ export class Compon 8000 entDecoratorHandler
// Only string literal values from the decorator are considered style resources
styleResources.add({
path: absoluteFrom(resourceUrl),
expression: styleUrl.expression,
node: styleUrl.expression,
});
}
const resourceStr = this.resourceLoader.load(resourceUrl);
Expand Down Expand Up @@ -934,6 +940,7 @@ export class ComponentDecoratorHandler
resources: {
styles: styleResources,
template: templateResource,
hostBindings: hostBindingResources,
},
isPoisoned,
animationTriggerNames,
Expand All @@ -944,6 +951,7 @@ export class ComponentDecoratorHandler
explicitlyDeferredTypes,
schemas,
decorator: (decorator?.node as ts.Decorator | null) ?? null,
hostBindingNodes: directiveResult.hostBindingNodes,
},
diagnostics,
};
Expand Down Expand Up @@ -1086,6 +1094,7 @@ export class ComponentDecoratorHandler
binder,
scope.schemas,
templateContext,
null,
meta.meta.isStandalone,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {ClassDeclaration} from '../../../reflection';
import {SubsetOfKeys} from '../../../util/src/typescript';

import {ParsedTemplateWithSource, StyleUrlMeta} from './resources';
import {HostBindingNodes} from '../../directive';

/**
* These fields of `R3ComponentMetadata` are updated in the `resolve` phase.
Expand Down Expand Up @@ -108,6 +109,9 @@ export interface ComponentAnalysisData {

/** Raw expression that defined the host directives array. Used for diagnostics. */
rawHostDirectives: ts.Expression | null;

/** Raw nodes representing the host bindings of the directive. */
hostBindingNodes: HostBindingNodes;
}

export interface ComponentResolutionData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -745,10 +745,10 @@ export function extractInlineStyleResources(component: Map<string, ts.Expression
if (stylesExpr !== undefined) {
if (ts.isArrayLiteralExpression(stylesExpr)) {
for (const expression of stringLiteralElements(stylesExpr)) {
styles.add({path: null, expression});
styles.add({path: null, node: expression});
}
} else if (ts.isStringLiteralLike(stylesExpr)) {
styles.add({path: null, expression: stylesExpr});
styles.add({path: null, node: stylesExpr});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ runInEachFileSystem(() => {
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.resources.template?.path).toBeNull();
expect(analysis?.resources.template?.expression.getText()).toEqual(`'${template}'`);
expect(analysis?.resources.template?.node.getText()).toEqual(`'${template}'`);
});

it('should keep track of external template', () => {
Expand Down Expand Up @@ -264,7 +264,7 @@ runInEachFileSystem(() => {
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.resources.template?.path).toContain(templateUrl);
expect(analysis?.resources.template?.expression.getText()).toContain(`'${templateUrl}'`);
expect(analysis?.resources.template?.node.getText()).toContain(`'${templateUrl}'`);
});

it('should keep track of internal and external styles', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from '../../../incremental/semantic_graph';
import {
ClassPropertyMapping,
DirectiveResources,
DirectiveTypeCheckMeta,
extractDirectiveTypeCheckMeta,
HostDirectiveMeta,
Expand All @@ -35,6 +36,7 @@ import {
MetadataReader,
MetadataRegistry,
MetaKind,
ResourceRegistry,
} from '../../../metadata';
import {PartialEvaluator} from '../../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../../perf';
Expand Down Expand Up @@ -74,7 +76,7 @@ import {
validateHostDirectives,
} from '../../common';

import {extractDirectiveMetadata} from './shared';
import {extractDirectiveMetadata, extractHostBindingResources, HostBindingNodes} from './shared';
import {DirectiveSymbol} from './symbol';
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';

Expand Down Expand Up @@ -113,6 +115,8 @@ export interface DirectiveHandlerData {
decorator: ts.Decorator | null;
hostDirectives: HostDirectiveMeta[] | null;
rawHostDirectives: ts.Expression | null;
hostBindingNodes: HostBindingNodes;
resources: DirectiveResources;
}

export class DirectiveDecoratorHandler
Expand All @@ -136,6 +140,7 @@ export class DirectiveDecoratorHandler
private includeClassMetadata: boolean,
private readonly compilationMode: CompilationMode,
private readonly jitDeclarationRegistry: JitDeclarationRegistry,
private readonly resourceRegistry: ResourceRegistry,
private readonly strictStandalone: boolean,
private readonly implicitStandaloneValue: boolean,
) {}
Expand Down Expand Up @@ -231,6 +236,12 @@ export class DirectiveDecoratorHandler
isPoisoned: false,
isStructural: directiveResult.isStructural,
decorator: (decorator?.node as ts.Decorator | null) ?? null,
hostBindingNodes: directiveResult.hostBindingNodes,
resources: {
template: null,
styles: null,
hostBindings: extractHostBindingResources(directiveResult.hostBindingNodes),
},
},
};
}
Expand Down Expand Up @@ -286,6 +297,7 @@ export class DirectiveDecoratorHandler
isExplicitlyDeferred: false,
});

this.resourceRegistry.registerResources(analysis.resources, node);
this.injectableRegistry.registerInjectable(node, {
ctorDeps: analysis.meta.deps,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
InputMapping,
InputOrOutput,
isHostDirectiveMetaForGlobalMode,
Resource,
} from '../../../metadata';
import {
DynamicValue,
Expand Down Expand Up @@ -97,6 +98,12 @@ export const queryDecoratorNames: QueryDecoratorName[] = [
'ContentChildren',
];

export interface HostBindingNodes {
literal: ts.ObjectLiteralExpression | null;
bindingDecorators: Set<ts.Decorator>;
listenerDecorators: Set<ts.Decorator>;
}

const QUERY_TYPES = new Set<string>(queryDecoratorNames);

/**
Expand Down Expand Up @@ -130,6 +137,7 @@ export function extractDirectiveMetadata(
hostDirectives: HostDirectiveMeta[] | null;
rawHostDirectives: ts.Expression | null;
inputFieldNamesFromMetadataArray: Set<string>;
hostBindingNodes: HostBindingNodes;
}
| {jitForced: true} {
let directive: Map<string, ts.Expression>;
Expand Down Expand Up @@ -272,11 +280,18 @@ export function extractDirectiveMetadata(
}
}

const hostBindingNodes: HostBindingNodes = {
literal: null,
bindingDecorators: new Set<ts.Decorator>(),
listenerDecorators: new Set<ts.Decorator>(),
};

const host = extractHostBindings(
decoratedElements,
evaluator,
coreModule,
compilationMode,
hostBindingNodes,
directive,
);

Expand Down Expand Up @@ -434,6 +449,7 @@ export function extractDirectiveMetadata(
isStructural,
hostDirectives,
rawHostDirectives,
hostBindingNodes,
// Track inputs from class metadata. This is useful for migration efforts.
inputFieldNamesFromMetadataArray: new Set(
Object.values(inputsFromMeta).map((i) => i.classPropertyName),
Expand Down Expand Up @@ -558,16 +574,21 @@ export function extractDecoratorQueryMetadata(
};
}

export function extractHostBindings(
function extractHostBindings(
members: ClassMember[],
evaluator: PartialEvaluator,
coreModule: string | undefined,
compilationMode: CompilationMode,
hostBindingNodes: HostBindingNodes,
metadata?: Map<string, ts.Expression>,
): ParsedHostBindings {
let bindings: ParsedHostBindings;
if (metadata && metadata.has('host')) {
bindings = evaluateHostExpressionBindings(metadata.get('host')!, evaluator);
const hostExpression = metadata.get('host')!;
bindings = evaluateHostExpressionBindings(hostExpression, evaluator);
if (ts.isObjectLiteralExpression(hostExpression)) {
hostBindingNodes.literal = hostExpression;
}
} else {
bindings = parseHostBindings({});
}
Expand Down Expand Up @@ -610,6 +631,10 @@ export function extractHostBindings(
hostPropertyName = resolved;
}

if (ts.isDecorator(decorator.node)) {
hostBindingNodes.bindingDecorators.add(decorator.node);
}

// Since this is a decorator, we know that the value is a class member. Always access it
// through `this` so that further down the line it can't be confused for a literal value
// (e.g. if there's a property called `true`). There is no size penalty, because all
Expand Down Expand Up @@ -671,6 +696,10 @@ export function extractHostBindings(
}
}

if (ts.isDecorator(decorator.node)) {
hostBindingNodes.listenerDecorators.add(decorator.node);
}

bindings.listeners[eventName] = `${member.name}(${args.join(',')})`;
});
},
Expand Down Expand Up @@ -1826,3 +1855,21 @@ function toR3InputMetadata(mapping: InputMapping): R3InputMetadata {
isSignal: mapping.isSignal,
};
}

export function extractHostBindingResources(nodes: HostBindingNodes): ReadonlySet<Resource> {
const result = new Set<Resource>();

if (nodes.literal !== null) {
result.add({path: null, node: nodes.literal});
}

for (const current of nodes.bindingDecorators) {
result.add({path: null, node: current.expression});
}

for (const current of nodes.listenerDecorators) {
result.add({path: null, node: current.expression});
}

return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import ts from 'typescript';
import {absoluteFrom} from '../../../file_system';
import { 1241 runInEachFileSystem} from '../../../file_system/testing';
import {ImportedSymbolsTracker, ReferenceEmitter} from '../../../imports';
import {CompoundMetadataReader, DtsMetadataReader, LocalMetadataRegistry} from '../../../metadata';
import {
CompoundMetadataReader,
DtsMetadataReader,
LocalMetadataRegistry,
ResourceRegistry,
} from '../../../metadata';
import {PartialEvaluator} from '../../../partial_evaluator';
import {NOOP_PERF_RECORDER} from '../../../perf';
import {
Expand Down Expand Up @@ -195,6 +200,7 @@ runInEachFileSystem(() => {
const injectableRegistry = new InjectableClassRegistry(reflectionHost, /* isCore */ false);
const importTracker = new ImportedSymbolsTracker();
const jitDeclarationRegistry = new JitDeclarationRegistry();
const resourceRegistry = new ResourceRegistry();

const handler = new DirectiveDecoratorHandler(
reflectionHost,
Expand All @@ -214,6 +220,7 @@ runInEachFileSystem(() => {
/*includeClassMetadata*/ true,
/*compilationMode */ CompilationMode.FULL,
jitDeclarationRegistry,
resourceRegistry,
/* strictStandalone */ false,
/* implicitStandaloneValue */ true,
);
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,8 @@ export class NgCompiler {
const {resourceRegistry} = this.ensureAnalyzed();
const styles = resourceRegistry.getStyles(classDecl);
const template = resourceRegistry.getTemplate(classDecl);
return {styles, template};
const hostBindings = resourceRegistry.getHostBindings(classDecl);
return {styles, template, hostBindings};
}

getMeta(classDecl: DeclarationNode): PipeMeta | DirectiveMeta | null {
Expand Down Expand Up @@ -1524,6 +1525,7 @@ export class NgCompiler {
supportTestBed,
compilationMode,
jitDeclarationRegistry,
resourceRegistry,
!!this.options.strictStandalone,
this.implicitStandaloneValue,
) as Readonly<DecoratorHandler<unknown, unknown, SemanticSymbol | null, unknown>>,
Expand Down
Loading
0