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 {
|
getNonNullableType(symbol: Symbol): Symbol {
|
||||||
// TODO: Replace with typeChecker API when available;
|
if (symbol instanceof TypeWrapper && (typeof this.checker.getNonNullableType == 'function')) {
|
||||||
return symbol;
|
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 {
|
getPipes(): SymbolTable {
|
||||||
|
@ -95,7 +95,7 @@ describe('diagnostics', () => {
|
|||||||
const code = '\n@Component({template: \'<form></form>\'}) export class MyComponent {}';
|
const code = '\n@Component({template: \'<form></form>\'}) export class MyComponent {}';
|
||||||
addCode(code, (fileName, content) => {
|
addCode(code, (fileName, content) => {
|
||||||
const diagnostics = ngService.getDiagnostics(fileName);
|
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'; }`;
|
` @Component({template: \`<div *ngIf="something === 'foo'"></div>\`}) export class MyComponent { something: 'foo' | 'bar'; }`;
|
||||||
addCode(code, fileName => {
|
addCode(code, fileName => {
|
||||||
const diagnostics = ngService.getDiagnostics(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'; }})`;
|
` @Component({template: \`<div *ngIf="something === undefined"></div>\`}) export class MyComponent { something = 'foo'; }})`;
|
||||||
addCode(code, fileName => {
|
addCode(code, fileName => {
|
||||||
const diagnostics = ngService.getDiagnostics(fileName);
|
const diagnostics = ngService.getDiagnostics(fileName);
|
||||||
onlyModuleDiagnostics(diagnostics !);
|
expectOnlyModuleDiagnostics(diagnostics !);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -233,7 +233,7 @@ describe('diagnostics', () => {
|
|||||||
})
|
})
|
||||||
export class MyComponent {}
|
export class MyComponent {}
|
||||||
`,
|
`,
|
||||||
fileName => onlyModuleDiagnostics(ngService.getDiagnostics(fileName)));
|
fileName => expectOnlyModuleDiagnostics(ngService.getDiagnostics(fileName)));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Issue #15625
|
// Issue #15625
|
||||||
@ -254,10 +254,33 @@ describe('diagnostics', () => {
|
|||||||
`,
|
`,
|
||||||
fileName => {
|
fileName => {
|
||||||
const diagnostics = ngService.getDiagnostics(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) {
|
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
|
||||||
const fileName = '/app/app.component.ts';
|
const fileName = '/app/app.component.ts';
|
||||||
const originalContent = mockHost.getFileContent(fileName);
|
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 only the 'MyComponent' diagnostic
|
||||||
expect(diagnostics.length).toBe(1);
|
expect(diagnostics.length).toBe(1);
|
||||||
if (diagnostics.length > 1) {
|
if (diagnostics.length > 1) {
|
||||||
|
@ -68,6 +68,7 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
|||||||
private scriptVersion = new Map<string, number>();
|
private scriptVersion = new Map<string, number>();
|
||||||
private overrides = new Map<string, string>();
|
private overrides = new Map<string, string>();
|
||||||
private projectVersion = 0;
|
private projectVersion = 0;
|
||||||
|
private options: ts.CompilerOptions;
|
||||||
|
|
||||||
constructor(private scriptNames: string[], private data: MockData) {
|
constructor(private scriptNames: string[], private data: MockData) {
|
||||||
const moduleFilename = module.filename.replace(/\\/g, '/');
|
const moduleFilename = module.filename.replace(/\\/g, '/');
|
||||||
@ -77,6 +78,16 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
|||||||
let distIndex = moduleFilename.indexOf('/dist/all');
|
let distIndex = moduleFilename.indexOf('/dist/all');
|
||||||
if (distIndex >= 0)
|
if (distIndex >= 0)
|
||||||
this.nodeModulesPath = path.join(moduleFilename.substr(0, distIndex), 'node_modules');
|
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) {
|
override(fileName: string, content: string) {
|
||||||
@ -99,19 +110,13 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
|||||||
|
|
||||||
forgetAngular() { this.angularPath = undefined; }
|
forgetAngular() { this.angularPath = undefined; }
|
||||||
|
|
||||||
getCompilationSettings(): ts.CompilerOptions {
|
overrideOptions(cb: (options: ts.CompilerOptions) => ts.CompilerOptions) {
|
||||||
return {
|
this.options = cb(this.options);
|
||||||
target: ts.ScriptTarget.ES5,
|
this.projectVersion++;
|
||||||
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'],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCompilationSettings(): ts.CompilerOptions { return this.options; }
|
||||||
|
|
||||||
getProjectVersion(): string { return this.projectVersion.toString(); }
|
getProjectVersion(): string { return this.projectVersion.toString(); }
|
||||||
|
|
||||||
getScriptFileNames(): string[] { return this.scriptNames; }
|
getScriptFileNames(): string[] { return this.scriptNames; }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user