Skip to content

Commit

Permalink
refactor(compiler-cli): split the 'annotations' package into sub-pack…
Browse files Browse the repository at this point in the history
…ages (#44812)

Previously each `DecoratorHandler` in the compiler was stored in a single file
in the 'annotations' package. The `ComponentDecoratorHandler` in particular was
several thousand lines long.

Prior to implementing the new standalone functionality for components, this
commit refactors 'annotations' to split these large files into their own build
targets with multiple separate files. This should make the implementation of
standalone significantly cleaner.

PR Close #44812
  • Loading branch information
alxhub authored and dylhunn committed Feb 3, 2022
1 parent 1aae414 commit cc0d73d
Show file tree
Hide file tree
Showing 49 changed files with 2,887 additions and 2,377 deletions.
1 change: 1 addition & 0 deletions packages/compiler-cli/ngcc/BUILD.bazel
Expand Up @@ -15,6 +15,7 @@ ts_library(
"//packages/compiler-cli",
"//packages/compiler-cli:import_meta_url_types",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/annotations/common",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
Expand Down
Expand Up @@ -8,7 +8,7 @@

import ts from 'typescript';

import {readBaseClass} from '../../../src/ngtsc/annotations/src/util';
import {readBaseClass} from '../../../src/ngtsc/annotations/common';
import {Reference} from '../../../src/ngtsc/imports';
import {ClassDeclaration} from '../../../src/ngtsc/reflection';
import {HandlerFlags} from '../../../src/ngtsc/transform';
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel
Expand Up @@ -9,6 +9,10 @@ ts_library(
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations/common",
"//packages/compiler-cli/src/ngtsc/annotations/component",
"//packages/compiler-cli/src/ngtsc/annotations/directive",
"//packages/compiler-cli/src/ngtsc/annotations/ng_module",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
Expand Down
19 changes: 19 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/README.md
@@ -0,0 +1,19 @@
# What is the 'annotations' package?

This package implements compilation of Angular-annotated classes - those with `@Component`, `@NgModule`, etc. decorators. (Note that the compiler uses 'decorator' and 'annotation' interchangeably, despite them having slightly different semantics).

The 'transform' package of the compiler provides an abstraction for a `DecoratorHandler`, which defines how to compile a class decorated with a particular Angular decorator. This package implements a `DecoratorHandler` for each Angular type. The methods of these `DecoratorHandler`s then allow the rest of the compiler to process each decorated class through the phases of compilation.

# Anatomy of `DecoratorHandler`s

Each handler implemented here performs some similar operations:

* It uses the `PartialEvaluator` to resolve expressions within the decorator metadata or other decorated fields that need to be understood statically.
* It extracts information from constructors of decorated classes which is required to generate dependency injection instructions.
* It reports errors when developers have misused or misconfigured the decorators.
* It populates registries that describe decorated classes to the rest of the compiler.
* It uses those same registries to understand decorated classes within the context of the compilation (for example, to understand which dependencies are used in a given template).
* It creates `SemanticSymbol`s which allow for accurate incremental compilation when reacting to input changes.
* It builds metadata objects for `@angular/compiler` which describe the decorated classes, which can then perform the actual code generation.

Since there is significant overlap between `DecoratorHandler` implementations, much of this functionality is implemented in a shared 'common' sub-package.
23 changes: 23 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/common/BUILD.bazel
@@ -0,0 +1,23 @@
load("//tools:defaults.bzl", "ts_library")

package(default_visibility = ["//visibility:public"])

ts_library(
name = "common",
srcs = ["index.ts"] + glob([
"src/**/*.ts",
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/node",
"@npm//typescript",
],
)
9 changes: 9 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/common/README.md
@@ -0,0 +1,9 @@
# What is the 'annotations/common' package?

This package contains common code related to the processing of Angular-decorated classes by `DecoratorHandler` implementations. Some common utilities provided by this package help with:

* Static evaluation of different kinds of expressions
* Construction of various diagnostics
* Extraction of dependency injection information
* Compilation of dependency injection factories
* Extraction of raw metadata suitable for generating `setClassMetadata` calls
16 changes: 16 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/common/index.ts
@@ -0,0 +1,16 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export * from './src/api';
export * from './src/di';
export * from './src/diagnostics';
export * from './src/evaluation';
export * from './src/factory';
export * from './src/metadata';
export * from './src/references_registry';
export * from './src/util';
224 changes: 224 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/common/src/di.ts
@@ -0,0 +1,224 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Expression, LiteralExpr, R3DependencyMetadata, WrappedNodeExpr} from '@angular/compiler';
import ts from 'typescript';

import {ErrorCode, FatalDiagnosticError, makeRelatedInformation} from '../../../diagnostics';
import {ClassDeclaration, CtorParameter, Decorator, ReflectionHost, TypeValueReferenceKind, UnavailableValue, ValueUnavailableKind} from '../../../reflection';

import {isAngularCore, valueReferenceToExpression} from './util';

export type ConstructorDeps = {
deps: R3DependencyMetadata[];
}|{
deps: null;
errors: ConstructorDepError[];
};

export interface ConstructorDepError {
index: number;
param: CtorParameter;
reason: UnavailableValue;
}

export function getConstructorDependencies(
clazz: ClassDeclaration, reflector: ReflectionHost, isCore: boolean): ConstructorDeps|null {
const deps: R3DependencyMetadata[] = [];
const errors: ConstructorDepError[] = [];
let ctorParams = reflector.getConstructorParameters(clazz);
if (ctorParams === null) {
if (reflector.hasBaseClass(clazz)) {
return null;
} else {
ctorParams = [];
}
}
ctorParams.forEach((param, idx) => {
let token = valueReferenceToExpression(param.typeValueReference);
let attributeNameType: Expression|null = null;
let optional = false, self = false, skipSelf = false, host = false;

(param.decorators || []).filter(dec => isCore || isAngularCore(dec)).forEach(dec => {
const name = isCore || dec.import === null ? dec.name : dec.import!.name;
if (name === 'Inject') {
if (dec.args === null || dec.args.length !== 1) {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_ARITY_WRONG, Decorator.nodeForError(dec),
`Unexpected number of arguments to @Inject().`);
}
token = new WrappedNodeExpr(dec.args[0]);
} else if (name === 'Optional') {
optional = true;
} else if (name === 'SkipSelf') {
skipSelf = true;
} else if (name === 'Self') {
self = true;
} else if (name === 'Host') {
host = true;
} else if (name === 'Attribute') {
if (dec.args === null || dec.args.length !== 1) {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_ARITY_WRONG, Decorator.nodeForError(dec),
`Unexpected number of arguments to @Attribute().`);
}
const attributeName = dec.args[0];
token = new WrappedNodeExpr(attributeName);
if (ts.isStringLiteralLike(attributeName)) {
attributeNameType = new LiteralExpr(attributeName.text);
} else {
attributeNameType =
new WrappedNodeExpr(ts.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword));
}
} else {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_UNEXPECTED, Decorator.nodeForError(dec),
`Unexpected decorator ${name} on parameter.`);
}
});

if (token === null) {
if (param.typeValueReference.kind !== TypeValueReferenceKind.UNAVAILABLE) {
throw new Error(
'Illegal state: expected value reference to be unavailable if no token is present');
}
errors.push({
index: idx,
param,
reason: param.typeValueReference.reason,
});
} else {
deps.push({token, attributeNameType, optional, self, skipSelf, host});
}
});
if (errors.length === 0) {
return {deps};
} else {
return {deps: null, errors};
}
}


