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
Closed
13 changes: 7 additions & 6 deletions goldens/public-api/compiler-cli/compiler_options.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ export interface MiscOptions {
}

// @public
export interface StrictTemplateOptions {
export interface TargetOptions {
compilationMode?: 'full' | 'partial' | 'experimental-local';
}

// @public
export interface TypeCheckingOptions {
strictAttributeTypes?: boolean;
strictContextGenerics?: boolean;
strictDomEventTypes?: boolean;
Expand All @@ -75,11 +80,7 @@ export interface StrictTemplateOptions {
strictOutputEventTypes?: boolean;
strictSafeNavigationTypes?: boolean;
strictTemplates?: boolean;
}

// @public
export interface TargetOptions {
compilationMode?: 'full' | 'partial' | 'experimental-local';
typeCheckHostBindings?: boolean;
}

// (No @packageDocumentation comment for this package)
Expand Down
1 change: 1 addition & 0 deletions packages/bazel/src/ngc-wrapped/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export async function runOneBuild(
'_enableLetSyntax',
'_enableHmr',
'strictStandalone',
'typeCheckHostBindings',
]);

const userOverrides = Object.entries(userOptions)
Expand Down
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 @@ -184,7 +189,7 @@ import {
collectAnimationNames,
validateAndFlattenComponentImports, 1E0A
} from './util';
import {getTemplateDiagnostics} from '../../../typecheck';
import {getTemplateDiagnostics, createHostElement} from '../../../typecheck';
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
import {extractHmrMetatadata, getHmrUpdateDeclaration} from '../../../hmr';
import {getProjectRelativePath} from '../../../util/src/path';
Expand Down Expand Up @@ -266,6 +271,7 @@ export class ComponentDecoratorHandler
private readonly strictStandalone: boolean,
private readonly enableHmr: boolean,
private readonly implicitStandaloneValue: boolean,
private readonly typeCheckHostBindings: boolean,
) {
this.extractTemplateOptions = {
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
Expand Down Expand Up @@ -742,11 +748,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 +766,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 +788,7 @@ export class ComponentDecoratorHandler
// 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 +941,7 @@ export class ComponentDecoratorHandler
resources: {
styles: styleResources,
template: templateResource,
hostBindings: hostBindingResources,
},
isPoisoned,
animationTriggerNames,
Expand All @@ -944,6 +952,7 @@ export class ComponentDecoratorHandler
explicitlyDeferredTypes,
schemas,
decorator: (decorator?.node as ts.Decorator | null) ?? null,
hostBindingNodes: directiveResult.hostBindingNodes,
},
diagnostics,
};
Expand Down Expand Up @@ -1058,11 +1067,7 @@ export class ComponentDecoratorHandler
node: ClassDeclaration,
meta: Readonly<ComponentAnalysisData>,
): void {
if (this.typeCheckScopeRegistry === null || !ts.isClassDeclaration(node)) {
return;
}

if (meta.isPoisoned && !this.usePoisonedData) {
if (!ts.isClassDeclaration(node) || (meta.isPoisoned && !this.usePoisonedData)) {
return;
}
const scope = this.typeCheckScopeRegistry.getTypeCheckScope(node);
Expand All @@ -1081,11 +1086,30 @@ export class ComponentDecoratorHandler
preserveWhitespaces: meta.meta.template.preserveWhitespaces ?? false,
};

const hostElement = this.typeCheckHostBindings
? createHostElement(
'component',
meta.meta.selector,
node,
meta.hostBindingNodes.literal,
meta.hostBindingNodes.bindingDecorators,
meta.hostBindingNodes.listenerDecorators,
)
: null;
const hostBindingsContext: HostBindingsContext | null =
hostElement === null
? null
: {
node: hostElement,
sourceMapping: {type: 'direct', node},
};

ctx.addDirective(
new Reference(node),
binder,
scope.schemas,
templateContext,
hostBindingsContext,
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 @@ -160,6 +160,7 @@ function setup(
/* strictStandalone */ false,
/* enableHmr */ false,
/* implicitStandaloneValue */ true,
/* typeCheckHostBindings */ true,
);
return {reflectionHost, handler, resourceLoader, metaRegistry};
}
Expand Down Expand Up @@ -231,7 +232,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 +265,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 @@ -20,6 +20,8 @@ ts_library(
"//packages/compiler-cli/src/ng 57AE tsc/scope",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"@npm//@types/node",
"@npm//typescript",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
makeBindingParser,
R3ClassMetadata,
R3DirectiveMetadata,
R3TargetBinder,
WrappedNodeExpr,
} from '@angular/compiler';
import ts from 'typescript';
Expand All @@ -27,6 +28,7 @@ import {
} from '../../../incremental/semantic_graph';
import {
ClassPropertyMapping,
DirectiveResources,
DirectiveTypeCheckMeta,
extractDirectiveTypeCheckMeta,
HostDirectiveMeta,
Expand All @@ -35,6 +37,7 @@ import {
MetadataReader,
MetadataRegistry,
MetaKind,
ResourceRegistry,
} from '../../../metadata';
import {PartialEvaluator} from '../../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../../perf';
Expand All @@ -45,7 +48,7 @@ import {
Decorator,
ReflectionHost,
} from '../../../reflection';
import {LocalModuleScopeRegistry} from '../../../scope';
import {LocalModuleScopeRegistry, TypeCheckScopeRegistry} from '../../../scope';
import {
AnalysisOutput,
CompilationMode,
Expand Down Expand Up @@ -74,9 +77,15 @@ 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';
import {
HostBindingsContext,
TypeCheckableDirectiveMeta,
TypeCheckContext,
} from '../../../typecheck/api';
import {createHostElement} from '../../../typecheck';

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

export class DirectiveDecoratorHandler
Expand All @@ -134,10 +145,14 @@ export class DirectiveDecoratorHandler
private perf: PerfRecorder,
private importTracker: ImportedSymbolsTracker,
private includeClassMetadata: boolean,
private typeCheckScopeRegistry: TypeCheckScopeRegistry,
private readonly compilationMode: CompilationMode,
private readonly jitDeclarationRegistry: JitDeclarationRegistry,
private readonly resourceRegistry: ResourceRegistry,
private readonly strictStandalone: boolean,
private readonly implicitStandaloneValue: boolean,
private readonly usePoisonedData: boolean,
private readonly typeCheckHostBindings: boolean,
) {}

readonly precedence = HandlerPrecedence.PRIMARY;
Expand Down Expand Up @@ -231,6 +246,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,11 +307,59 @@ export class DirectiveDecoratorHandler
isExplicitlyDeferred: false,
});

