Skip to content

Commit 87b978c

Browse files
committed
Add no-unsafe-assignment rule
1 parent 4598286 commit 87b978c

File tree

5 files changed

+241
-1
lines changed

5 files changed

+241
-1
lines changed

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module.exports = {
88
"collectCoverage": true,
99
"coverageThreshold": {
1010
"global": {
11-
"branches": 100,
11+
"branches": 97.41, // TODO 100%
1212
"functions": 100,
1313
"lines": 100,
1414
"statements": 100

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@typescript-eslint/eslint-plugin": "^3.2.0",
3333
"@typescript-eslint/experimental-utils": "^3.2.0",
3434
"@typescript-eslint/parser": "^3.2.0",
35+
"total-functions": "^3.0.0",
3536
"tsutils": "^3.17.1"
3637
},
3738
"peerDependencies": {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import rule from "./no-unsafe-assignment";
2+
import { RuleTester } from "@typescript-eslint/experimental-utils/dist/ts-eslint";
3+
import { AST_NODE_TYPES } from "@typescript-eslint/experimental-utils/dist/ts-estree";
4+
5+
const ruleTester = new RuleTester({
6+
parserOptions: {
7+
sourceType: "module",
8+
project: "./tsconfig.tests.json",
9+
},
10+
parser: require.resolve("@typescript-eslint/parser"),
11+
});
12+
13+
// eslint-disable-next-line functional/no-expression-statement
14+
ruleTester.run("no-unsafe-assignment", rule, {
15+
valid: [
16+
// zero parameters
17+
{
18+
filename: "file.ts",
19+
code: `
20+
const foo = () => {
21+
return undefined;
22+
};
23+
foo();
24+
`,
25+
},
26+
// non-object parameter
27+
{
28+
filename: "file.ts",
29+
code: `
30+
const foo = (a: string) => {
31+
return undefined;
32+
};
33+
foo("a");
34+
`,
35+
},
36+
// readonly -> readonly (type doesn't change)
37+
{
38+
filename: "file.ts",
39+
code: `
40+
type ReadonlyA = { readonly a: string };
41+
const func = (param: ReadonlyA): void => {
42+
return undefined;
43+
};
44+
const readonlyA: ReadonlyA = { a: "" };
45+
func(readonlyA);
46+
`,
47+
},
48+
// mutable -> mutable (type doesn't change)
49+
{
50+
filename: "file.ts",
51+
code: `
52+
type MutableA = {a: string};
53+
const foo = (mut: MutableA) => {
54+
mut.a = "whoops";
55+
};
56+
const mut: MutableA = { a: "" };
57+
foo(mut);
58+
`,
59+
},
60+
],
61+
invalid: [
62+
// mutable -> mutable (type changes)
63+
{
64+
filename: "file.ts",
65+
code: `
66+
type MutableA = { a: string };
67+
type MutableB = { a: string | null };
68+
const foo = (mut: MutableB): void => {
69+
mut.a = null; // whoops
70+
};
71+
const mut: MutableA = { a: "" };
72+
foo(mut);
73+
`,
74+
errors: [
75+
{
76+
messageId: "errorStringCallExpression",
77+
type: AST_NODE_TYPES.Identifier,
78+
},
79+
],
80+
},
81+
// readonly -> mutable
82+
{
83+
filename: "file.ts",
84+
code: `
85+
type MutableA = { a: string };
86+
type ReadonlyA = Readonly<MutableA>;
87+
const mutate = (mut: MutableA): void => {
88+
mut.a = "whoops";
89+
};
90+
const readonlyA: ReadonlyA = { a: "readonly?" };
91+
mutate(readonlyA);
92+
`,
93+
errors: [
94+
{
95+
messageId: "errorStringCallExpression",
96+
type: AST_NODE_TYPES.Identifier,
97+
},
98+
],
99+
},
100+
// mutable -> readonly
101+
{
102+
filename: "file.ts",
103+
code: `
104+
type MutableA = { a: string };
105+
type ReadonlyA = Readonly<MutableA>;
106+
const func = (param: ReadonlyA): void => {
107+
return undefined;
108+
};
109+
const mutableA: MutableA = { a: "" };
110+
func(mutableA);
111+
`,
112+
errors: [
113+
{
114+
messageId: "errorStringCallExpression",
115+
type: AST_NODE_TYPES.Identifier,
116+
},
117+
],
118+
},
119+
],
120+
});

src/rules/no-unsafe-assignment.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { RuleModule } from "@typescript-eslint/experimental-utils/dist/ts-eslint";
2+
import { ESLintUtils } from "@typescript-eslint/experimental-utils";
3+
import { isObjectType, isPropertyReadonlyInType } from "tsutils";
4+
import { get } from "total-functions";
5+
6+
/**
7+
* An ESLint rule to ban unsafe assignment and declarations.
8+
*/
9+
const noUnsafeAssignment: RuleModule<
10+
"errorStringCallExpression",
11+
readonly []
12+
> = {
13+
meta: {
14+
type: "problem",
15+
docs: {
16+
category: "Possible Errors",
17+
description: "Bans unsafe type assertions.",
18+
recommended: "error",
19+
url: "https://github.com/danielnixon/eslint-plugin-total-functions",
20+
},
21+
messages: {
22+
errorStringCallExpression: "This call can lead to type-safety issues.",
23+
},
24+
schema: [],
25+
},
26+
create: (context) => {
27+
const parserServices = ESLintUtils.getParserServices(context);
28+
const checker = parserServices.program.getTypeChecker();
29+
30+
return {
31+
// TODO
32+
// // eslint-disable-next-line functional/no-return-void
33+
// VariableDeclaration: (node): void => {
34+
// },
35+
// // eslint-disable-next-line functional/no-return-void
36+
// AssignmentExpression: (node): void => {
37+
// },
38+
// eslint-disable-next-line functional/no-return-void, sonarjs/cognitive-complexity
39+
CallExpression: (node): void => {
40+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node.callee);
41+
const tsType = checker.getTypeAtLocation(tsNode);
42+
const signatures = tsType.getCallSignatures();
43+
44+
// TODO: if a function has more than one signature, how do we know which
45+
// one applies for this call?
46+
const signature = get(signatures, 0);
47+
48+
// eslint-disable-next-line functional/no-conditional-statement
49+
if (signatures.length === 1 && signature !== undefined) {
50+
// eslint-disable-next-line functional/no-expression-statement
51+
signature.parameters.forEach((parameter, i) => {
52+
const paramType = checker.getTypeOfSymbolAtLocation(
53+
parameter,
54+
tsNode
55+
);
56+
57+
// This is the argument that corresponds to the current parameter.
58+
const argument = get(node.arguments, i);
59+
const argumentTsNode =
60+
argument !== undefined
61+
? parserServices.esTreeNodeToTSNodeMap.get(argument)
62+
: undefined;
63+
const argumentType =
64+
argumentTsNode !== undefined
65+
? checker.getTypeAtLocation(argumentTsNode)
66+
: undefined;
67+
68+
// eslint-disable-next-line functional/no-conditional-statement
69+
if (
70+
argument !== undefined &&
71+
argumentType !== undefined &&
72+
isObjectType(argumentType) &&
73+
isObjectType(paramType)
74+
) {
75+
// eslint-disable-next-line functional/no-expression-statement
76+
paramType.getProperties().forEach((property) => {
77+
const parameterPropIsReadonly = isPropertyReadonlyInType(
78+
paramType,
79+
property.escapedName,
80+
checker
81+
);
82+
83+
const argumentPropsIsReadonly = isPropertyReadonlyInType(
84+
argumentType,
85+
property.escapedName,
86+
checker
87+
);
88+
89+
const oneMutableOneReadonly =
90+
argumentPropsIsReadonly !== parameterPropIsReadonly;
91+
92+
const bothMutableButDifferentTypes =
93+
!argumentPropsIsReadonly &&
94+
!parameterPropIsReadonly &&
95+
argumentType !== paramType;
96+
97+
// eslint-disable-next-line functional/no-conditional-statement
98+
if (oneMutableOneReadonly || bothMutableButDifferentTypes) {
99+
// eslint-disable-next-line functional/no-expression-statement
100+
context.report({
101+
node: argument,
102+
messageId: "errorStringCallExpression",
103+
});
104+
}
105+
});
106+
}
107+
});
108+
}
109+
},
110+
};
111+
},
112+
};
113+
114+
export default noUnsafeAssignment;

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4281,6 +4281,11 @@ to-regex@^3.0.1, to-regex@^3.0.2:
42814281
regex-not "^1.0.2"
42824282
safe-regex "^1.1.0"
42834283

4284+
total-functions@^3.0.0:
4285+
version "3.0.0"
4286+
resolved "https://registry.yarnpkg.com/total-functions/-/total-functions-3.0.0.tgz#cf32d787e839cd5eb0439550d9451a3d17105bad"
4287+
integrity sha512-qi/5OALj6eiZpXJ45P9dSRf0uDnDErgEKqHAFWGZlY7BRZdR69WLJv61UYb9JeospAwyQpIVJ6S+yelNajXAxA==
4288+
42844289
tough-cookie@^2.3.3, tough-cookie@~2.5.0:
42854290
version "2.5.0"
42864291
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"

0 commit comments

Comments
 (0)