feat(compiler-cli): improve error messages produced during structural errors (#20459)

The errors produced when error were encountered while interpreting the
content of a directive was often incomprehencible. With this change
these kind of error messages should be easier to understand and diagnose.

PR Close #20459
This commit is contained in:
Chuck Jazdzewski
2017-11-14 17:49:47 -08:00
committed by Miško Hevery
parent 1366762d12
commit 8ecda94899
25 changed files with 1247 additions and 308 deletions

View File

@ -0,0 +1,60 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {syntaxError} from '../util';
export interface Position {
fileName: string;
line: number;
column: number;
}
export interface FormattedMessageChain {
message: string;
position?: Position;
next?: FormattedMessageChain;
}
export type FormattedError = Error & {
chain: FormattedMessageChain;
position?: Position;
};
const FORMATTED_MESSAGE = 'ngFormattedMessage';
function indentStr(level: number): string {
if (level <= 0) return '';
if (level < 6) return ['', ' ', ' ', ' ', ' ', ' '][level];
const half = indentStr(Math.floor(level / 2));
return half + half + (level % 2 === 1 ? ' ' : '');
}
function formatChain(chain: FormattedMessageChain | undefined, indent: number = 0): string {
if (!chain) return '';
const position = chain.position ?
`${chain.position.fileName}(${chain.position.line+1},${chain.position.column+1})` :
'';
const prefix = position && indent === 0 ? `${position}: ` : '';
const postfix = position && indent !== 0 ? ` at ${position}` : '';
const message = `${prefix}${chain.message}${postfix}`;
return `${indentStr(indent)}${message}${(chain.next && ('\n' + formatChain(chain.next, indent + 2))) || ''}`;
}
export function formattedError(chain: FormattedMessageChain): FormattedError {
const message = formatChain(chain) + '.';
const error = syntaxError(message) as FormattedError;
(error as any)[FORMATTED_MESSAGE] = true;
error.chain = chain;
error.position = chain.position;
return error;
}
export function isFormattedError(error: Error): error is FormattedError {
return !!(error as any)[FORMATTED_MESSAGE];
}

View File

@ -13,6 +13,7 @@ import * as o from '../output/output_ast';
import {SummaryResolver} from '../summary_resolver';
import {syntaxError} from '../util';
import {FormattedMessageChain, formattedError} from './formatted_error';
import {StaticSymbol} from './static_symbol';
import {StaticSymbolResolver} from './static_symbol_resolver';
@ -98,11 +99,16 @@ export class StaticReflector implements CompileReflector {
findSymbolDeclaration(symbol: StaticSymbol): StaticSymbol {
const resolvedSymbol = this.symbolResolver.resolveSymbol(symbol);
if (resolvedSymbol && resolvedSymbol.metadata instanceof StaticSymbol) {
return this.findSymbolDeclaration(resolvedSymbol.metadata);
} else {
return symbol;
if (resolvedSymbol) {
let resolvedMetadata = resolvedSymbol.metadata;
if (resolvedMetadata && resolvedMetadata.__symbolic === 'resolved') {
resolvedMetadata = resolvedMetadata.symbol;
}
if (resolvedMetadata instanceof StaticSymbol) {
return this.findSymbolDeclaration(resolvedSymbol.metadata);
}
}
return symbol;
}
public annotations(type: StaticSymbol): any[] {
@ -130,9 +136,12 @@ export class StaticReflector implements CompileReflector {
(requiredType) => ownAnnotations.some(ann => requiredType.isTypeOf(ann)));
if (!typeHasRequiredAnnotation) {
this.reportError(
syntaxError(
`Class ${type.name} in ${type.filePath} extends from a ${CompileSummaryKind[summary.type.summaryKind!]} in another compilation unit without duplicating the decorator. ` +
`Please add a ${requiredAnnotationTypes.map((type) => type.ngMetadataName).join(' or ')} decorator to the class.`),
formatMetadataError(
metadataError(
`Class ${type.name} in ${type.filePath} extends from a ${CompileSummaryKind[summary.type.summaryKind!]} in another compilation unit without duplicating the decorator`,
/* summary */ undefined,
`Please add a ${requiredAnnotationTypes.map((type) => type.ngMetadataName).join(' or ')} decorator to the class`),
type),
type);
}
}
@ -334,14 +343,6 @@ export class StaticReflector implements CompileReflector {
return this.symbolResolver.getStaticSymbol(declarationFile, name, members);
}
private reportError(error: Error, context: StaticSymbol, path?: string) {
if (this.errorRecorder) {
this.errorRecorder(error, (context && context.filePath) || path);
} else {
throw error;
}
}
/**
* Simplify but discard any errors
*/
@ -358,6 +359,7 @@ export class StaticReflector implements CompileReflector {
const self = this;
let scope = BindingScope.empty;
const calling = new Map<StaticSymbol, boolean>();
const rootContext = context;
function simplifyInContext(
context: StaticSymbol, value: any, depth: number, references: number): any {
@ -366,17 +368,64 @@ export class StaticReflector implements CompileReflector {
return resolvedSymbol ? resolvedSymbol.metadata : null;
}
function simplifyCall(functionSymbol: StaticSymbol, targetFunction: any, args: any[]) {
function simplifyEagerly(value: any): any {
return simplifyInContext(context, value, depth, 0);
}
function simplifyLazily(value: any): any {
return simplifyInContext(context, value, depth, references + 1);
}
function simplifyNested(nestedContext: StaticSymbol, value: any): any {
if (nestedContext === context) {
// If the context hasn't changed let the exception propagate unmodified.
return simplifyInContext(nestedContext, value, depth + 1, references);
}
try {
return simplifyInContext(nestedContext, value, depth + 1, references);
} catch (e) {
if (isMetadataError(e)) {
// Propagate the message text up but add a message to the chain that explains how we got
// here.
// e.chain implies e.symbol
const summaryMsg = e.chain ? 'references \'' + e.symbol !.name + '\'' : errorSummary(e);
const summary = `'${nestedContext.name}' ${summaryMsg}`;
const chain = {message: summary, position: e.position, next: e.chain};
// TODO(chuckj): retrieve the position information indirectly from the collectors node
// map if the metadata is from a .ts file.
self.error(
{
message: e.message,
advise: e.advise,
context: e.context, chain,
symbol: nestedContext
},
context);
} else {
// It is probably an internal error.
throw e;
}
}
}
function simplifyCall(
functionSymbol: StaticSymbol, targetFunction: any, args: any[], targetExpression: any) {
if (targetFunction && targetFunction['__symbolic'] == 'function') {
if (calling.get(functionSymbol)) {
throw new Error('Recursion not supported');
self.error(
{
message: 'Recursion is not supported',
summary: `called '${functionSymbol.name}' recursively`,
value: targetFunction
},
functionSymbol);
}
try {
const value = targetFunction['value'];
if (value && (depth != 0 || value.__symbolic != 'error')) {
const parameters: string[] = targetFunction['parameters'];
const defaults: any[] = targetFunction.defaults;
args = args.map(arg => simplifyInContext(context, arg, depth + 1, references))
args = args.map(arg => simplifyNested(context, arg))
.map(arg => shouldIgnore(arg) ? undefined : arg);
if (defaults && defaults.length > args.length) {
args.push(...defaults.slice(args.length).map((value: any) => simplify(value)));
@ -390,7 +439,7 @@ export class StaticReflector implements CompileReflector {
let result: any;
try {
scope = functionScope.done();
result = simplifyInContext(functionSymbol, value, depth + 1, references);
result = simplifyNested(functionSymbol, value);
} finally {
scope = oldScope;
}
@ -407,8 +456,22 @@ export class StaticReflector implements CompileReflector {
// non-angular decorator, and we should just ignore it.
return IGNORE;
}
return simplify(
{__symbolic: 'error', message: 'Function call not supported', context: functionSymbol});
let position: Position|undefined = undefined;
if (targetExpression && targetExpression.__symbolic == 'resolved') {
const line = targetExpression.line;
const character = targetExpression.character;
const fileName = targetExpression.fileName;
if (fileName != null && line != null && character != null) {
position = {fileName, line, column: character};
}
}
self.error(
{
message: FUNCTION_CALL_NOT_SUPPORTED,
context: functionSymbol,
value: targetFunction, position
},
context);
}
function simplify(expression: any): any {
@ -422,7 +485,7 @@ export class StaticReflector implements CompileReflector {
if (item && item.__symbolic === 'spread') {
// We call with references as 0 because we require the actual value and cannot
// tolerate a reference here.
const spreadArray = simplifyInContext(context, item.expression, depth, 0);
const spreadArray = simplifyEagerly(item.expression);
if (Array.isArray(spreadArray)) {
for (const spreadItem of spreadArray) {
result.push(spreadItem);
@ -448,7 +511,7 @@ export class StaticReflector implements CompileReflector {
const staticSymbol = expression;
const declarationValue = resolveReferenceValue(staticSymbol);
if (declarationValue != null) {
return simplifyInContext(staticSymbol, declarationValue, depth + 1, references);
return simplifyNested(staticSymbol, declarationValue);
} else {
return staticSymbol;
}
@ -525,8 +588,8 @@ export class StaticReflector implements CompileReflector {
}
return null;
case 'index':
let indexTarget = simplifyInContext(context, expression['expression'], depth, 0);
let index = simplifyInContext(context, expression['index'], depth, 0);
let indexTarget = simplifyEagerly(expression['expression']);
let index = simplifyEagerly(expression['index']);
if (indexTarget && isPrimitive(index)) return indexTarget[index];
return null;
case 'select':
@ -539,26 +602,41 @@ export class StaticReflector implements CompileReflector {
self.getStaticSymbol(selectTarget.filePath, selectTarget.name, members);
const declarationValue = resolveReferenceValue(selectContext);
if (declarationValue != null) {
return simplifyInContext(
selectContext, declarationValue, depth + 1, references);
return simplifyNested(selectContext, declarationValue);
} else {
return selectContext;
}
}
if (selectTarget && isPrimitive(member))
return simplifyInContext(
selectContext, selectTarget[member], depth + 1, references);
return simplifyNested(selectContext, selectTarget[member]);
return null;
case 'reference':
// Note: This only has to deal with variable references,
// as symbol references have been converted into StaticSymbols already
// in the StaticSymbolResolver!
// Note: This only has to deal with variable references, as symbol references have
// been converted into 'resolved'
// in the StaticSymbolResolver.
const name: string = expression['name'];
const localValue = scope.resolve(name);
if (localValue != BindingScope.missing) {
return localValue;
}
break;
case 'resolved':
try {
return simplify(expression.symbol);
} catch (e) {
// If an error is reported evaluating the symbol record the position of the
// reference in the error so it can
// be reported in the error message generated from the exception.
if (isMetadataError(e) && expression.fileName != null &&
expression.line != null && expression.character != null) {
e.position = {
fileName: expression.fileName,
line: expression.line,
column: expression.character
};
}
throw e;
}
case 'class':
return context;
case 'function':
@ -580,29 +658,34 @@ export class StaticReflector implements CompileReflector {
const argExpressions: any[] = expression['arguments'] || [];
let converter = self.conversionMap.get(staticSymbol);
if (converter) {
const args =
argExpressions
.map(arg => simplifyInContext(context, arg, depth + 1, references))
.map(arg => shouldIgnore(arg) ? undefined : arg);
const args = argExpressions.map(arg => simplifyNested(context, arg))
.map(arg => shouldIgnore(arg) ? undefined : arg);
return converter(context, args);
} else {
// Determine if the function is one we can simplify.
const targetFunction = resolveReferenceValue(staticSymbol);
return simplifyCall(staticSymbol, targetFunction, argExpressions);
return simplifyCall(
staticSymbol, targetFunction, argExpressions, expression['expression']);
}
}
return IGNORE;
case 'error':
let message = produceErrorMessage(expression);
if (expression['line']) {
message =
`${message} (position ${expression['line']+1}:${expression['character']+1} in the original .ts file)`;
self.reportError(
positionalError(
message, context.filePath, expression['line'], expression['character']),
let message = expression.message;
if (expression['line'] != null) {
self.error(
{
message,
context: expression.context,
value: expression,
position: {
fileName: expression['fileName'],
line: expression['line'],
column: expression['character']
}
},
context);
} else {
self.reportError(new Error(message), context);
self.error({message, context: expression.context}, context);
}
return IGNORE;
case 'ignore':
@ -620,7 +703,7 @@ export class StaticReflector implements CompileReflector {
return simplify(value);
}
}
return simplifyInContext(context, value, depth, references + 1);
return simplifyLazily(value);
}
return simplify(value);
});
@ -628,29 +711,19 @@ export class StaticReflector implements CompileReflector {
return IGNORE;
}
try {
return simplify(value);
} catch (e) {
const members = context.members.length ? `.${context.members.join('.')}` : '';
const message =
`${e.message}, resolving symbol ${context.name}${members} in ${context.filePath}`;
if (e.fileName) {
throw positionalError(message, e.fileName, e.line, e.column);
}
throw syntaxError(message);
}
return simplify(value);
}
const recordedSimplifyInContext = (context: StaticSymbol, value: any) => {
try {
return simplifyInContext(context, value, 0, 0);
} catch (e) {
let result: any;
try {
result = simplifyInContext(context, value, 0, 0);
} catch (e) {
if (this.errorRecorder) {
this.reportError(e, context);
} else {
throw formatMetadataError(e, context);
}
};
const result = this.errorRecorder ? recordedSimplifyInContext(context, value) :
simplifyInContext(context, value, 0, 0);
}
if (shouldIgnore(result)) {
return undefined;
}
@ -662,40 +735,166 @@ export class StaticReflector implements CompileReflector {
return resolvedSymbol && resolvedSymbol.metadata ? resolvedSymbol.metadata :
{__symbolic: 'class'};
}
}
function expandedMessage(error: any): string {
switch (error.message) {
case 'Reference to non-exported class':
if (error.context && error.context.className) {
return `Reference to a non-exported class ${error.context.className}. Consider exporting the class`;
}
break;
case 'Variable not initialized':
return 'Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler';
case 'Destructuring not supported':
return 'Referencing an exported destructured variable or constant is not supported by the template compiler. Consider simplifying this to avoid destructuring';
case 'Could not resolve type':
if (error.context && error.context.typeName) {
return `Could not resolve type ${error.context.typeName}`;
}
break;
case 'Function call not supported':
let prefix =
error.context && error.context.name ? `Calling function '${error.context.name}', f` : 'F';
return prefix +
'unction calls are not supported. Consider replacing the function or lambda with a reference to an exported function';
case 'Reference to a local symbol':
if (error.context && error.context.name) {
return `Reference to a local (non-exported) symbol '${error.context.name}'. Consider exporting the symbol`;
}
break;
private reportError(error: Error, context: StaticSymbol, path?: string) {
if (this.errorRecorder) {
this.errorRecorder(
formatMetadataError(error, context), (context && context.filePath) || path);
} else {
throw error;
}
}
private error(
{message, summary, advise, position, context, value, symbol, chain}: {
message: string,
summary?: string,
advise?: string,
position?: Position,
context?: any,
value?: any,
symbol?: StaticSymbol,
chain?: MetadataMessageChain
},
reportingContext: StaticSymbol) {
this.reportError(
metadataError(message, summary, advise, position, symbol, context, chain),
reportingContext);
}
return error.message;
}
function produceErrorMessage(error: any): string {
return `Error encountered resolving symbol values statically. ${expandedMessage(error)}`;
interface Position {
fileName: string;
line: number;
column: number;
}
interface MetadataMessageChain {
message: string;
summary?: string;
position?: Position;
context?: any;
symbol?: StaticSymbol;
next?: MetadataMessageChain;
}
type MetadataError = Error & {
position?: Position;
advise?: string;
summary?: string;
context?: any;
symbol?: StaticSymbol;
chain?: MetadataMessageChain;
};
const METADATA_ERROR = 'ngMetadataError';
function metadataError(
message: string, summary?: string, advise?: string, position?: Position, symbol?: StaticSymbol,
context?: any, chain?: MetadataMessageChain): MetadataError {
const error = syntaxError(message) as MetadataError;
(error as any)[METADATA_ERROR] = true;
if (advise) error.advise = advise;
if (position) error.position = position;
if (summary) error.summary = summary;
if (context) error.context = context;
if (chain) error.chain = chain;
if (symbol) error.symbol = symbol;
return error;
}
function isMetadataError(error: Error): error is MetadataError {
return !!(error as any)[METADATA_ERROR];
}
const REFERENCE_TO_NONEXPORTED_CLASS = 'Reference to non-exported class';
const VARIABLE_NOT_INITIALIZED = 'Variable not initialized';
const DESTRUCTURE_NOT_SUPPORTED = 'Destructuring not supported';
const COULD_NOT_RESOLVE_TYPE = 'Could not resolve type';
const FUNCTION_CALL_NOT_SUPPORTED = 'Function call not supported';
const REFERENCE_TO_LOCAL_SYMBOL = 'Reference to a local symbol';
const LAMBDA_NOT_SUPPORTED = 'Lambda not supported';
function expandedMessage(message: string, context: any): string {
switch (message) {
case REFERENCE_TO_NONEXPORTED_CLASS:
if (context && context.className) {
return `References to a non-exported class are not supported in decorators but ${context.className} was referenced.`;
}
break;
case VARIABLE_NOT_INITIALIZED:
return 'Only initialized variables and constants can be referenced in decorators because the value of this variable is needed by the template compiler';
case DESTRUCTURE_NOT_SUPPORTED:
return 'Referencing an exported destructured variable or constant is not supported in decorators and this value is needed by the template compiler';
case COULD_NOT_RESOLVE_TYPE:
if (context && context.typeName) {
return `Could not resolve type ${context.typeName}`;
}
break;
case FUNCTION_CALL_NOT_SUPPORTED:
if (context && context.name) {
return `Function calls are not supported in decorators but '${context.name}' was called`;
}
return 'Function calls are not supported in decorators';
case REFERENCE_TO_LOCAL_SYMBOL:
if (context && context.name) {
return `Reference to a local (non-exported) symbols are not supported in decorators but '${context.name}' was referenced`;
}
break;
case LAMBDA_NOT_SUPPORTED:
return `Function expressions are not supported in decorators`;
}
return message;
}
function messageAdvise(message: string, context: any): string|undefined {
switch (message) {
case REFERENCE_TO_NONEXPORTED_CLASS:
if (context && context.className) {
return `Consider exporting '${context.className}'`;
}
break;
case DESTRUCTURE_NOT_SUPPORTED:
return 'Consider simplifying to avoid destructuring';
case REFERENCE_TO_LOCAL_SYMBOL:
if (context && context.name) {
return `Consider exporting '${context.name}'`;
}
break;
case LAMBDA_NOT_SUPPORTED:
return `Consider changing the function expression into an exported function`;
}
return undefined;
}
function errorSummary(error: MetadataError): string {
if (error.summary) {
return error.summary;
}
switch (error.message) {
case REFERENCE_TO_NONEXPORTED_CLASS:
if (error.context && error.context.className) {
return `references non-exported class ${error.context.className}`;
}
break;
case VARIABLE_NOT_INITIALIZED:
return 'is not initialized';
case DESTRUCTURE_NOT_SUPPORTED:
return 'is a destructured variable';
case COULD_NOT_RESOLVE_TYPE:
return 'could not be resolved';
case FUNCTION_CALL_NOT_SUPPORTED:
if (error.context && error.context.name) {
return `calls '${error.context.name}'`;
}
return `calls a function`;
case REFERENCE_TO_LOCAL_SYMBOL:
if (error.context && error.context.name) {
return `references local variable ${error.context.name}`;
}
return `references a local variable`;
}
return 'contains the error';
}
function mapStringMap(input: {[key: string]: any}, transform: (value: any, key: string) => any):
@ -751,10 +950,30 @@ class PopulatedScope extends BindingScope {
}
}
function positionalError(message: string, fileName: string, line: number, column: number): Error {
const result = syntaxError(message);
(result as any).fileName = fileName;
(result as any).line = line;
(result as any).column = column;
return result;
}
function formatMetadataMessageChain(
chain: MetadataMessageChain, advise: string | undefined): FormattedMessageChain {
const expanded = expandedMessage(chain.message, chain.context);
const nesting = chain.symbol ? ` in '${chain.symbol.name}'` : '';
const message = `${expanded}${nesting}`;
const position = chain.position;
const next: FormattedMessageChain|undefined = chain.next ?
formatMetadataMessageChain(chain.next, advise) :
advise ? {message: advise} : undefined;
return {message, position, next};
}
function formatMetadataError(e: Error, context: StaticSymbol): Error {
if (isMetadataError(e)) {
// Produce a formatted version of the and leaving enough information in the original error
// to recover the formatting information to eventually produce a diagnostic error message.
const position = e.position;
const chain: MetadataMessageChain = {
message: `Error during template compile of '${context.name}'`,
position: position,
next: {message: e.message, next: e.chain, context: e.context, symbol: e.symbol}
};
const advise = e.advise || messageAdvise(e.message, e.context);
return formattedError(formatMetadataMessageChain(chain, advise));
}
return e;
}

View File

@ -146,9 +146,9 @@ export class StaticSymbolResolver {
if (isGeneratedFile(staticSymbol.filePath)) {
return null;
}
let resolvedSymbol = this.resolveSymbol(staticSymbol);
let resolvedSymbol = unwrapResolvedMetadata(this.resolveSymbol(staticSymbol));
while (resolvedSymbol && resolvedSymbol.metadata instanceof StaticSymbol) {
resolvedSymbol = this.resolveSymbol(resolvedSymbol.metadata);
resolvedSymbol = unwrapResolvedMetadata(this.resolveSymbol(resolvedSymbol.metadata));
}
return (resolvedSymbol && resolvedSymbol.metadata && resolvedSymbol.metadata.arity) || null;
}
@ -204,7 +204,7 @@ export class StaticSymbolResolver {
if (!baseResolvedSymbol) {
return null;
}
const baseMetadata = baseResolvedSymbol.metadata;
let baseMetadata = unwrapResolvedMetadata(baseResolvedSymbol.metadata);
if (baseMetadata instanceof StaticSymbol) {
return new ResolvedStaticSymbol(
staticSymbol, this.getStaticSymbol(baseMetadata.filePath, baseMetadata.name, members));
@ -374,6 +374,19 @@ export class StaticSymbolResolver {
return new ResolvedStaticSymbol(sourceSymbol, transformedMeta);
}
let _originalFileMemo: string|undefined;
const getOriginalName: () => string = () => {
if (!_originalFileMemo) {
// Guess what hte original file name is from the reference. If it has a `.d.ts` extension
// replace it with `.ts`. If it already has `.ts` just leave it in place. If it doesn't have
// .ts or .d.ts, append `.ts'. Also, if it is in `node_modules`, trim the `node_module`
// location as it is not important to finding the file.
_originalFileMemo =
topLevelPath.replace(/((\.ts)|(\.d\.ts)|)$/, '.ts').replace(/^.*node_modules[/\\]/, '');
}
return _originalFileMemo;
};
const self = this;
class ReferenceTransformer extends ValueTransformer {
@ -397,10 +410,19 @@ export class StaticSymbolResolver {
if (!filePath) {
return {
__symbolic: 'error',
message: `Could not resolve ${module} relative to ${sourceSymbol.filePath}.`
message: `Could not resolve ${module} relative to ${sourceSymbol.filePath}.`,
line: map.line,
character: map.character,
fileName: getOriginalName()
};
}
return self.getStaticSymbol(filePath, name);
return {
__symbolic: 'resolved',
symbol: self.getStaticSymbol(filePath, name),
line: map.line,
character: map.character,
fileName: getOriginalName()
};
} else if (functionParams.indexOf(name) >= 0) {
// reference to a function parameter
return {__symbolic: 'reference', name: name};
@ -411,14 +433,17 @@ export class StaticSymbolResolver {
// ambient value
null;
}
} else if (symbolic === 'error') {
return {...map, fileName: getOriginalName()};
} else {
return super.visitStringMap(map, functionParams);
}
}
}
const transformedMeta = visitValue(metadata, new ReferenceTransformer(), []);
if (transformedMeta instanceof StaticSymbol) {
return this.createExport(sourceSymbol, transformedMeta);
let unwrappedTransformedMeta = unwrapResolvedMetadata(transformedMeta);
if (unwrappedTransformedMeta instanceof StaticSymbol) {
return this.createExport(sourceSymbol, unwrappedTransformedMeta);
}
return new ResolvedStaticSymbol(sourceSymbol, transformedMeta);
}
@ -505,3 +530,10 @@ export class StaticSymbolResolver {
export function unescapeIdentifier(identifier: string): string {
return identifier.startsWith('___') ? identifier.substr(1) : identifier;
}
export function unwrapResolvedMetadata(metadata: any): any {
if (metadata && metadata.__symbolic === 'resolved') {
return metadata.symbol;
}
return metadata;
}

View File

@ -35,6 +35,7 @@ export * from './aot/compiler';
export * from './aot/generated_file';
export * from './aot/compiler_options';
export * from './aot/compiler_host';
export * from './aot/formatted_error';
export * from './aot/static_reflector';
export * from './aot/static_symbol';
export * from './aot/static_symbol_resolver';

View File

@ -768,9 +768,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({providers: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Injectable in another compilation unit without duplicating the decorator. ' +
'Please add a Injectable or Pipe or Directive or Component or NgModule decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Injectable in another compilation unit without duplicating the decorator
Please add a Injectable or Pipe or Directive or Component or NgModule decorator to the class.`);
});
});
@ -792,9 +792,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({declarations: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator. ' +
'Please add a Directive or Component decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator
Please add a Directive or Component decorator to the class.`);
});
});
@ -816,9 +816,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({declarations: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator. ' +
'Please add a Directive or Component decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator
Please add a Directive or Component decorator to the class.`);
});
});
@ -840,9 +840,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({declarations: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Pipe in another compilation unit without duplicating the decorator. ' +
'Please add a Pipe decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Pipe in another compilation unit without duplicating the decorator
Please add a Pipe decorator to the class.`);
});
});
@ -864,9 +864,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a NgModule in another compilation unit without duplicating the decorator. ' +
'Please add a NgModule decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a NgModule in another compilation unit without duplicating the decorator
Please add a NgModule decorator to the class.`);
});
});
}

View File

@ -107,8 +107,11 @@ describe('StaticReflector', () => {
it('should provide context for errors reported by the collector', () => {
const SomeClass = reflector.findDeclaration('src/error-reporting', 'SomeClass');
expect(() => reflector.annotations(SomeClass))
.toThrow(new Error(
'Error encountered resolving symbol values statically. A reasonable error message (position 13:34 in the original .ts file), resolving symbol ErrorSym in /tmp/src/error-references.d.ts, resolving symbol Link2 in /tmp/src/error-references.d.ts, resolving symbol Link1 in /tmp/src/error-references.d.ts, resolving symbol SomeClass in /tmp/src/error-reporting.d.ts, resolving symbol SomeClass in /tmp/src/error-reporting.d.ts'));
.toThrow(new Error(`Error during template compile of 'SomeClass'
A reasonable error message in 'Link1'
'Link1' references 'Link2'
'Link2' references 'ErrorSym'
'ErrorSym' contains the error at /tmp/src/error-references.ts(13,34).`));
});
it('should simplify primitive into itself', () => {
@ -330,10 +333,12 @@ describe('StaticReflector', () => {
it('should error on direct recursive calls', () => {
expect(
() => simplify(
reflector.getStaticSymbol('/tmp/src/function-reference.ts', ''),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'MyComp'),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'recursion')))
.toThrow(new Error(
'Recursion not supported, resolving symbol recursive in /tmp/src/function-recursive.d.ts, resolving symbol recursion in /tmp/src/function-reference.ts, resolving symbol in /tmp/src/function-reference.ts'));
.toThrow(new Error(`Error during template compile of 'MyComp'
Recursion is not supported in 'recursion'
'recursion' references 'recursive'
'recursive' called 'recursive' recursively.`));
});
it('should throw a SyntaxError without stack trace when the required resource cannot be resolved',
@ -345,8 +350,8 @@ describe('StaticReflector', () => {
message:
'Could not resolve ./does-not-exist.component relative to /tmp/src/function-reference.ts'
})))
.toThrowError(
'Error encountered resolving symbol values statically. Could not resolve ./does-not-exist.component relative to /tmp/src/function-reference.ts, resolving symbol AppModule in /tmp/src/function-reference.ts');
.toThrowError(`Error during template compile of 'AppModule'
Could not resolve ./does-not-exist.component relative to /tmp/src/function-reference.ts.`);
});
it('should record data about the error in the exception', () => {
@ -361,7 +366,7 @@ describe('StaticReflector', () => {
simplify(
reflector.getStaticSymbol('/tmp/src/invalid-metadata.ts', ''), classData.decorators[0]);
} catch (e) {
expect(e.fileName).toBe('/tmp/src/invalid-metadata.ts');
expect(e.position).toBeDefined();
threw = true;
}
expect(threw).toBe(true);
@ -370,10 +375,13 @@ describe('StaticReflector', () => {
it('should error on indirect recursive calls', () => {
expect(
() => simplify(
reflector.getStaticSymbol('/tmp/src/function-reference.ts', ''),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'MyComp'),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'indirectRecursion')))
.toThrow(new Error(
'Recursion not supported, resolving symbol indirectRecursion2 in /tmp/src/function-recursive.d.ts, resolving symbol indirectRecursion1 in /tmp/src/function-recursive.d.ts, resolving symbol indirectRecursion in /tmp/src/function-reference.ts, resolving symbol in /tmp/src/function-reference.ts'));
.toThrow(new Error(`Error during template compile of 'MyComp'
Recursion is not supported in 'indirectRecursion'
'indirectRecursion' references 'indirectRecursion1'
'indirectRecursion1' references 'indirectRecursion2'
'indirectRecursion2' called 'indirectRecursion1' recursively.`));
});
it('should simplify a spread expression', () => {
@ -401,7 +409,8 @@ describe('StaticReflector', () => {
() => reflector.annotations(
reflector.getStaticSymbol('/tmp/src/invalid-calls.ts', 'MyComponent')))
.toThrow(new Error(
`Error encountered resolving symbol values statically. Calling function 'someFunction', function calls are not supported. Consider replacing the function or lambda with a reference to an exported function, resolving symbol MyComponent in /tmp/src/invalid-calls.ts, resolving symbol MyComponent in /tmp/src/invalid-calls.ts`));
`/tmp/src/invalid-calls.ts(8,29): Error during template compile of 'MyComponent'
Function calls are not supported in decorators but 'someFunction' was called.`));
});
it('should be able to get metadata for a class containing a static method call', () => {
@ -962,7 +971,7 @@ describe('StaticReflector', () => {
});
// Regression #18170
it('should agressively evaluate enums selects', () => {
it('should eagerly evaluate enums selects', () => {
const data = Object.create(DEFAULT_TEST_DATA);
const file = '/tmp/src/my_component.ts';
data[file] = `
@ -1078,6 +1087,228 @@ describe('StaticReflector', () => {
expect(symbolResolver.getKnownModuleName(symbol.filePath)).toBe('a');
});
});
describe('formatted error reporting', () => {
describe('function calls', () => {
const fileName = '/tmp/src/invalid/components.ts';
beforeEach(() => {
const localData = {
'/tmp/src/invalid/function-call.ts': `
import {functionToCall} from 'some-module';
export const CALL_FUNCTION = functionToCall();
`,
'/tmp/src/invalid/indirect.ts': `
import {CALL_FUNCTION} from './function-call';
export const INDIRECT_CALL_FUNCTION = CALL_FUNCTION + 1;
`,
'/tmp/src/invalid/two-levels-indirect.ts': `
import {INDIRECT_CALL_FUNCTION} from './indirect';
export const TWO_LEVELS_INDIRECT_CALL_FUNCTION = INDIRECT_CALL_FUNCTION + 1;
`,
'/tmp/src/invalid/components.ts': `
import {functionToCall} from 'some-module';
import {Component} from '@angular/core';
import {CALL_FUNCTION} from './function-call';
import {INDIRECT_CALL_FUNCTION} from './indirect';
import {TWO_LEVELS_INDIRECT_CALL_FUNCTION} from './two-levels-indirect';
@Component({
value: functionToCall()
})
export class CallImportedFunction {}
@Component({
value: CALL_FUNCTION
})
export class ReferenceCalledFunction {}
@Component({
value: INDIRECT_CALL_FUNCTION
})
export class IndirectReferenceCalledFunction {}
@Component({
value: TWO_LEVELS_INDIRECT_CALL_FUNCTION
})
export class TwoLevelsIndirectReferenceCalledFunction {}
`
};
init({...DEFAULT_TEST_DATA, ...localData});
});
it('should report a formatted error for a direct function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'CallImportedFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(9,18): Error during template compile of 'CallImportedFunction'
Function calls are not supported in decorators but 'functionToCall' was called.`);
});
it('should report a formatted error for a refernce to a function call', () => {
expect(() => {
return reflector.annotations(
reflector.getStaticSymbol(fileName, 'ReferenceCalledFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(14,18): Error during template compile of 'ReferenceCalledFunction'
Function calls are not supported in decorators but 'functionToCall' was called in 'CALL_FUNCTION'
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
it('should report a formatted error for an indirect reference to a function call', () => {
expect(() => {
return reflector.annotations(
reflector.getStaticSymbol(fileName, 'IndirectReferenceCalledFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(19,18): Error during template compile of 'IndirectReferenceCalledFunction'
Function calls are not supported in decorators but 'functionToCall' was called in 'INDIRECT_CALL_FUNCTION'
'INDIRECT_CALL_FUNCTION' references 'CALL_FUNCTION' at /tmp/src/invalid/indirect.ts(4,47)
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
it('should report a formatted error for a double-indirect refernce to a function call', () => {
expect(() => {
return reflector.annotations(
reflector.getStaticSymbol(fileName, 'TwoLevelsIndirectReferenceCalledFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(24,18): Error during template compile of 'TwoLevelsIndirectReferenceCalledFunction'
Function calls are not supported in decorators but 'functionToCall' was called in 'TWO_LEVELS_INDIRECT_CALL_FUNCTION'
'TWO_LEVELS_INDIRECT_CALL_FUNCTION' references 'INDIRECT_CALL_FUNCTION' at /tmp/src/invalid/two-levels-indirect.ts(4,58)
'INDIRECT_CALL_FUNCTION' references 'CALL_FUNCTION' at /tmp/src/invalid/indirect.ts(4,47)
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
});
describe('macro functions', () => {
const fileName = '/tmp/src/invalid/components.ts';
beforeEach(() => {
const localData = {
'/tmp/src/invalid/function-call.ts': `
import {functionToCall} from 'some-module';
export const CALL_FUNCTION = functionToCall();
`,
'/tmp/src/invalid/indirect.ts': `
import {CALL_FUNCTION} from './function-call';
export const INDIRECT_CALL_FUNCTION = CALL_FUNCTION + 1;
`,
'/tmp/src/invalid/macros.ts': `
export function someMacro(value: any) {
return [ { provide: 'key', value: value } ];
}
`,
'/tmp/src/invalid/components.ts': `
import {Component} from '@angular/core';
import {functionToCall} from 'some-module';
import {someMacro} from './macros';
import {CALL_FUNCTION} from './function-call';
import {INDIRECT_CALL_FUNCTION} from './indirect';
@Component({
template: someMacro(functionToCall())
})
export class DirectCall {}
@Component({
template: someMacro(CALL_FUNCTION)
})
export class IndirectCall {}
@Component({
template: someMacro(INDIRECT_CALL_FUNCTION)
})
export class DoubleIndirectCall {}
`
};
init({...DEFAULT_TEST_DATA, ...localData});
});
it('should report a formatted error for a direct function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'DirectCall'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(9,31): Error during template compile of 'DirectCall'
Function calls are not supported in decorators but 'functionToCall' was called.`);
});
it('should report a formatted error for a reference to a function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'IndirectCall'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(14,31): Error during template compile of 'IndirectCall'
Function calls are not supported in decorators but 'functionToCall' was called in 'CALL_FUNCTION'
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
it('should report a formatted error for an indirect refernece to a function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'DoubleIndirectCall'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(19,31): Error during template compile of 'DoubleIndirectCall'
Function calls are not supported in decorators but 'functionToCall' was called in 'INDIRECT_CALL_FUNCTION'
'INDIRECT_CALL_FUNCTION' references 'CALL_FUNCTION' at /tmp/src/invalid/indirect.ts(4,47)
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
});
describe('and give advice', () => {
// If in a reference expression, advice the user to replace with a reference.
const fileName = '/tmp/src/invalid/components.ts';
function collectError(symbol: string): string {
try {
reflector.annotations(reflector.getStaticSymbol(fileName, symbol));
} catch (e) {
return e.message;
}
fail('Expected an exception to be thrown');
return '';
}
function initWith(content: string) {
init({
...DEFAULT_TEST_DATA,
[fileName]: `import {Component} from '@angular/core';\n${content}`
});
}
it('should advise exorting a local', () => {
initWith(`const f: string; @Component({value: f}) export class MyComp {}`);
expect(collectError('MyComp')).toContain(`Consider exporting 'f'`);
});
it('should advise export a class', () => {
initWith('class Foo {} @Component({value: Foo}) export class MyComp {}');
expect(collectError('MyComp')).toContain(`Consider exporting 'Foo'`);
});
it('should advise avoiding destructuring', () => {
initWith(
'export const {foo, bar} = {foo: 1, bar: 2}; @Component({value: foo}) export class MyComp {}');
expect(collectError('MyComp')).toContain(`Consider simplifying to avoid destructuring`);
});
it('should advise converting an arrow function into an exported function', () => {
initWith('@Component({value: () => true}) export class MyComp {}');
expect(collectError('MyComp'))
.toContain(`Consider changing the function expression into an exported function`);
});
it('should advise converting a function expression into an exported function', () => {
initWith('@Component({value: function () { return true; }}) export class MyComp {}');
expect(collectError('MyComp'))
.toContain(`Consider changing the function expression into an exported function`);
});
});
});
});
const DEFAULT_TEST_DATA: {[key: string]: any} = {
@ -1467,5 +1698,5 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
export class Dep {
@Input f: Forward;
}
`
`,
};

View File

@ -234,15 +234,25 @@ describe('StaticSymbolResolver', () => {
});
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'a')).metadata)
.toEqual(symbolCache.get('/test2.ts', 'b'));
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'x')).metadata).toEqual([
symbolCache.get('/test2.ts', 'y')
]);
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'x')).metadata).toEqual([{
__symbolic: 'resolved',
symbol: symbolCache.get('/test2.ts', 'y'),
line: 3,
character: 24,
fileName: '/test.ts'
}]);
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'simpleFn')).metadata).toEqual({
__symbolic: 'function',
parameters: ['fnArg'],
value: [
symbolCache.get('/test.ts', 'a'), symbolCache.get('/test2.ts', 'y'),
Object({__symbolic: 'reference', name: 'fnArg'})
symbolCache.get('/test.ts', 'a'), {
__symbolic: 'resolved',
symbol: symbolCache.get('/test2.ts', 'y'),
line: 6,
character: 21,
fileName: '/test.ts'
},
{__symbolic: 'reference', name: 'fnArg'}
]
});
});