fix(language-service): tolerate errors in decorators (#14634)
Fixes #14631
This commit is contained in:
parent
7a66a4115b
commit
6bae7378b1
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AotCompilerHost, StaticSymbol} from '@angular/compiler';
|
import {AotCompilerHost, StaticSymbol} from '@angular/compiler';
|
||||||
import {AngularCompilerOptions, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
|
import {AngularCompilerOptions, CollectorOptions, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
@ -37,7 +37,7 @@ export class CompilerHost implements AotCompilerHost {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected program: ts.Program, protected options: AngularCompilerOptions,
|
protected program: ts.Program, protected options: AngularCompilerOptions,
|
||||||
protected context: CompilerHostContext) {
|
protected context: CompilerHostContext, collectorOptions?: CollectorOptions) {
|
||||||
// normalize the path so that it never ends with '/'.
|
// normalize the path so that it never ends with '/'.
|
||||||
this.basePath = path.normalize(path.join(this.options.basePath, '.')).replace(/\\/g, '/');
|
this.basePath = path.normalize(path.join(this.options.basePath, '.')).replace(/\\/g, '/');
|
||||||
this.genDir = path.normalize(path.join(this.options.genDir, '.')).replace(/\\/g, '/');
|
this.genDir = path.normalize(path.join(this.options.genDir, '.')).replace(/\\/g, '/');
|
||||||
|
|
|
@ -15,6 +15,14 @@ const ANGULAR_CORE = '@angular/core';
|
||||||
|
|
||||||
const HIDDEN_KEY = /^\$.*\$$/;
|
const HIDDEN_KEY = /^\$.*\$$/;
|
||||||
|
|
||||||
|
const IGNORE = {
|
||||||
|
__symbolic: 'ignore'
|
||||||
|
};
|
||||||
|
|
||||||
|
function shouldIgnore(value: any): boolean {
|
||||||
|
return value && value.__symbolic == 'ignore';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A static reflector implements enough of the Reflector API that is necessary to compile
|
* A static reflector implements enough of the Reflector API that is necessary to compile
|
||||||
* templates statically.
|
* templates statically.
|
||||||
|
@ -332,7 +340,8 @@ export class StaticReflector implements ɵReflectorReader {
|
||||||
if (value && (depth != 0 || value.__symbolic != 'error')) {
|
if (value && (depth != 0 || value.__symbolic != 'error')) {
|
||||||
const parameters: string[] = targetFunction['parameters'];
|
const parameters: string[] = targetFunction['parameters'];
|
||||||
const defaults: any[] = targetFunction.defaults;
|
const defaults: any[] = targetFunction.defaults;
|
||||||
args = args.map(arg => simplifyInContext(context, arg, depth + 1));
|
args = args.map(arg => simplifyInContext(context, arg, depth + 1))
|
||||||
|
.map(arg => shouldIgnore(arg) ? undefined : arg);
|
||||||
if (defaults && defaults.length > args.length) {
|
if (defaults && defaults.length > args.length) {
|
||||||
args.push(...defaults.slice(args.length).map((value: any) => simplify(value)));
|
args.push(...defaults.slice(args.length).map((value: any) => simplify(value)));
|
||||||
}
|
}
|
||||||
|
@ -359,7 +368,7 @@ export class StaticReflector implements ɵReflectorReader {
|
||||||
// If depth is 0 we are evaluating the top level expression that is describing element
|
// If depth is 0 we are evaluating the top level expression that is describing element
|
||||||
// decorator. In this case, it is a decorator we don't understand, such as a custom
|
// decorator. In this case, it is a decorator we don't understand, such as a custom
|
||||||
// non-angular decorator, and we should just ignore it.
|
// non-angular decorator, and we should just ignore it.
|
||||||
return {__symbolic: 'ignore'};
|
return IGNORE;
|
||||||
}
|
}
|
||||||
return simplify(
|
return simplify(
|
||||||
{__symbolic: 'error', message: 'Function call not supported', context: functionSymbol});
|
{__symbolic: 'error', message: 'Function call not supported', context: functionSymbol});
|
||||||
|
@ -526,7 +535,8 @@ export class StaticReflector implements ɵReflectorReader {
|
||||||
let converter = self.conversionMap.get(staticSymbol);
|
let converter = self.conversionMap.get(staticSymbol);
|
||||||
if (converter) {
|
if (converter) {
|
||||||
const args =
|
const args =
|
||||||
argExpressions.map(arg => simplifyInContext(context, arg, depth + 1));
|
argExpressions.map(arg => simplifyInContext(context, arg, depth + 1))
|
||||||
|
.map(arg => shouldIgnore(arg) ? undefined : arg);
|
||||||
return converter(context, args);
|
return converter(context, args);
|
||||||
} else {
|
} else {
|
||||||
// Determine if the function is one we can simplify.
|
// Determine if the function is one we can simplify.
|
||||||
|
@ -540,16 +550,22 @@ export class StaticReflector implements ɵReflectorReader {
|
||||||
if (expression['line']) {
|
if (expression['line']) {
|
||||||
message =
|
message =
|
||||||
`${message} (position ${expression['line']+1}:${expression['character']+1} in the original .ts file)`;
|
`${message} (position ${expression['line']+1}:${expression['character']+1} in the original .ts file)`;
|
||||||
throw positionalError(
|
self.reportError(
|
||||||
message, context.filePath, expression['line'], expression['character']);
|
positionalError(
|
||||||
|
message, context.filePath, expression['line'], expression['character']),
|
||||||
|
context);
|
||||||
|
} else {
|
||||||
|
self.reportError(new Error(message), context);
|
||||||
}
|
}
|
||||||
throw new Error(message);
|
return IGNORE;
|
||||||
|
case 'ignore':
|
||||||
|
return expression;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return mapStringMap(expression, (value, name) => simplify(value));
|
return mapStringMap(expression, (value, name) => simplify(value));
|
||||||
}
|
}
|
||||||
return null;
|
return IGNORE;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -675,10 +691,6 @@ class PopulatedScope extends BindingScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldIgnore(value: any): boolean {
|
|
||||||
return value && value.__symbolic == 'ignore';
|
|
||||||
}
|
|
||||||
|
|
||||||
function positionalError(message: string, fileName: string, line: number, column: number): Error {
|
function positionalError(message: string, fileName: string, line: number, column: number): Error {
|
||||||
const result = new Error(message);
|
const result = new Error(message);
|
||||||
(result as any).fileName = fileName;
|
(result as any).fileName = fileName;
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
Tests in this directory are excluded from running in the browser and only running
|
Tests in this directory are excluded from running in the browser and only run in node.
|
||||||
in node.
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import {StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost} from '@angular/compiler';
|
import {StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost} from '@angular/compiler';
|
||||||
import {HostListener, Inject, animate, group, keyframes, sequence, state, style, transition, trigger} from '@angular/core';
|
import {HostListener, Inject, animate, group, keyframes, sequence, state, style, transition, trigger} from '@angular/core';
|
||||||
|
import {CollectorOptions} from '@angular/tsc-wrapped';
|
||||||
|
|
||||||
import {MockStaticSymbolResolverHost, MockSummaryResolver} from './static_symbol_resolver_spec';
|
import {MockStaticSymbolResolverHost, MockSummaryResolver} from './static_symbol_resolver_spec';
|
||||||
|
|
||||||
|
@ -19,11 +20,13 @@ describe('StaticReflector', () => {
|
||||||
|
|
||||||
function init(
|
function init(
|
||||||
testData: {[key: string]: any} = DEFAULT_TEST_DATA,
|
testData: {[key: string]: any} = DEFAULT_TEST_DATA,
|
||||||
decorators: {name: string, filePath: string, ctor: any}[] = []) {
|
decorators: {name: string, filePath: string, ctor: any}[] = [],
|
||||||
|
errorRecorder?: (error: any, fileName: string) => void, collectorOptions?: CollectorOptions) {
|
||||||
const symbolCache = new StaticSymbolCache();
|
const symbolCache = new StaticSymbolCache();
|
||||||
host = new MockStaticSymbolResolverHost(testData);
|
host = new MockStaticSymbolResolverHost(testData, collectorOptions);
|
||||||
symbolResolver = new StaticSymbolResolver(host, symbolCache, new MockSummaryResolver([]));
|
symbolResolver =
|
||||||
reflector = new StaticReflector(symbolResolver, decorators);
|
new StaticSymbolResolver(host, symbolCache, new MockSummaryResolver([]), errorRecorder);
|
||||||
|
reflector = new StaticReflector(symbolResolver, decorators, [], errorRecorder);
|
||||||
noContext = reflector.getStaticSymbol('', '');
|
noContext = reflector.getStaticSymbol('', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -492,6 +495,31 @@ describe('StaticReflector', () => {
|
||||||
expect(() => reflector.propMetadata(appComponent)).not.toThrow();
|
expect(() => reflector.propMetadata(appComponent)).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should produce a annotation even if it contains errors', () => {
|
||||||
|
const data = Object.create(DEFAULT_TEST_DATA);
|
||||||
|
const file = '/tmp/src/invalid-component.ts';
|
||||||
|
data[file] = `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tmp',
|
||||||
|
template: () => {},
|
||||||
|
providers: [1, 2, (() => {}), 3, !(() => {}), 4, 5, (() => {}) + (() => {}), 6, 7]
|
||||||
|
})
|
||||||
|
export class BadComponent {
|
||||||
|
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
init(data, [], () => {}, {verboseInvalidExpression: true});
|
||||||
|
|
||||||
|
const badComponent = reflector.getStaticSymbol(file, 'BadComponent');
|
||||||
|
const annotations = reflector.annotations(badComponent);
|
||||||
|
const annotation = annotations[0];
|
||||||
|
expect(annotation.selector).toEqual('tmp');
|
||||||
|
expect(annotation.template).toBeUndefined();
|
||||||
|
expect(annotation.providers).toEqual([1, 2, 3, 4, 5, 6, 7]);
|
||||||
|
});
|
||||||
|
|
||||||
describe('inheritance', () => {
|
describe('inheritance', () => {
|
||||||
class ClassDecorator {
|
class ClassDecorator {
|
||||||
constructor(public value: any) {}
|
constructor(public value: any) {}
|
||||||
|
@ -1264,5 +1292,5 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
|
||||||
export class Dep {
|
export class Dep {
|
||||||
@Input f: Forward;
|
@Input f: Forward;
|
||||||
}
|
}
|
||||||
`,
|
`
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,11 +7,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, Summary, SummaryResolver} from '@angular/compiler';
|
import {StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, Summary, SummaryResolver} from '@angular/compiler';
|
||||||
import {MetadataCollector} from '@angular/tsc-wrapped';
|
import {CollectorOptions, MetadataCollector} from '@angular/tsc-wrapped';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// This matches .ts files but not .d.ts files.
|
// This matches .ts files but not .d.ts files.
|
||||||
const TS_EXT = /(^.|(?!\.d)..)\.ts$/;
|
const TS_EXT = /(^.|(?!\.d)..)\.ts$/;
|
||||||
|
|
||||||
|
@ -366,9 +365,11 @@ export class MockSummaryResolver implements SummaryResolver<StaticSymbol> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockStaticSymbolResolverHost implements StaticSymbolResolverHost {
|
export class MockStaticSymbolResolverHost implements StaticSymbolResolverHost {
|
||||||
private collector = new MetadataCollector();
|
private collector: MetadataCollector;
|
||||||
|
|
||||||
constructor(private data: {[key: string]: any}) {}
|
constructor(private data: {[key: string]: any}, collectorOptions?: CollectorOptions) {
|
||||||
|
this.collector = new MetadataCollector(collectorOptions);
|
||||||
|
}
|
||||||
|
|
||||||
// In tests, assume that symbols are not re-exported
|
// In tests, assume that symbols are not re-exported
|
||||||
moduleNameToFileName(modulePath: string, containingFile?: string): string {
|
moduleNameToFileName(modulePath: string, containingFile?: string): string {
|
||||||
|
|
|
@ -33,7 +33,8 @@ export class ReflectorHost extends CompilerHost {
|
||||||
options: AngularCompilerOptions) {
|
options: AngularCompilerOptions) {
|
||||||
super(
|
super(
|
||||||
null, options,
|
null, options,
|
||||||
new ModuleResolutionHostAdapter(new ReflectorModuleModuleResolutionHost(serviceHost)));
|
new ModuleResolutionHostAdapter(new ReflectorModuleModuleResolutionHost(serviceHost)),
|
||||||
|
{verboseInvalidExpression: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get program() { return this.getProgram(); }
|
protected get program() { return this.getProgram(); }
|
||||||
|
|
|
@ -37,6 +37,11 @@ export class CollectorOptions {
|
||||||
* the source.
|
* the source.
|
||||||
*/
|
*/
|
||||||
quotedNames?: boolean;
|
quotedNames?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not simplify invalid expressions.
|
||||||
|
*/
|
||||||
|
verboseInvalidExpression?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -227,6 +227,10 @@ export class Evaluator {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFoldableError(value: any): value is MetadataError {
|
||||||
|
return !t.options.verboseInvalidExpression && isMetadataError(value);
|
||||||
|
}
|
||||||
|
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
case ts.SyntaxKind.ObjectLiteralExpression:
|
case ts.SyntaxKind.ObjectLiteralExpression:
|
||||||
let obj: {[name: string]: any} = {};
|
let obj: {[name: string]: any} = {};
|
||||||
|
@ -241,14 +245,14 @@ export class Evaluator {
|
||||||
quoted.push(name);
|
quoted.push(name);
|
||||||
}
|
}
|
||||||
const propertyName = this.nameOf(assignment.name);
|
const propertyName = this.nameOf(assignment.name);
|
||||||
if (isMetadataError(propertyName)) {
|
if (isFoldableError(propertyName)) {
|
||||||
error = propertyName;
|
error = propertyName;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const propertyValue = isPropertyAssignment(assignment) ?
|
const propertyValue = isPropertyAssignment(assignment) ?
|
||||||
this.evaluateNode(assignment.initializer) :
|
this.evaluateNode(assignment.initializer) :
|
||||||
{__symbolic: 'reference', name: propertyName};
|
{__symbolic: 'reference', name: propertyName};
|
||||||
if (isMetadataError(propertyValue)) {
|
if (isFoldableError(propertyValue)) {
|
||||||
error = propertyValue;
|
error = propertyValue;
|
||||||
return true; // Stop the forEachChild.
|
return true; // Stop the forEachChild.
|
||||||
} else {
|
} else {
|
||||||
|
@ -267,7 +271,7 @@ export class Evaluator {
|
||||||
const value = this.evaluateNode(child);
|
const value = this.evaluateNode(child);
|
||||||
|
|
||||||
// Check for error
|
// Check for error
|
||||||
if (isMetadataError(value)) {
|
if (isFoldableError(value)) {
|
||||||
error = value;
|
error = value;
|
||||||
return true; // Stop the forEachChild.
|
return true; // Stop the forEachChild.
|
||||||
}
|
}
|
||||||
|
@ -299,14 +303,14 @@ export class Evaluator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
|
const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
|
||||||
if (args.some(isMetadataError)) {
|
if (!this.options.verboseInvalidExpression && args.some(isMetadataError)) {
|
||||||
return args.find(isMetadataError);
|
return args.find(isMetadataError);
|
||||||
}
|
}
|
||||||
if (this.isFoldable(callExpression)) {
|
if (this.isFoldable(callExpression)) {
|
||||||
if (isMethodCallOf(callExpression, 'concat')) {
|
if (isMethodCallOf(callExpression, 'concat')) {
|
||||||
const arrayValue = <MetadataValue[]>this.evaluateNode(
|
const arrayValue = <MetadataValue[]>this.evaluateNode(
|
||||||
(<ts.PropertyAccessExpression>callExpression.expression).expression);
|
(<ts.PropertyAccessExpression>callExpression.expression).expression);
|
||||||
if (isMetadataError(arrayValue)) return arrayValue;
|
if (isFoldableError(arrayValue)) return arrayValue;
|
||||||
return arrayValue.concat(args[0]);
|
return arrayValue.concat(args[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -315,7 +319,7 @@ export class Evaluator {
|
||||||
return recordEntry(args[0], node);
|
return recordEntry(args[0], node);
|
||||||
}
|
}
|
||||||
const expression = this.evaluateNode(callExpression.expression);
|
const expression = this.evaluateNode(callExpression.expression);
|
||||||
if (isMetadataError(expression)) {
|
if (isFoldableError(expression)) {
|
||||||
return recordEntry(expression, node);
|
return recordEntry(expression, node);
|
||||||
}
|
}
|
||||||
let result: MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression};
|
let result: MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression};
|
||||||
|
@ -326,7 +330,7 @@ export class Evaluator {
|
||||||
case ts.SyntaxKind.NewExpression:
|
case ts.SyntaxKind.NewExpression:
|
||||||
const newExpression = <ts.NewExpression>node;
|
const newExpression = <ts.NewExpression>node;
|
||||||
const newArgs = newExpression.arguments.map(arg => this.evaluateNode(arg));
|
const newArgs = newExpression.arguments.map(arg => this.evaluateNode(arg));
|
||||||
if (newArgs.some(isMetadataError)) {
|
if (!this.options.verboseInvalidExpression && newArgs.some(isMetadataError)) {
|
||||||
return recordEntry(newArgs.find(isMetadataError), node);
|
return recordEntry(newArgs.find(isMetadataError), node);
|
||||||
}
|
}
|
||||||
const newTarget = this.evaluateNode(newExpression.expression);
|
const newTarget = this.evaluateNode(newExpression.expression);
|
||||||
|
@ -341,11 +345,11 @@ export class Evaluator {
|
||||||
case ts.SyntaxKind.PropertyAccessExpression: {
|
case ts.SyntaxKind.PropertyAccessExpression: {
|
||||||
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
|
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
|
||||||
const expression = this.evaluateNode(propertyAccessExpression.expression);
|
const expression = this.evaluateNode(propertyAccessExpression.expression);
|
||||||
if (isMetadataError(expression)) {
|
if (isFoldableError(expression)) {
|
||||||
return recordEntry(expression, node);
|
return recordEntry(expression, node);
|
||||||
}
|
}
|
||||||
const member = this.nameOf(propertyAccessExpression.name);
|
const member = this.nameOf(propertyAccessExpression.name);
|
||||||
if (isMetadataError(member)) {
|
if (isFoldableError(member)) {
|
||||||
return recordEntry(member, node);
|
return recordEntry(member, node);
|
||||||
}
|
}
|
||||||
if (expression && this.isFoldable(propertyAccessExpression.expression))
|
if (expression && this.isFoldable(propertyAccessExpression.expression))
|
||||||
|
@ -361,11 +365,11 @@ export class Evaluator {
|
||||||
case ts.SyntaxKind.ElementAccessExpression: {
|
case ts.SyntaxKind.ElementAccessExpression: {
|
||||||
const elementAccessExpression = <ts.ElementAccessExpression>node;
|
const elementAccessExpression = <ts.ElementAccessExpression>node;
|
||||||
const expression = this.evaluateNode(elementAccessExpression.expression);
|
const expression = this.evaluateNode(elementAccessExpression.expression);
|
||||||
if (isMetadataError(expression)) {
|
if (isFoldableError(expression)) {
|
||||||
return recordEntry(expression, node);
|
return recordEntry(expression, node);
|
||||||
}
|
}
|
||||||
const index = this.evaluateNode(elementAccessExpression.argumentExpression);
|
const index = this.evaluateNode(elementAccessExpression.argumentExpression);
|
||||||
if (isMetadataError(expression)) {
|
if (isFoldableError(expression)) {
|
||||||
return recordEntry(expression, node);
|
return recordEntry(expression, node);
|
||||||
}
|
}
|
||||||
if (this.isFoldable(elementAccessExpression.expression) &&
|
if (this.isFoldable(elementAccessExpression.expression) &&
|
||||||
|
@ -404,7 +408,7 @@ export class Evaluator {
|
||||||
} else {
|
} else {
|
||||||
const identifier = <ts.Identifier>typeNameNode;
|
const identifier = <ts.Identifier>typeNameNode;
|
||||||
const symbol = this.symbols.resolve(identifier.text);
|
const symbol = this.symbols.resolve(identifier.text);
|
||||||
if (isMetadataError(symbol) || isMetadataSymbolicReferenceExpression(symbol)) {
|
if (isFoldableError(symbol) || isMetadataSymbolicReferenceExpression(symbol)) {
|
||||||
return recordEntry(symbol, node);
|
return recordEntry(symbol, node);
|
||||||
}
|
}
|
||||||
return recordEntry(
|
return recordEntry(
|
||||||
|
@ -412,7 +416,7 @@ export class Evaluator {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const typeReference = getReference(typeNameNode);
|
const typeReference = getReference(typeNameNode);
|
||||||
if (isMetadataError(typeReference)) {
|
if (isFoldableError(typeReference)) {
|
||||||
return recordEntry(typeReference, node);
|
return recordEntry(typeReference, node);
|
||||||
}
|
}
|
||||||
if (!isMetadataModuleReferenceExpression(typeReference) &&
|
if (!isMetadataModuleReferenceExpression(typeReference) &&
|
||||||
|
|
Loading…
Reference in New Issue