Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new rule: no-invalid-assertion #179

Merged
merged 7 commits into from
Apr 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export {};

declare function get<T>(): T;

'foo' as 'foo';
'foo' as 'boo';
~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '"foo"' cannot be converted to type '"boo"'.]
'foo' as any;
'1' as 1;
1 as 1;
1 as 2;
~~~~~~ [error no-invalid-assertion: Type '1' cannot be converted to type '2'.]
true as false;
~~~~~~~~~~~~~ [error no-invalid-assertion: Type 'true' cannot be converted to type 'false'.]
<true>false;
~~~~~~~~~~~ [error no-invalid-assertion: Type 'false' cannot be converted to type 'true'.]

get<boolean>() as true;
get<string>() as 'foo';
get<number>() as 1;

1 as number;
true as boolean;
<string>'foo';

get<any>() as 'foo';
get<never>() as 'foo';

function test<T extends 'foo'>(param: T) {
param as 'bar';
param as 'foo';
1 as T;
'foo' as T;
'bar' as T;
~~~~~~~~~~ [error no-invalid-assertion: Type '"bar"' cannot be converted to type '"foo"'.]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export {};

declare function get<T>(): T;

get<'foo' | 'bar'>() as 'foo';
get<'foo' | 'bar'>() as 'bar';
get<'foo' | 'bar'>() as 'boo';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '"foo" | "bar"' cannot be converted to type '"boo"'.]

get<'foo'>() as 'foo' | 'bar';
get<'foo'>() as 'boo' | 'bar';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '"foo"' cannot be converted to type '"boo" | "bar"'.]

get<'foo'>() as 'foo' | 1;
get<'foo'>() as 'boo' | 1;
~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '"foo"' cannot be converted to type '"boo"'.]

get<'foo' | 1>() as 'foo';
get<'foo' | 1>() as 'boo';
~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '"foo"' cannot be converted to type '"boo"'.]

get<'foo' | 1>() as 1;
get<'foo' | 1>() as 2;
~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '1' cannot be converted to type '2'.]

get<'foo' | 'bar'>() as 'bar' | 'baz';
get<'foo' | 'bar'>() as 'baz' | 'bas';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '"foo" | "bar"' cannot be converted to type '"baz" | "bas"'.]

get<'foo' | 'bar' | 1 | 2 | true>() as 'foo';
get<'foo' | 'bar' | 1 | 2 | true>() as 1;
get<'foo' | 'bar' | 1 | 2 | true>() as 1 | 'foo';
get<'foo' | 'bar' | 1 | 2 | true>() as 1 | 'foo' | false;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type 'true' cannot be converted to type 'false'.]
get<'foo' | 'bar' | 1 | 2 | true>() as 3;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '1 | 2' cannot be converted to type '3'.]
get<'foo' | 'bar' | 1 | 2 | true>() as false;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type 'true' cannot be converted to type 'false'.]
get<'foo' | 'bar' | 1 | 2 | true>() as 'boo';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '"foo" | "bar"' cannot be converted to type '"boo"'.]
get<'foo' | 'bar' | 1 | 2 | true>() as 'boo' | 3 | false;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '"foo" | "bar" | 1 | 2 | true' cannot be converted to type '"boo" | 3 | false'.]

get<'foo' | 'bar' | 1 | 2 | true | object>() as 'boo' | object;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-invalid-assertion: Type '"foo" | "bar"' cannot be converted to type '"boo"'.]
1 change: 1 addition & 0 deletions packages/mimir/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Rule | Description | Difference to TSLint rule / Why you should use it
`no-duplicate-case` | Detects `switch` statements where multiple `case` clauses check for the same value. *uses type information if available* | This implementation tries to infer the value instead of just comparing the source code.
`no-fallthrough` | Prevents unintentional fallthough in `switch` statements from one case to another. If the fallthrough is intended, add a comment that matches `/^\s*falls? ?through\b/i`. | Allows more comment variants such as `fallthrough` or `fall through`.
`no-inferred-empty-object` | Warns if a type parameter is inferred as `{}` because the compiler cannot find any inference site. *requires type information* | Really checks every type parameter of function, method and constructor calls. Correctly handles type parameters from JSDoc comments. Recognises type parameter defaults on all merged declarations.
`no-invalid-assertion` | Disallows asserting a literal type to a different literal type of the same widened type, e.g. `'foo' as 'bar'`. *requires type information* | TSLint has no similar rule.
`no-misused-generics` | Detects generic type parameters that cannot be inferred from the functions parameters. It also detects generics that don't enforce any constraint between types. | There's no similar TSLint rule.
`no-nan-compare` | Don't compare with `NaN`, use `isNaN(number)` or `Number.isNaN(number)` instead. | Performance!
`no-return-await` | Warns for unnecesary `return await foo;` when you can simply `return foo;` | The same as TSLint's rule. I wrote both, but this one is faster.
Expand Down
1 change: 1 addition & 0 deletions packages/mimir/recommended.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ rules:
no-duplicate-case: error
no-fallthrough: error
no-inferred-empty-object: error
no-invalid-assertion: error
no-return-await: error
no-misused-generics: error
no-nan-compare: error
Expand Down
94 changes: 94 additions & 0 deletions packages/mimir/src/rules/no-invalid-assertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { excludeDeclarationFiles, typescriptOnly, TypedRule } from '@fimbul/ymir';
import { unionTypeParts } from 'tsutils';
import * as ts from 'typescript';

