Skip to content
Merged
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
4 changes: 3 additions & 1 deletion packages/compiler-cli/src/ngtsc/core/api/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type ExtendedCompilerHostMethods =
// Used to normalize filenames for the host system. Important for proper case-sensitive file
// handling.
| 'getCanonicalFileName'
| 'getSourceFile'
// An optional method of `ts.CompilerHost` where an implementer can override module resolution.
| 'resolveModuleNames'
// Retrieve the current working directory. Unlike in `ts.ModuleResolutionHost`, this is a
Expand All @@ -44,7 +45,8 @@ export interface NgCompilerAdapter
// getCurrentDirectory is removed from `ts.ModuleResolutionHost` because it's optional, and
// incompatible with the `ts.CompilerHost` version which isn't. The combination of these two
// still satisfies `ts.ModuleResolutionHost`.
extends Omit<ts.ModuleResolutionHost, 'getCurrentDirectory'>,
extends
Omit<ts.ModuleResolutionHost, 'getCurrentDirectory'>,
Pick<ExtendedTsCompilerHost, 'getCurrentDirectory' | ExtendedCompilerHostMethods>,
SourceFileTypeIdentifier {
/**
Expand Down
12 changes: 12 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ To understand and check the types of various operations and structures within te

TCBs are not ever emitted, nor are they referenced from any other code (they're unused code as far as TypeScript is concerned). Their _runtime_ effect is therefore unimportant. What matters is that they express to TypeScript the type relationships of directives, bindings, and other entities in the template. Type errors within TCBs translate directly to type errors in the original template.

### AST-Free Metadata & Preprocessor Integration

As Angular evolves toward supporting alternative compilation pipelines (such as a native Rust compiler or a fast native TS parser like `ts-go`), the mechanism for providing information about components, directives, and pipes to the TCB generator has been abstracted.

Instead of directly passing TypeScript AST nodes representing the original program (`ts.Node`, `ts.Declaration`, `ClassDeclaration`, etc.), the type checking system relies on "AST-free" metadata interfaces (e.g., `TcbDirectiveMetadata`, `TcbReferenceMetadata`).

When operating in the traditional TypeScript compiler (`ngc`), a "TCB Adapter" translates the internal TS-bound metadata into these AST-free structures.

When operating in an environment where the Angular application is analyzed by a separate preprocessor process (whether `ts-go` or Rust), the indexer performs the analysis and serializes this exact same AST-free metadata over an IPC boundary directly to the TS-based TCB generator. This enables the existing, robust TS-based template type checker (which heavily relies on TypeScript's inference engines) to continue functioning without needing to port the entire template type checking and `TcbOp` implementation to the native preprocessor.

**Note on AST Generation:** While the metadata is "AST-free" in the sense that it is entirely disconnected from the original user's `ts.Program`, the TCB generation process is still fundamentally tied to TypeScript factory APIs. Because of this, certain metadata fields (such as `TcbInputMapping.transformType` and `TcbComponentMetadata.typeParameters`) are passed as strings which are parsed into AST nodes by the TCB generator. In a hybrid architecture, the native preprocessor is expected to serialize these scopes and types as strings, and the TypeScript coordinator (Node.js) is responsible for parsing these strings back into detached TypeScript AST nodes before passing them to the TCB generator. This avoids unnecessary serialization costs when the TCB generator runs directly within the existing TypeScript codebase.

### Theory

Given a component `SomeCmp`, its TCB takes the form of a function:
Expand Down
103 changes: 102 additions & 1 deletion packages/compiler-cli/src/ngtsc/typecheck/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,125 @@

import {
AbsoluteSourceSpan,
AST,
BoundTarget,
DirectiveMeta,
DirectiveOwner,
LegacyAnimationTriggerNames,
ParseSourceSpan,
SchemaMetadata,
TemplateEntity,
TmplAstBoundAttribute,
TmplAstBoundEvent,
TmplAstComponent,
TmplAstDirective,
TmplAstElement,
TmplAstReference,
TmplAstTemplate,
TmplAstTextAttribute,
} from '@angular/compiler';
import ts from 'typescript';

import {ErrorCode} from '../../diagnostics';
import {Reference} from '../../imports';
import {
ClassPropertyMapping,
ClassPropertyName,
DirectiveTypeCheckMeta,
HostDirectiveMeta,
InputMapping,
InputOrOutput,
PipeMeta,
TemplateGuardMeta,
} from '../../metadata';
import {ClassDeclaration} from '../../reflection';

export interface TcbReferenceMetadata {
/** The name of the class */
name: string;
/** The module path where the symbol is located, or null if local/ambient */
moduleName: string | null;
/** True if the symbol successfully emitted locally (no external import required) */
isLocal: boolean;
/** If the reference could not be externally emitted, this string holds the diagnostic reason why */
unexportedDiagnostic: string | null;
/**
* Defines the `AbsoluteSourceSpan` of the target's node name, if available.
*/
nodeNameSpan?: AbsoluteSourceSpan;

/**
* The absolute path to the file containing the reference node, if available.
*/
nodeFilePath?: string;
}

export type TcbReferenceKey = string & {__brand: 'TcbReferenceKey'};

export interface TcbTypeParameter {
name: string;
representation: string;
representationWithDefault: string;
}

export type TcbInputMapping = InputOrOutput & {
required: boolean;

/**
* AST-free string representation of the transform type of the input, if available.
*/
transformType?: string;
};

export interface TcbPipeMetadata {
name: string;
ref: TcbReferenceMetadata;
isExplicitlyDeferred: boolean;
}