this.resourceRegistry.registerResources(analysis.resources, node);
this.injectableRegistry.registerInjectable(node, {
ctorDeps: analysis.meta.deps,
});
}

typeCheck(
ctx: TypeCheckContext,
node: ClassDeclaration,
meta: Readonly<DirectiveHandlerData>,
): void {
// Currently type checking in directives is only supported for host bindings
// so we can skip everything below if type checking is disabled.
if (!this.typeCheckHostBindings) {
return;
}

if (!ts.isClassDeclaration(node) || (meta.isPoisoned && !this.usePoisonedData)) {
return;
}
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;
}

const hostElement = createHostElement(
'directive',
meta.meta.selector,
node,
meta.hostBindingNodes.literal,
meta.hostBindingNodes.bindingDecorators,
meta.hostBindingNodes.listenerDecorators,
);

if (hostElement !== null) {
const binder = new R3TargetBinder<TypeCheckableDirectiveMeta>(scope.matcher);
const hostBindingsContext: HostBindingsContext = {
node: hostElement,
sourceMapping: {type: 'direct', node},
};

ctx.addDirective(
new Reference(node),
binder,
scope.schemas,
null,
hostBindingsContext,
meta.meta.isStandalone,
);
}
}

resolve(
node: ClassDeclaration,
analysis: DirectiveHandlerData,
Expand Down Expand Up @@ -342,7 +411,13 @@ export class DirectiveDecoratorHandler
diagnostics.push(...hostDirectivesDiagnotics);
}

return {diagnostics: diagnostics.length > 0 ? diagnostics : undefined};
if (diagnostics.length > 0) {
return {diagnostics};
}

// Note: we need to produce *some* sort of the data in order
// for the host binding diagnostics to be surfaced.
return {data: {}};
}

compileFull(
Expand Down
Loading
0