/**
* Convert `ConstructorDeps` into the `R3DependencyMetadata` array for those deps if they're valid,
* or into an `'invalid'` signal if they're not.
*
* This is a companion function to `validateConstructorDependencies` which accepts invalid deps.
*/
export function unwrapConstructorDependencies(deps: ConstructorDeps|null): R3DependencyMetadata[]|
'invalid'|null {
if (deps === null) {
return null;
} else if (deps.deps !== null) {
// These constructor dependencies are valid.
return deps.deps;
} else {
// These deps are invalid.
return 'invalid';
}
}

export function getValidConstructorDependencies(
clazz: ClassDeclaration, reflector: ReflectionHost, isCore: boolean): R3DependencyMetadata[]|
null {
return validateConstructorDependencies(
clazz, getConstructorDependencies(clazz, reflector, isCore));
}

/**
* Validate that `ConstructorDeps` does not have any invalid dependencies and convert them into the
* `R3DependencyMetadata` array if so, or raise a diagnostic if some deps are invalid.
*
* This is a companion function to `unwrapConstructorDependencies` which does not accept invalid
* deps.
*/
export function validateConstructorDependencies(
clazz: ClassDeclaration, deps: ConstructorDeps|null): R3DependencyMetadata[]|null {
if (deps === null) {
return null;
} else if (deps.deps !== null) {
return deps.deps;
} else {
// TODO(alxhub): this cast is necessary because the g3 typescript version doesn't narrow here.
// There is at least one error.
const error = (deps as {errors: ConstructorDepError[]}).errors[0];
throw createUnsuitableInjectionTokenError(clazz, error);
}
}

