refactor(language-service): Return ts.Diagnostic[] for getDiagnostics (#32115)

Part 2/3 of language service refactoring:
Now that the language service is a proper tsserver plugin, all LS
interfaces should return TS values. This PR refactors the
ng.getDiagnostics() API to return ts.Diagnostic[] instead of
ng.Diagnostic[].

PR Close #32115
This commit is contained in:
Keen Yee Liau
2019-08-09 15:52:49 -07:00
committed by Kara Erickson
parent a91ab15525
commit a5f39aeda6
7 changed files with 117 additions and 111 deletions

View File

@ -7,13 +7,52 @@
*/
import {NgAnalyzedModules, StaticSymbol} from '@angular/compiler';
import {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics} from '@angular/compiler-cli/src/language_services';
import * as ts from 'typescript';
import {AstResult} from './common';
import {Declarations, Diagnostic, DiagnosticKind, DiagnosticMessageChain, Diagnostics, Span, TemplateSource} from './types';
import {offsetSpan, spanOf} from './utils';
export interface AstProvider {
getTemplateAst(template: TemplateSource, fileName: string): AstResult;
}
export function getTemplateDiagnostics(template: TemplateSource, ast: AstResult): Diagnostics {
const results: Diagnostics = [];
if (ast.parseErrors && ast.parseErrors.length) {
results.push(...ast.parseErrors.map<Diagnostic>(e => {
return {
kind: DiagnosticKind.Error,
span: offsetSpan(spanOf(e.span), template.span.start),
message: e.msg,
};
}));
} else if (ast.templateAst && ast.htmlAst) {
const info: DiagnosticTemplateInfo = {
templateAst: ast.templateAst,
htmlAst: ast.htmlAst,
offset: template.span.start,
query: template.query,
members: template.members,
};
const expressionDiagnostics = getTemplateExpressionDiagnostics(info);
results.push(...expressionDiagnostics);
}
if (ast.errors) {
results.push(...ast.errors.map<Diagnostic>(e => {
return {
kind: e.kind,
span: e.span || template.span,
message: e.message,
};
}));
}
return results;
}
export function getDeclarationDiagnostics(
declarations: Declarations, modules: NgAnalyzedModules): Diagnostics {
const results: Diagnostics = [];
@ -60,3 +99,34 @@ export function getDeclarationDiagnostics(
return results;
}
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);
}
export function ngDiagnosticToTsDiagnostic(
d: Diagnostic, file: ts.SourceFile | undefined): ts.Diagnostic {
return {
file,
start: d.span.start,
length: d.span.end - d.span.start,
messageText: diagnosticMessageToDiagnosticMessageText(d.message),
category: ts.DiagnosticCategory.Error,
code: 0,
source: 'ng',
};
}

View File

