refactor(compiler-cli): carry type information into AST value types (#39961)
This change allows the `AstObject` and `AstValue` types to provide their represented type as a generic type argument, which is helpful for documentation and discoverability purposes. PR Close #39961
This commit is contained in:
parent
5fa026fd81
commit
c0bccc39db
|
@ -9,18 +9,60 @@ import * as o from '@angular/compiler';
|
|||
import {FatalLinkerError} from '../fatal_linker_error';
|
||||
import {AstHost, Range} from './ast_host';
|
||||
|
||||
/**
|
||||
* Represents only those types in `T` that are object types.
|
||||
*/
|
||||
type ObjectType<T> = Extract<T, object>;
|
||||
|
||||
/**
|
||||
* Represents the value type of an object literal.
|
||||
*/
|
||||
type ObjectValueType<T> = T extends Record<string, infer R>? R : never;
|
||||
|
||||
/**
|
||||
* Represents the value type of an array literal.
|
||||
*/
|
||||
type ArrayValueType<T> = T extends Array<infer R>? R : never;
|
||||
|
||||
/**
|
||||
* Ensures that `This` has its generic type `Actual` conform to the expected generic type in
|
||||
* `Expected`, to disallow calling a method if the generic type does not conform.
|
||||
*/
|
||||
type ConformsTo<This, Actual, Expected> = Actual extends Expected ? This : never;
|
||||
|
||||
/**
|
||||
* Ensures that `This` is an `AstValue` whose generic type conforms to `Expected`, to disallow
|
||||
* calling a method if the value's type does not conform.
|
||||
*/
|
||||
type HasValueType<This, Expected> =
|
||||
This extends AstValue<infer Actual, any>? ConformsTo<This, Actual, Expected>: never;
|
||||
|
||||
/**
|
||||
* Represents only the string keys of type `T`.
|
||||
*/
|
||||
type PropertyKey<T> = keyof T&string;
|
||||
|
||||
/**
|
||||
* This helper class wraps an object expression along with an `AstHost` object, exposing helper
|
||||
* methods that make it easier to extract the properties of the object.
|
||||
*
|
||||
* The generic `T` is used as reference type of the expected structure that is represented by this
|
||||
* object. It does not achieve full type-safety for the provided operations in correspondence with
|
||||
* `T`; its main goal is to provide references to a documented type and ensure that the properties
|
||||
* that are read from the object are present.
|
||||
*
|
||||
* Unfortunately, the generic types are unable to prevent reading an optional property from the
|
||||
* object without first having called `has` to ensure that the property exists. This is one example
|
||||
* of where full type-safety is not achieved.
|
||||
*/
|
||||
export class AstObject<TExpression> {
|
||||
export class AstObject<T extends object, TExpression> {
|
||||
/**
|
||||
* Create a new `AstObject` from the given `expression` and `host`.
|
||||
*/
|
||||
static parse<TExpression>(expression: TExpression, host: AstHost<TExpression>):
|
||||
AstObject<TExpression> {
|
||||
static parse<T extends object, TExpression>(expression: TExpression, host: AstHost<TExpression>):
|
||||
AstObject<T, TExpression> {
|
||||
const obj = host.parseObjectLiteral(expression);
|
||||
return new AstObject<TExpression>(expression, obj, host);
|
||||
return new AstObject(expression, obj, host);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
|
@ -30,7 +72,7 @@ export class AstObject<TExpression> {
|
|||
/**
|
||||
* Returns true if the object has a property called `propertyName`.
|
||||
*/
|
||||
has(propertyName: string): boolean {
|
||||
has(propertyName: PropertyKey<T>): boolean {
|
||||
return this.obj.has(propertyName);
|
||||
}
|
||||
|
||||
|
@ -39,7 +81,8 @@ export class AstObject<TExpression> {
|
|||
*
|
||||
* Throws an error if there is no such property or the property is not a number.
|
||||
*/
|
||||
getNumber(propertyName: string): number {
|
||||
getNumber<K extends PropertyKey<T>>(this: ConformsTo<this, T[K], number>, propertyName: K):
|
||||
number {
|
||||
return this.host.parseNumericLiteral(this.getRequiredProperty(propertyName));
|
||||
}
|
||||
|
||||
|
@ -48,7 +91,8 @@ export class AstObject<TExpression> {
|
|||
*
|
||||
* Throws an error if there is no such property or the property is not a string.
|
||||
*/
|
||||
getString(propertyName: string): string {
|
||||
getString<K extends PropertyKey<T>>(this: ConformsTo<this, T[K], string>, propertyName: K):
|
||||
string {
|
||||
return this.host.parseStringLiteral(this.getRequiredProperty(propertyName));
|
||||
}
|
||||
|
||||
|
@ -57,8 +101,9 @@ export class AstObject<TExpression> {
|
|||
*
|
||||
* Throws an error if there is no such property or the property is not a boolean.
|
||||
*/
|
||||
getBoolean(propertyName: string): boolean {
|
||||
return this.host.parseBooleanLiteral(this.getRequiredProperty(propertyName));
|
||||
getBoolean<K extends PropertyKey<T>>(this: ConformsTo<this, T[K], boolean>, propertyName: K):
|
||||
boolean {
|
||||
return this.host.parseBooleanLiteral(this.getRequiredProperty(propertyName)) as any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,7 +111,8 @@ export class AstObject<TExpression> {
|
|||
*
|
||||
* Throws an error if there is no such property or the property is not an object.
|
||||
*/
|
||||
getObject(propertyName: string): AstObject<TExpression> {
|
||||
getObject<K extends PropertyKey<T>>(this: ConformsTo<this, T[K], object>, propertyName: K):
|
||||
AstObject<ObjectType<T[K]>, TExpression> {
|
||||
const expr = this.getRequiredProperty(propertyName);
|
||||
const obj = this.host.parseObjectLiteral(expr);
|
||||
return new AstObject(expr, obj, this.host);
|
||||
|
@ -77,7 +123,8 @@ export class AstObject<TExpression> {
|
|||
*
|
||||
* Throws an error if there is no such property or the property is not an array.
|
||||
*/
|
||||
getArray(propertyName: string): AstValue<TExpression>[] {
|
||||
getArray<K extends PropertyKey<T>>(this: ConformsTo<this, T[K], unknown[]>, propertyName: K):
|
||||
AstValue<ArrayValueType<T[K]>, TExpression>[] {
|
||||
const arr = this.host.parseArrayLiteral(this.getRequiredProperty(propertyName));
|
||||
return arr.map(entry => new AstValue(entry, this.host));
|
||||
}
|
||||
|
@ -88,7 +135,7 @@ export class AstObject<TExpression> {
|
|||
*
|
||||
* Throws an error if there is no such property.
|
||||
*/
|
||||
getOpaque(propertyName: string): o.WrappedNodeExpr<TExpression> {
|
||||
getOpaque(propertyName: PropertyKey<T>): o.WrappedNodeExpr<TExpression> {
|
||||
return new o.WrappedNodeExpr(this.getRequiredProperty(propertyName));
|
||||
}
|
||||
|
||||
|
@ -97,7 +144,7 @@ export class AstObject<TExpression> {
|
|||
*
|
||||
* Throws an error if there is no such property.
|
||||
*/
|
||||
getNode(propertyName: string): TExpression {
|
||||
getNode(propertyName: PropertyKey<T>): TExpression {
|
||||
return this.getRequiredProperty(propertyName);
|
||||
}
|
||||
|
||||
|
@ -106,7 +153,7 @@ export class AstObject<TExpression> {
|
|||
*
|
||||
* Throws an error if there is no such property.
|
||||
*/
|
||||
getValue(propertyName: string): AstValue<TExpression> {
|
||||
getValue<K extends PropertyKey<T>>(propertyName: K): AstValue<T[K], TExpression> {
|
||||
return new AstValue(this.getRequiredProperty(propertyName), this.host);
|
||||
}
|
||||
|
||||
|
@ -114,8 +161,8 @@ export class AstObject<TExpression> {
|
|||
* Converts the AstObject to a raw JavaScript object, mapping each property value (as an
|
||||
* `AstValue`) to the generic type (`T`) via the `mapper` function.
|
||||
*/
|
||||
toLiteral<T>(mapper: (value: AstValue<TExpression>) => T): {[key: string]: T} {
|
||||
const result: {[key: string]: T} = {};
|
||||
toLiteral<V>(mapper: (value: AstValue<ObjectValueType<T>, TExpression>) => V): Record<string, V> {
|
||||
const result: Record<string, V> = {};
|
||||
for (const [key, expression] of this.obj) {
|
||||
result[key] = mapper(new AstValue(expression, this.host));
|
||||
}
|
||||
|
@ -126,15 +173,15 @@ export class AstObject<TExpression> {
|
|||
* Converts the AstObject to a JavaScript Map, mapping each property value (as an
|
||||
* `AstValue`) to the generic type (`T`) via the `mapper` function.
|
||||
*/
|
||||
toMap<T>(mapper: (value: AstValue<TExpression>) => T): Map<string, T> {
|
||||
const result = new Map<string, T>();
|
||||
toMap<V>(mapper: (value: AstValue<ObjectValueType<T>, TExpression>) => V): Map<string, V> {
|
||||
const result = new Map<string, V>();
|
||||
for (const [key, expression] of this.obj) {
|
||||
result.set(key, mapper(new AstValue(expression, this.host)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private getRequiredProperty(propertyName: string): TExpression {
|
||||
private getRequiredProperty(propertyName: PropertyKey<T>): TExpression {
|
||||
if (!this.obj.has(propertyName)) {
|
||||
throw new FatalLinkerError(
|
||||
this.expression, `Expected property '${propertyName}' to be present.`);
|
||||
|
@ -146,8 +193,12 @@ export class AstObject<TExpression> {
|
|||
/**
|
||||
* This helper class wraps an `expression`, exposing methods that use the `host` to give
|
||||
* access to the underlying value of the wrapped expression.
|
||||
*
|
||||
* The generic `T` is used as reference type of the expected type that is represented by this value.
|
||||
* It does not achieve full type-safety for the provided operations in correspondence with `T`; its
|
||||
* main goal is to provide references to a documented type.
|
||||
*/
|
||||
export class AstValue<TExpression> {
|
||||
export class AstValue<T, TExpression> {
|
||||
constructor(readonly expression: TExpression, private host: AstHost<TExpression>) {}
|
||||
|
||||
/**
|
||||
|
@ -168,7 +219,7 @@ export class AstValue<TExpression> {
|
|||
/**
|
||||
* Parse the number from this value, or error if it is not a number.
|
||||
*/
|
||||
getNumber(): number {
|
||||
getNumber(this: HasValueType<this, number>): number {
|
||||
return this.host.parseNumericLiteral(this.expression);
|
||||
}
|
||||
|
||||
|
@ -182,7 +233,7 @@ export class AstValue<TExpression> {
|
|||
/**
|
||||
* Parse the string from this value, or error if it is not a string.
|
||||
*/
|
||||
getString(): string {
|
||||
getString(this: HasValueType<this, string>): string {
|
||||
return this.host.parseStringLiteral(this.expression);
|
||||
}
|
||||
|
||||
|
@ -196,7 +247,7 @@ export class AstValue<TExpression> {
|
|||
/**
|
||||
* Parse the boolean from this value, or error if it is not a boolean.
|
||||
*/
|
||||
getBoolean(): boolean {
|
||||
getBoolean(this: HasValueType<this, boolean>): boolean {
|
||||
return this.host.parseBooleanLiteral(this.expression);
|
||||
}
|
||||
|
||||
|
@ -210,7 +261,7 @@ export class AstValue<TExpression> {
|
|||
/**
|
||||
* Parse this value into an `AstObject`, or error if it is not an object literal.
|
||||
*/
|
||||
getObject(): AstObject<TExpression> {
|
||||
getObject(this: HasValueType<this, object>): AstObject<ObjectType<T>, TExpression> {
|
||||
return AstObject.parse(this.expression, this.host);
|
||||
}
|
||||
|
||||
|
@ -224,7 +275,7 @@ export class AstValue<TExpression> {
|
|||
/**
|
||||
* Parse this value into an array of `AstValue` objects, or error if it is not an array literal.
|
||||
*/
|
||||
getArray(): AstValue<TExpression>[] {
|
||||
getArray(this: HasValueType<this, unknown[]>): AstValue<ArrayValueType<T>, TExpression>[] {
|
||||
const arr = this.host.parseArrayLiteral(this.expression);
|
||||
return arr.map(entry => new AstValue(entry, this.host));
|
||||
}
|
||||
|
@ -240,7 +291,7 @@ export class AstValue<TExpression> {
|
|||
* Extract the return value as an `AstValue` from this value as a function expression, or error if
|
||||
* it is not a function expression.
|
||||
*/
|
||||
getFunctionReturnValue(): AstValue<TExpression> {
|
||||
getFunctionReturnValue<R>(this: HasValueType<this, Function>): AstValue<R, TExpression> {
|
||||
return new AstValue(this.host.parseReturnValue(this.expression), this.host);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 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 {R3PartialDeclaration} from '@angular/compiler';
|
||||
import {AstObject} from '../ast/ast_value';
|
||||
import {DeclarationScope} from './declaration_scope';
|
||||
import {EmitScope} from './emit_scopes/emit_scope';
|
||||
|
@ -53,7 +54,8 @@ export class FileLinker<TConstantScope, TStatement, TExpression> {
|
|||
args.length}.`);
|
||||
}
|
||||
|
||||
const metaObj = AstObject.parse(args[0], this.linkerEnvironment.host);
|
||||
const metaObj =
|
||||
AstObject.parse<R3PartialDeclaration, TExpression>(args[0], this.linkerEnvironment.host);
|
||||
const ngImport = metaObj.getNode('ngImport');
|
||||
const emitScope = this.getEmitScope(ngImport, declarationScope);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 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 {compileComponentFromMetadata, ConstantPool, DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig, makeBindingParser, parseTemplate, R3ComponentMetadata, R3UsedDirectiveMetadata} from '@angular/compiler';
|
||||
import {compileComponentFromMetadata, ConstantPool, DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig, makeBindingParser, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3PartialDeclaration, R3UsedDirectiveMetadata} from '@angular/compiler';
|
||||
import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/compiler/src/core';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
|
||||
|
@ -25,7 +25,7 @@ export class PartialComponentLinkerVersion1<TExpression> implements PartialLinke
|
|||
|
||||
linkPartialDeclaration(
|
||||
sourceUrl: string, code: string, constantPool: ConstantPool,
|
||||
metaObj: AstObject<TExpression>): o.Expression {
|
||||
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
|
||||
const meta = toR3ComponentMeta(metaObj, code, sourceUrl, this.options);
|
||||
const def = compileComponentFromMetadata(meta, constantPool, makeBindingParser());
|
||||
return def.expression;
|
||||
|
@ -36,7 +36,7 @@ export class PartialComponentLinkerVersion1<TExpression> implements PartialLinke
|
|||
* This function derives the `R3ComponentMetadata` from the provided AST object.
|
||||
*/
|
||||
export function toR3ComponentMeta<TExpression>(
|
||||
metaObj: AstObject<TExpression>, code: string, sourceUrl: string,
|
||||
metaObj: AstObject<R3DeclareComponentMetadata, TExpression>, code: string, sourceUrl: string,
|
||||
options: LinkerOptions): R3ComponentMetadata {
|
||||
let interpolation = DEFAULT_INTERPOLATION_CONFIG;
|
||||
if (metaObj.has('interpolation')) {
|
||||
|
@ -133,7 +133,8 @@ export function toR3ComponentMeta<TExpression>(
|
|||
/**
|
||||
* Determines the `ViewEncapsulation` mode from the AST value's symbol name.
|
||||
*/
|
||||
function parseEncapsulation<TExpression>(encapsulation: AstValue<TExpression>): ViewEncapsulation {
|
||||
function parseEncapsulation<TExpression>(encapsulation: AstValue<ViewEncapsulation, TExpression>):
|
||||
ViewEncapsulation {
|
||||
const symbolName = encapsulation.getSymbolName();
|
||||
if (symbolName === null) {
|
||||
throw new FatalLinkerError(
|
||||
|
@ -149,7 +150,8 @@ function parseEncapsulation<TExpression>(encapsulation: AstValue<TExpression>):
|
|||
/**
|
||||
* Determines the `ChangeDetectionStrategy` from the AST value's symbol name.
|
||||
*/
|
||||
function parseChangeDetectionStrategy<TExpression>(changeDetectionStrategy: AstValue<TExpression>):
|
||||
function parseChangeDetectionStrategy<TExpression>(
|
||||
changeDetectionStrategy: AstValue<ChangeDetectionStrategy, TExpression>):
|
||||
ChangeDetectionStrategy {
|
||||
const symbolName = changeDetectionStrategy.getSymbolName();
|
||||
if (symbolName === null) {
|
||||
|
@ -168,7 +170,8 @@ function parseChangeDetectionStrategy<TExpression>(changeDetectionStrategy: AstV
|
|||
/**
|
||||
* Update the range to remove the start and end chars, which should be quotes around the template.
|
||||
*/
|
||||
function getTemplateRange<TExpression>(templateNode: AstValue<TExpression>, code: string): Range {
|
||||
function getTemplateRange<TExpression>(
|
||||
templateNode: AstValue<unknown, TExpression>, code: string): Range {
|
||||
const {startPos, endPos, startLine, startCol} = templateNode.getRange();
|
||||
|
||||
if (!/["'`]/.test(code[startPos]) || code[startPos] !== code[endPos - 1]) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 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 {compileDirectiveFromMetadata, ConstantPool, makeBindingParser, ParseLocation, ParseSourceFile, ParseSourceSpan, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata, R3Reference} from '@angular/compiler';
|
||||
import {compileDirectiveFromMetadata, ConstantPool, makeBindingParser, ParseLocation, ParseSourceFile, ParseSourceSpan, R3DeclareDirectiveMetadata, R3DeclareQueryMetadata, R3DirectiveMetadata, R3HostMetadata, R3PartialDeclaration, R3QueryMetadata, R3Reference} from '@angular/compiler';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
|
||||
import {Range} from '../../ast/ast_host';
|
||||
|
@ -20,7 +20,7 @@ import {PartialLinker} from './partial_linker';
|
|||
export class PartialDirectiveLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
|
||||
linkPartialDeclaration(
|
||||
sourceUrl: string, code: string, constantPool: ConstantPool,
|
||||
metaObj: AstObject<TExpression>): o.Expression {
|
||||
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
|
||||
const meta = toR3DirectiveMeta(metaObj, code, sourceUrl);
|
||||
const def = compileDirectiveFromMetadata(meta, constantPool, makeBindingParser());
|
||||
return def.expression;
|
||||
|
@ -31,7 +31,8 @@ export class PartialDirectiveLinkerVersion1<TExpression> implements PartialLinke
|
|||
* Derives the `R3DirectiveMetadata` structure from the AST object.
|
||||
*/
|
||||
export function toR3DirectiveMeta<TExpression>(
|
||||
metaObj: AstObject<TExpression>, code: string, sourceUrl: string): R3DirectiveMetadata {
|
||||
metaObj: AstObject<R3DeclareDirectiveMetadata, TExpression>, code: string,
|
||||
sourceUrl: string): R3DirectiveMetadata {
|
||||
const typeExpr = metaObj.getValue('type');
|
||||
const typeName = typeExpr.getSymbolName();
|
||||
if (typeName === null) {
|
||||
|
@ -73,7 +74,8 @@ export function toR3DirectiveMeta<TExpression>(
|
|||
/**
|
||||
* Decodes the AST value for a single input to its representation as used in the metadata.
|
||||
*/
|
||||
function toInputMapping<TExpression>(value: AstValue<TExpression>): string|[string, string] {
|
||||
function toInputMapping<TExpression>(value: AstValue<string|[string, string], TExpression>):
|
||||
string|[string, string] {
|
||||
if (value.isString()) {
|
||||
return value.getString();
|
||||
}
|
||||
|
@ -90,7 +92,8 @@ function toInputMapping<TExpression>(value: AstValue<TExpression>): string|[stri
|
|||
/**
|
||||
* Extracts the host metadata configuration from the AST metadata object.
|
||||
*/
|
||||
function toHostMetadata<TExpression>(metaObj: AstObject<TExpression>): R3HostMetadata {
|
||||
function toHostMetadata<TExpression>(metaObj: AstObject<R3DeclareDirectiveMetadata, TExpression>):
|
||||
R3HostMetadata {
|
||||
if (!metaObj.has('host')) {
|
||||
return {
|
||||
attributes: {},
|
||||
|
@ -127,7 +130,8 @@ function toHostMetadata<TExpression>(metaObj: AstObject<TExpression>): R3HostMet
|
|||
/**
|
||||
* Extracts the metadata for a single query from an AST object.
|
||||
*/
|
||||
function toQueryMetadata<TExpression>(obj: AstObject<TExpression>): R3QueryMetadata {
|
||||
function toQueryMetadata<TExpression>(obj: AstObject<R3DeclareQueryMetadata, TExpression>):
|
||||
R3QueryMetadata {
|
||||
let predicate: R3QueryMetadata['predicate'];
|
||||
const predicateExpr = obj.getValue('predicate');
|
||||
if (predicateExpr.isArray()) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 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 {ConstantPool} from '@angular/compiler';
|
||||
import {ConstantPool, R3PartialDeclaration} from '@angular/compiler';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
import {AstObject} from '../../ast/ast_value';
|
||||
|
||||
|
@ -21,5 +21,5 @@ export interface PartialLinker<TExpression> {
|
|||
*/
|
||||
linkPartialDeclaration(
|
||||
sourceUrl: string, code: string, constantPool: ConstantPool,
|
||||
metaObj: AstObject<TExpression>): o.Expression;
|
||||
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression;
|
||||
}
|
||||
|
|
|
@ -9,10 +9,20 @@ import {WrappedNodeExpr} from '@angular/compiler';
|
|||
import {TypeScriptAstFactory} from '@angular/compiler-cli/src/ngtsc/translator';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AstHost} from '../../src/ast/ast_host';
|
||||
import {AstObject, AstValue} from '../../src/ast/ast_value';
|
||||
import {TypeScriptAstHost} from '../../src/ast/typescript/typescript_ast_host';
|
||||
|
||||
const host = new TypeScriptAstHost();
|
||||
interface TestObject {
|
||||
a: number;
|
||||
b: string;
|
||||
c: boolean;
|
||||
d: {x: number; y: string};
|
||||
e: number[];
|
||||
missing: unknown;
|
||||
}
|
||||
|
||||
const host: AstHost<ts.Expression> = new TypeScriptAstHost();
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const nestedObj = factory.createObjectLiteral([
|
||||
{propertyName: 'x', quoted: false, value: factory.createLiteral(42)},
|
||||
|
@ -20,7 +30,7 @@ const nestedObj = factory.createObjectLiteral([
|
|||
]);
|
||||
const nestedArray =
|
||||
factory.createArrayLiteral([factory.createLiteral(1), factory.createLiteral(2)]);
|
||||
const obj = AstObject.parse(
|
||||
const obj = AstObject.parse<TestObject, ts.Expression>(
|
||||
factory.createObjectLiteral([
|
||||
{propertyName: 'a', quoted: false, value: factory.createLiteral(42)},
|
||||
{propertyName: 'b', quoted: false, value: factory.createLiteral('X')},
|
||||
|
@ -35,7 +45,10 @@ describe('AstObject', () => {
|
|||
it('should return true if the property exists on the object', () => {
|
||||
expect(obj.has('a')).toBe(true);
|
||||
expect(obj.has('b')).toBe(true);
|
||||
expect(obj.has('z')).toBe(false);
|
||||
expect(obj.has('missing')).toBe(false);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(obj.has('x')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -45,6 +58,7 @@ describe('AstObject', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the property is not a number', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => obj.getNumber('b'))
|
||||
.toThrowError('Unsupported syntax, expected a numeric literal.');
|
||||
});
|
||||
|
@ -56,6 +70,7 @@ describe('AstObject', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the property is not a string', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => obj.getString('a'))
|
||||
.toThrowError('Unsupported syntax, expected a string literal.');
|
||||
});
|
||||
|
@ -67,6 +82,7 @@ describe('AstObject', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the property is not a boolean', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => obj.getBoolean('b'))
|
||||
.toThrowError('Unsupported syntax, expected a boolean literal.');
|
||||
});
|
||||
|
@ -78,6 +94,7 @@ describe('AstObject', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the property is not an object expression', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => obj.getObject('b'))
|
||||
.toThrowError('Unsupported syntax, expected an object literal.');
|
||||
});
|
||||
|
@ -93,6 +110,7 @@ describe('AstObject', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the property is not an array of expressions', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => obj.getArray('b'))
|
||||
.toThrowError('Unsupported syntax, expected an array literal.');
|
||||
});
|
||||
|
@ -105,7 +123,11 @@ describe('AstObject', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the property does not exist', () => {
|
||||
expect(() => obj.getOpaque('x')).toThrowError('Expected property \'x\' to be present.');
|
||||
expect(() => obj.getOpaque('missing'))
|
||||
.toThrowError(`Expected property 'missing' to be present.`);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(() => obj.getOpaque('x')).toThrowError(`Expected property 'x' to be present.`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -115,7 +137,11 @@ describe('AstObject', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the property does not exist', () => {
|
||||
expect(() => obj.getNode('x')).toThrowError('Expected property \'x\' to be present.');
|
||||
expect(() => obj.getNode('missing'))
|
||||
.toThrowError(`Expected property 'missing' to be present.`);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(() => obj.getNode('x')).toThrowError(`Expected property 'x' to be present.`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -126,7 +152,11 @@ describe('AstObject', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the property does not exist', () => {
|
||||
expect(() => obj.getValue('x')).toThrowError('Expected property \'x\' to be present.');
|
||||
expect(() => obj.getValue('missing'))
|
||||
.toThrowError(`Expected property 'missing' to be present.`);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(() => obj.getValue('x')).toThrowError(`Expected property 'x' to be present.`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -156,126 +186,136 @@ describe('AstObject', () => {
|
|||
});
|
||||
|
||||
describe('AstValue', () => {
|
||||
function createAstValue<T>(node: ts.Expression): AstValue<T, ts.Expression> {
|
||||
return new AstValue<T, ts.Expression>(node, host);
|
||||
}
|
||||
|
||||
describe('getSymbolName', () => {
|
||||
it('should return the name of an identifier', () => {
|
||||
expect(new AstValue(factory.createIdentifier('Foo'), host).getSymbolName()).toEqual('Foo');
|
||||
expect(createAstValue(factory.createIdentifier('Foo')).getSymbolName()).toEqual('Foo');
|
||||
});
|
||||
|
||||
it('should return the name of a property access', () => {
|
||||
const propertyAccess = factory.createPropertyAccess(
|
||||
factory.createIdentifier('Foo'), factory.createIdentifier('Bar'));
|
||||
expect(new AstValue(propertyAccess, host).getSymbolName()).toEqual('Bar');
|
||||
expect(createAstValue(propertyAccess).getSymbolName()).toEqual('Bar');
|
||||
});
|
||||
|
||||
it('should return null if no symbol name is available', () => {
|
||||
expect(new AstValue(factory.createLiteral('a'), host).getSymbolName()).toBeNull();
|
||||
expect(createAstValue(factory.createLiteral('a')).getSymbolName()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNumber', () => {
|
||||
it('should return true if the value is a number', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isNumber()).toEqual(true);
|
||||
expect(createAstValue(factory.createLiteral(42)).isNumber()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a number', () => {
|
||||
expect(new AstValue(factory.createLiteral('a'), host).isNumber()).toEqual(false);
|
||||
expect(createAstValue(factory.createLiteral('a')).isNumber()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNumber', () => {
|
||||
it('should return the number value of the AstValue', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).getNumber()).toEqual(42);
|
||||
expect(createAstValue<number>(factory.createLiteral(42)).getNumber()).toEqual(42);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a number', () => {
|
||||
expect(() => new AstValue(factory.createLiteral('a'), host).getNumber())
|
||||
// @ts-expect-error
|
||||
expect(() => createAstValue<string>(factory.createLiteral('a')).getNumber())
|
||||
.toThrowError('Unsupported syntax, expected a numeric literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isString', () => {
|
||||
it('should return true if the value is a string', () => {
|
||||
expect(new AstValue(factory.createLiteral('a'), host).isString()).toEqual(true);
|
||||
expect(createAstValue(factory.createLiteral('a')).isString()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a string', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isString()).toEqual(false);
|
||||
expect(createAstValue(factory.createLiteral(42)).isString()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getString', () => {
|
||||
it('should return the string value of the AstValue', () => {
|
||||
expect(new AstValue(factory.createLiteral('X'), host).getString()).toEqual('X');
|
||||
expect(createAstValue<string>(factory.createLiteral('X')).getString()).toEqual('X');
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a string', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getString())
|
||||
// @ts-expect-error
|
||||
expect(() => createAstValue<number>(factory.createLiteral(42)).getString())
|
||||
.toThrowError('Unsupported syntax, expected a string literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBoolean', () => {
|
||||
it('should return true if the value is a boolean', () => {
|
||||
expect(new AstValue(factory.createLiteral(true), host).isBoolean()).toEqual(true);
|
||||
expect(createAstValue(factory.createLiteral(true)).isBoolean()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a boolean', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isBoolean()).toEqual(false);
|
||||
expect(createAstValue(factory.createLiteral(42)).isBoolean()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBoolean', () => {
|
||||
it('should return the boolean value of the AstValue', () => {
|
||||
expect(new AstValue(factory.createLiteral(true), host).getBoolean()).toEqual(true);
|
||||
expect(createAstValue<boolean>(factory.createLiteral(true)).getBoolean()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a boolean', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getBoolean())
|
||||
// @ts-expect-error
|
||||
expect(() => createAstValue<number>(factory.createLiteral(42)).getBoolean())
|
||||
.toThrowError('Unsupported syntax, expected a boolean literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isObject', () => {
|
||||
it('should return true if the value is an object literal', () => {
|
||||
expect(new AstValue(nestedObj, host).isObject()).toEqual(true);
|
||||
expect(createAstValue(nestedObj).isObject()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not an object literal', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isObject()).toEqual(false);
|
||||
expect(createAstValue(factory.createLiteral(42)).isObject()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getObject', () => {
|
||||
it('should return the AstObject value of the AstValue', () => {
|
||||
expect(new AstValue(nestedObj, host).getObject()).toEqual(AstObject.parse(nestedObj, host));
|
||||
expect(createAstValue<object>(nestedObj).getObject())
|
||||
.toEqual(AstObject.parse(nestedObj, host));
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not an object literal', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getObject())
|
||||
// @ts-expect-error
|
||||
expect(() => createAstValue<number>(factory.createLiteral(42)).getObject())
|
||||
.toThrowError('Unsupported syntax, expected an object literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isArray', () => {
|
||||
it('should return true if the value is an array literal', () => {
|
||||
expect(new AstValue(nestedArray, host).isArray()).toEqual(true);
|
||||
expect(createAstValue(nestedArray).isArray()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not an object literal', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isArray()).toEqual(false);
|
||||
expect(createAstValue(factory.createLiteral(42)).isArray()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArray', () => {
|
||||
it('should return an array of AstValue objects from the AstValue', () => {
|
||||
expect(new AstValue(nestedArray, host).getArray()).toEqual([
|
||||
new AstValue(factory.createLiteral(1), host),
|
||||
new AstValue(factory.createLiteral(2), host),
|
||||
expect(createAstValue<number[]>(nestedArray).getArray()).toEqual([
|
||||
createAstValue(factory.createLiteral(1)),
|
||||
createAstValue(factory.createLiteral(2)),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not an array', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getArray())
|
||||
// @ts-expect-error
|
||||
expect(() => createAstValue<number>(factory.createLiteral(42)).getArray())
|
||||
.toThrowError('Unsupported syntax, expected an array literal.');
|
||||
});
|
||||
});
|
||||
|
@ -285,11 +325,11 @@ describe('AstValue', () => {
|
|||
const funcExpr = factory.createFunctionExpression(
|
||||
'foo', [],
|
||||
factory.createBlock([factory.createReturnStatement(factory.createLiteral(42))]));
|
||||
expect(new AstValue(funcExpr, host).isFunction()).toEqual(true);
|
||||
expect(createAstValue(funcExpr).isFunction()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a function expression', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isFunction()).toEqual(false);
|
||||
expect(createAstValue(factory.createLiteral(42)).isFunction()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -298,12 +338,13 @@ describe('AstValue', () => {
|
|||
const funcExpr = factory.createFunctionExpression(
|
||||
'foo', [],
|
||||
factory.createBlock([factory.createReturnStatement(factory.createLiteral(42))]));
|
||||
expect(new AstValue(funcExpr, host).getFunctionReturnValue())
|
||||
.toEqual(new AstValue(factory.createLiteral(42), host));
|
||||
expect(createAstValue<Function>(funcExpr).getFunctionReturnValue())
|
||||
.toEqual(createAstValue(factory.createLiteral(42)));
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a function expression', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getFunctionReturnValue())
|
||||
// @ts-expect-error
|
||||
expect(() => createAstValue<number>(factory.createLiteral(42)).getFunctionReturnValue())
|
||||
.toThrowError('Unsupported syntax, expected a function.');
|
||||
});
|
||||
|
||||
|
@ -312,7 +353,7 @@ describe('AstValue', () => {
|
|||
const funcExpr = factory.createFunctionExpression(
|
||||
'foo', [], factory.createBlock([factory.createExpressionStatement(
|
||||
factory.createLiteral('do nothing'))]));
|
||||
expect(() => new AstValue(funcExpr, host).getFunctionReturnValue())
|
||||
expect(() => createAstValue<Function>(funcExpr).getFunctionReturnValue())
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
});
|
||||
|
@ -320,9 +361,9 @@ describe('AstValue', () => {
|
|||
|
||||
describe('getOpaque()', () => {
|
||||
it('should return the value wrapped in a `WrappedNodeExpr`', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).getOpaque())
|
||||
expect(createAstValue(factory.createLiteral(42)).getOpaque())
|
||||
.toEqual(jasmine.any(WrappedNodeExpr));
|
||||
expect(new AstValue(factory.createLiteral(42), host).getOpaque().node)
|
||||
expect(createAstValue(factory.createLiteral(42)).getOpaque().node)
|
||||
.toEqual(factory.createLiteral(42));
|
||||
});
|
||||
});
|
||||
|
@ -339,7 +380,7 @@ describe('AstValue', () => {
|
|||
(stmt.expression as ts.AssignmentExpression<ts.Token<ts.SyntaxKind.EqualsToken>>).right;
|
||||
|
||||
// Check that this string literal has the expected range.
|
||||
expect(new AstValue(mooString, host).getRange())
|
||||
expect(createAstValue(mooString).getRange())
|
||||
.toEqual({startLine: 1, startCol: 4, startPos: 16, endPos: 21});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -91,6 +91,7 @@ export {ViewCompiler} from './view_compiler/view_compiler';
|
|||
export {getParseErrors, isSyntaxError, syntaxError, Version} from './util';
|
||||
export {SourceMap} from './output/source_map';
|
||||
export * from './injectable_compiler_2';
|
||||
export * from './render3/partial/api';
|
||||
export * from './render3/view/api';
|
||||
export {BoundAttribute as TmplAstBoundAttribute, BoundEvent as TmplAstBoundEvent, BoundText as TmplAstBoundText, Content as TmplAstContent, Element as TmplAstElement, Icu as TmplAstIcu, Node as TmplAstNode, RecursiveVisitor as TmplAstRecursiveVisitor, Reference as TmplAstReference, Template as TmplAstTemplate, Text as TmplAstText, TextAttribute as TmplAstTextAttribute, Variable as TmplAstVariable} from './render3/r3_ast';
|
||||
export * from './render3/view/t2_api';
|
||||
|
|
|
@ -6,23 +6,29 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {ChangeDetectionStrategy, ViewEncapsulation} from '../../core';
|
||||
import {InterpolationConfig} from '../../ml_parser/interpolation_config';
|
||||
import * as o from '../../output/output_ast';
|
||||
|
||||
export interface R3PartialDeclaration {
|
||||
/**
|
||||
* Version number of the Angular compiler that was used to compile this declaration. The linker
|
||||
* will be able to detect which version a library is using and interpret its metadata accordingly.
|
||||
*/
|
||||
version: string;
|
||||
|
||||
/**
|
||||
* A reference to the `@angular/core` ES module, which allows access
|
||||
* to all Angular exports, including Ivy instructions.
|
||||
*/
|
||||
ngImport: o.Expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface describes the shape of the object that partial directive declarations are compiled
|
||||
* into. This serves only as documentation, as conformance of this interface is not enforced during
|
||||
* the generation of the partial declaration, nor when the linker applies full compilation from the
|
||||
* partial declaration.
|
||||
*/
|
||||
export interface R3DeclareDirectiveMetadata {
|
||||
/**
|
||||
* Version number of the metadata format. This is used to evolve the metadata
|
||||
* interface later - the linker will be able to detect which version a library
|
||||
* is using and interpret its metadata accordingly.
|
||||
*/
|
||||
version: string;
|
||||
|
||||
export interface R3DeclareDirectiveMetadata extends R3PartialDeclaration {
|
||||
/**
|
||||
* Unparsed selector of the directive.
|
||||
*/
|
||||
|
@ -106,12 +112,6 @@ export interface R3DeclareDirectiveMetadata {
|
|||
* Whether the directive implements the `ngOnChanges` hook. Defaults to false.
|
||||
*/
|
||||
usesOnChanges?: boolean;
|
||||
|
||||
/**
|
||||
* A reference to the `@angular/core` ES module, which allows access
|
||||
* to all Angular exports, including Ivy instructions.
|
||||
*/
|
||||
ngImport: o.Expression;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -206,7 +206,7 @@ export interface R3DeclareComponentMetadata extends R3DeclareDirectiveMetadata {
|
|||
/**
|
||||
* Overrides the default interpolation start and end delimiters. Defaults to {{ and }}.
|
||||
*/
|
||||
interpolation?: InterpolationConfig;
|
||||
interpolation?: [string, string];
|
||||
|
||||
/**
|
||||
* Whether whitespace in the template should be preserved. Defaults to false.
|
||||
|
|
Loading…
Reference in New Issue