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:
parent
1366762d12
commit
8ecda94899
|
@ -20,7 +20,6 @@ import {GENERATED_FILES} from './transformers/util';
|
|||
|
||||
import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult, filterErrorsAndWarnings} from './perform_compile';
|
||||
import {performWatchCompilation, createPerformWatchHost} from './perform_watch';
|
||||
import {isSyntaxError} from '@angular/compiler';
|
||||
|
||||
export function main(
|
||||
args: string[], consoleError: (s: string) => void = console.error,
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Evaluator, errorSymbol} from './evaluator';
|
||||
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, InterfaceMetadata, METADATA_VERSION, MemberMetadata, MetadataEntry, MetadataError, MetadataMap, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression, isMethodMetadata} from './schema';
|
||||
import {Evaluator, errorSymbol, recordMapEntry} from './evaluator';
|
||||
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, InterfaceMetadata, METADATA_VERSION, MemberMetadata, MetadataEntry, MetadataError, MetadataMap, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportDefaultReference, isMetadataImportedSymbolReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression, isMethodMetadata} from './schema';
|
||||
import {Symbols} from './symbols';
|
||||
|
||||
const isStatic = (node: ts.Node) => ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static;
|
||||
|
@ -76,8 +76,7 @@ export class MetadataCollector {
|
|||
}
|
||||
|
||||
function recordEntry<T extends MetadataEntry>(entry: T, node: ts.Node): T {
|
||||
nodeMap.set(entry, node);
|
||||
return entry;
|
||||
return recordMapEntry(entry, node, nodeMap, sourceFile);
|
||||
}
|
||||
|
||||
function errorSym(
|
||||
|
|
|
@ -9,10 +9,11 @@
|
|||
import * as ts from 'typescript';
|
||||
|
||||
import {CollectorOptions} from './collector';
|
||||
import {MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema';
|
||||
import {ClassMetadata, FunctionMetadata, InterfaceMetadata, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSourceLocationInfo, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportDefaultReference, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema';
|
||||
import {Symbols} from './symbols';
|
||||
|
||||
|
||||
|
||||
// In TypeScript 2.1 the spread element kind was renamed.
|
||||
const spreadElementSyntaxKind: ts.SyntaxKind =
|
||||
(ts.SyntaxKind as any).SpreadElement || (ts.SyntaxKind as any).SpreadElementExpression;
|
||||
|
@ -38,6 +39,24 @@ function isCallOf(callExpression: ts.CallExpression, ident: string): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function recordMapEntry<T extends MetadataEntry>(
|
||||
entry: T, node: ts.Node,
|
||||
nodeMap: Map<MetadataValue|ClassMetadata|InterfaceMetadata|FunctionMetadata, ts.Node>,
|
||||
sourceFile?: ts.SourceFile) {
|
||||
if (!nodeMap.has(entry)) {
|
||||
nodeMap.set(entry, node);
|
||||
if (node && (isMetadataImportedSymbolReferenceExpression(entry) ||
|
||||
isMetadataImportDefaultReference(entry)) &&
|
||||
entry.line == null) {
|
||||
const info = sourceInfo(node, sourceFile);
|
||||
if (info.line != null) entry.line = info.line;
|
||||
if (info.character != null) entry.character = info.character;
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* ts.forEachChild stops iterating children when the callback return a truthy value.
|
||||
* This method inverts this to implement an `every` style iterator. It will return
|
||||
|
@ -77,21 +96,22 @@ function getSourceFileOfNode(node: ts.Node | undefined): ts.SourceFile {
|
|||
}
|
||||
|
||||
/* @internal */
|
||||
export function errorSymbol(
|
||||
message: string, node?: ts.Node, context?: {[name: string]: string},
|
||||
sourceFile?: ts.SourceFile): MetadataError {
|
||||
let result: MetadataError|undefined = undefined;
|
||||
export function sourceInfo(
|
||||
node: ts.Node | undefined, sourceFile: ts.SourceFile | undefined): MetadataSourceLocationInfo {
|
||||
if (node) {
|
||||
sourceFile = sourceFile || getSourceFileOfNode(node);
|
||||
if (sourceFile) {
|
||||
const {line, character} =
|
||||
ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
||||
result = {__symbolic: 'error', message, line, character};
|
||||
return ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
||||
}
|
||||
}
|
||||
if (!result) {
|
||||
result = {__symbolic: 'error', message};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function errorSymbol(
|
||||
message: string, node?: ts.Node, context?: {[name: string]: string},
|
||||
sourceFile?: ts.SourceFile): MetadataError {
|
||||
const result: MetadataError = {__symbolic: 'error', message, ...sourceInfo(node, sourceFile)};
|
||||
if (context) {
|
||||
result.context = context;
|
||||
}
|
||||
|
@ -242,8 +262,7 @@ export class Evaluator {
|
|||
}
|
||||
entry = newEntry;
|
||||
}
|
||||
t.nodeMap.set(entry, node);
|
||||
return entry;
|
||||
return recordMapEntry(entry, node, t.nodeMap);
|
||||
}
|
||||
|
||||
function isFoldableError(value: any): value is MetadataError {
|
||||
|
@ -256,6 +275,9 @@ export class Evaluator {
|
|||
// Encode as a global reference. StaticReflector will check the reference.
|
||||
return recordEntry({__symbolic: 'reference', name}, node);
|
||||
}
|
||||
if (reference && isMetadataSymbolicReferenceExpression(reference)) {
|
||||
return recordEntry({...reference}, node);
|
||||
}
|
||||
return reference;
|
||||
};
|
||||
|
||||
|
@ -628,7 +650,7 @@ export class Evaluator {
|
|||
return recordEntry({__symbolic: 'if', condition, thenExpression, elseExpression}, node);
|
||||
case ts.SyntaxKind.FunctionExpression:
|
||||
case ts.SyntaxKind.ArrowFunction:
|
||||
return recordEntry(errorSymbol('Function call not supported', node), node);
|
||||
return recordEntry(errorSymbol('Lambda not supported', node), node);
|
||||
case ts.SyntaxKind.TaggedTemplateExpression:
|
||||
return recordEntry(
|
||||
errorSymbol('Tagged template expressions are not supported in metadata', node), node);
|
||||
|
|
|
@ -178,7 +178,20 @@ export function isMetadataSymbolicIfExpression(value: any): value is MetadataSym
|
|||
return value && value.__symbolic === 'if';
|
||||
}
|
||||
|
||||
export interface MetadataGlobalReferenceExpression extends MetadataSymbolicExpression {
|
||||
export interface MetadataSourceLocationInfo {
|
||||
/**
|
||||
* The line number of the error in the .ts file the metadata was created for.
|
||||
*/
|
||||
line?: number;
|
||||
|
||||
/**
|
||||
* The number of utf8 code-units from the beginning of the file of the error.
|
||||
*/
|
||||
character?: number;
|
||||
}
|
||||
|
||||
export interface MetadataGlobalReferenceExpression extends MetadataSymbolicExpression,
|
||||
MetadataSourceLocationInfo {
|
||||
__symbolic: 'reference';
|
||||
name: string;
|
||||
arguments?: MetadataValue[];
|
||||
|
@ -188,7 +201,8 @@ export function isMetadataGlobalReferenceExpression(value: any):
|
|||
return value && value.name && !value.module && isMetadataSymbolicReferenceExpression(value);
|
||||
}
|
||||
|
||||
export interface MetadataModuleReferenceExpression extends MetadataSymbolicExpression {
|
||||
export interface MetadataModuleReferenceExpression extends MetadataSymbolicExpression,
|
||||
MetadataSourceLocationInfo {
|
||||
__symbolic: 'reference';
|
||||
module: string;
|
||||
}
|
||||
|
@ -198,7 +212,8 @@ export function isMetadataModuleReferenceExpression(value: any):
|
|||
isMetadataSymbolicReferenceExpression(value);
|
||||
}
|
||||
|
||||
export interface MetadataImportedSymbolReferenceExpression extends MetadataSymbolicExpression {
|
||||
export interface MetadataImportedSymbolReferenceExpression extends MetadataSymbolicExpression,
|
||||
MetadataSourceLocationInfo {
|
||||
__symbolic: 'reference';
|
||||
module: string;
|
||||
name: string;
|
||||
|
@ -209,7 +224,8 @@ export function isMetadataImportedSymbolReferenceExpression(value: any):
|
|||
return value && value.module && !!value.name && isMetadataSymbolicReferenceExpression(value);
|
||||
}
|
||||
|
||||
export interface MetadataImportedDefaultReferenceExpression extends MetadataSymbolicExpression {
|
||||
export interface MetadataImportedDefaultReferenceExpression extends MetadataSymbolicExpression,
|
||||
MetadataSourceLocationInfo {
|
||||
__symbolic: 'reference';
|
||||
module: string;
|
||||
default:
|
||||
|
@ -218,7 +234,7 @@ export interface MetadataImportedDefaultReferenceExpression extends MetadataSymb
|
|||
}
|
||||
export function isMetadataImportDefaultReference(value: any):
|
||||
value is MetadataImportedDefaultReferenceExpression {
|
||||
return value.module && value.default && isMetadataSymbolicReferenceExpression(value);
|
||||
return value && value.module && value.default && isMetadataSymbolicReferenceExpression(value);
|
||||
}
|
||||
|
||||
export type MetadataSymbolicReferenceExpression = MetadataGlobalReferenceExpression |
|
||||
|
@ -248,7 +264,7 @@ export function isMetadataSymbolicSpreadExpression(value: any):
|
|||
return value && value.__symbolic === 'spread';
|
||||
}
|
||||
|
||||
export interface MetadataError {
|
||||
export interface MetadataError extends MetadataSourceLocationInfo {
|
||||
__symbolic: 'error';
|
||||
|
||||
/**
|
||||
|
@ -259,16 +275,6 @@ export interface MetadataError {
|
|||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* The line number of the error in the .ts file the metadata was created for.
|
||||
*/
|
||||
line?: number;
|
||||
|
||||
/**
|
||||
* The number of utf8 code-units from the beginning of the file of the error.
|
||||
*/
|
||||
character?: number;
|
||||
|
||||
/**
|
||||
* The module of the error (only used in bundled metadata)
|
||||
*/
|
||||
|
@ -280,6 +286,7 @@ export interface MetadataError {
|
|||
*/
|
||||
context?: {[name: string]: string};
|
||||
}
|
||||
|
||||
export function isMetadataError(value: any): value is MetadataError {
|
||||
return value && value.__symbolic === 'error';
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {isSyntaxError, syntaxError} from '@angular/compiler';
|
||||
import {Position, isSyntaxError, syntaxError} from '@angular/compiler';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
@ -29,30 +29,76 @@ const defaultFormatHost: ts.FormatDiagnosticsHost = {
|
|||
getNewLine: () => ts.sys.newLine
|
||||
};
|
||||
|
||||
function displayFileName(fileName: string, host: ts.FormatDiagnosticsHost): string {
|
||||
return path.relative(host.getCurrentDirectory(), host.getCanonicalFileName(fileName));
|
||||
}
|
||||
|
||||
export function formatDiagnosticPosition(
|
||||
position: Position, host: ts.FormatDiagnosticsHost = defaultFormatHost): string {
|
||||
return `${displayFileName(position.fileName, host)}(${position.line + 1},${position.column+1})`;
|
||||
}
|
||||
|
||||
export function flattenDiagnosticMessageChain(
|
||||
chain: api.DiagnosticMessageChain, host: ts.FormatDiagnosticsHost = defaultFormatHost): string {
|
||||
let result = chain.messageText;
|
||||
let indent = 1;
|
||||
let current = chain.next;
|
||||
const newLine = host.getNewLine();
|
||||
while (current) {
|
||||
result += newLine;
|
||||
for (let i = 0; i < indent; i++) {
|
||||
result += ' ';
|
||||
}
|
||||
result += current.messageText;
|
||||
const position = current.position;
|
||||
if (position) {
|
||||
result += ` at ${formatDiagnosticPosition(position, host)}`;
|
||||
}
|
||||
current = current.next;
|
||||
indent++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function formatDiagnostic(
|
||||
diagnostic: api.Diagnostic, host: ts.FormatDiagnosticsHost = defaultFormatHost) {
|
||||
let result = '';
|
||||
const newLine = host.getNewLine();
|
||||
const span = diagnostic.span;
|
||||
if (span) {
|
||||
result += `${formatDiagnosticPosition({
|
||||
fileName: span.start.file.url,
|
||||
line: span.start.line,
|
||||
column: span.start.col
|
||||
}, host)}: `;
|
||||
} else if (diagnostic.position) {
|
||||
result += `${formatDiagnosticPosition(diagnostic.position, host)}: `;
|
||||
}
|
||||
if (diagnostic.span && diagnostic.span.details) {
|
||||
result += `: ${diagnostic.span.details}, ${diagnostic.messageText}${newLine}`;
|
||||
} else if (diagnostic.chain) {
|
||||
result += `${flattenDiagnosticMessageChain(diagnostic.chain, host)}.${newLine}`;
|
||||
} else {
|
||||
result += `: ${diagnostic.messageText}${newLine}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function formatDiagnostics(
|
||||
diags: Diagnostics, tsFormatHost: ts.FormatDiagnosticsHost = defaultFormatHost): string {
|
||||
diags: Diagnostics, host: ts.FormatDiagnosticsHost = defaultFormatHost): string {
|
||||
if (diags && diags.length) {
|
||||
return diags
|
||||
.map(d => {
|
||||
if (api.isTsDiagnostic(d)) {
|
||||
return ts.formatDiagnostics([d], tsFormatHost);
|
||||
.map(diagnostic => {
|
||||
if (api.isTsDiagnostic(diagnostic)) {
|
||||
return ts.formatDiagnostics([diagnostic], host);
|
||||
} else {
|
||||
let res = ts.DiagnosticCategory[d.category];
|
||||
if (d.span) {
|
||||
res +=
|
||||
` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`;
|
||||
}
|
||||
if (d.span && d.span.details) {
|
||||
res += `: ${d.span.details}, ${d.messageText}\n`;
|
||||
} else {
|
||||
res += `: ${d.messageText}\n`;
|
||||
}
|
||||
return res;
|
||||
return formatDiagnostic(diagnostic, host);
|
||||
}
|
||||
})
|
||||
.join('');
|
||||
} else
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export interface ParsedConfiguration {
|
||||
|
|
|
@ -6,16 +6,24 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {GeneratedFile, ParseSourceSpan} from '@angular/compiler';
|
||||
import {GeneratedFile, ParseSourceSpan, Position} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export const DEFAULT_ERROR_CODE = 100;
|
||||
export const UNKNOWN_ERROR_CODE = 500;
|
||||
export const SOURCE = 'angular' as 'angular';
|
||||
|
||||
export interface DiagnosticMessageChain {
|
||||
messageText: string;
|
||||
position?: Position;
|
||||
next?: DiagnosticMessageChain;
|
||||
}
|
||||
|
||||
export interface Diagnostic {
|
||||
messageText: string;
|
||||
span?: ParseSourceSpan;
|
||||
position?: Position;
|
||||
chain?: DiagnosticMessageChain;
|
||||
category: ts.DiagnosticCategory;
|
||||
code: number;
|
||||
source: 'angular';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError} from '@angular/compiler';
|
||||
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
@ -14,14 +14,13 @@ import * as ts from 'typescript';
|
|||
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
|
||||
import {ModuleMetadata, createBundleIndexHost} from '../metadata/index';
|
||||
|
||||
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
|
||||
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
|
||||
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
|
||||
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
|
||||
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
|
||||
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused} from './util';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Maximum number of files that are emitable via calling ts.Program.emit
|
||||
* passing individual targetSourceFiles.
|
||||
|
@ -378,10 +377,12 @@ class AngularCompilerProgram implements Program {
|
|||
}
|
||||
|
||||
private get structuralDiagnostics(): Diagnostic[] {
|
||||
if (!this._structuralDiagnostics) {
|
||||
let diagnostics = this._structuralDiagnostics;
|
||||
if (!diagnostics) {
|
||||
this.initSync();
|
||||
diagnostics = (this._structuralDiagnostics = this._structuralDiagnostics || []);
|
||||
}
|
||||
return this._structuralDiagnostics !;
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
private get tsProgram(): ts.Program {
|
||||
|
@ -430,16 +431,9 @@ class AngularCompilerProgram implements Program {
|
|||
this.rootNames, this.options, this.host, this.metadataCache, codegen,
|
||||
this.oldProgramLibrarySummaries);
|
||||
const aotOptions = getAotCompilerOptions(this.options);
|
||||
this._structuralDiagnostics = [];
|
||||
const errorCollector =
|
||||
(this.options.collectAllErrors || this.options.fullTemplateTypeCheck) ? (err: any) => {
|
||||
this._structuralDiagnostics !.push({
|
||||
messageText: err.toString(),
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
});
|
||||
} : undefined;
|
||||
const errorCollector = (this.options.collectAllErrors || this.options.fullTemplateTypeCheck) ?
|
||||
(err: any) => this._addStructuralDiagnostics(err) :
|
||||
undefined;
|
||||
this._compiler = createAotCompiler(this._hostAdapter, aotOptions, errorCollector).compiler;
|
||||
}
|
||||
|
||||
|
@ -522,33 +516,26 @@ class AngularCompilerProgram implements Program {
|
|||
this._hostAdapter.isSourceFile = () => false;
|
||||
this._tsProgram = ts.createProgram(this.rootNames, this.options, this.hostAdapter);
|
||||
if (isSyntaxError(e)) {
|
||||
const parserErrors = getParseErrors(e);
|
||||
if (parserErrors && parserErrors.length) {
|
||||
this._structuralDiagnostics = [
|
||||
...(this._structuralDiagnostics || []),
|
||||
...parserErrors.map<Diagnostic>(e => ({
|
||||
messageText: e.contextualMessage(),
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
span: e.span,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
}))
|
||||
];
|
||||
} else {
|
||||
this._structuralDiagnostics = [
|
||||
...(this._structuralDiagnostics || []), {
|
||||
messageText: e.message,
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
}
|
||||
];
|
||||
}
|
||||
this._addStructuralDiagnostics(e);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
private _addStructuralDiagnostics(error: Error) {
|
||||
const diagnostics = this._structuralDiagnostics || (this._structuralDiagnostics = []);
|
||||
if (isSyntaxError(error)) {
|
||||
diagnostics.push(...syntaxErrorToDiagnostics(error));
|
||||
} else {
|
||||
diagnostics.push({
|
||||
messageText: error.toString(),
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this returns a ts.Diagnostic so that we
|
||||
// can return errors in a ts.EmitResult
|
||||
private generateFilesForEmit(emitFlags: EmitFlags):
|
||||
|
@ -843,3 +830,56 @@ function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
|
|||
}
|
||||
return {diagnostics, emitSkipped, emittedFiles};
|
||||
}
|
||||
|
||||
function diagnosticSourceOfSpan(span: ParseSourceSpan): ts.SourceFile {
|
||||
// For diagnostics, TypeScript only uses the fileName and text properties.
|
||||
// The redundant '()' are here is to avoid having clang-format breaking the line incorrectly.
|
||||
return ({ fileName: span.start.file.url, text: span.start.file.content } as any);
|
||||
}
|
||||
|
||||
function diagnosticSourceOfFileName(fileName: string, program: ts.Program): ts.SourceFile {
|
||||
const sourceFile = program.getSourceFile(fileName);
|
||||
if (sourceFile) return sourceFile;
|
||||
|
||||
// If we are reporting diagnostics for a source file that is not in the project then we need
|
||||
// to fake a source file so the diagnostic formatting routines can emit the file name.
|
||||
// The redundant '()' are here is to avoid having clang-format breaking the line incorrectly.
|
||||
return ({ fileName, text: '' } as any);
|
||||
}
|
||||
|
||||
|
||||
function diagnosticChainFromFormattedDiagnosticChain(chain: FormattedMessageChain):
|
||||
DiagnosticMessageChain {
|
||||
return {
|
||||
messageText: chain.message,
|
||||
next: chain.next && diagnosticChainFromFormattedDiagnosticChain(chain.next),
|
||||
position: chain.position
|
||||
};
|
||||
}
|
||||
|
||||
function syntaxErrorToDiagnostics(error: Error): Diagnostic[] {
|
||||
const parserErrors = getParseErrors(error);
|
||||
if (parserErrors && parserErrors.length) {
|
||||
return parserErrors.map<Diagnostic>(e => ({
|
||||
messageText: e.contextualMessage(),
|
||||
file: diagnosticSourceOfSpan(e.span),
|
||||
start: e.span.start.offset,
|
||||
length: e.span.end.offset - e.span.start.offset,
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
}));
|
||||
} else {
|
||||
if (isFormattedError(error)) {
|
||||
return [{
|
||||
messageText: error.message,
|
||||
chain: error.chain && diagnosticChainFromFormattedDiagnosticChain(error.chain),
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE,
|
||||
position: error.position
|
||||
}];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
|
@ -112,7 +112,13 @@ describe('Collector', () => {
|
|||
__symbolic: 'class',
|
||||
decorators: [{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/core',
|
||||
name: 'Component',
|
||||
line: 4,
|
||||
character: 7
|
||||
},
|
||||
arguments: [{
|
||||
selector: 'my-hero-detail',
|
||||
template: `
|
||||
|
@ -132,8 +138,13 @@ describe('Collector', () => {
|
|||
__symbolic: 'property',
|
||||
decorators: [{
|
||||
__symbolic: 'call',
|
||||
expression:
|
||||
{__symbolic: 'reference', module: 'angular2/core', name: 'Input'}
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/core',
|
||||
name: 'Input',
|
||||
line: 18,
|
||||
character: 9
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
@ -153,7 +164,13 @@ describe('Collector', () => {
|
|||
__symbolic: 'class',
|
||||
decorators: [{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/core',
|
||||
name: 'Component',
|
||||
line: 9,
|
||||
character: 7
|
||||
},
|
||||
arguments: [{
|
||||
selector: 'my-app',
|
||||
template: `
|
||||
|
@ -172,20 +189,52 @@ describe('Collector', () => {
|
|||
__symbolic: 'reference',
|
||||
module: './hero-detail.component',
|
||||
name: 'HeroDetailComponent',
|
||||
line: 22,
|
||||
character: 21
|
||||
},
|
||||
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'}
|
||||
{
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/common',
|
||||
name: 'NgFor',
|
||||
line: 22,
|
||||
character: 42
|
||||
}
|
||||
],
|
||||
providers: [{__symbolic: 'reference', module: './hero.service', default: true}],
|
||||
providers: [{
|
||||
__symbolic: 'reference',
|
||||
module: './hero.service',
|
||||
default: true,
|
||||
line: 23,
|
||||
character: 20
|
||||
}],
|
||||
pipes: [
|
||||
{__symbolic: 'reference', module: 'angular2/common', name: 'LowerCasePipe'},
|
||||
{__symbolic: 'reference', module: 'angular2/common', name: 'UpperCasePipe'}
|
||||
{
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/common',
|
||||
name: 'LowerCasePipe',
|
||||
line: 24,
|
||||
character: 16
|
||||
},
|
||||
{
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/common',
|
||||
name: 'UpperCasePipe',
|
||||
line: 24,
|
||||
character: 38
|
||||
}
|
||||
]
|
||||
}]
|
||||
}],
|
||||
members: {
|
||||
__ctor__: [{
|
||||
__symbolic: 'constructor',
|
||||
parameters: [{__symbolic: 'reference', module: './hero.service', default: true}]
|
||||
parameters: [{
|
||||
__symbolic: 'reference',
|
||||
module: './hero.service',
|
||||
default: true,
|
||||
line: 31,
|
||||
character: 42
|
||||
}]
|
||||
}],
|
||||
onSelect: [{__symbolic: 'method'}],
|
||||
ngOnInit: [{__symbolic: 'method'}],
|
||||
|
@ -236,22 +285,23 @@ describe('Collector', () => {
|
|||
});
|
||||
|
||||
it('should record annotations on set and get declarations', () => {
|
||||
const propertyData = {
|
||||
const propertyData = (line: number) => ({
|
||||
name: [{
|
||||
__symbolic: 'property',
|
||||
decorators: [{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Input'},
|
||||
expression:
|
||||
{__symbolic: 'reference', module: 'angular2/core', name: 'Input', line, character: 9},
|
||||
arguments: ['firstName']
|
||||
}]
|
||||
}]
|
||||
};
|
||||
});
|
||||
const caseGetProp = <ClassMetadata>casesMetadata.metadata['GetProp'];
|
||||
expect(caseGetProp.members).toEqual(propertyData);
|
||||
expect(caseGetProp.members).toEqual(propertyData(11));
|
||||
const caseSetProp = <ClassMetadata>casesMetadata.metadata['SetProp'];
|
||||
expect(caseSetProp.members).toEqual(propertyData);
|
||||
expect(caseSetProp.members).toEqual(propertyData(19));
|
||||
const caseFullProp = <ClassMetadata>casesMetadata.metadata['FullProp'];
|
||||
expect(caseFullProp.members).toEqual(propertyData);
|
||||
expect(caseFullProp.members).toEqual(propertyData(27));
|
||||
});
|
||||
|
||||
it('should record references to parameterized types', () => {
|
||||
|
@ -260,7 +310,13 @@ describe('Collector', () => {
|
|||
__symbolic: 'class',
|
||||
decorators: [{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Injectable'}
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/core',
|
||||
name: 'Injectable',
|
||||
line: 40,
|
||||
character: 7
|
||||
}
|
||||
}],
|
||||
members: {
|
||||
__ctor__: [{
|
||||
|
@ -313,7 +369,7 @@ describe('Collector', () => {
|
|||
const ctor = <ConstructorMetadata>someClass.members !['__ctor__'][0];
|
||||
const parameters = ctor.parameters;
|
||||
expect(parameters).toEqual([
|
||||
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'}
|
||||
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor', line: 6, character: 29}
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -398,7 +454,7 @@ describe('Collector', () => {
|
|||
const ctor = <ConstructorMetadata>someClass.members !['__ctor__'][0];
|
||||
const parameters = ctor.parameters;
|
||||
expect(parameters).toEqual([
|
||||
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'}
|
||||
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor', line: 6, character: 29}
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -427,7 +483,13 @@ describe('Collector', () => {
|
|||
B: 1,
|
||||
C: 30,
|
||||
D: 40,
|
||||
E: {__symbolic: 'reference', module: './exported-consts', name: 'constValue'}
|
||||
E: {
|
||||
__symbolic: 'reference',
|
||||
module: './exported-consts',
|
||||
name: 'constValue',
|
||||
line: 5,
|
||||
character: 75
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -457,13 +519,25 @@ describe('Collector', () => {
|
|||
expect(classData).toBeDefined();
|
||||
expect(classData.decorators).toEqual([{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/core',
|
||||
name: 'Component',
|
||||
line: 4,
|
||||
character: 5
|
||||
},
|
||||
arguments: [{
|
||||
providers: {
|
||||
__symbolic: 'call',
|
||||
expression: {
|
||||
__symbolic: 'select',
|
||||
expression: {__symbolic: 'reference', module: './static-method', name: 'MyModule'},
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: './static-method',
|
||||
name: 'MyModule',
|
||||
line: 5,
|
||||
character: 17
|
||||
},
|
||||
member: 'with'
|
||||
},
|
||||
arguments: ['a']
|
||||
|
@ -489,13 +563,25 @@ describe('Collector', () => {
|
|||
expect(classData).toBeDefined();
|
||||
expect(classData.decorators).toEqual([{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/core',
|
||||
name: 'Component',
|
||||
line: 4,
|
||||
character: 5
|
||||
},
|
||||
arguments: [{
|
||||
providers: [{
|
||||
provide: 'a',
|
||||
useValue: {
|
||||
__symbolic: 'select',
|
||||
expression: {__symbolic: 'reference', module: './static-field', name: 'MyModule'},
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: './static-field',
|
||||
name: 'MyModule',
|
||||
line: 5,
|
||||
character: 45
|
||||
},
|
||||
member: 'VALUE'
|
||||
}
|
||||
}]
|
||||
|
@ -578,8 +664,20 @@ describe('Collector', () => {
|
|||
const metadata = collector.getMetadata(source) !;
|
||||
expect(metadata.metadata).toEqual({
|
||||
MyClass: Object({__symbolic: 'class'}),
|
||||
OtherModule: {__symbolic: 'reference', module: './static-field-reference', name: 'Foo'},
|
||||
MyOtherModule: {__symbolic: 'reference', module: './static-field', name: 'MyModule'}
|
||||
OtherModule: {
|
||||
__symbolic: 'reference',
|
||||
module: './static-field-reference',
|
||||
name: 'Foo',
|
||||
line: 4,
|
||||
character: 12
|
||||
},
|
||||
MyOtherModule: {
|
||||
__symbolic: 'reference',
|
||||
module: './static-field',
|
||||
name: 'MyModule',
|
||||
line: 4,
|
||||
character: 25
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -598,7 +696,13 @@ describe('Collector', () => {
|
|||
__symbolic: 'class',
|
||||
decorators: [{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/core',
|
||||
name: 'Component',
|
||||
line: 11,
|
||||
character: 5
|
||||
},
|
||||
arguments: [{providers: [{__symbolic: 'reference', name: 'REQUIRED_VALIDATOR'}]}]
|
||||
}]
|
||||
}
|
||||
|
@ -620,7 +724,13 @@ describe('Collector', () => {
|
|||
__symbolic: 'class',
|
||||
decorators: [{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/core',
|
||||
name: 'Component',
|
||||
line: 11,
|
||||
character: 5
|
||||
},
|
||||
arguments: [{providers: [{__symbolic: 'reference', name: 'REQUIRED_VALIDATOR'}]}]
|
||||
}]
|
||||
}
|
||||
|
@ -653,7 +763,13 @@ describe('Collector', () => {
|
|||
__symbolic: 'constructor',
|
||||
parameterDecorators: [[{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Inject'},
|
||||
expression: {
|
||||
__symbolic: 'reference',
|
||||
module: 'angular2/core',
|
||||
name: 'Inject',
|
||||
line: 6,
|
||||
character: 19
|
||||
},
|
||||
arguments: ['a']
|
||||
}]],
|
||||
parameters: [{__symbolic: 'reference', name: 'any'}]
|
||||
|
@ -687,13 +803,20 @@ describe('Collector', () => {
|
|||
__symbolic: 'reference',
|
||||
module: './external',
|
||||
name: 'external',
|
||||
line: 0,
|
||||
character: 68,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should simplify a redundant template', () => {
|
||||
e('`${external}`', 'import {external} from "./external";')
|
||||
.toEqual({__symbolic: 'reference', module: './external', name: 'external'});
|
||||
e('`${external}`', 'import {external} from "./external";').toEqual({
|
||||
__symbolic: 'reference',
|
||||
module: './external',
|
||||
name: 'external',
|
||||
line: 0,
|
||||
character: 59
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to collect complex template with imported references', () => {
|
||||
|
@ -710,11 +833,18 @@ describe('Collector', () => {
|
|||
__symbolic: 'binop',
|
||||
operator: '+',
|
||||
left: 'foo:',
|
||||
right: {__symbolic: 'reference', module: './external', name: 'foo'}
|
||||
right: {
|
||||
__symbolic: 'reference',
|
||||
module: './external',
|
||||
name: 'foo',
|
||||
line: 0,
|
||||
character: 63
|
||||
}
|
||||
},
|
||||
right: ', bar:'
|
||||
},
|
||||
right: {__symbolic: 'reference', module: './external', name: 'bar'}
|
||||
right:
|
||||
{__symbolic: 'reference', module: './external', name: 'bar', line: 0, character: 75}
|
||||
},
|
||||
right: ', end'
|
||||
});
|
||||
|
@ -741,11 +871,11 @@ describe('Collector', () => {
|
|||
__ctor__: [{
|
||||
__symbolic: 'constructor',
|
||||
parameters: [
|
||||
{__symbolic: 'reference', module: './foo', name: 'Foo'},
|
||||
{__symbolic: 'reference', module: './foo', name: 'Foo'},
|
||||
{__symbolic: 'reference', module: './foo', name: 'Foo'},
|
||||
{__symbolic: 'reference', module: './foo', name: 'Foo'},
|
||||
{__symbolic: 'reference', module: './foo', name: 'Foo'}
|
||||
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
|
||||
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
|
||||
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
|
||||
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
|
||||
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24}
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
@ -825,7 +955,9 @@ describe('Collector', () => {
|
|||
extends: {
|
||||
__symbolic: 'reference',
|
||||
module: './class-inheritance-parent',
|
||||
name: 'ParentClassFromOtherFile'
|
||||
name: 'ParentClassFromOtherFile',
|
||||
line: 9,
|
||||
character: 45,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -149,12 +149,14 @@ describe('Evaluator', () => {
|
|||
const newExpression = program.getSourceFile('newExpression.ts');
|
||||
expect(evaluator.evaluateNode(findVarInitializer(newExpression, 'someValue'))).toEqual({
|
||||
__symbolic: 'new',
|
||||
expression: {__symbolic: 'reference', name: 'Value', module: './classes'},
|
||||
expression:
|
||||
{__symbolic: 'reference', name: 'Value', module: './classes', line: 4, character: 33},
|
||||
arguments: ['name', 12]
|
||||
});
|
||||
expect(evaluator.evaluateNode(findVarInitializer(newExpression, 'complex'))).toEqual({
|
||||
__symbolic: 'new',
|
||||
expression: {__symbolic: 'reference', name: 'Value', module: './classes'},
|
||||
expression:
|
||||
{__symbolic: 'reference', name: 'Value', module: './classes', line: 5, character: 42},
|
||||
arguments: ['name', 12]
|
||||
});
|
||||
});
|
||||
|
@ -173,8 +175,7 @@ describe('Evaluator', () => {
|
|||
const errors = program.getSourceFile('errors.ts');
|
||||
const fDecl = findVar(errors, 'f') !;
|
||||
expect(evaluator.evaluateNode(fDecl.initializer !))
|
||||
.toEqual(
|
||||
{__symbolic: 'error', message: 'Function call not supported', line: 1, character: 12});
|
||||
.toEqual({__symbolic: 'error', message: 'Lambda not supported', line: 1, character: 12});
|
||||
const eDecl = findVar(errors, 'e') !;
|
||||
expect(evaluator.evaluateNode(eDecl.type !)).toEqual({
|
||||
__symbolic: 'error',
|
||||
|
|
|
@ -184,8 +184,7 @@ describe('ngc transformer command-line', () => {
|
|||
|
||||
const exitCode = main(['-p', basePath], errorSpy);
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(errorSpy.calls.mostRecent().args[0])
|
||||
.toContain('Error at ' + path.join(basePath, 'mymodule.ts.MyComp.html'));
|
||||
expect(errorSpy.calls.mostRecent().args[0]).toContain('mymodule.ts.MyComp.html');
|
||||
expect(errorSpy.calls.mostRecent().args[0])
|
||||
.toContain(`Property 'unknownProp' does not exist on type 'MyComp'`);
|
||||
|
||||
|
@ -215,8 +214,7 @@ describe('ngc transformer command-line', () => {
|
|||
|
||||
const exitCode = main(['-p', basePath], errorSpy);
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(errorSpy.calls.mostRecent().args[0])
|
||||
.toContain('Error at ' + path.join(basePath, 'my.component.html(1,5):'));
|
||||
expect(errorSpy.calls.mostRecent().args[0]).toContain('my.component.html(1,5):');
|
||||
expect(errorSpy.calls.mostRecent().args[0])
|
||||
.toContain(`Property 'unknownProp' does not exist on type 'MyComp'`);
|
||||
|
||||
|
@ -1566,4 +1564,49 @@ describe('ngc transformer command-line', () => {
|
|||
expect(main(['-p', path.join(basePath, 'src/tsconfig.json')])).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatted messages', () => {
|
||||
it('should emit a formatted error message for a structural error', () => {
|
||||
write('src/tsconfig.json', `{
|
||||
"extends": "../tsconfig-base.json",
|
||||
"files": ["test-module.ts"]
|
||||
}`);
|
||||
write('src/lib/indirect2.ts', `
|
||||
declare var f: any;
|
||||
|
||||
export const t2 = f\`<p>hello</p>\`;
|
||||
`);
|
||||
write('src/lib/indirect1.ts', `
|
||||
import {t2} from './indirect2';
|
||||
export const t1 = t2 + ' ';
|
||||
`);
|
||||
write('src/lib/test.component.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {t1} from './indirect1';
|
||||
|
||||
@Component({
|
||||
template: t1,
|
||||
styleUrls: ['./test.component.css']
|
||||
})
|
||||
export class TestComponent {}
|
||||
`);
|
||||
write('src/test-module.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {TestComponent} from './lib/test.component';
|
||||
|
||||
@NgModule({declarations: [TestComponent]})
|
||||
export class TestModule {}
|
||||
`);
|
||||
const messages: string[] = [];
|
||||
const exitCode =
|
||||
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
|
||||
expect(exitCode).toBe(1, 'Compile was expected to fail');
|
||||
expect(messages[0])
|
||||
.toEqual(`lib/test.component.ts(6,21): Error during template compile of 'TestComponent'
|
||||
Tagged template expressions are not supported in metadata in 't1'
|
||||
't1' references 't2' at lib/indirect1.ts(3,27)
|
||||
't2' contains the error at lib/indirect2.ts(4,27).
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -143,7 +143,7 @@ describe('perform watch', () => {
|
|||
|
||||
const errDiags = host.diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error);
|
||||
expect(errDiags.length).toBe(1);
|
||||
expect(errDiags[0].messageText).toContain('Function calls are not supported.');
|
||||
expect(errDiags[0].messageText).toContain('Function expressions are not supported');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -930,7 +930,7 @@ describe('ng program', () => {
|
|||
|
||||
const structuralErrors = program.getNgStructuralDiagnostics();
|
||||
expect(structuralErrors.length).toBe(1);
|
||||
expect(structuralErrors[0].messageText).toContain('Function calls are not supported.');
|
||||
expect(structuralErrors[0].messageText).toContain('Function expressions are not supported');
|
||||
});
|
||||
|
||||
it('should not throw on structural errors but collect them (loadNgStructureAsync)', (done) => {
|
||||
|
@ -943,7 +943,7 @@ describe('ng program', () => {
|
|||
program.loadNgStructureAsync().then(() => {
|
||||
const structuralErrors = program.getNgStructuralDiagnostics();
|
||||
expect(structuralErrors.length).toBe(1);
|
||||
expect(structuralErrors[0].messageText).toContain('Function calls are not supported.');
|
||||
expect(structuralErrors[0].messageText).toContain('Function expressions are not supported');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -982,7 +982,8 @@ describe('ng program', () => {
|
|||
const program = ng.createProgram({rootNames: allRootNames, options, host});
|
||||
const structuralErrors = program.getNgStructuralDiagnostics();
|
||||
expect(structuralErrors.length).toBe(1);
|
||||
expect(structuralErrors[0].messageText).toContain('Function calls are not supported.');
|
||||
expect(structuralErrors[0].messageText)
|
||||
.toContain('Function expressions are not supported');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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];
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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.`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -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'}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import {NgAnalyzedModules, StaticSymbol} from '@angular/compiler';
|
|||
import {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics} from '@angular/compiler-cli/src/language_services';
|
||||
|
||||
import {AstResult} from './common';
|
||||
import {Declarations, Diagnostic, DiagnosticKind, Diagnostics, Span, TemplateSource} from './types';
|
||||
import {Declarations, Diagnostic, DiagnosticKind, DiagnosticMessageChain, Diagnostics, Span, TemplateSource} from './types';
|
||||
import {offsetSpan, spanOf} from './utils';
|
||||
|
||||
export interface AstProvider {
|
||||
|
@ -56,7 +56,7 @@ export function getDeclarationDiagnostics(
|
|||
|
||||
let directives: Set<StaticSymbol>|undefined = undefined;
|
||||
for (const declaration of declarations) {
|
||||
const report = (message: string, span?: Span) => {
|
||||
const report = (message: string | DiagnosticMessageChain, span?: Span) => {
|
||||
results.push(<Diagnostic>{
|
||||
kind: DiagnosticKind.Error,
|
||||
span: span || declaration.declarationSpan, message
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import * as ts from 'typescript';
|
||||
|
||||
import {createLanguageService} from './language_service';
|
||||
import {Completion, Diagnostic, LanguageService, LanguageServiceHost} from './types';
|
||||
import {Completion, Diagnostic, DiagnosticMessageChain, LanguageService, LanguageServiceHost} from './types';
|
||||
import {TypeScriptServiceHost} from './typescript_host';
|
||||
|
||||
const projectHostMap = new WeakMap<any, TypeScriptServiceHost>();
|
||||
|
@ -188,12 +188,30 @@ export function create(info: any /* ts.server.PluginCreateInfo */): ts.LanguageS
|
|||
};
|
||||
}
|
||||
|
||||
function diagnosticChainToDiagnosticChain(chain: DiagnosticMessageChain):
|
||||
ts.DiagnosticMessageChain {
|
||||
return {
|
||||
messageText: chain.message,
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
code: 0,
|
||||
next: chain.next ? diagnosticChainToDiagnosticChain(chain.next) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
function diagnosticMessageToDiagnosticMessageText(message: string | DiagnosticMessageChain):
|
||||
string|ts.DiagnosticMessageChain {
|
||||
if (typeof message === 'string') {
|
||||
return message;
|
||||
}
|
||||
return diagnosticChainToDiagnosticChain(message);
|
||||
}
|
||||
|
||||
function diagnosticToDiagnostic(d: Diagnostic, file: ts.SourceFile): ts.Diagnostic {
|
||||
const result = {
|
||||
file,
|
||||
start: d.span.start,
|
||||
length: d.span.end - d.span.start,
|
||||
messageText: d.message,
|
||||
messageText: diagnosticMessageToDiagnosticMessageText(d.message),
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
code: 0,
|
||||
source: 'ng'
|
||||
|
|
|
@ -81,7 +81,7 @@ export type TemplateSources = TemplateSource[] | undefined;
|
|||
/**
|
||||
* Error information found getting declaration information
|
||||
*
|
||||
* A host type; see `LanagueServiceHost`.
|
||||
* A host type; see `LanguageServiceHost`.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
|
@ -92,9 +92,10 @@ export interface DeclarationError {
|
|||
readonly span: Span;
|
||||
|
||||
/**
|
||||
* The message to display describing the error.
|
||||
* The message to display describing the error or a chain
|
||||
* of messages.
|
||||
*/
|
||||
readonly message: string;
|
||||
readonly message: string|DiagnosticMessageChain;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -255,6 +256,28 @@ export enum DiagnosticKind {
|
|||
Warning,
|
||||
}
|
||||
|
||||
/**
|
||||
* A template diagnostics message chain. This is similar to the TypeScript
|
||||
* DiagnosticMessageChain. The messages are intended to be formatted as separate
|
||||
* sentence fragments and indented.
|
||||
*
|
||||
* For compatiblity previous implementation, the values are expected to override
|
||||
* toString() to return a formatted message.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export interface DiagnosticMessageChain {
|
||||
/**
|
||||
* The text of the diagnostic message to display.
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* The next message in the chain.
|
||||
*/
|
||||
next?: DiagnosticMessageChain;
|
||||
}
|
||||
|
||||
/**
|
||||
* An template diagnostic message to display.
|
||||
*
|
||||
|
@ -272,9 +295,9 @@ export interface Diagnostic {
|
|||
span: Span;
|
||||
|
||||
/**
|
||||
* The text of the diagnostic message to display.
|
||||
* The text of the diagnostic message to display or a chain of messages.
|
||||
*/
|
||||
message: string;
|
||||
message: string|DiagnosticMessageChain;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, createOfflineCompileUrlResolver} from '@angular/compiler';
|
||||
import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, createOfflineCompileUrlResolver, isFormattedError} from '@angular/compiler';
|
||||
import {CompilerOptions, getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli/src/language_services';
|
||||
import {ViewEncapsulation, ɵConsole as Console} from '@angular/core';
|
||||
import * as fs from 'fs';
|
||||
|
@ -15,10 +15,11 @@ import * as ts from 'typescript';
|
|||
|
||||
import {createLanguageService} from './language_service';
|
||||
import {ReflectorHost} from './reflector_host';
|
||||
import {BuiltinType, Declaration, DeclarationError, DeclarationKind, Declarations, Definition, LanguageService, LanguageServiceHost, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable, TemplateSource, TemplateSources} from './types';
|
||||
import {BuiltinType, Declaration, DeclarationError, DeclarationKind, Declarations, Definition, DiagnosticMessageChain, LanguageService, LanguageServiceHost, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable, TemplateSource, TemplateSources} from './types';
|
||||
import {isTypescriptVersion} from './utils';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a `LanguageServiceHost`
|
||||
*/
|
||||
|
@ -494,7 +495,13 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
|
|||
private getCollectedErrors(defaultSpan: Span, sourceFile: ts.SourceFile): DeclarationError[] {
|
||||
const errors = (this.collectedErrors && this.collectedErrors.get(sourceFile.fileName));
|
||||
return (errors && errors.map((e: any) => {
|
||||
return {message: e.message, span: spanAt(sourceFile, e.line, e.column) || defaultSpan};
|
||||
const line = e.line || (e.position && e.position.line);
|
||||
const column = e.column || (e.position && e.position.column);
|
||||
const span = spanAt(sourceFile, line, column) || defaultSpan;
|
||||
if (isFormattedError(e)) {
|
||||
return errorToDiagnosticWithChain(e, span);
|
||||
}
|
||||
return {message: e.message, span};
|
||||
})) ||
|
||||
[];
|
||||
}
|
||||
|
@ -599,3 +606,20 @@ function spanAt(sourceFile: ts.SourceFile, line: number, column: number): Span|u
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function chainedMessage(chain: DiagnosticMessageChain, indent = ''): string {
|
||||
return indent + chain.message + (chain.next ? chainedMessage(chain.next, indent + ' ') : '');
|
||||
}
|
||||
|
||||
class DiagnosticMessageChainImpl implements DiagnosticMessageChain {
|
||||
constructor(public message: string, public next?: DiagnosticMessageChain) {}
|
||||
toString(): string { return chainedMessage(this); }
|
||||
}
|
||||
|
||||
function convertChain(chain: FormattedMessageChain): DiagnosticMessageChain {
|
||||
return {message: chain.message, next: chain.next ? convertChain(chain.next) : undefined};
|
||||
}
|
||||
|
||||
function errorToDiagnosticWithChain(error: FormattedError, span: Span): DeclarationError {
|
||||
return {message: error.chain ? convertChain(error.chain) : error.message, span};
|
||||
}
|
|
@ -13,7 +13,7 @@ import {Diagnostics} from '../src/types';
|
|||
import {TypeScriptServiceHost} from '../src/typescript_host';
|
||||
|
||||
import {toh} from './test_data';
|
||||
import {MockTypescriptHost, includeDiagnostic, noDiagnostics} from './test_utils';
|
||||
import {MockTypescriptHost, diagnosticMessageContains, findDiagnostic, includeDiagnostic, noDiagnostics} from './test_utils';
|
||||
|
||||
describe('diagnostics', () => {
|
||||
let documentRegistry = ts.createDocumentRegistry();
|
||||
|
@ -123,7 +123,8 @@ describe('diagnostics', () => {
|
|||
addCode(code, (fileName, content) => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
includeDiagnostic(
|
||||
diagnostics !, 'Function calls are not supported.', '() => \'foo\'', content);
|
||||
diagnostics !, 'Function expressions are not supported in decorators', '() => \'foo\'',
|
||||
content);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -168,8 +169,7 @@ describe('diagnostics', () => {
|
|||
const code =
|
||||
` @Component({template: '<p> Using an invalid pipe {{data | dat}} </p>'}) export class MyComponent { data = 'some data'; }`;
|
||||
addCode(code, fileName => {
|
||||
const diagnostic =
|
||||
ngService.getDiagnostics(fileName) !.filter(d => d.message.indexOf('pipe') > 0)[0];
|
||||
const diagnostic = findDiagnostic(ngService.getDiagnostics(fileName) !, 'pipe') !;
|
||||
expect(diagnostic).not.toBeUndefined();
|
||||
expect(diagnostic.span.end - diagnostic.span.start).toBeLessThan(11);
|
||||
});
|
||||
|
@ -216,8 +216,8 @@ describe('diagnostics', () => {
|
|||
`,
|
||||
fileName => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName) !;
|
||||
const expected = diagnostics.find(d => d.message.startsWith('Invalid providers for'));
|
||||
const notExpected = diagnostics.find(d => d.message.startsWith('Cannot read property'));
|
||||
const expected = findDiagnostic(diagnostics, 'Invalid providers for');
|
||||
const notExpected = findDiagnostic(diagnostics, 'Cannot read property');
|
||||
expect(expected).toBeDefined();
|
||||
expect(notExpected).toBeUndefined();
|
||||
});
|
||||
|
@ -355,12 +355,12 @@ describe('diagnostics', () => {
|
|||
expect(diagnostics.length).toBe(1);
|
||||
if (diagnostics.length > 1) {
|
||||
for (const diagnostic of diagnostics) {
|
||||
if (diagnostic.message.indexOf('MyComponent') >= 0) continue;
|
||||
if (diagnosticMessageContains(diagnostic.message, 'MyComponent')) continue;
|
||||
fail(`(${diagnostic.span.start}:${diagnostic.span.end}): ${diagnostic.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
expect(diagnostics[0].message.indexOf('MyComponent') >= 0).toBeTruthy();
|
||||
expect(diagnosticMessageContains(diagnostics[0].message, 'MyComponent')).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Diagnostic, Diagnostics, Span} from '../src/types';
|
||||
import {Diagnostic, DiagnosticMessageChain, Diagnostics, Span} from '../src/types';
|
||||
|
||||
export type MockData = string | MockDirectory;
|
||||
|
||||
|
@ -317,6 +317,25 @@ export function noDiagnostics(diagnostics: Diagnostics) {
|
|||
}
|
||||
}
|
||||
|
||||
export function diagnosticMessageContains(
|
||||
message: string | DiagnosticMessageChain, messageFragment: string): boolean {
|
||||
if (typeof message == 'string') {
|
||||
return message.indexOf(messageFragment) >= 0;
|
||||
}
|
||||
if (message.message.indexOf(messageFragment) >= 0) {
|
||||
return true;
|
||||
}
|
||||
if (message.next) {
|
||||
return diagnosticMessageContains(message.next, messageFragment);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function findDiagnostic(diagnostics: Diagnostic[], messageFragment: string): Diagnostic|
|
||||
undefined {
|
||||
return diagnostics.find(d => diagnosticMessageContains(d.message, messageFragment));
|
||||
}
|
||||
|
||||
export function includeDiagnostic(
|
||||
diagnostics: Diagnostics, message: string, text?: string, len?: string): void;
|
||||
export function includeDiagnostic(
|
||||
|
@ -324,14 +343,18 @@ export function includeDiagnostic(
|
|||
export function includeDiagnostic(diagnostics: Diagnostics, message: string, p1?: any, p2?: any) {
|
||||
expect(diagnostics).toBeDefined();
|
||||
if (diagnostics) {
|
||||
const diagnostic = diagnostics.find(d => d.message.indexOf(message) >= 0) as Diagnostic;
|
||||
expect(diagnostic).toBeDefined();
|
||||
const diagnostic = findDiagnostic(diagnostics, message);
|
||||
expect(diagnostic).toBeDefined(`no diagnostic contains '${message}`);
|
||||
if (diagnostic && p1 != null) {
|
||||
const at = typeof p1 === 'number' ? p1 : p2.indexOf(p1);
|
||||
const len = typeof p2 === 'number' ? p2 : p1.length;
|
||||
expect(diagnostic.span.start).toEqual(at);
|
||||
expect(diagnostic.span.start)
|
||||
.toEqual(
|
||||
at,
|
||||
`expected message '${message}' was reported at ${diagnostic.span.start} but should be ${at}`);
|
||||
if (len != null) {
|
||||
expect(diagnostic.span.end - diagnostic.span.start).toEqual(len);
|
||||
expect(diagnostic.span.end - diagnostic.span.start)
|
||||
.toEqual(len, `expected '${message}'s span length to be ${len}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue