-
-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add no-property-in-node rule (#433)
* feat: add no-property-in-node rule * Add ☑️ emoji for recommended-type-checked * tests: valid before invalid * Also check for whether the node has a 'type' * Added docs and example to isAstNodeType * Expanded rule details * Add more valid test cases * Fixed test path to fixtures * Use parserOptions.project: true for eslint-remote-tester on TS files * nit: avoid shadowing name for typePart * <!-- omit from toc --> * Downgraded to typescript-eslint@v5 * Also remove @typescript-eslint/utils * Or rather, make @typescript-eslint/utils a -D * Remove ts-api-utils too * Removed recommended-type-checked * Removed README.md section too * Removed eslint-remote-tester.config.js parserOptions.project too * Redid README.md presets table * Added all-type-checked * Removed file notice
- Loading branch information
1 parent
6ae0ee6
commit d2b9372
Showing
11 changed files
with
381 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
'use strict'; | ||
|
||
const mod = require('../lib/index.js'); | ||
|
||
module.exports = { | ||
plugins: { 'eslint-plugin': mod }, | ||
rules: mod.configs['all-type-checked'].rules, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# Disallow using `in` to narrow node types instead of looking at properties (`eslint-plugin/no-property-in-node`) | ||
|
||
💭 This rule requires type information. | ||
|
||
<!-- end auto-generated rule header --> | ||
|
||
When working with a node of type `ESTree.Node` or `TSESTree.Node`, it can be tempting to use the `'in'` operator to narrow the node's type. | ||
`'in'` narrowing is susceptible to confusing behavior from quirks of ASTs, such as node properties sometimes being omitted from nodes and other times explicitly being set to `null` or `undefined`. | ||
|
||
Instead, checking a node's `type` property is generally considered preferable. | ||
|
||
## Rule Details | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```ts | ||
/* eslint eslint-plugin/no-property-in-node: error */ | ||
|
||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
meta: { | ||
/* ... */ | ||
}, | ||
create(context) { | ||
return { | ||
'ClassDeclaration, FunctionDeclaration'(node) { | ||
if ('superClass' in node) { | ||
console.log('This is a class declaration:', node); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```ts | ||
/* eslint eslint-plugin/no-property-in-node: error */ | ||
|
||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
meta: { | ||
/* ... */ | ||
}, | ||
create(context) { | ||
return { | ||
'ClassDeclaration, FunctionDeclaration'(node) { | ||
if (node.type === 'ClassDeclaration') { | ||
console.log('This is a class declaration:', node); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
'use strict'; | ||
|
||
const typedNodeSourceFileTesters = [ | ||
/@types[/\\]estree[/\\]index\.d\.ts/, | ||
/@typescript-eslint[/\\]types[/\\]dist[/\\]generated[/\\]ast-spec\.d\.ts/, | ||
]; | ||
|
||
/** | ||
* Given a TypeScript type, determines whether the type appears to be for a known | ||
* AST type from the typings of @typescript-eslint/types or estree. | ||
* We check based on two rough conditions: | ||
* - The type has a 'kind' property (as AST node types all do) | ||
* - The type is declared in one of those package's .d.ts types | ||
* | ||
* @example | ||
* ``` | ||
* module.exports = { | ||
* create(context) { | ||
* BinaryExpression(node) { | ||
* const type = services.getTypeAtLocation(node.right); | ||
* // ^^^^ | ||
* // This variable's type will be TSESTree.BinaryExpression | ||
* } | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* @param {import('typescript').Type} type | ||
* @returns Whether the type seems to include a known ESTree or TSESTree AST node. | ||
*/ | ||
function isAstNodeType(type) { | ||
return (type.types || [type]) | ||
.filter((typePart) => typePart.getProperty('type')) | ||
.flatMap( | ||
(typePart) => (typePart.symbol && typePart.symbol.declarations) || [] | ||
) | ||
.some((declaration) => { | ||
const fileName = declaration.getSourceFile().fileName; | ||
return ( | ||
fileName && | ||
typedNodeSourceFileTesters.some((tester) => tester.test(fileName)) | ||
); | ||
}); | ||
} | ||
|
||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: | ||
'disallow using `in` to narrow node types instead of looking at properties', | ||
category: 'Rules', | ||
recommended: false, | ||
requiresTypeChecking: true, | ||
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-property-in-node.md', | ||
}, | ||
schema: [], | ||
messages: { | ||
in: 'Prefer checking specific node properties instead of a broad `in`.', | ||
}, | ||
}, | ||
|
||
create(context) { | ||
return { | ||
'BinaryExpression[operator=in]'(node) { | ||
// TODO: Switch this to ESLintUtils.getParserServices with typescript-eslint@>=6 | ||
// https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/269 | ||
const services = (context.sourceCode || context).parserServices; | ||
if (!services.program) { | ||
throw new Error( | ||
'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.' | ||
); | ||
} | ||
|
||
const checker = services.program.getTypeChecker(); | ||
const tsNode = services.esTreeNodeToTSNodeMap.get(node.right); | ||
const type = checker.getTypeAtLocation(tsNode); | ||
|
||
if (isAstNodeType(type)) { | ||
context.report({ messageId: 'in', node }); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"compilerOptions": { | ||
"moduleResolution": "NodeNext" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
'use strict'; | ||
|
||
const RuleTester = require('eslint').RuleTester; | ||
const path = require('path'); | ||
const rule = require('../../../lib/rules/no-property-in-node'); | ||
|
||
const ruleTester = new RuleTester({ | ||
parser: require.resolve('@typescript-eslint/parser'), | ||
parserOptions: { | ||
project: './tsconfig.json', | ||
tsconfigRootDir: path.join(__dirname, '../fixtures'), | ||
}, | ||
}); | ||
|
||
ruleTester.run('no-property-in-node', rule, { | ||
valid: [ | ||
`'a' in window;`, | ||
` | ||
declare const node: Node; | ||
'a' in node; | ||
`, | ||
` | ||
type Node = { unrelated: true; }; | ||
declare const node: Node; | ||
'a' in node; | ||
`, | ||
` | ||
interface Node { | ||
unrelated: true; | ||
}; | ||
declare const node: Node; | ||
'a' in node; | ||
`, | ||
` | ||
declare const node: UnresolvedType; | ||
'a' in node; | ||
`, | ||
` | ||
import * as ESTree from 'estree'; | ||
declare const loc: ESTree.SourceLocation; | ||
'a' in loc; | ||
`, | ||
` | ||
import * as ESTree from 'estree'; | ||
declare const node: ESTree.Node; | ||
a.superClass; | ||
`, | ||
` | ||
import * as ESTree from 'estree'; | ||
declare const node: ESTree.Node; | ||
a.type; | ||
`, | ||
` | ||
import * as ESTree from 'estree'; | ||
declare const node: ESTree.Node; | ||
a.type === 'ClassDeclaration'; | ||
`, | ||
` | ||
import * as ESTree from 'estree'; | ||
declare const node: ESTree.ClassDeclaration | ESTree.FunctionDeclaration; | ||
a.type === 'ClassDeclaration'; | ||
`, | ||
` | ||
import { TSESTree } from '@typescript-eslint/utils'; | ||
declare const node: TSESTree.Node; | ||
node.superClass; | ||
`, | ||
` | ||
import { TSESTree } from '@typescript-eslint/utils'; | ||
declare const node: TSESTree.Node; | ||
node.type; | ||
`, | ||
` | ||
import { TSESTree } from '@typescript-eslint/utils'; | ||
declare const node: TSESTree.ClassDeclaration | TSESTree.FunctionDeclaration; | ||
node.type === 'ClassDeclaration'; | ||
`, | ||
` | ||
import * as eslint from 'eslint'; | ||
const listener: eslint.Rule.RuleListener = { | ||
ClassDeclaration(node) { | ||
node.type; | ||
}, | ||
}; | ||
`, | ||
` | ||
import * as eslint from 'eslint'; | ||
const listener: eslint.Rule.RuleListener = { | ||
'ClassDeclaration, FunctionDeclaration'(node) { | ||
node.type === 'ClassDeclaration'; | ||
}, | ||
}; | ||
`, | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
import { TSESTree } from '@typescript-eslint/utils'; | ||
declare const node: TSESTree.Node; | ||
'a' in node; | ||
`, | ||
errors: [ | ||
{ | ||
column: 9, | ||
line: 4, | ||
endColumn: 20, | ||
endLine: 4, | ||
messageId: 'in', | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
import { TSESTree } from '@typescript-eslint/utils'; | ||
type Other = { key: true }; | ||
declare const node: TSESTree.Node | Other; | ||
'a' in node; | ||
`, | ||
errors: [ | ||
{ | ||
column: 9, | ||
line: 5, | ||
endColumn: 20, | ||
endLine: 5, | ||
messageId: 'in', | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
import * as ESTree from 'estree'; | ||
declare const node: ESTree.Node; | ||
'a' in node; | ||
`, | ||
errors: [ | ||
{ | ||
column: 9, | ||
line: 4, | ||
endColumn: 20, | ||
endLine: 4, | ||
messageId: 'in', | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
import * as eslint from 'eslint'; | ||
const listener: eslint.Rule.RuleListener = { | ||
ClassDeclaration(node) { | ||
'a' in node; | ||
}, | ||
}; | ||
`, | ||
errors: [ | ||
{ | ||
column: 13, | ||
line: 5, | ||
endColumn: 24, | ||
endLine: 5, | ||
messageId: 'in', | ||
}, | ||
], | ||
}, | ||
], | ||
}); |