@excludeDeclarationFiles
@typescriptOnly
export class Rule extends TypedRule {
public apply() {
for (const node of this.context.getFlatAst()) {
switch (node.kind) {
case ts.SyntaxKind.AsExpression:
case ts.SyntaxKind.TypeAssertionExpression:
this.checkAssertion(<ts.AssertionExpression>node);
}
}
}

private checkAssertion(node: ts.AssertionExpression) {
let assertedType = this.checker.getTypeFromTypeNode(node.type);
assertedType = this.checker.getBaseConstraintOfType(assertedType) || assertedType;
const assertedLiterals = getLiteralsByType(unionTypeParts(assertedType));
if (isEmpty(assertedLiterals))
return;
// if expression is a type variable, the type checker already handles everything as expected
const originalTypeParts = getLiteralsByType(unionTypeParts(this.checker.getTypeAtLocation(node.expression)));
if (isEmpty(originalTypeParts))
return;
match(originalTypeParts, assertedLiterals);
if (!isEmpty(assertedLiterals))
this.addFailureAtNode(node, `Type '${format(originalTypeParts)}' cannot be converted to type '${format(assertedLiterals)}'.`);
}
}

function format(literals: LiteralInfo) {
const result = [];
if (literals.string !== undefined)
result.push(`"${literals.string.join('" | "')}"`);
if (literals.number !== undefined)
result.push(literals.number.join(' | '));
if (literals.boolean !== undefined)
result.push(literals.boolean.join(' | '));
return result.join(' | ');
}

function match(a: LiteralInfo, b: LiteralInfo) {
if (a.string === undefined || b.string === undefined || intersects(a.string, b.string))
a.string = b.string = undefined;
if (a.number === undefined || b.number === undefined || intersects(a.number, b.number))
a.number = b.number = undefined;
if (a.boolean === undefined || b.boolean === undefined || intersects(a.boolean, b.boolean))
a.boolean = b.boolean = undefined;
}

function intersects<T>(arr: T[], other: T[]): boolean {
for (const element of arr)
if (other.includes(element))
return true;
return false;
}

function isEmpty(literals: LiteralInfo) {
return literals.string === undefined && literals.number === undefined && literals.boolean === undefined;
}

interface LiteralInfo {
string: string[] | undefined;
number: number[] | undefined;
boolean: boolean[] | undefined;
}

function getLiteralsByType(types: ReadonlyArray<ts.Type>) {
const result: LiteralInfo = {
string: undefined,
number: undefined,
boolean: undefined,
};
for (const type of types) {
if (type.flags & ts.TypeFlags.StringLiteral) {
result.string = append(result.string, (<ts.StringLiteralType>type).value);
} else if (type.flags & ts.TypeFlags.NumberLiteral) {
result.number = append(result.number, (<ts.NumberLiteralType>type).value);
} else if (type.flags & ts.TypeFlags.BooleanLiteral) {
result.boolean = append(result.boolean, (<{intrinsicName: string}><{}>type).intrinsicName === 'true');
}
}
return result;
}

function append<T>(arr: T[] | undefined, v: T) {
if (arr === undefined)
return [v];
arr.push(v);
return arr;
}
2 changes: 2 additions & 0 deletions packages/mimir/test/no-invalid-assertion/.wotanrc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rules:
no-invalid-assertion: error
1 change: 1 addition & 0 deletions packages/mimir/test/no-invalid-assertion/default.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
31 changes: 31 additions & 0 deletions packages/mimir/test/no-invalid-assertion/literal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export {};

declare function get<T>(): T;

'foo' as 'foo';
'foo' as 'boo';
'foo' as any;
'1' as 1;
1 as 1;
1 as 2;
true as false;
<true>false;

get<boolean>() as true;
get<string>() as 'foo';
get<number>() as 1;

1 as number;
true as boolean;
<string>'foo';

get<any>() as 'foo';
get<never>() as 'foo';

function test<T extends 'foo'>(param: T) {
param as 'bar';
param as 'foo';
1 as T;
'foo' as T;
'bar' as T;
}
5 changes: 5 additions & 0 deletions packages/mimir/test/no-invalid-assertion/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"strict": true
}
}
33 changes: 33 additions & 0 deletions packages/mimir/test/no-invalid-assertion/union.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export {};

declare function get<T>(): T;

get<'foo' | 'bar'>() as 'foo';
get<'foo' | 'bar'>() as 'bar';
get<'foo' | 'bar'>() as 'boo';

get<'foo'>() as 'foo' | 'bar';
get<'foo'>() as 'boo' | 'bar';

get<'foo'>() as 'foo' | 1;
get<'foo'>() as 'boo' | 1;

get<'foo' | 1>() as 'foo';
get<'foo' | 1>() as 'boo';

get<'foo' | 1>() as 1;
get<'foo' | 1>() as 2;

get<'foo' | 'bar'>() as 'bar' | 'baz';
get<'foo' | 'bar'>() as 'baz' | 'bas';

get<'foo' | 'bar' | 1 | 2 | true>() as 'foo';
get<'foo' | 'bar' | 1 | 2 | true>() as 1;
get<'foo' | 'bar' | 1 | 2 | true>() as 1 | 'foo';
get<'foo' | 'bar' | 1 | 2 | true>() as 1 | 'foo' | false;
get<'foo' | 'bar' | 1 | 2 | true>() as 3;
get<'foo' | 'bar' | 1 | 2 | true>() as false;
get<'foo' | 'bar' | 1 | 2 | true>() as 'boo';
get<'foo' | 'bar' | 1 | 2 | true>() as 'boo' | 3 | false;

get<'foo' | 'bar' | 1 | 2 | true | object>() as 'boo' | object;