Skip to content
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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -119,6 +119,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | | | :thought_balloon: |
| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/no-invalid-this`](./docs/rules/no-invalid-this.md) | Disallow `this` keywords outside of classes or class-like objects | | | |
| [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :heavy_check_mark: | | |
| [`@typescript-eslint/no-misused-promises`](./docs/rules/no-misused-promises.md) | Avoid using promises in places not designed to handle them | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow the use of custom TypeScript modules and namespaces | :heavy_check_mark: | | |
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/docs/rules/no-invalid-this.md
@@ -0,0 +1 @@
# Disallow `this` keywords outside of classes or class-like objects (`no-invalid-this`)
Copy link
Member

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:

  • explain what we've added
  • show how to turn it on
  • link to the base rule docs.

1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -46,6 +46,7 @@
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-invalid-this": "error",
"no-magic-numbers": "off",
"@typescript-eslint/no-magic-numbers": "error",
"@typescript-eslint/no-misused-new": "error",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -39,6 +39,7 @@ import noFloatingPromises from './no-floating-promises';
import noForInArray from './no-for-in-array';
import noImpliedEval from './no-implied-eval';
import noInferrableTypes from './no-inferrable-types';
import noInvalidThis from './no-invalid-this';
import noMagicNumbers from './no-magic-numbers';
import noMisusedNew from './no-misused-new';
import noMisusedPromises from './no-misused-promises';
Expand Down Expand Up @@ -136,6 +137,7 @@ export default {
'no-for-in-array': noForInArray,
'no-implied-eval': noImpliedEval,
'no-inferrable-types': noInferrableTypes,
'no-invalid-this': noInvalidThis,
'no-magic-numbers': noMagicNumbers,
'no-misused-new': noMisusedNew,
'no-misused-promises': noMisusedPromises,
Expand Down
159 changes: 159 additions & 0 deletions packages/eslint-plugin/src/rules/no-invalid-this.ts
@@ -0,0 +1,159 @@
import baseRule from 'eslint/lib/rules/no-invalid-this';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like you're not actually using baseRule inside this rule because you had to fork the logic.
In this case we should locally define the types and the schema.

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 Options/MessageIds to match as well - we don't need the separate module type declarations.

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mark the rule as an extension rule.
You'll have to regen the configs and docs after this change.

Suggested change
recommended: false,
recommended: false,
extendsBaseRule: true,

},
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
const stack: CheckingContext[] = [],
sourceCode = context.getSourceCode();
const stack: CheckingContext[] = [];
const sourceCode = context.getSourceCode();


/**
* 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' });
}
},
};
},
});
93 changes: 93 additions & 0 deletions packages/eslint-plugin/tests/rules/no-invalid-this.test.ts
@@ -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,
},
],
},
],
});

/*


*/
32 changes: 32 additions & 0 deletions packages/eslint-plugin/typings/eslint-rules.d.ts
Expand Up @@ -203,6 +203,28 @@ declare module 'eslint/lib/rules/no-implicit-globals' {
export = rule;
}

declare module 'eslint/lib/rules/no-invalid-this' {
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';

const rule: TSESLint.RuleModule<
'noInvalidThis',
[
{
capIsConstructor?: boolean;
},
],
{
Program(node: TSESTree.Program): void;
FunctionDeclaration(node: TSESTree.FunctionDeclaration): void;
FunctionExpression(node: TSESTree.FunctionExpression): void;
'Program:exit'(): void;
'FunctionDeclaration:exit'(): void;
'FunctionExpression:exit'(): void;
}
>;
export = rule;
}

declare module 'eslint/lib/rules/no-magic-numbers' {
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';

Expand Down Expand Up @@ -543,3 +565,13 @@ declare module 'eslint/lib/rules/no-extra-semi' {
>;
export = rule;
}

declare module 'eslint/lib/rules/utils/ast-utils' {
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';

export function isDefaultThisBinding(
node: TSESTree.Node,
sourceCode: TSESLint.SourceCode,
options?: { capIsConstructor: boolean },
): boolean;
}