fix(language-service): Do not produce diagnostics if metadata for NgModule not found (#34113)

The language service incorrectly reports an error if it fails to find
NgModule metadata for a particular Component / Directive. In many cases,
the use case is legit, particularly in test.

This commit removes such diagnostic message and cleans up the interface
for `TypeScriptHost.getTemplateAst()`.

PR closes https://github.com/angular/vscode-ng-language-service/issues/463

PR Close #34113
This commit is contained in:
Keen Yee Liau 2019-11-27 15:46:58 -08:00 committed by Miško Hevery
parent c16a79df5c
commit 39722df41e
6 changed files with 53 additions and 68 deletions

View File

@ -8,7 +8,7 @@
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CssSelector, Node as HtmlAst, ParseError, Parser, TemplateAst} from '@angular/compiler'; import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CssSelector, Node as HtmlAst, ParseError, Parser, TemplateAst} from '@angular/compiler';
import {Diagnostic, TemplateSource} from './types'; import {TemplateSource} from './types';
export interface AstResult { export interface AstResult {
htmlAst: HtmlAst[]; htmlAst: HtmlAst[];
@ -25,7 +25,3 @@ export type SelectorInfo = {
selectors: CssSelector[], selectors: CssSelector[],
map: Map<CssSelector, CompileDirectiveSummary> map: Map<CssSelector, CompileDirectiveSummary>
}; };
export function isAstResult(result: AstResult | Diagnostic): result is AstResult {
return result.hasOwnProperty('templateAst');
}

View File

@ -8,7 +8,6 @@
import * as tss from 'typescript/lib/tsserverlibrary'; import * as tss from 'typescript/lib/tsserverlibrary';
import {isAstResult} from './common';
import {getTemplateCompletions} from './completions'; import {getTemplateCompletions} from './completions';
import {getDefinitionAndBoundSpan, getTsDefinitionAndBoundSpan} from './definitions'; import {getDefinitionAndBoundSpan, getTsDefinitionAndBoundSpan} from './definitions';
import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic, uniqueBySpan} from './diagnostics'; import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic, uniqueBySpan} from './diagnostics';
@ -34,11 +33,9 @@ class LanguageServiceImpl implements LanguageService {
const templates = this.host.getTemplates(fileName); const templates = this.host.getTemplates(fileName);
for (const template of templates) { for (const template of templates) {
const astOrDiagnostic = this.host.getTemplateAst(template); const ast = this.host.getTemplateAst(template);
if (isAstResult(astOrDiagnostic)) { if (ast) {
results.push(...getTemplateDiagnostics(astOrDiagnostic)); results.push(...getTemplateDiagnostics(ast));
} else {
results.push(astOrDiagnostic);
} }
} }

View File

@ -8,7 +8,6 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {isAstResult} from './common';
import {createGlobalSymbolTable} from './global_symbols'; import {createGlobalSymbolTable} from './global_symbols';
import * as ng from './types'; import * as ng from './types';
import {TypeScriptServiceHost} from './typescript_host'; import {TypeScriptServiceHost} from './typescript_host';
@ -73,7 +72,7 @@ abstract class BaseTemplate implements ng.TemplateSource {
// TODO: There is circular dependency here between TemplateSource and // TODO: There is circular dependency here between TemplateSource and
// TypeScriptHost. Consider refactoring the code to break this cycle. // TypeScriptHost. Consider refactoring the code to break this cycle.
const ast = this.host.getTemplateAst(this); const ast = this.host.getTemplateAst(this);
const pipes = isAstResult(ast) ? ast.pipes : []; const pipes = (ast && ast.pipes) || [];
return getPipesTable(sourceFile, program, typeChecker, pipes); return getPipesTable(sourceFile, program, typeChecker, pipes);
}); });
} }

View File