export interface TcbDirectiveMetadata {
ref: TcbReferenceMetadata;
name: string;
selector: string | null;
isComponent: boolean;
isGeneric: boolean;
isStructural: boolean;
isStandalone: boolean;
isExplicitlyDeferred: boolean;
preserveWhitespaces: boolean;
exportAs: string[] | null;

/** Type parameters of the directive, if available. */
typeParameters: TcbTypeParameter[] | null;
inputs: ClassPropertyMapping<TcbInputMapping>;
outputs: ClassPropertyMapping;
hasRequiresInlineTypeCtor: boolean;
ngTemplateGuards: TemplateGuardMeta[];
hasNgTemplateContextGuard: boolean;
hasNgFieldDirective: boolean;
coercedInputFields: Set<ClassPropertyName>;
restrictedInputFields: Set<ClassPropertyName>;
stringLiteralInputFields: Set<ClassPropertyName>;
undeclaredInputFields: Set<ClassPropertyName>;
publicMethods: Set<string>;
ngContentSelectors: string[] | null;
animationTriggerNames: LegacyAnimationTriggerNames | null;
}

export interface TcbComponentMetadata {
ref: TcbReferenceMetadata;
typeParameters: TcbTypeParameter[] | null;
}

export interface TcbTypeCheckBlockMetadata {
id: TypeCheckId;
boundTarget: BoundTarget<TcbDirectiveMetadata>;
pipes: Map<string, TcbPipeMetadata> | null;
schemas: SchemaMetadata[];
isStandalone: boolean;
preserveWhitespaces: boolean;
}

/**
* Extension of `DirectiveMeta` that includes additional information required to type-check the
* usage of a particular directive.
Expand Down Expand Up @@ -119,7 +220,7 @@ export interface TypeCtorMetadata {
/**
* Input, output, and query field names in the type which should be included as constructor input.
*/
fields: {inputs: ClassPropertyMapping<InputMapping>; queries: string[]};
fields: {inputs: ClassPropertyMapping<TcbInputMapping>};

/**
* `Set` of field names which have type coercion enabled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,52 @@ runInEachFileSystem(() => {
expect(diags.length).toBe(0);
});

it('should *not* produce a warning for custom structural directives imported from another file', () => {
const fileName = absoluteFrom('/main.ts');
const dirFileName = absoluteFrom('/dir.ts');
const {program, templateTypeChecker} = setup([
{
fileName: dirFileName,
source: `export class Foo {}`,
},
{
fileName,
templates: {
'TestCmp': `<div *foo="exp"></div>`,
},
source: `
import {Foo} from './dir';
export class TestCmp {}
`,
declarations: [
{
type: 'directive',
name: 'Foo',
selector: `[foo]`,
file: dirFileName,
},
{
name: 'TestCmp',
type: 'directive',
selector: `[test-cmp]`,
isStandalone: true,
},
],
},
]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker,
program.getTypeChecker(),
[missingStructuralDirectiveCheck],
{strictNullChecks: true} /* options */,
);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
// No diagnostic messages are expected.
expect(diags.length).toBe(0);
});

it('should *not* produce a warning for non-standalone components', () => {
const fileName = absoluteFrom('/main.ts');

Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
private config: TypeCheckingConfig,
private refEmitter: ReferenceEmitter,
private reflector: ReflectionHost,
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName' | 'getSourceFile'>,
private priorBuild: IncrementalBuild<unknown, FileTypeCheckingData>,
private readonly metaReader: MetadataReader,
private readonly localMetaReader: MetadataReaderWithIndex,
Expand Down
14 changes: 9 additions & 5 deletions packages/compiler-cli/src/ngtsc/typecheck/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from '../api';
import {makeTemplateDiagnostic} from '../diagnostics';

import {adaptTypeCheckBlockMetadata} from './tcb_adapter';
import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom';
import {Environment} from './environment';
import {OutOfBandDiagnosticRecorder, OutOfBandDiagnosticRecorderImpl} from './oob';
Expand Down Expand Up @@ -204,7 +205,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {

constructor(
private config: TypeCheckingConfig,
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName' | 'getSourceFile'>,
private refEmitter: ReferenceEmitter,
private reflector: ReflectionHost,
private host: TypeCheckingHost,
Expand Down Expand Up @@ -290,7 +291,6 @@ export class TypeCheckContextImpl implements TypeCheckContext {
fields: {
inputs: dir.inputs,
// TODO(alxhub): support queries
queries: dir.queries,
},
coercedInputFields: dir.coercedInputFields,
});
Expand Down Expand Up @@ -568,7 +568,9 @@ export class TypeCheckContextImpl implements TypeCheckContext {
if (!fileData.shimData.has(shimPath)) {
fileData.shimData.set(shimPath, {
domSchemaChecker: new RegistryDomSchemaChecker(fileData.sourceManager),
oobRecorder: new OutOfBandDiagnosticRecorderImpl(fileData.sourceManager),
oobRecorder: new OutOfBandDiagnosticRecorderImpl(fileData.sourceManager, (name) =>
this.compilerHost.getSourceFile(name, ts.ScriptTarget.Latest),
),
file: new TypeCheckFile(
shimPath,
this.config,
Expand Down Expand Up @@ -669,13 +671,15 @@ class InlineTcbOp implements Op {
const env = new Environment(this.config, im, refEmitter, this.reflector, sf);
const fnName = ts.factory.createIdentifier(`_tcb_${this.ref.node.pos}`);

const {tcbMeta, component} = adaptTypeCheckBlockMetadata(this.ref, this.meta, env);

// Inline TCBs should copy any generic type parameter nodes directly, as the TCB code is
// inlined into the class in a context where that will always be legal.
const fn = generateTypeCheckBlock(
env,
this.ref,
component,
fnName,
this.meta,
tcbMeta,
this.domSchemaChecker,
this.oobRecorder,
TcbGenericContextBehavior.CopyClassNodes,
Expand Down
Loading