fix(language-service): infer correct type of `?.` expressions
Fixes #15885
This commit is contained in:
parent
5a88d2f68b
commit
0a3a9afe58
|
@ -649,8 +649,14 @@ class TypeScriptSymbolQuery implements SymbolQuery {
|
|||
}
|
||||
|
||||
getNonNullableType(symbol: Symbol): Symbol {
|
||||
// TODO: Replace with typeChecker API when available;
|
||||
return symbol;
|
||||
if (symbol instanceof TypeWrapper && (typeof this.checker.getNonNullableType == 'function')) {
|
||||
const tsType = symbol.tsType;
|
||||
const nonNullableType = this.checker.getNonNullableType(tsType);
|
||||
if (nonNullableType != tsType) {
|
||||
return new TypeWrapper(nonNullableType, symbol.context);
|
||||
}
|
||||
}
|
||||
return this.getBuiltinType(BuiltinType.Any);
|
||||
}
|
||||
|
||||
getPipes(): SymbolTable {
|
||||
|
|
|
@ -95,7 +95,7 @@ describe('diagnostics', () => {
|
|||
const code = '\n@Component({template: \'<form></form>\'}) export class MyComponent {}';
|
||||
addCode(code, (fileName, content) => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
onlyModuleDiagnostics(diagnostics !);
|
||||
expectOnlyModuleDiagnostics(diagnostics !);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -134,7 +134,7 @@ describe('diagnostics', () => {
|
|||
` @Component({template: \`<div *ngIf="something === 'foo'"></div>\`}) export class MyComponent { something: 'foo' | 'bar'; }`;
|
||||
addCode(code, fileName => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
onlyModuleDiagnostics(diagnostics !);
|
||||
expectOnlyModuleDiagnostics(diagnostics !);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -155,7 +155,7 @@ describe('diagnostics', () => {
|
|||
` @Component({template: \`<div *ngIf="something === undefined"></div>\`}) export class MyComponent { something = 'foo'; }})`;
|
||||
addCode(code, fileName => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
onlyModuleDiagnostics(diagnostics !);
|
||||
expectOnlyModuleDiagnostics(diagnostics !);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -233,7 +233,7 @@ describe('diagnostics', () => {
|
|||
})
|
||||
export class MyComponent {}
|
||||
`,
|
||||
fileName => onlyModuleDiagnostics(ngService.getDiagnostics(fileName)));
|
||||
fileName => expectOnlyModuleDiagnostics(ngService.getDiagnostics(fileName)));
|
||||
});
|
||||
|
||||
// Issue #15625
|
||||
|
@ -254,10 +254,33 @@ describe('diagnostics', () => {
|
|||
`,
|
||||
fileName => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
onlyModuleDiagnostics(diagnostics);
|
||||
expectOnlyModuleDiagnostics(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
// Issue #15885
|
||||
it('should be able to remove null and undefined from a type', () => {
|
||||
mockHost.overrideOptions(options => {
|
||||
options.strictNullChecks = true;
|
||||
return options;
|
||||
});
|
||||
addCode(
|
||||
`
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \` {{test?.a}}
|
||||
\`
|
||||
})
|
||||
export class MyComponent {
|
||||
test: {a: number, b: number} | null = {
|
||||
a: 1,
|
||||
b: 2
|
||||
};
|
||||
}
|
||||
`,
|
||||
fileName => expectOnlyModuleDiagnostics(ngService.getDiagnostics(fileName)));
|
||||
});
|
||||
|
||||
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
|
||||
const fileName = '/app/app.component.ts';
|
||||
const originalContent = mockHost.getFileContent(fileName);
|
||||
|
@ -271,7 +294,7 @@ describe('diagnostics', () => {
|
|||
}
|
||||
}
|
||||
|
||||
function onlyModuleDiagnostics(diagnostics: Diagnostics) {
|
||||
function expectOnlyModuleDiagnostics(diagnostics: Diagnostics) {
|
||||
// Expect only the 'MyComponent' diagnostic
|
||||
expect(diagnostics.length).toBe(1);
|
||||
if (diagnostics.length > 1) {
|
||||
|
|
|
@ -68,6 +68,7 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
|||
private scriptVersion = new Map<string, number>();
|
||||
private overrides = new Map<string, string>();
|
||||
private projectVersion = 0;
|
||||
private options: ts.CompilerOptions;
|
||||
|
||||
constructor(private scriptNames: string[], private data: MockData) {
|
||||
const moduleFilename = module.filename.replace(/\\/g, '/');
|
||||
|
@ -77,6 +78,16 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
|||
let distIndex = moduleFilename.indexOf('/dist/all');
|
||||
if (distIndex >= 0)
|
||||
this.nodeModulesPath = path.join(moduleFilename.substr(0, distIndex), 'node_modules');
|
||||
this.options = {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||
emitDecoratorMetadata: true,
|
||||
experimentalDecorators: true,
|
||||
removeComments: false,
|
||||
noImplicitAny: false,
|
||||
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
|
||||
};
|
||||
}
|
||||
|
||||
override(fileName: string, content: string) {
|
||||
|
@ -99,19 +110,13 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
|||
|
||||
forgetAngular() { this.angularPath = undefined; }
|
||||
|
||||
getCompilationSettings(): ts.CompilerOptions {
|
||||
return {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||
emitDecoratorMetadata: true,
|
||||
experimentalDecorators: true,
|
||||
removeComments: false,
|
||||
noImplicitAny: false,
|
||||
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
|
||||
};
|
||||
overrideOptions(cb: (options: ts.CompilerOptions) => ts.CompilerOptions) {
|
||||
this.options = cb(this.options);
|
||||
this.projectVersion++;
|
||||
}
|
||||
|
||||
getCompilationSettings(): ts.CompilerOptions { return this.options; }
|
||||
|
||||
getProjectVersion(): string { return this.projectVersion.toString(); }
|
||||
|
||||
getScriptFileNames(): string[] { return this.scriptNames; }
|
||||
|
|
Loading…
Reference in New Issue