@ -193,7 +193,7 @@ export interface LanguageServiceHost {
/** /**
* Return the AST for both HTML and template for the contextFile. * Return the AST for both HTML and template for the contextFile.
*/ */
getTemplateAst(template: TemplateSource): AstResult|Diagnostic; getTemplateAst(template: TemplateSource): AstResult|undefined;
/** /**
* Return the template AST for the node that corresponds to the position. * Return the template AST for the node that corresponds to the position.

View File

@ -11,11 +11,11 @@ import {SchemaMetadata, ViewEncapsulation, ɵConsole as Console} from '@angular/
import * as ts from 'typescript'; import * as ts from 'typescript';
import * as tss from 'typescript/lib/tsserverlibrary'; import * as tss from 'typescript/lib/tsserverlibrary';
import {AstResult, isAstResult} from './common'; import {AstResult} from './common';
import {createLanguageService} from './language_service'; import {createLanguageService} from './language_service';
import {ReflectorHost} from './reflector_host'; import {ReflectorHost} from './reflector_host';
import {ExternalTemplate, InlineTemplate, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './template'; import {ExternalTemplate, InlineTemplate, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './template';
import {Declaration, DeclarationError, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types'; import {Declaration, DeclarationError, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types';
import {findTightestNode, getDirectiveClassLike} from './utils'; import {findTightestNode, getDirectiveClassLike} from './utils';
@ -456,11 +456,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (!template) { if (!template) {
return; return;
} }
const astResult = this.getTemplateAst(template); return this.getTemplateAst(template);
if (!isAstResult(astResult)) {
return;
}
return astResult;
} }
/** /**
@ -505,20 +501,14 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
} }
/** /**
* Parse the `template` and return its AST if there's no error. Otherwise * Parse the `template` and return its AST, if any.
* return a Diagnostic message.
* @param template template to be parsed * @param template template to be parsed
*/ */
getTemplateAst(template: TemplateSource): AstResult|Diagnostic { getTemplateAst(template: TemplateSource): AstResult|undefined {
const {type: classSymbol, fileName} = template; const {type: classSymbol, fileName} = template;
try {
const data = this.resolver.getNonNormalizedDirectiveMetadata(classSymbol); const data = this.resolver.getNonNormalizedDirectiveMetadata(classSymbol);
if (!data) { if (!data) {
return { return;
kind: DiagnosticKind.Error,
message: `No metadata found for '${classSymbol.name}' in ${fileName}.`,
span: template.span,
};
} }
const htmlParser = new I18NHtmlParser(new HtmlParser()); const htmlParser = new I18NHtmlParser(new HtmlParser());
const expressionParser = new Parser(new Lexer()); const expressionParser = new Parser(new Lexer());
@ -533,14 +523,9 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
preserveLineEndings: true, // do not convert CRLF to LF preserveLineEndings: true, // do not convert CRLF to LF
}); });
const {directives, pipes, schemas} = this.getModuleMetadataForDirective(classSymbol); const {directives, pipes, schemas} = this.getModuleMetadataForDirective(classSymbol);
const parseResult = const parseResult = parser.tryParseHtml(htmlResult, data.metadata, directives, pipes, schemas);
parser.tryParseHtml(htmlResult, data.metadata, directives, pipes, schemas);
if (!parseResult.templateAst) { if (!parseResult.templateAst) {
return { return;
kind: DiagnosticKind.Error,
message: `Failed to parse template for '${classSymbol.name}' in ${fileName}`,
span: template.span,
};
} }
return { return {
htmlAst: htmlResult.rootNodes, htmlAst: htmlResult.rootNodes,
@ -548,14 +533,6 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
directive: data.metadata, directives, pipes, directive: data.metadata, directives, pipes,
parseErrors: parseResult.errors, expressionParser, template, parseErrors: parseResult.errors, expressionParser, template,
}; };
} catch (e) {
return {
kind: DiagnosticKind.Error,
message: e.message,
span:
e.fileName === fileName && template.query.getSpanAt(e.line, e.column) || template.span,
};
}
} }
/** /**

View File

@ -829,4 +829,20 @@ describe('diagnostics', () => {
expect(content.substring(start !, start ! + length !)).toBe(`line${i}`); expect(content.substring(start !, start ! + length !)).toBe(`line${i}`);
} }
}); });
it('should not produce diagnostics for non-exported directives', () => {
const fileName = '/app/test.component.ts';
mockHost.addScript(fileName, `
import {Component} from '@angular/core';
@Component({
template: '<test-comp></test-comp>'
})
class TestHostComponent {}
`);
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
expect(tsDiags).toEqual([]);
const ngDiags = ngLS.getDiagnostics(fileName);
expect(ngDiags).toEqual([]);
});
}); });