refactor(compiler-cli): Move diagnostics files to language service (#33809)
The following files are consumed only by the language service and do not have to be in compiler-cli: 1. expression_diagnostics.ts 2. expression_type.ts 3. typescript_symbols.ts 4. symbols.ts PR Close #33809
This commit is contained in:

committed by
Alex Rickabaugh

parent
784fd26473
commit
9935aa43ad
@ -1,28 +1,11 @@
|
||||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "mocks",
|
||||
testonly = True,
|
||||
srcs = [
|
||||
"mocks.ts",
|
||||
],
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/test:test_utils",
|
||||
"//packages/core",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
||||
# check_types_spec
|
||||
ts_library(
|
||||
name = "check_types_lib",
|
||||
testonly = True,
|
||||
srcs = ["check_types_spec.ts"],
|
||||
deps = [
|
||||
":mocks",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/test:test_utils",
|
||||
"@npm//typescript",
|
||||
@ -49,69 +32,6 @@ jasmine_node_test(
|
||||
],
|
||||
)
|
||||
|
||||
# expression_diagnostics_spec
|
||||
ts_library(
|
||||
name = "expression_diagnostics_lib",
|
||||
testonly = True,
|
||||
srcs = ["expression_diagnostics_spec.ts"],
|
||||
deps = [
|
||||
":mocks",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/test:test_utils",
|
||||
"//packages/language-service",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "expression_diagnostics",
|
||||
bootstrap = ["angular/tools/testing/init_node_spec.js"],
|
||||
data = [
|
||||
"//packages/common:npm_package",
|
||||
"//packages/core:npm_package",
|
||||
"//packages/forms:npm_package",
|
||||
],
|
||||
tags = [
|
||||
# Disabled as these tests pertain to diagnostics in the old ngc compiler. The Ivy ngtsc
|
||||
# compiler has its own tests for diagnostics.
|
||||
"no-ivy-aot",
|
||||
],
|
||||
deps = [
|
||||
":expression_diagnostics_lib",
|
||||
"//packages/core",
|
||||
"//tools/testing:node",
|
||||
],
|
||||
)
|
||||
|
||||
# typescript_symbols_spec
|
||||
ts_library(
|
||||
name = "typescript_symbols_lib",
|
||||
testonly = True,
|
||||
srcs = ["typescript_symbols_spec.ts"],
|
||||
deps = [
|
||||
":mocks",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/test:test_utils",
|
||||
"//packages/compiler/test:test_utils",
|
||||
"//packages/language-service",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "typescript_symbols",
|
||||
bootstrap = ["angular/tools/testing/init_node_spec.js"],
|
||||
data = [
|
||||
],
|
||||
deps = [
|
||||
":typescript_symbols_lib",
|
||||
"//packages/core",
|
||||
"//tools/testing:node",
|
||||
],
|
||||
)
|
||||
|
||||
# typescript_version_spec
|
||||
ts_library(
|
||||
name = "typescript_version_lib",
|
||||
|
@ -1,254 +0,0 @@
|
||||
/**
|
||||
* @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 {StaticSymbol} from '@angular/compiler';
|
||||
import {ReflectorHost} from '@angular/language-service/src/reflector_host';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {getTemplateExpressionDiagnostics} from '../../src/diagnostics/expression_diagnostics';
|
||||
import {Directory} from '../mocks';
|
||||
|
||||
import {DiagnosticContext, MockLanguageServiceHost, getDiagnosticTemplateInfo} from './mocks';
|
||||
|
||||
describe('expression diagnostics', () => {
|
||||
let registry: ts.DocumentRegistry;
|
||||
|
||||
let host: MockLanguageServiceHost;
|
||||
let service: ts.LanguageService;
|
||||
let context: DiagnosticContext;
|
||||
let type: StaticSymbol;
|
||||
|
||||
beforeAll(() => {
|
||||
registry = ts.createDocumentRegistry(false, '/src');
|
||||
host = new MockLanguageServiceHost(['app/app.component.ts'], FILES, '/src');
|
||||
service = ts.createLanguageService(host, registry);
|
||||
const program = service.getProgram() !;
|
||||
const checker = program.getTypeChecker();
|
||||
const symbolResolverHost = new ReflectorHost(() => program !, host);
|
||||
context = new DiagnosticContext(service, program !, checker, symbolResolverHost);
|
||||
type = context.getStaticSymbol('app/app.component.ts', 'AppComponent');
|
||||
});
|
||||
|
||||
it('should have no diagnostics in default app', () => {
|
||||
function messageToString(messageText: string | ts.DiagnosticMessageChain): string {
|
||||
if (typeof messageText == 'string') {
|
||||
return messageText;
|
||||
} else {
|
||||
if (messageText.next)
|
||||
return messageText.messageText + messageText.next.map(messageToString);
|
||||
return messageText.messageText;
|
||||
}
|
||||
}
|
||||
|
||||
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
|
||||
if (diagnostics && diagnostics.length) {
|
||||
const message =
|
||||
'messages: ' + diagnostics.map(d => messageToString(d.messageText)).join('\n');
|
||||
expect(message).toEqual('');
|
||||
}
|
||||
}
|
||||
|
||||
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
|
||||
expectNoDiagnostics(service.getSyntacticDiagnostics('app/app.component.ts'));
|
||||
expectNoDiagnostics(service.getSemanticDiagnostics('app/app.component.ts'));
|
||||
});
|
||||
|
||||
|
||||
function accept(template: string) {
|
||||
const info = getDiagnosticTemplateInfo(context, type, 'app/app.component.html', template);
|
||||
if (info) {
|
||||
const diagnostics = getTemplateExpressionDiagnostics(info);
|
||||
if (diagnostics && diagnostics.length) {
|
||||
const message = diagnostics.map(d => d.message).join('\n ');
|
||||
throw new Error(`Unexpected diagnostics: ${message}`);
|
||||
}
|
||||
} else {
|
||||
expect(info).toBeDefined();
|
||||
}
|
||||
}
|
||||
|
||||
function reject(template: string, expected: string) {
|
||||
const info = getDiagnosticTemplateInfo(context, type, 'app/app.component.html', template);
|
||||
if (info) {
|
||||
const diagnostics = getTemplateExpressionDiagnostics(info);
|
||||
if (diagnostics && diagnostics.length) {
|
||||
const messages = diagnostics.map(d => d.message).join('\n ');
|
||||
expect(messages).toContain(expected);
|
||||
} else {
|
||||
throw new Error(`Expected an error containing "${expected} in template "${template}"`);
|
||||
}
|
||||
} else {
|
||||
expect(info).toBeDefined();
|
||||
}
|
||||
}
|
||||
|
||||
it('should accept a simple template', () => accept('App works!'));
|
||||
it('should accept an interpolation', () => accept('App works: {{person.name.first}}'));
|
||||
it('should reject misspelled access',
|
||||
() => reject('{{persson}}', 'Identifier \'persson\' is not defined'));
|
||||
it('should reject access to private',
|
||||
() =>
|
||||
reject('{{private_person}}', 'Identifier \'private_person\' refers to a private member'));
|
||||
it('should accept an *ngIf', () => accept('<div *ngIf="person">{{person.name.first}}</div>'));
|
||||
it('should reject *ngIf of misspelled identifier',
|
||||
() => reject(
|
||||
'<div *ngIf="persson">{{person.name.first}}</div>',
|
||||
'Identifier \'persson\' is not defined'));
|
||||
it('should reject *ngIf of misspelled identifier in PrefixNot node',
|
||||
() =>
|
||||
reject('<div *ngIf="people && !persson"></div>', 'Identifier \'persson\' is not defined'));
|
||||
it('should accept an *ngFor', () => accept(`
|
||||
<div *ngFor="let p of people">
|
||||
{{p.name.first}} {{p.name.last}}
|
||||
</div>
|
||||
`));
|
||||
it('should reject misspelled field in *ngFor', () => reject(
|
||||
`
|
||||
<div *ngFor="let p of people">
|
||||
{{p.names.first}} {{p.name.last}}
|
||||
</div>
|
||||
`,
|
||||
'Identifier \'names\' is not defined'));
|
||||
it('should accept an async expression',
|
||||
() => accept('{{(promised_person | async)?.name.first || ""}}'));
|
||||
it('should reject an async misspelled field',
|
||||
() => reject(
|
||||
'{{(promised_person | async)?.nume.first || ""}}', 'Identifier \'nume\' is not defined'));
|
||||
it('should accept an async *ngFor', () => accept(`
|
||||
<div *ngFor="let p of promised_people | async">
|
||||
{{p.name.first}} {{p.name.last}}
|
||||
</div>
|
||||
`));
|
||||
it('should reject misspelled field an async *ngFor', () => reject(
|
||||
`
|
||||
<div *ngFor="let p of promised_people | async">
|
||||
{{p.name.first}} {{p.nume.last}}
|
||||
</div>
|
||||
`,
|
||||
'Identifier \'nume\' is not defined'));
|
||||
it('should accept an async *ngIf', () => accept(`
|
||||
<div *ngIf="promised_person | async as p">
|
||||
{{p.name.first}} {{p.name.last}}
|
||||
</div>
|
||||
`));
|
||||
it('should reject misspelled field in async *ngIf', () => reject(
|
||||
`
|
||||
<div *ngIf="promised_person | async as p">
|
||||
{{p.name.first}} {{p.nume.last}}
|
||||
</div>
|
||||
`,
|
||||
'Identifier \'nume\' is not defined'));
|
||||
it('should reject access to potentially undefined field',
|
||||
() => reject(`<div>{{maybe_person.name.first}}`, 'The expression might be null'));
|
||||
it('should accept a safe accss to an undefined field',
|
||||
() => accept(`<div>{{maybe_person?.name.first}}</div>`));
|
||||
it('should accept a type assert to an undefined field',
|
||||
() => accept(`<div>{{maybe_person!.name.first}}</div>`));
|
||||
it('should accept a # reference', () => accept(`
|
||||
<form #f="ngForm" novalidate>
|
||||
<input name="first" ngModel required #first="ngModel">
|
||||
<input name="last" ngModel>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
<p>First name value: {{ first.value }}</p>
|
||||
<p>First name valid: {{ first.valid }}</p>
|
||||
<p>Form value: {{ f.value | json }}</p>
|
||||
<p>Form valid: {{ f.valid }}</p>
|
||||
`));
|
||||
it('should reject a misspelled field of a # reference',
|
||||
() => reject(
|
||||
`
|
||||
<form #f="ngForm" novalidate>
|
||||
<input name="first" ngModel required #first="ngModel">
|
||||
<input name="last" ngModel>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
<p>First name value: {{ first.valwe }}</p>
|
||||
<p>First name valid: {{ first.valid }}</p>
|
||||
<p>Form value: {{ f.value | json }}</p>
|
||||
<p>Form valid: {{ f.valid }}</p>
|
||||
`,
|
||||
'Identifier \'valwe\' is not defined'));
|
||||
it('should accept a call to a method', () => accept('{{getPerson().name.first}}'));
|
||||
it('should reject a misspelled field of a method result',
|
||||
() => reject('{{getPerson().nume.first}}', 'Identifier \'nume\' is not defined'));
|
||||
it('should reject calling a uncallable member',
|
||||
() => reject('{{person().name.first}}', 'Member \'person\' is not callable'));
|
||||
it('should accept an event handler',
|
||||
() => accept('<div (click)="click($event)">{{person.name.first}}</div>'));
|
||||
it('should reject a misspelled event handler',
|
||||
() => reject(
|
||||
'<div (click)="clack($event)">{{person.name.first}}</div>', 'Unknown method \'clack\''));
|
||||
it('should reject an uncalled event handler',
|
||||
() => reject(
|
||||
'<div (click)="click">{{person.name.first}}</div>', 'Unexpected callable expression'));
|
||||
describe('with comparisons between nullable and non-nullable', () => {
|
||||
it('should accept ==', () => accept(`<div>{{e == 1 ? 'a' : 'b'}}</div>`));
|
||||
it('should accept ===', () => accept(`<div>{{e === 1 ? 'a' : 'b'}}</div>`));
|
||||
it('should accept !=', () => accept(`<div>{{e != 1 ? 'a' : 'b'}}</div>`));
|
||||
it('should accept !==', () => accept(`<div>{{e !== 1 ? 'a' : 'b'}}</div>`));
|
||||
it('should accept &&', () => accept(`<div>{{e && 1 ? 'a' : 'b'}}</div>`));
|
||||
it('should accept ||', () => accept(`<div>{{e || 1 ? 'a' : 'b'}}</div>`));
|
||||
it('should reject >',
|
||||
() => reject(`<div>{{e > 1 ? 'a' : 'b'}}</div>`, 'The expression might be null'));
|
||||
});
|
||||
});
|
||||
|
||||
const FILES: Directory = {
|
||||
'src': {
|
||||
'app': {
|
||||
'app.component.ts': `
|
||||
import { Component, NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
export interface Person {
|
||||
name: Name;
|
||||
address: Address;
|
||||
}
|
||||
|
||||
export interface Name {
|
||||
first: string;
|
||||
middle: string;
|
||||
last: string;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
street: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
templateUrl: './app.component.html'
|
||||
})
|
||||
export class AppComponent {
|
||||
person: Person;
|
||||
people: Person[];
|
||||
maybe_person?: Person;
|
||||
promised_person: Promise<Person>;
|
||||
promised_people: Promise<Person[]>;
|
||||
private private_person: Person;
|
||||
private private_people: Person[];
|
||||
e?: number;
|
||||
|
||||
getPerson(): Person { return this.person; }
|
||||
click() {}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule],
|
||||
declarations: [AppComponent]
|
||||
})
|
||||
export class AppModule {}
|
||||
`
|
||||
}
|
||||
}
|
||||
};
|
@ -1,253 +0,0 @@
|
||||
/**
|
||||
* @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 {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver} from '@angular/compiler';
|
||||
import {ViewEncapsulation, ɵConsole as Console} from '@angular/core';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {DiagnosticTemplateInfo} from '../../src/diagnostics/expression_diagnostics';
|
||||
import {getClassMembers, getPipesTable, getSymbolQuery} from '../../src/diagnostics/typescript_symbols';
|
||||
import {Directory, MockAotContext} from '../mocks';
|
||||
import {setup} from '../test_support';
|
||||
|
||||
const realFiles = new Map<string, string>();
|
||||
|
||||
export class MockLanguageServiceHost implements ts.LanguageServiceHost {
|
||||
private options: ts.CompilerOptions;
|
||||
private context: MockAotContext;
|
||||
private assumedExist = new Set<string>();
|
||||
|
||||
constructor(private scripts: string[], files: Directory, currentDirectory: string = '/') {
|
||||
const support = setup();
|
||||
|
||||
this.options = {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||
emitDecoratorMetadata: true,
|
||||
experimentalDecorators: true,
|
||||
removeComments: false,
|
||||
noImplicitAny: false,
|
||||
skipLibCheck: true,
|
||||
skipDefaultLibCheck: true,
|
||||
strictNullChecks: true,
|
||||
baseUrl: currentDirectory,
|
||||
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
|
||||
paths: {'@angular/*': [path.join(support.basePath, 'node_modules/@angular/*')]}
|
||||
};
|
||||
this.context = new MockAotContext(currentDirectory, files);
|
||||
}
|
||||
|
||||
getCompilationSettings(): ts.CompilerOptions { return this.options; }
|
||||
|
||||
getScriptFileNames(): string[] { return this.scripts; }
|
||||
|
||||
getScriptVersion(fileName: string): string { return '0'; }
|
||||
|
||||
getScriptSnapshot(fileName: string): ts.IScriptSnapshot|undefined {
|
||||
const content = this.internalReadFile(fileName);
|
||||
if (content) {
|
||||
return ts.ScriptSnapshot.fromString(content);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentDirectory(): string { return this.context.currentDirectory; }
|
||||
|
||||
getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
|
||||
|
||||
readFile(fileName: string): string { return this.internalReadFile(fileName) as string; }
|
||||
|
||||
readResource(fileName: string): Promise<string> { return Promise.resolve(''); }
|
||||
|
||||
assumeFileExists(fileName: string): void { this.assumedExist.add(fileName); }
|
||||
|
||||
fileExists(fileName: string): boolean {
|
||||
return this.assumedExist.has(fileName) || this.internalReadFile(fileName) != null;
|
||||
}
|
||||
|
||||
private internalReadFile(fileName: string): string|undefined {
|
||||
let basename = path.basename(fileName);
|
||||
if (/^lib.*\.d\.ts$/.test(basename)) {
|
||||
let libPath = path.posix.dirname(ts.getDefaultLibFilePath(this.getCompilationSettings()));
|
||||
fileName = path.posix.join(libPath, basename);
|
||||
}
|
||||
if (fileName.startsWith('app/')) {
|
||||
fileName = path.posix.join(this.context.currentDirectory, fileName);
|
||||
}
|
||||
if (this.context.fileExists(fileName)) {
|
||||
return this.context.readFile(fileName);
|
||||
}
|
||||
if (realFiles.has(fileName)) {
|
||||
return realFiles.get(fileName);
|
||||
}
|
||||
if (fs.existsSync(fileName)) {
|
||||
const content = fs.readFileSync(fileName, 'utf8');
|
||||
realFiles.set(fileName, content);
|
||||
return content;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const staticSymbolCache = new StaticSymbolCache();
|
||||
const summaryResolver = new AotSummaryResolver(
|
||||
{
|
||||
loadSummary(filePath: string) { return null; },
|
||||
isSourceFile(sourceFilePath: string) { return true; },
|
||||
toSummaryFileName(sourceFilePath: string) { return sourceFilePath; },
|
||||
fromSummaryFileName(filePath: string): string{return filePath;},
|
||||
},
|
||||
staticSymbolCache);
|
||||
|
||||
export class DiagnosticContext {
|
||||
// tslint:disable
|
||||
// TODO(issue/24571): remove '!'.
|
||||
_analyzedModules !: NgAnalyzedModules;
|
||||
_staticSymbolResolver: StaticSymbolResolver|undefined;
|
||||
_reflector: StaticReflector|undefined;
|
||||
_errors: {e: any, path?: string}[] = [];
|
||||
_resolver: CompileMetadataResolver|undefined;
|
||||
// tslint:enable
|
||||
|
||||
constructor(
|
||||
public service: ts.LanguageService, public program: ts.Program,
|
||||
public checker: ts.TypeChecker, public host: StaticSymbolResolverHost) {}
|
||||
|
||||
private collectError(e: any, path?: string) { this._errors.push({e, path}); }
|
||||
|
||||
private get staticSymbolResolver(): StaticSymbolResolver {
|
||||
let result = this._staticSymbolResolver;
|
||||
if (!result) {
|
||||
result = this._staticSymbolResolver = new StaticSymbolResolver(
|
||||
this.host, staticSymbolCache, summaryResolver,
|
||||
(e, filePath) => this.collectError(e, filePath));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
get reflector(): StaticReflector {
|
||||
if (!this._reflector) {
|
||||
const ssr = this.staticSymbolResolver;
|
||||
const result = this._reflector = new StaticReflector(
|
||||
summaryResolver, ssr, [], [], (e, filePath) => this.collectError(e, filePath !));
|
||||
this._reflector = result;
|
||||
return result;
|
||||
}
|
||||
return this._reflector;
|
||||
}
|
||||
|
||||
get resolver(): CompileMetadataResolver {
|
||||
let result = this._resolver;
|
||||
if (!result) {
|
||||
const moduleResolver = new NgModuleResolver(this.reflector);
|
||||
const directiveResolver = new DirectiveResolver(this.reflector);
|
||||
const pipeResolver = new PipeResolver(this.reflector);
|
||||
const elementSchemaRegistry = new DomElementSchemaRegistry();
|
||||
const resourceLoader = new class extends ResourceLoader {
|
||||
get(url: string): Promise<string> { return Promise.resolve(''); }
|
||||
};
|
||||
const urlResolver = createOfflineCompileUrlResolver();
|
||||
const htmlParser = new class extends HtmlParser {
|
||||
parse(): ParseTreeResult { return new ParseTreeResult([], []); }
|
||||
};
|
||||
|
||||
// This tracks the CompileConfig in codegen.ts. Currently these options
|
||||
// are hard-coded.
|
||||
const config =
|
||||
new CompilerConfig({defaultEncapsulation: ViewEncapsulation.Emulated, useJit: false});
|
||||
const directiveNormalizer =
|
||||
new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config);
|
||||
|
||||
result = this._resolver = new CompileMetadataResolver(
|
||||
config, htmlParser, moduleResolver, directiveResolver, pipeResolver,
|
||||
new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(),
|
||||
staticSymbolCache, this.reflector,
|
||||
(error, type) => this.collectError(error, type && type.filePath));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
get analyzedModules(): NgAnalyzedModules {
|
||||
let analyzedModules = this._analyzedModules;
|
||||
if (!analyzedModules) {
|
||||
const analyzeHost = {isSourceFile(filePath: string) { return true; }};
|
||||
const programFiles = this.program.getSourceFiles().map(sf => sf.fileName);
|
||||
analyzedModules = this._analyzedModules =
|
||||
analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, this.resolver);
|
||||
}
|
||||
return analyzedModules;
|
||||
}
|
||||
|
||||
getStaticSymbol(path: string, name: string): StaticSymbol {
|
||||
return staticSymbolCache.get(path, name);
|
||||
}
|
||||
}
|
||||
|
||||
function compileTemplate(context: DiagnosticContext, type: StaticSymbol, template: string) {
|
||||
// Compiler the template string.
|
||||
const resolvedMetadata = context.resolver.getNonNormalizedDirectiveMetadata(type);
|
||||
const metadata = resolvedMetadata && resolvedMetadata.metadata;
|
||||
if (metadata) {
|
||||
const rawHtmlParser = new HtmlParser();
|
||||
const htmlParser = new I18NHtmlParser(rawHtmlParser);
|
||||
const expressionParser = new Parser(new Lexer());
|
||||
const config = new CompilerConfig();
|
||||
const parser = new TemplateParser(
|
||||
config, context.reflector, expressionParser, new DomElementSchemaRegistry(), htmlParser,
|
||||
null !, []);
|
||||
const htmlResult = htmlParser.parse(template, '', {tokenizeExpansionForms: true});
|
||||
const analyzedModules = context.analyzedModules;
|
||||
// let errors: Diagnostic[]|undefined = undefined;
|
||||
let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(type);
|
||||
if (ngModule) {
|
||||
const resolvedDirectives = ngModule.transitiveModule.directives.map(
|
||||
d => context.resolver.getNonNormalizedDirectiveMetadata(d.reference));
|
||||
const directives = removeMissing(resolvedDirectives).map(d => d.metadata.toSummary());
|
||||
const pipes = ngModule.transitiveModule.pipes.map(
|
||||
p => context.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
|
||||
const schemas = ngModule.schemas;
|
||||
const parseResult = parser.tryParseHtml(htmlResult, metadata, directives, pipes, schemas);
|
||||
return {
|
||||
htmlAst: htmlResult.rootNodes,
|
||||
templateAst: parseResult.templateAst,
|
||||
directive: metadata, directives, pipes,
|
||||
parseErrors: parseResult.errors, expressionParser
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getDiagnosticTemplateInfo(
|
||||
context: DiagnosticContext, type: StaticSymbol, templateFile: string,
|
||||
template: string): DiagnosticTemplateInfo|undefined {
|
||||
const compiledTemplate = compileTemplate(context, type, template);
|
||||
if (compiledTemplate && compiledTemplate.templateAst) {
|
||||
const members = getClassMembers(context.program, context.checker, type);
|
||||
if (members) {
|
||||
const sourceFile = context.program.getSourceFile(type.filePath);
|
||||
if (sourceFile) {
|
||||
const query = getSymbolQuery(
|
||||
context.program, context.checker, sourceFile,
|
||||
() => getPipesTable(
|
||||
sourceFile, context.program, context.checker, compiledTemplate.pipes));
|
||||
return {
|
||||
fileName: templateFile,
|
||||
offset: 0, query, members,
|
||||
htmlAst: compiledTemplate.htmlAst,
|
||||
templateAst: compiledTemplate.templateAst
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeMissing<T>(values: (T | null | undefined)[]): T[] {
|
||||
return values.filter(e => !!e) as T[];
|
||||
}
|
@ -1,139 +0,0 @@
|
||||
/**
|
||||
* @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 {ReflectorHost} from '@angular/language-service/src/reflector_host';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {BuiltinType, Symbol, SymbolQuery, SymbolTable} from '../../src/diagnostics/symbols';
|
||||
import {getSymbolQuery, toSymbolTableFactory} from '../../src/diagnostics/typescript_symbols';
|
||||
import {Directory} from '../mocks';
|
||||
|
||||
import {DiagnosticContext, MockLanguageServiceHost} from './mocks';
|
||||
|
||||
function emptyPipes(): SymbolTable {
|
||||
return {
|
||||
size: 0,
|
||||
get(key: string) { return undefined; },
|
||||
has(key: string) { return false; },
|
||||
values(): Symbol[]{return [];}
|
||||
};
|
||||
}
|
||||
|
||||
describe('symbol query', () => {
|
||||
let program: ts.Program;
|
||||
let checker: ts.TypeChecker;
|
||||
let sourceFile: ts.SourceFile;
|
||||
let query: SymbolQuery;
|
||||
let context: DiagnosticContext;
|
||||
beforeEach(() => {
|
||||
const registry = ts.createDocumentRegistry(false, '/src');
|
||||
const host = new MockLanguageServiceHost(
|
||||
['/quickstart/app/app.component.ts'], QUICKSTART, '/quickstart');
|
||||
const service = ts.createLanguageService(host, registry);
|
||||
program = service.getProgram() !;
|
||||
checker = program.getTypeChecker();
|
||||
sourceFile = program.getSourceFile('/quickstart/app/app.component.ts') !;
|
||||
const symbolResolverHost = new ReflectorHost(() => program, host);
|
||||
context = new DiagnosticContext(service, program, checker, symbolResolverHost);
|
||||
query = getSymbolQuery(program, checker, sourceFile, emptyPipes);
|
||||
});
|
||||
|
||||
it('should be able to get undefined for an unknown symbol', () => {
|
||||
const unknownType = context.getStaticSymbol('/unkonwn/file.ts', 'UnknownType');
|
||||
const symbol = query.getTypeSymbol(unknownType);
|
||||
expect(symbol).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return correct built-in types', () => {
|
||||
const tests: Array<[BuiltinType, boolean, ts.TypeFlags?]> = [
|
||||
// builtinType, throws, want
|
||||
[BuiltinType.Any, false, ts.TypeFlags.Any],
|
||||
[BuiltinType.Boolean, false, ts.TypeFlags.BooleanLiteral],
|
||||
[BuiltinType.Null, false, ts.TypeFlags.Null],
|
||||
[BuiltinType.Number, false, ts.TypeFlags.NumberLiteral],
|
||||
[BuiltinType.String, false, ts.TypeFlags.StringLiteral],
|
||||
[BuiltinType.Undefined, false, ts.TypeFlags.Undefined],
|
||||
[BuiltinType.Unbound, true],
|
||||
[BuiltinType.Other, true],
|
||||
];
|
||||
for (const [builtinType, throws, want] of tests) {
|
||||
if (throws) {
|
||||
expect(() => query.getBuiltinType(builtinType)).toThrow();
|
||||
} else {
|
||||
const symbol = query.getBuiltinType(builtinType);
|
||||
const got: ts.TypeFlags = (symbol as any).tsType.flags;
|
||||
expect(got).toBe(want !);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('toSymbolTableFactory(tsVersion)', () => {
|
||||
it('should return a Map for versions of TypeScript >= 2.2 and a dictionary otherwise', () => {
|
||||
const a = { name: 'a' } as ts.Symbol;
|
||||
const b = { name: 'b' } as ts.Symbol;
|
||||
|
||||
expect(toSymbolTableFactory('2.1')([a, b]) instanceof Map).toEqual(false);
|
||||
expect(toSymbolTableFactory('2.4')([a, b]) instanceof Map).toEqual(true);
|
||||
|
||||
// Check that for the lower bound version `2.2`, toSymbolTableFactory('2.2') returns a map
|
||||
expect(toSymbolTableFactory('2.2')([a, b]) instanceof Map).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
function appComponentSource(template: string): string {
|
||||
return `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
export interface Person {
|
||||
name: string;
|
||||
address: Address;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
street: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '${template}'
|
||||
})
|
||||
export class AppComponent {
|
||||
name = 'Angular';
|
||||
person: Person;
|
||||
people: Person[];
|
||||
maybePerson?: Person;
|
||||
|
||||
getName(): string { return this.name; }
|
||||
getPerson(): Person { return this.person; }
|
||||
getMaybePerson(): Person | undefined { this.maybePerson; }
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
const QUICKSTART: Directory = {
|
||||
quickstart: {
|
||||
app: {
|
||||
'app.component.ts': appComponentSource('<h1>Hello {{name}}</h1>'),
|
||||
'app.module.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
import { toString } from './utils';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ AppComponent ],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
export class AppModule { }
|
||||
`
|
||||
}
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user