fix(language-service): properly evaluate types in comparable expressions (#36529)

This commit fixes how the language service evaluates the compatibility
of types to work with arbitrary union types. As a result, compatibility
checks are now more strict and can catch similarities or differences
more clearly.

```
number|string == string|null  // OK
number|string == number       // OK
number|string == null         // not comparable
number == string              // not comparable
```

Using Ivy as a backend should provide these diagnoses for free, but we
can backfill them for now.

Closes https://github.com/angular/vscode-ng-language-service/issues/723

PR Close #36529
This commit is contained in:
Ayaz Hafiz
2020-04-08 13:48:35 -07:00
committed by Matias Niemelä
parent 78840e59a7
commit 8be0972836
6 changed files with 69 additions and 81 deletions

View File

@ -23,8 +23,6 @@ import {MockTypescriptHost} from './test_utils';
* as well.
*/
const EXPRESSION_CASES = '/app/expression-cases.ts';
const NG_FOR_CASES = '/app/ng-for-cases.ts';
const TEST_TEMPLATE = '/app/test.ng';
const APP_COMPONENT = '/app/app.component.ts';
@ -121,8 +119,41 @@ describe('diagnostics', () => {
expect(diagnostics).toEqual([]);
});
describe('diagnostics for expression comparisons', () => {
for (let [left, right, leftTy, rightTy] of [
['\'abc\'', 1, 'string', 'number'],
['hero', 2, 'object', 'number'],
['strOrNumber', 'hero', 'string|number', 'object'],
]) {
it(`it should report errors for mismtched types in a comparison: ${leftTy} and ${rightTy}`,
() => {
mockHost.override(TEST_TEMPLATE, `{{ ${left} != ${right} }}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(`Expected operands to be of comparable types or any`);
});
}
for (let [left, right, leftTy, rightTy] of [
['\'abc\'', 'anyValue', 'string', 'any'],
['\'abc\'', null, 'string', 'null'],
['\'abc\'', undefined, 'string', 'undefined'],
[null, null, 'null', 'null'],
['{a: 1}', '{b: 2}', 'object', 'object'],
['strOrNumber', '1', 'string|number', 'number'],
]) {
it(`it should not report errors for compatible types in a comparison: ${leftTy} and ${
rightTy}`,
() => {
mockHost.override(TEST_TEMPLATE, `{{ ${left} != ${right} }}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(0);
});
}
});
describe('diagnostics for ngFor exported values', () => {
it('should report errors for mistmatched exported types', () => {
it('should report errors for mismatched exported types', () => {
mockHost.override(TEST_TEMPLATE, `
<div *ngFor="let hero of heroes; let i = index; let isFirst = first">
'i' is a number; 'isFirst' is a boolean
@ -131,7 +162,7 @@ describe('diagnostics', () => {
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(`Expected operands to be of similar type or any`);
expect(diags[0].messageText).toBe(`Expected operands to be of comparable types or any`);
});
it('should not report errors for matching exported type', () => {
@ -583,26 +614,6 @@ describe('diagnostics', () => {
expect(length).toBe(keyword.length - 2); // exclude leading and trailing quotes
});
// #13412
it('should not report an error for using undefined under non-strict mode', () => {
mockHost.override(APP_COMPONENT, `
import { Component } from '@angular/core';
@Component({
template: '<div *ngIf="something === undefined"></div>'
})
export class AppComponent {
something = 'foo';
}`);
mockHost.overrideOptions({
strict: false, // TODO: This test fails in strict mode
});
const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
expect(tsDiags).toEqual([]);
const ngDiags = ngLS.getSemanticDiagnostics(APP_COMPONENT);
expect(ngDiags).toEqual([]);
});
// Issue #13326
it('should report a narrow span for invalid pipes', () => {
const content = mockHost.override(APP_COMPONENT, `