/**
* Creates a fatal error with diagnostic for an invalid injection token.
* @param clazz The class for which the injection token was unavailable.
* @param error The reason why no valid injection token is available.
*/
function createUnsuitableInjectionTokenError(
clazz: ClassDeclaration, error: ConstructorDepError): FatalDiagnosticError {
const {param, index, reason} = error;
let chainMessage: string|undefined = undefined;
let hints: ts.DiagnosticRelatedInformation[]|undefined = undefined;
switch (reason.kind) {
case ValueUnavailableKind.UNSUPPORTED:
chainMessage = 'Consider using the @Inject decorator to specify an injection token.';
hints = [
makeRelatedInformation(reason.typeNode, 'This type is not supported as injection token.'),
];
break;
case ValueUnavailableKind.NO_VALUE_DECLARATION:
chainMessage = 'Consider using the @Inject decorator to specify an injection token.';
hints = [
makeRelatedInformation(
reason.typeNode,
'This type does not have a value, so it cannot be used as injection token.'),
];
if (reason.decl !== null) {
hints.push(makeRelatedInformation(reason.decl, 'The type is declared here.'));
}
break;
case ValueUnavailableKind.TYPE_ONLY_IMPORT:
chainMessage =
'Consider changing the type-only import to a regular import, or use the @Inject decorator to specify an injection token.';
hints = [
makeRelatedInformation(
reason.typeNode,
'This type is imported using a type-only import, which prevents it from being usable as an injection token.'),
makeRelatedInformation(reason.node, 'The type-only import occurs here.'),
];
break;
case ValueUnavailableKind.NAMESPACE:
chainMessage = 'Consider using the @Inject decorator to specify an injection token.';
hints = [
makeRelatedInformation(
reason.typeNode,
'This type corresponds with a namespace, which cannot be used as injection token.'),
makeRelatedInformation(reason.importClause, 'The namespace import occurs here.'),
];
break;
case ValueUnavailableKind.UNKNOWN_REFERENCE:
chainMessage = 'The type should reference a known declaration.';
hints = [makeRelatedInformation(reason.typeNode, 'This type could not be resolved.')];
break;
case ValueUnavailableKind.MISSING_TYPE:
chainMessage =
'Consider adding a type to the parameter or use the @Inject decorator to specify an injection token.';
break;
}

const chain: ts.DiagnosticMessageChain = {
messageText: `No suitable injection token for parameter '${param.name || index}' of class '${
clazz.name.text}'.`,
category: ts.DiagnosticCategory.Error,
code: 0,
next: [{
messageText: chainMessage,
category: ts.DiagnosticCategory.Message,
code: 0,
}],
};

return new FatalDiagnosticError(ErrorCode.PARAM_MISSING_TOKEN, param.nameNode, chain, hints);
}
Expand Up @@ -8,15 +8,46 @@

import ts from 'typescript';

import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics';
import {Reference} from '../../imports';
import {InjectableClassRegistry, MetadataReader} from '../../metadata';
import {describeResolvedType, DynamicValue, PartialEvaluator, ResolvedValue, traceDynamicValue} from '../../partial_evaluator';
import {ClassDeclaration, ReflectionHost} from '../../reflection';
import {LocalModuleScopeRegistry} from '../../scope';
import {identifierOfNode} from '../../util/src/typescript';

import {makeDuplicateDeclarationError, readBaseClass} from './util';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics';
import {Reference} from '../../../imports';
import {InjectableClassRegistry, MetadataReader} from '../../../metadata';
import {describeResolvedType, DynamicValue, PartialEvaluator, ResolvedValue, traceDynamicValue} from '../../../partial_evaluator';
import {ClassDeclaration, ReflectionHost} from '../../../reflection';
import {DeclarationData, LocalModuleScopeRegistry} from '../../../scope';
import {identifierOfNode} from '../../../util/src/typescript';

import {readBaseClass} from './util';


/**
* Create a `ts.Diagnostic` which indicates the given class is part of the declarations of two or
* more NgModules.
*
* The resulting `ts.Diagnostic` will have a context entry for each NgModule showing the point where
* the directive/pipe exists in its `declarations` (if possible).
*/
export function makeDuplicateDeclarationError(
node: ClassDeclaration, data: DeclarationData[], kind: string): ts.Diagnostic {
const context: ts.DiagnosticRelatedInformation[] = [];
for (const decl of data) {
if (decl.rawDeclarations === null) {
continue;
}
// Try to find the reference to the declaration within the declarations array, to hang the
// error there. If it can't be found, fall back on using the NgModule's name.
const contextNode = decl.ref.getOriginForDiagnostics(decl.rawDeclarations, decl.ngModule.name);
context.push(makeRelatedInformation(
contextNode,
`'${node.name.text}' is listed in the declarations of the NgModule '${
decl.ngModule.name.text}'.`));
}

// Finally, produce the diagnostic.
return makeDiagnostic(
ErrorCode.NGMODULE_DECLARATION_NOT_UNIQUE, node.name,
`The ${kind} '${node.name.text}' is declared by more than one NgModule.`, context);
}


/**
* Creates a `FatalDiagnosticError` for a node that did not evaluate to the expected type. The
Expand Down

0 comments on commit cc0d73d

Please sign in to comment.