@ -6,16 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompileMetadataResolver, CompilePipeSummary} from '@angular/compiler';
import {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics} from '@angular/compiler-cli/src/language_services';
import {CompilePipeSummary} from '@angular/compiler';
import * as tss from 'typescript/lib/tsserverlibrary';
import {getTemplateCompletions} from './completions';
import {getDefinitionAndBoundSpan} from './definitions';
import {getDeclarationDiagnostics} from './diagnostics';
import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic} from './diagnostics';
import {getHover} from './hover';
import {Completion, Diagnostic, DiagnosticKind, Diagnostics, Hover, LanguageService, LanguageServiceHost, Location, Span, TemplateSource} from './types';
import {offsetSpan, spanOf} from './utils';
import {Completion, Diagnostic, LanguageService, Span} from './types';
import {TypeScriptServiceHost} from './typescript_host';
/**
@ -23,20 +22,21 @@ import {offsetSpan, spanOf} from './utils';
*
* @publicApi
*/
export function createLanguageService(host: LanguageServiceHost): LanguageService {
export function createLanguageService(host: TypeScriptServiceHost): LanguageService {
return new LanguageServiceImpl(host);
}
class LanguageServiceImpl implements LanguageService {
constructor(private host: LanguageServiceHost) {}
constructor(private readonly host: TypeScriptServiceHost) {}
getTemplateReferences(): string[] { return this.host.getTemplateReferences(); }
getDiagnostics(fileName: string): Diagnostic[] {
getDiagnostics(fileName: string): tss.Diagnostic[] {
const results: Diagnostic[] = [];
const templates = this.host.getTemplates(fileName);
if (templates && templates.length) {
results.push(...this.getTemplateDiagnostics(fileName, templates));
for (const template of templates) {
const ast = this.host.getTemplateAst(template, fileName);
results.push(...getTemplateDiagnostics(template, ast));
}
const declarations = this.host.getDeclarations(fileName);
@ -44,8 +44,11 @@ class LanguageServiceImpl implements LanguageService {
const summary = this.host.getAnalyzedModules();
results.push(...getDeclarationDiagnostics(declarations, summary));
}
return uniqueBySpan(results);
if (!results.length) {
return [];
}
const sourceFile = fileName.endsWith('.ts') ? this.host.getSourceFile(fileName) : undefined;
return uniqueBySpan(results).map(d => ngDiagnosticToTsDiagnostic(d, sourceFile));
}
getPipesAt(fileName: string, position: number): CompilePipeSummary[] {
@ -76,38 +79,6 @@ class LanguageServiceImpl implements LanguageService {
return getHover(templateInfo);
}
}
private getTemplateDiagnostics(fileName: string, templates: TemplateSource[]): Diagnostics {
const results: Diagnostics = [];
for (const template of templates) {
const ast = this.host.getTemplateAst(template, fileName);
if (ast) {
if (ast.parseErrors && ast.parseErrors.length) {
results.push(...ast.parseErrors.map<Diagnostic>(
e => ({
kind: DiagnosticKind.Error,
span: offsetSpan(spanOf(e.span), template.span.start),
message: e.msg
})));
} else if (ast.templateAst && ast.htmlAst) {
const info: DiagnosticTemplateInfo = {
templateAst: ast.templateAst,
htmlAst: ast.htmlAst,
offset: template.span.start,
query: template.query,
members: template.members
};
const expressionDiagnostics = getTemplateExpressionDiagnostics(info);
results.push(...expressionDiagnostics);
}
if (ast.errors) {
results.push(...ast.errors.map<Diagnostic>(
e => ({kind: e.kind, span: e.span || template.span, message: e.message})));
}
}
}
return results;
}
}
function uniqueBySpan<T extends{span: Span}>(elements: T[]): T[] {

View File

@ -10,7 +10,7 @@ import * as ts from 'typescript'; // used as value, passed in by tsserver at run
import * as tss from 'typescript/lib/tsserverlibrary'; // used as type only
import {createLanguageService} from './language_service';
import {Completion, Diagnostic, DiagnosticMessageChain} from './types';
import {Completion} from './types';
import {TypeScriptServiceHost} from './typescript_host';
const projectHostMap = new WeakMap<tss.server.Project, TypeScriptServiceHost>();
@ -33,36 +33,6 @@ function completionToEntry(c: Completion): tss.CompletionEntry {
};
}
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|
tss.DiagnosticMessageChain {
if (typeof message === 'string') {
return message;
}
return diagnosticChainToDiagnosticChain(message);
}
function diagnosticToDiagnostic(d: Diagnostic, file: tss.SourceFile | undefined): tss.Diagnostic {
return {
file,
start: d.span.start,
length: d.span.end - d.span.start,
messageText: diagnosticMessageToDiagnosticMessageText(d.message),
category: ts.DiagnosticCategory.Error,
code: 0,
source: 'ng'
};
}
export function create(info: tss.server.PluginCreateInfo): tss.LanguageService {
const {project, languageService: tsLS, languageServiceHost: tsLSHost, config} = info;
// This plugin could operate under two different modes:
@ -115,16 +85,10 @@ export function create(info: tss.server.PluginCreateInfo): tss.LanguageService {
function getSemanticDiagnostics(fileName: string): tss.Diagnostic[] {
const results: tss.Diagnostic[] = [];
if (!angularOnly) {
const tsResults = tsLS.getSemanticDiagnostics(fileName);
results.push(...tsResults);
results.push(...tsLS.getSemanticDiagnostics(fileName));
}
// For semantic diagnostics we need to combine both TS + Angular results
const ngResults = ngLS.getDiagnostics(fileName);
if (!ngResults.length) {
return results;
}
const sourceFile = fileName.endsWith('.ts') ? ngLSHost.getSourceFile(fileName) : undefined;
results.push(...ngResults.map(d => diagnosticToDiagnostic(d, sourceFile)));
results.push(...ngLS.getDiagnostics(fileName));
return results;
}

View File

@ -190,7 +190,7 @@ export interface LanguageServiceHost {
* Return the template source information for all templates in `fileName` or for `fileName` if
* it is a template file.
*/
getTemplates(fileName: string): TemplateSources;
getTemplates(fileName: string): TemplateSource[];
/**
* Returns the Angular declarations in the given file.
@ -386,7 +386,7 @@ export interface LanguageService {
/**
* Returns a list of all error for all templates in the given file.
*/
getDiagnostics(fileName: string): Diagnostic[];
getDiagnostics(fileName: string): tss.Diagnostic[];
/**
* Return the completions at the given position.

View File

@ -166,16 +166,16 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return analyzedModules;
}
getTemplates(fileName: string): TemplateSources {
getTemplates(fileName: string): TemplateSource[] {
const results: TemplateSource[] = [];
if (fileName.endsWith('.ts')) {
let version = this.host.getScriptVersion(fileName);
let result: TemplateSource[] = [];
// Find each template string in the file
let visit = (child: ts.Node) => {
let templateSource = this.getSourceFromNode(fileName, version, child);
if (templateSource) {
result.push(templateSource);
results.push(templateSource);
} else {
ts.forEachChild(child, visit);
}
@ -185,17 +185,17 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (sourceFile) {
ts.forEachChild(sourceFile, visit);
}
return result.length ? result : undefined;
} else {
this.ensureTemplateMap();
const componentSymbol = this.fileToComponent.get(fileName);
if (componentSymbol) {
const templateSource = this.getTemplateAt(fileName, 0);
if (templateSource) {
return [templateSource];
results.push(templateSource);
}
}
}
return results;
}
getDeclarations(fileName: string): Declarations {