fix(language-service): tolerate errors in decorators (#14634)

Fixes #14631
This commit is contained in:
Chuck Jazdzewski 2017-03-01 13:23:34 -08:00 committed by Igor Minar
parent 7a66a4115b
commit 6bae7378b1
8 changed files with 88 additions and 38 deletions

View File

@ -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, '/');

View File

@ -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;

View File

@ -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.

View File

@ -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;
} }
`, `
}; };

View File

@ -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 {

View File

@ -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(); }

View File

@ -37,6 +37,11 @@ export class CollectorOptions {
* the source. * the source.
*/ */
quotedNames?: boolean; quotedNames?: boolean;
/**
* Do not simplify invalid expressions.
*/
verboseInvalidExpression?: boolean;
} }
/** /**

View File

@ -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) &&