Skip to content

Commit 56ea7c9

Browse files
authored
feat(eslint-plugin-internal): add rule no-poorly-typed-ts-props (#1949)
1 parent 2dd1638 commit 56ea7c9

File tree

5 files changed

+218
-0
lines changed

5 files changed

+218
-0
lines changed

Diff for: packages/eslint-plugin-internal/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import noPoorlyTypedTsProps from './no-poorly-typed-ts-props';
12
import noTypescriptDefaultImport from './no-typescript-default-import';
23
import noTypescriptEstreeImport from './no-typescript-estree-import';
34
import pluginTestFormatting from './plugin-test-formatting';
45
import preferASTTypesEnum from './prefer-ast-types-enum';
56

67
export default {
8+
'no-poorly-typed-ts-props': noPoorlyTypedTsProps,
79
'no-typescript-default-import': noTypescriptDefaultImport,
810
'no-typescript-estree-import': noTypescriptEstreeImport,
911
'plugin-test-formatting': pluginTestFormatting,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
TSESTree,
3+
ESLintUtils,
4+
TSESLint,
5+
} from '@typescript-eslint/experimental-utils';
6+
import { createRule } from '../util';
7+
8+
/*
9+
TypeScript declares some bad types for certain properties.
10+
See: https://github.com/microsoft/TypeScript/issues/24706
11+
12+
This rule simply warns against using them, as using them will likely introduce type safety holes.
13+
*/
14+
15+
const BANNED_PROPERTIES = [
16+
// {
17+
// type: 'Node',
18+
// property: 'parent',
19+
// fixWith: null,
20+
// },
21+
{
22+
type: 'Symbol',
23+
property: 'declarations',
24+
fixWith: 'getDeclarations()',
25+
},
26+
{
27+
type: 'Type',
28+
property: 'symbol',
29+
fixWith: 'getSymbol()',
30+
},
31+
];
32+
33+
export default createRule({
34+
name: 'no-poorly-typed-ts-props',
35+
meta: {
36+
type: 'problem',
37+
docs: {
38+
description:
39+
"Enforces rules don't use TS API properties with known bad type definitions",
40+
category: 'Possible Errors',
41+
recommended: 'error',
42+
requiresTypeChecking: true,
43+
},
44+
fixable: 'code',
45+
schema: [],
46+
messages: {
47+
doNotUse: 'Do not use {{type}}.{{property}} because it is poorly typed.',
48+
doNotUseWithFixer:
49+
'Do not use {{type}}.{{property}} because it is poorly typed. Use {{type}}.{{fixWith}} instead.',
50+
suggestedFix: 'Use {{type}}.{{fixWith}} instead.',
51+
},
52+
},
53+
defaultOptions: [],
54+
create(context) {
55+
const { program, esTreeNodeToTSNodeMap } = ESLintUtils.getParserServices(
56+
context,
57+
);
58+
const checker = program.getTypeChecker();
59+
60+
return {
61+
':matches(MemberExpression, OptionalMemberExpression)[computed = false]'(
62+
node:
63+
| TSESTree.MemberExpressionNonComputedName
64+
| TSESTree.OptionalMemberExpressionNonComputedName,
65+
): void {
66+
for (const banned of BANNED_PROPERTIES) {
67+
if (node.property.name !== banned.property) {
68+
continue;
69+
}
70+
71+
// make sure the type name matches
72+
const tsObjectNode = esTreeNodeToTSNodeMap.get(node.object);
73+
const objectType = checker.getTypeAtLocation(tsObjectNode);
74+
const objectSymbol = objectType.getSymbol();
75+
if (objectSymbol?.getName() !== banned.type) {
76+
continue;
77+
}
78+
79+
const tsNode = esTreeNodeToTSNodeMap.get(node.property);
80+
const symbol = checker.getSymbolAtLocation(tsNode);
81+
const decls = symbol?.getDeclarations();
82+
const isFromTs = decls?.some(decl =>
83+
decl.getSourceFile().fileName.includes('/node_modules/typescript/'),
84+
);
85+
if (isFromTs !== true) {
86+
continue;
87+
}
88+
89+
return context.report({
90+
node,
91+
messageId: banned.fixWith ? 'doNotUseWithFixer' : 'doNotUse',
92+
data: banned,
93+
suggest: [
94+
{
95+
messageId: 'suggestedFix',
96+
fix(fixer): TSESLint.RuleFix | null {
97+
if (banned.fixWith == null) {
98+
return null;
99+
}
100+
101+
return fixer.replaceText(node.property, banned.fixWith);
102+
},
103+
},
104+
],
105+
});
106+
}
107+
},
108+
};
109+
},
110+
});

Diff for: packages/eslint-plugin-internal/tests/fixtures/file.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"jsx": "preserve",
4+
"target": "es5",
5+
"module": "commonjs",
6+
"strict": true,
7+
"esModuleInterop": true,
8+
"lib": ["es2015", "es2017", "esnext"],
9+
"experimentalDecorators": true
10+
},
11+
"include": ["file.ts"]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import rule from '../../src/rules/no-poorly-typed-ts-props';
2+
import { RuleTester, getFixturesRootDir } from '../RuleTester';
3+
4+
const ruleTester = new RuleTester({
5+
parser: '@typescript-eslint/parser',
6+
parserOptions: {
7+
project: './tsconfig.json',
8+
tsconfigRootDir: getFixturesRootDir(),
9+
sourceType: 'module',
10+
},
11+
});
12+
13+
ruleTester.run('no-poorly-typed-ts-props', rule, {
14+
valid: [
15+
`
16+
declare const foo: { declarations: string[] };
17+
foo.declarations.map(decl => console.log(decl));
18+
`,
19+
`
20+
declare const bar: Symbol;
21+
bar.declarations.map(decl => console.log(decl));
22+
`,
23+
`
24+
declare const baz: Type;
25+
baz.symbol.name;
26+
`,
27+
],
28+
invalid: [
29+
{
30+
code: `
31+
import ts from 'typescript';
32+
declare const thing: ts.Symbol;
33+
thing.declarations.map(decl => {});
34+
`.trimRight(),
35+
errors: [
36+
{
37+
messageId: 'doNotUseWithFixer',
38+
data: {
39+
type: 'Symbol',
40+
property: 'declarations',
41+
fixWith: 'getDeclarations()',
42+
},
43+
line: 4,
44+
suggestions: [
45+
{
46+
messageId: 'suggestedFix',
47+
data: {
48+
type: 'Symbol',
49+
fixWith: 'getDeclarations()',
50+
},
51+
output: `
52+
import ts from 'typescript';
53+
declare const thing: ts.Symbol;
54+
thing.getDeclarations().map(decl => {});
55+
`.trimRight(),
56+
},
57+
],
58+
},
59+
],
60+
},
61+
{
62+
code: `
63+
import ts from 'typescript';
64+
declare const thing: ts.Type;
65+
thing.symbol;
66+
`.trimRight(),
67+
errors: [
68+
{
69+
messageId: 'doNotUseWithFixer',
70+
data: {
71+
type: 'Type',
72+
property: 'symbol',
73+
fixWith: 'getSymbol()',
74+
},
75+
line: 4,
76+
suggestions: [
77+
{
78+
messageId: 'suggestedFix',
79+
data: {
80+
type: 'Type',
81+
fixWith: 'getSymbol()',
82+
},
83+
output: `
84+
import ts from 'typescript';
85+
declare const thing: ts.Type;
86+
thing.getSymbol();
87+
`.trimRight(),
88+
},
89+
],
90+
},
91+
],
92+
},
93+
],
94+
});

0 commit comments

Comments
 (0)