Skip to content

Commit

Permalink
feat(eslint-plugin-internal): add rule no-poorly-typed-ts-props (#1949)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Apr 29, 2020
1 parent 2dd1638 commit 56ea7c9
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/eslint-plugin-internal/src/rules/index.ts
@@ -1,9 +1,11 @@
import noPoorlyTypedTsProps from './no-poorly-typed-ts-props';
import noTypescriptDefaultImport from './no-typescript-default-import';
import noTypescriptEstreeImport from './no-typescript-estree-import';
import pluginTestFormatting from './plugin-test-formatting';
import preferASTTypesEnum from './prefer-ast-types-enum';

export default {
'no-poorly-typed-ts-props': noPoorlyTypedTsProps,
'no-typescript-default-import': noTypescriptDefaultImport,
'no-typescript-estree-import': noTypescriptEstreeImport,
'plugin-test-formatting': pluginTestFormatting,
Expand Down
110 changes: 110 additions & 0 deletions packages/eslint-plugin-internal/src/rules/no-poorly-typed-ts-props.ts
@@ -0,0 +1,110 @@
import {
TSESTree,
ESLintUtils,
TSESLint,
} from '@typescript-eslint/experimental-utils';
import { createRule } from '../util';

/*
TypeScript declares some bad types for certain properties.
See: https://github.com/microsoft/TypeScript/issues/24706
This rule simply warns against using them, as using them will likely introduce type safety holes.
*/

const BANNED_PROPERTIES = [
// {
// type: 'Node',
// property: 'parent',
// fixWith: null,
// },
{
type: 'Symbol',
property: 'declarations',
fixWith: 'getDeclarations()',
},
{
type: 'Type',
property: 'symbol',
fixWith: 'getSymbol()',
},
];

export default createRule({
name: 'no-poorly-typed-ts-props',
meta: {
type: 'problem',
docs: {
description:
"Enforces rules don't use TS API properties with known bad type definitions",
category: 'Possible Errors',
recommended: 'error',
requiresTypeChecking: true,
},
fixable: 'code',
schema: [],
messages: {
doNotUse: 'Do not use {{type}}.{{property}} because it is poorly typed.',
doNotUseWithFixer:
'Do not use {{type}}.{{property}} because it is poorly typed. Use {{type}}.{{fixWith}} instead.',
suggestedFix: 'Use {{type}}.{{fixWith}} instead.',
},
},
defaultOptions: [],
create(context) {
const { program, esTreeNodeToTSNodeMap } = ESLintUtils.getParserServices(
context,
);
const checker = program.getTypeChecker();

return {
':matches(MemberExpression, OptionalMemberExpression)[computed = false]'(
node:
| TSESTree.MemberExpressionNonComputedName
| TSESTree.OptionalMemberExpressionNonComputedName,
): void {
for (const banned of BANNED_PROPERTIES) {
if (node.property.name !== banned.property) {
continue;
}

// make sure the type name matches
const tsObjectNode = esTreeNodeToTSNodeMap.get(node.object);
const objectType = checker.getTypeAtLocation(tsObjectNode);
const objectSymbol = objectType.getSymbol();
if (objectSymbol?.getName() !== banned.type) {
continue;
}

const tsNode = esTreeNodeToTSNodeMap.get(node.property);
const symbol = checker.getSymbolAtLocation(tsNode);
const decls = symbol?.getDeclarations();
const isFromTs = decls?.some(decl =>
decl.getSourceFile().fileName.includes('/node_modules/typescript/'),
);
if (isFromTs !== true) {
continue;
}

return context.report({
node,
messageId: banned.fixWith ? 'doNotUseWithFixer' : 'doNotUse',
data: banned,
suggest: [
{
messageId: 'suggestedFix',
fix(fixer): TSESLint.RuleFix | null {
if (banned.fixWith == null) {
return null;
}

return fixer.replaceText(node.property, banned.fixWith);
},
},
],
});
}
},
};
},
});
Empty file.
12 changes: 12 additions & 0 deletions packages/eslint-plugin-internal/tests/fixtures/tsconfig.json
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"jsx": "preserve",
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"lib": ["es2015", "es2017", "esnext"],
"experimentalDecorators": true
},
"include": ["file.ts"]
}
@@ -0,0 +1,94 @@
import rule from '../../src/rules/no-poorly-typed-ts-props';
import { RuleTester, getFixturesRootDir } from '../RuleTester';

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: getFixturesRootDir(),
sourceType: 'module',
},
});

ruleTester.run('no-poorly-typed-ts-props', rule, {
valid: [
`
declare const foo: { declarations: string[] };
foo.declarations.map(decl => console.log(decl));
`,
`
declare const bar: Symbol;
bar.declarations.map(decl => console.log(decl));
`,
`
declare const baz: Type;
baz.symbol.name;
`,
],
invalid: [
{
code: `
import ts from 'typescript';
declare const thing: ts.Symbol;
thing.declarations.map(decl => {});
`.trimRight(),
errors: [
{
messageId: 'doNotUseWithFixer',
data: {
type: 'Symbol',
property: 'declarations',
fixWith: 'getDeclarations()',
},
line: 4,
suggestions: [
{
messageId: 'suggestedFix',
data: {
type: 'Symbol',
fixWith: 'getDeclarations()',
},
output: `
import ts from 'typescript';
declare const thing: ts.Symbol;
thing.getDeclarations().map(decl => {});
`.trimRight(),
},
],
},
],
},
{
code: `
import ts from 'typescript';
declare const thing: ts.Type;
thing.symbol;
`.trimRight(),
errors: [
{
messageId: 'doNotUseWithFixer',
data: {
type: 'Type',
property: 'symbol',
fixWith: 'getSymbol()',
},
line: 4,
suggestions: [
{
messageId: 'suggestedFix',
data: {
type: 'Type',
fixWith: 'getSymbol()',
},
output: `
import ts from 'typescript';
declare const thing: ts.Type;
thing.getSymbol();
`.trimRight(),
},
],
},
],
},
],
});

0 comments on commit 56ea7c9

Please sign in to comment.