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
feat(eslint-plugin): add extension rule [no-invalid-this] too support arrow function class properties #1794
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Disallow `this` keywords outside of classes or class-like objects (`no-invalid-this`) | ||
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,159 @@ | ||||||||||
import baseRule from 'eslint/lib/rules/no-invalid-this'; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it looks like you're not actually using We don't want to reference the parent rule's schema here because if they add a new option, we won't have handling for it until we add it to our fork. Locally define the |
||||||||||
import astUtils from 'eslint/lib/rules/utils/ast-utils'; | ||||||||||
import { | ||||||||||
AST_NODE_TYPES, | ||||||||||
TSESTree, | ||||||||||
} from '@typescript-eslint/experimental-utils'; | ||||||||||
import * as util from '../util'; | ||||||||||
|
||||||||||
type CheckingContextNode = | ||||||||||
| TSESTree.Program | ||||||||||
| TSESTree.FunctionDeclaration | ||||||||||
| TSESTree.FunctionExpression | ||||||||||
| TSESTree.ArrowFunctionExpression; | ||||||||||
|
||||||||||
interface CheckingContext { | ||||||||||
init: boolean; | ||||||||||
node: CheckingContextNode; | ||||||||||
valid: boolean; | ||||||||||
} | ||||||||||
|
||||||||||
type Options = util.InferOptionsTypeFromRule<typeof baseRule>; | ||||||||||
type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>; | ||||||||||
|
||||||||||
export default util.createRule<Options, MessageIds>({ | ||||||||||
name: 'no-invalid-this', | ||||||||||
meta: { | ||||||||||
type: 'suggestion', | ||||||||||
docs: { | ||||||||||
description: | ||||||||||
'Disallow `this` keywords outside of classes or class-like objects', | ||||||||||
category: 'Best Practices', | ||||||||||
recommended: false, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mark the rule as an extension rule.
Suggested change
|
||||||||||
}, | ||||||||||
messages: { | ||||||||||
noInvalidThis: "Unexpected 'this'.", | ||||||||||
}, | ||||||||||
schema: baseRule.meta.schema, | ||||||||||
}, | ||||||||||
defaultOptions: [ | ||||||||||
{ | ||||||||||
capIsConstructor: true, | ||||||||||
}, | ||||||||||
], | ||||||||||
create(context, [options]) { | ||||||||||
/** Modified version of "eslint/lib/rules/no-invalid-this" */ | ||||||||||
|
||||||||||
const capIsConstructor = options.capIsConstructor !== false; | ||||||||||
const stack: CheckingContext[] = [], | ||||||||||
sourceCode = context.getSourceCode(); | ||||||||||
Comment on lines
+48
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit
Suggested change
|
||||||||||
|
||||||||||
/** | ||||||||||
* Gets the current checking context. | ||||||||||
* | ||||||||||
* The return value has a flag that whether or not `this` keyword is valid. | ||||||||||
* The flag is initialized when got at the first time. | ||||||||||
* @returns {{valid: boolean}} | ||||||||||
* an object which has a flag that whether or not `this` keyword is valid. | ||||||||||
*/ | ||||||||||
function getCurrentCheckingContext(): CheckingContext { | ||||||||||
const current = stack[stack.length - 1]; | ||||||||||
|
||||||||||
if (!current.init) { | ||||||||||
current.init = true; | ||||||||||
current.valid = !astUtils.isDefaultThisBinding( | ||||||||||
current.node, | ||||||||||
sourceCode, | ||||||||||
{ capIsConstructor }, | ||||||||||
); | ||||||||||
} | ||||||||||
return current; | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Pushes new checking context into the stack. | ||||||||||
* | ||||||||||
* The checking context is not initialized yet. | ||||||||||
* Because most functions don't have `this` keyword. | ||||||||||
* When `this` keyword was found, the checking context is initialized. | ||||||||||
* @param {ASTNode} node A function node that was entered. | ||||||||||
* @returns {void} | ||||||||||
*/ | ||||||||||
function enterFunction(node: CheckingContextNode): void { | ||||||||||
// `this` can be invalid only under strict mode. | ||||||||||
stack.push({ | ||||||||||
init: !context.getScope().isStrict, | ||||||||||
node, | ||||||||||
valid: true, | ||||||||||
}); | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Pushes new checking context into the stack. | ||||||||||
* | ||||||||||
* The checking context is not initialized yet. | ||||||||||
* Because most functions don't have `this` keyword. | ||||||||||
* When `this` keyword was found, the checking context is initialized. | ||||||||||
* @param {ASTNode} node A function node that was entered. | ||||||||||
* @returns {void} | ||||||||||
*/ | ||||||||||
function enterArrowFunction(node: CheckingContextNode): void { | ||||||||||
// `this` can only be valid inside class methods. | ||||||||||
stack.push({ | ||||||||||
init: true, | ||||||||||
node, | ||||||||||
valid: node.parent?.type === AST_NODE_TYPES.ClassProperty, | ||||||||||
}); | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Pops the current checking context from the stack. | ||||||||||
* @returns {void} | ||||||||||
*/ | ||||||||||
function exitFunction(): void { | ||||||||||
stack.pop(); | ||||||||||
} | ||||||||||
|
||||||||||
return { | ||||||||||
/* | ||||||||||
* `this` is invalid only under strict mode. | ||||||||||
* Modules is always strict mode. | ||||||||||
*/ | ||||||||||
Program(node): void { | ||||||||||
const scope = context.getScope(), | ||||||||||
features = context.parserOptions.ecmaFeatures ?? {}; | ||||||||||
|
||||||||||
stack.push({ | ||||||||||
init: true, | ||||||||||
node, | ||||||||||
valid: !( | ||||||||||
scope.isStrict || | ||||||||||
node.sourceType === 'module' || | ||||||||||
(features.globalReturn && scope.childScopes[0].isStrict) | ||||||||||
), | ||||||||||
}); | ||||||||||
}, | ||||||||||
|
||||||||||
'Program:exit'(): void { | ||||||||||
stack.pop(); | ||||||||||
}, | ||||||||||
|
||||||||||
FunctionDeclaration: enterFunction, | ||||||||||
'FunctionDeclaration:exit': exitFunction, | ||||||||||
FunctionExpression: enterFunction, | ||||||||||
'FunctionExpression:exit': exitFunction, | ||||||||||
|
||||||||||
// Introduce handling of ArrowFunctionExpressions not present in baseRule | ||||||||||
ArrowFunctionExpression: enterArrowFunction, | ||||||||||
'ArrowFunctionExpression:exit': exitFunction, | ||||||||||
|
||||||||||
// Reports if `this` of the current context is invalid. | ||||||||||
ThisExpression(node): void { | ||||||||||
const current = getCurrentCheckingContext(); | ||||||||||
if (current && !current.valid) { | ||||||||||
context.report({ node, messageId: 'noInvalidThis' }); | ||||||||||
} | ||||||||||
}, | ||||||||||
}; | ||||||||||
}, | ||||||||||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import rule from '../../src/rules/no-invalid-this'; | ||
import { RuleTester } from '../RuleTester'; | ||
|
||
const ruleTester = new RuleTester({ | ||
parserOptions: { | ||
sourceType: 'module', | ||
}, | ||
parser: '@typescript-eslint/parser', | ||
}); | ||
|
||
ruleTester.run('no-invalid-this', rule, { | ||
valid: [ | ||
`class A { | ||
private barA = 0; | ||
fooA = () => { | ||
this.barA = 1; | ||
} | ||
}`, | ||
`class B { | ||
private barB() {} | ||
fooB = () => { | ||
this.barB(); | ||
} | ||
}`, | ||
`const C = class { | ||
private barC = 0; | ||
fooC = () => { | ||
this.barC = 1; | ||
} | ||
}`, | ||
`const D = class { | ||
private barD() {} | ||
fooD = () => { | ||
this.barD(); | ||
} | ||
}`, | ||
], | ||
invalid: [ | ||
{ | ||
code: `function invalidFoo() { | ||
this.x = 1; | ||
}`, | ||
errors: [ | ||
{ | ||
messageId: 'noInvalidThis', | ||
line: 2, | ||
column: 11, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: `function invalidBar() { | ||
this.invalidMethod(); | ||
}`, | ||
errors: [ | ||
{ | ||
messageId: 'noInvalidThis', | ||
line: 2, | ||
column: 11, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: `const invalidBazz = () => { | ||
this.x = 1; | ||
}`, | ||
errors: [ | ||
{ | ||
messageId: 'noInvalidThis', | ||
line: 2, | ||
column: 11, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: `const invalidBuzz = () => { | ||
this.invalidMethod(); | ||
}`, | ||
errors: [ | ||
{ | ||
messageId: 'noInvalidThis', | ||
line: 2, | ||
column: 11, | ||
}, | ||
], | ||
}, | ||
], | ||
}); | ||
|
||
/* | ||
|
||
|
||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example of an extension rule doc:
https://github.com/typescript-eslint/typescript-eslint/blob/1fd86befa6a940a0354c619dd2da08a5c5d69fb4/packages/eslint-plugin/docs/rules/comma-spacing.md
Key points: