Skip to content

Commit 6bae737

Browse files
chuckjazIgorMinar
authored andcommitted
fix(language-service): tolerate errors in decorators (#14634)
Fixes #14631
1 parent 7a66a41 commit 6bae737

File tree

8 files changed

+88
-38
lines changed

8 files changed

+88
-38
lines changed

modules/@angular/compiler-cli/src/compiler_host.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {AotCompilerHost, StaticSymbol} from '@angular/compiler';
10-
import {AngularCompilerOptions, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
10+
import {AngularCompilerOptions, CollectorOptions, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
1111
import * as fs from 'fs';
1212
import * as path from 'path';
1313
import * as ts from 'typescript';
@@ -37,7 +37,7 @@ export class CompilerHost implements AotCompilerHost {
3737

3838
constructor(
3939
protected program: ts.Program, protected options: AngularCompilerOptions,
40-
protected context: CompilerHostContext) {
40+
protected context: CompilerHostContext, collectorOptions?: CollectorOptions) {
4141
// normalize the path so that it never ends with '/'.
4242
this.basePath = path.normalize(path.join(this.options.basePath, '.')).replace(/\\/g, '/');
4343
this.genDir = path.normalize(path.join(this.options.genDir, '.')).replace(/\\/g, '/');

modules/@angular/compiler/src/aot/static_reflector.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ const ANGULAR_CORE = '@angular/core';
1515

1616
const HIDDEN_KEY = /^\$.*\$$/;
1717

18+
const IGNORE = {
19+
__symbolic: 'ignore'
20+
};
21+
22+
function shouldIgnore(value: any): boolean {
23+
return value && value.__symbolic == 'ignore';
24+
}
25+
1826
/**
1927
* A static reflector implements enough of the Reflector API that is necessary to compile
2028
* templates statically.
@@ -332,7 +340,8 @@ export class StaticReflector implements ɵReflectorReader {
332340
if (value && (depth != 0 || value.__symbolic != 'error')) {
333341
const parameters: string[] = targetFunction['parameters'];
334342
const defaults: any[] = targetFunction.defaults;
335-
args = args.map(arg => simplifyInContext(context, arg, depth + 1));
343+
args = args.map(arg => simplifyInContext(context, arg, depth + 1))
344+
.map(arg => shouldIgnore(arg) ? undefined : arg);
336345
if (defaults && defaults.length > args.length) {
337346
args.push(...defaults.slice(args.length).map((value: any) => simplify(value)));
338347
}
@@ -359,7 +368,7 @@ export class StaticReflector implements ɵReflectorReader {
359368
// If depth is 0 we are evaluating the top level expression that is describing element
360369
// decorator. In this case, it is a decorator we don't understand, such as a custom
361370
// non-angular decorator, and we should just ignore it.
362-
return {__symbolic: 'ignore'};
371+
return IGNORE;
363372
}
364373
return simplify(
365374
{__symbolic: 'error', message: 'Function call not supported', context: functionSymbol});
@@ -526,7 +535,8 @@ export class StaticReflector implements ɵReflectorReader {
526535
let converter = self.conversionMap.get(staticSymbol);
527536
if (converter) {
528537
const args =
529-
argExpressions.map(arg => simplifyInContext(context, arg, depth + 1));
538+
argExpressions.map(arg => simplifyInContext(context, arg, depth + 1))
539+
.map(arg => shouldIgnore(arg) ? undefined : arg);
530540
return converter(context, args);
531541
} else {
532542
// Determine if the function is one we can simplify.
@@ -540,16 +550,22 @@ export class StaticReflector implements ɵReflectorReader {
540550
if (expression['line']) {
541551
message =
542552
`${message} (position ${expression['line']+1}:${expression['character']+1} in the original .ts file)`;
543-
throw positionalError(
544-
message, context.filePath, expression['line'], expression['character']);
553+
self.reportError(
554+
positionalError(
555+
message, context.filePath, expression['line'], expression['character']),
556+
context);
557+
} else {
558+
self.reportError(new Error(message), context);
545559
}
546-
throw new Error(message);
560+
return IGNORE;
561+
case 'ignore':
562+
return expression;
547563
}
548564
return null;
549565
}
550566
return mapStringMap(expression, (value, name) => simplify(value));
551567
}
552-
return null;
568+
return IGNORE;
553569
}
554570

555571
try {
@@ -675,10 +691,6 @@ class PopulatedScope extends BindingScope {
675691
}
676692
}
677693

678-
function shouldIgnore(value: any): boolean {
679-
return value && value.__symbolic == 'ignore';
680-
}
681-
682694
function positionalError(message: string, fileName: string, line: number, column: number): Error {
683695
const result = new Error(message);
684696
(result as any).fileName = fileName;
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
Tests in this directory are excluded from running in the browser and only running
2-
in node.
1+
Tests in this directory are excluded from running in the browser and only run in node.

modules/@angular/compiler/test/aot/static_reflector_spec.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost} from '@angular/compiler';
1010
import {HostListener, Inject, animate, group, keyframes, sequence, state, style, transition, trigger} from '@angular/core';
11+
import {CollectorOptions} from '@angular/tsc-wrapped';
1112

1213
import {MockStaticSymbolResolverHost, MockSummaryResolver} from './static_symbol_resolver_spec';
1314

@@ -19,11 +20,13 @@ describe('StaticReflector', () => {
1920

2021
function init(
2122
testData: {[key: string]: any} = DEFAULT_TEST_DATA,
22-
decorators: {name: string, filePath: string, ctor: any}[] = []) {
23+
decorators: {name: string, filePath: string, ctor: any}[] = [],
24+
errorRecorder?: (error: any, fileName: string) => void, collectorOptions?: CollectorOptions) {
2325
const symbolCache = new StaticSymbolCache();
24-
host = new MockStaticSymbolResolverHost(testData);
25-
symbolResolver = new StaticSymbolResolver(host, symbolCache, new MockSummaryResolver([]));
26-
reflector = new StaticReflector(symbolResolver, decorators);
26+
host = new MockStaticSymbolResolverHost(testData, collectorOptions);
27+
symbolResolver =
28+
new StaticSymbolResolver(host, symbolCache, new MockSummaryResolver([]), errorRecorder);
29+
reflector = new StaticReflector(symbolResolver, decorators, [], errorRecorder);
2730
noContext = reflector.getStaticSymbol('', '');
2831
}
2932

@@ -492,6 +495,31 @@ describe('StaticReflector', () => {
492495
expect(() => reflector.propMetadata(appComponent)).not.toThrow();
493496
});
494497

498+
it('should produce a annotation even if it contains errors', () => {
499+
const data = Object.create(DEFAULT_TEST_DATA);
500+
const file = '/tmp/src/invalid-component.ts';
501+
data[file] = `
502+
import {Component} from '@angular/core';
503+
504+
@Component({
505+
selector: 'tmp',
506+
template: () => {},
507+
providers: [1, 2, (() => {}), 3, !(() => {}), 4, 5, (() => {}) + (() => {}), 6, 7]
508+
})
509+
export class BadComponent {
510+
511+
}
512+
`;
513+
init(data, [], () => {}, {verboseInvalidExpression: true});
514+
515+
const badComponent = reflector.getStaticSymbol(file, 'BadComponent');
516+
const annotations = reflector.annotations(badComponent);
517+
const annotation = annotations[0];
518+
expect(annotation.selector).toEqual('tmp');
519+
expect(annotation.template).toBeUndefined();
520+
expect(annotation.providers).toEqual([1, 2, 3, 4, 5, 6, 7]);
521+
});
522+
495523
describe('inheritance', () => {
496524
class ClassDecorator {
497525
constructor(public value: any) {}
@@ -1264,5 +1292,5 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
12641292
export class Dep {
12651293
@Input f: Forward;
12661294
}
1267-
`,
1295+
`
12681296
};

modules/@angular/compiler/test/aot/static_symbol_resolver_spec.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
*/
88

99
import {StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, Summary, SummaryResolver} from '@angular/compiler';
10-
import {MetadataCollector} from '@angular/tsc-wrapped';
10+
import {CollectorOptions, MetadataCollector} from '@angular/tsc-wrapped';
1111
import * as ts from 'typescript';
1212

1313

14-
1514
// This matches .ts files but not .d.ts files.
1615
const TS_EXT = /(^.|(?!\.d)..)\.ts$/;
1716

@@ -366,9 +365,11 @@ export class MockSummaryResolver implements SummaryResolver<StaticSymbol> {
366365
}
367366

368367
export class MockStaticSymbolResolverHost implements StaticSymbolResolverHost {
369-
private collector = new MetadataCollector();
368+
private collector: MetadataCollector;
370369

371-
constructor(private data: {[key: string]: any}) {}
370+
constructor(private data: {[key: string]: any}, collectorOptions?: CollectorOptions) {
371+
this.collector = new MetadataCollector(collectorOptions);
372+
}
372373

373374
// In tests, assume that symbols are not re-exported
374375
moduleNameToFileName(modulePath: string, containingFile?: string): string {

modules/@angular/language-service/src/reflector_host.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export class ReflectorHost extends CompilerHost {
3333
options: AngularCompilerOptions) {
3434
super(
3535
null, options,
36-
new ModuleResolutionHostAdapter(new ReflectorModuleModuleResolutionHost(serviceHost)));
36+
new ModuleResolutionHostAdapter(new ReflectorModuleModuleResolutionHost(serviceHost)),
37+
{verboseInvalidExpression: true});
3738
}
3839

3940
protected get program() { return this.getProgram(); }

tools/@angular/tsc-wrapped/src/collector.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export class CollectorOptions {
3737
* the source.
3838
*/
3939
quotedNames?: boolean;
40+
41+
/**
42+
* Do not simplify invalid expressions.
43+
*/
44+
verboseInvalidExpression?: boolean;
4045
}
4146

4247
/**

tools/@angular/tsc-wrapped/src/evaluator.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ export class Evaluator {
227227
return entry;
228228
}
229229

230+
function isFoldableError(value: any): value is MetadataError {
231+
return !t.options.verboseInvalidExpression && isMetadataError(value);
232+
}
233+
230234
switch (node.kind) {
231235
case ts.SyntaxKind.ObjectLiteralExpression:
232236
let obj: {[name: string]: any} = {};
@@ -241,14 +245,14 @@ export class Evaluator {
241245
quoted.push(name);
242246
}
243247
const propertyName = this.nameOf(assignment.name);
244-
if (isMetadataError(propertyName)) {
248+
if (isFoldableError(propertyName)) {
245249
error = propertyName;
246250
return true;
247251
}
248252
const propertyValue = isPropertyAssignment(assignment) ?
249253
this.evaluateNode(assignment.initializer) :
250254
{__symbolic: 'reference', name: propertyName};
251-
if (isMetadataError(propertyValue)) {
255+
if (isFoldableError(propertyValue)) {
252256
error = propertyValue;
253257
return true; // Stop the forEachChild.
254258
} else {
@@ -267,7 +271,7 @@ export class Evaluator {
267271
const value = this.evaluateNode(child);
268272

269273
// Check for error
270-
if (isMetadataError(value)) {
274+
if (isFoldableError(value)) {
271275
error = value;
272276
return true; // Stop the forEachChild.
273277
}
@@ -299,14 +303,14 @@ export class Evaluator {
299303
}
300304
}
301305
const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
302-
if (args.some(isMetadataError)) {
306+
if (!this.options.verboseInvalidExpression && args.some(isMetadataError)) {
303307
return args.find(isMetadataError);
304308
}
305309
if (this.isFoldable(callExpression)) {
306310
if (isMethodCallOf(callExpression, 'concat')) {
307311
const arrayValue = <MetadataValue[]>this.evaluateNode(
308312
(<ts.PropertyAccessExpression>callExpression.expression).expression);
309-
if (isMetadataError(arrayValue)) return arrayValue;
313+
if (isFoldableError(arrayValue)) return arrayValue;
310314
return arrayValue.concat(args[0]);
311315
}
312316
}
@@ -315,7 +319,7 @@ export class Evaluator {
315319
return recordEntry(args[0], node);
316320
}
317321
const expression = this.evaluateNode(callExpression.expression);
318-
if (isMetadataError(expression)) {
322+
if (isFoldableError(expression)) {
319323
return recordEntry(expression, node);
320324
}
321325
let result: MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression};
@@ -326,7 +330,7 @@ export class Evaluator {
326330
case ts.SyntaxKind.NewExpression:
327331
const newExpression = <ts.NewExpression>node;
328332
const newArgs = newExpression.arguments.map(arg => this.evaluateNode(arg));
329-
if (newArgs.some(isMetadataError)) {
333+
if (!this.options.verboseInvalidExpression && newArgs.some(isMetadataError)) {
330334
return recordEntry(newArgs.find(isMetadataError), node);
331335
}
332336
const newTarget = this.evaluateNode(newExpression.expression);
@@ -341,11 +345,11 @@ export class Evaluator {
341345
case ts.SyntaxKind.PropertyAccessExpression: {
342346
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
343347
const expression = this.evaluateNode(propertyAccessExpression.expression);
344-
if (isMetadataError(expression)) {
348+
if (isFoldableError(expression)) {
345349
return recordEntry(expression, node);
346350
}
347351
const member = this.nameOf(propertyAccessExpression.name);
348-
if (isMetadataError(member)) {
352+
if (isFoldableError(member)) {
349353
return recordEntry(member, node);
350354
}
351355
if (expression && this.isFoldable(propertyAccessExpression.expression))
@@ -361,11 +365,11 @@ export class Evaluator {
361365
case ts.SyntaxKind.ElementAccessExpression: {
362366
const elementAccessExpression = <ts.ElementAccessExpression>node;
363367
const expression = this.evaluateNode(elementAccessExpression.expression);
364-
if (isMetadataError(expression)) {
368+
if (isFoldableError(expression)) {
365369
return recordEntry(expression, node);
366370
}
367371
const index = this.evaluateNode(elementAccessExpression.argumentExpression);
368-
if (isMetadataError(expression)) {
372+
if (isFoldableError(expression)) {
369373
return recordEntry(expression, node);
370374
}
371375
if (this.isFoldable(elementAccessExpression.expression) &&
@@ -404,15 +408,15 @@ export class Evaluator {
404408
} else {
405409
const identifier = <ts.Identifier>typeNameNode;
406410
const symbol = this.symbols.resolve(identifier.text);
407-
if (isMetadataError(symbol) || isMetadataSymbolicReferenceExpression(symbol)) {
411+
if (isFoldableError(symbol) || isMetadataSymbolicReferenceExpression(symbol)) {
408412
return recordEntry(symbol, node);
409413
}
410414
return recordEntry(
411415
errorSymbol('Could not resolve type', node, {typeName: identifier.text}), node);
412416
}
413417
};
414418
const typeReference = getReference(typeNameNode);
415-
if (isMetadataError(typeReference)) {
419+
if (isFoldableError(typeReference)) {
416420
return recordEntry(typeReference, node);
417421
}
418422
if (!isMetadataModuleReferenceExpression(typeReference) &&

0 commit comments

Comments
 (0)