Skip to content

Commit

Permalink
Fix compatability with eslint-plugin-react 7.30.0
Browse files Browse the repository at this point in the history
We reached into eslint-plugin-react to access some of its internals, and
they moved around in v7.30.0.

Copy the utilities that we depend upon into our codebase, and remove the
need touch eslint-plugin-react's internals. Also remove usage of
Component.detect as it is no longer needed.
  • Loading branch information
BPScott committed May 27, 2022
1 parent cbb3a66 commit 3fd2ebf
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 134 deletions.
6 changes: 5 additions & 1 deletion packages/eslint-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

<!-- ## Unreleased -->
## Unreleased

### Changed

- Update `eslint-plugin-react` dependency to `^7.30.0`. Fix breakages by no longer reaching into `eslint-plugin-react`'s internals. [[#332](https://github.com/Shopify/web-configs/pull/332)]

## 41.2.1 - 2022-04-04

Expand Down
9 changes: 4 additions & 5 deletions packages/eslint-plugin/lib/rules/react-initialize-state.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const Components = require('eslint-plugin-react/lib/util/Components');

const {docsUrl, uncast, getName} = require('../utilities');
const {isES6Component} = require('../utilities/component-utils');

module.exports = {
meta: {
Expand All @@ -14,12 +13,12 @@ module.exports = {
schema: [],
},

create: Components.detect((context, components, utils) => {
create(context) {
let classInfo = null;

return {
ClassDeclaration(node) {
if (!utils.isES6Component(node)) {
if (!isES6Component(node, context)) {
return;
}

Expand Down Expand Up @@ -68,7 +67,7 @@ module.exports = {
classInfo = null;
},
};
}),
},
};

function classHasEmptyStateType({superTypeParameters}) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const Components = require('eslint-plugin-react/lib/util/Components');

const {docsUrl} = require('../utilities');
const {isES6Component} = require('../utilities/component-utils');

const message = [
'Don’t use multiple render methods in a single component;',
Expand All @@ -20,8 +19,8 @@ module.exports = {
},
},

create: Components.detect((context, components, utils) => {
let isES6Component = false;
create(context) {
let componentIsES6 = false;

function report(node) {
const name = getMethodName(node);
Expand All @@ -35,26 +34,26 @@ module.exports = {

return {
ClassDeclaration(node) {
isES6Component = utils.isES6Component(node);
componentIsES6 = isES6Component(node, context);
},

MethodDefinition(node) {
if (!isES6Component || !isRenderMethod(node)) {
if (!componentIsES6 || !isRenderMethod(node)) {
return;
}

report(node);
},

ArrowFunctionExpression(node) {
if (!isES6Component || !isRenderMethod(node)) {
if (!componentIsES6 || !isRenderMethod(node)) {
return;
}

report(node);
},
};
}),
},
};

function isRenderMethod(node) {
Expand Down
20 changes: 10 additions & 10 deletions packages/eslint-plugin/lib/rules/react-prefer-private-members.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const {pascalCase} = require('change-case');
const Components = require('eslint-plugin-react/lib/util/Components');

const {docsUrl} = require('../utilities');
const {isES6Component} = require('../utilities/component-utils');

module.exports = {
meta: {
Expand All @@ -13,8 +13,8 @@ module.exports = {
},
},

create: Components.detect((context, components, utils) => {
let isES6Component = 0;
create(context) {
let componentIsES6 = 0;
let componentName = null;

function report({node, componentName: classComponent}) {
Expand All @@ -31,32 +31,32 @@ module.exports = {

return {
ClassDeclaration(node) {
if (utils.isES6Component(node)) {
isES6Component++;
if (isES6Component(node, context)) {
componentIsES6++;
}
componentName = node.id.name;
},
'ClassDeclaration:exit': function (node) {
if (utils.isES6Component(node)) {
isES6Component--;
if (isES6Component(node, context)) {
componentIsES6--;
}
},
'ClassProperty,PropertyDefinition': function (node) {
if (isES6Component === 0 || isValid(node)) {
if (componentIsES6 === 0 || isValid(node)) {
return;
}

report({node, componentName});
},
MethodDefinition(node) {
if (isES6Component === 0 || isValid(node)) {
if (componentIsES6 === 0 || isValid(node)) {
return;
}

report({node, componentName});
},
};
}),
},
};

function isValid(node) {
Expand Down
9 changes: 4 additions & 5 deletions packages/eslint-plugin/lib/rules/react-type-state.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const Components = require('eslint-plugin-react/lib/util/Components');

const {docsUrl, getName} = require('../utilities');
const {isES6Component} = require('../utilities/component-utils');

module.exports = {
meta: {
Expand All @@ -13,12 +12,12 @@ module.exports = {
schema: [],
},

create: Components.detect((context, components, utils) => {
create(context) {
let inTypeScriptReactComponent = false;

function looksLikeTypeScriptComponent(node) {
return (
utils.isES6Component(node) &&
isES6Component(node, context) &&
Boolean(node.superTypeParameters) &&
Boolean(node.superTypeParameters.params) &&
node.superTypeParameters.params.length > 0 &&
Expand All @@ -45,5 +44,5 @@ module.exports = {
);
},
};
}),
},
};
101 changes: 101 additions & 0 deletions packages/eslint-plugin/lib/utilities/component-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copied from eslint-plugin-react's lib/util/componentUtil.js
// Because we don't want to reach deep into that packages's internals
// https://github.com/jsx-eslint/eslint-plugin-react/blob/18de0a653122c4ffd586a0269a7355c15ef05e23/lib/util/componentUtil.js

const doctrine = require('doctrine');

const pragmaUtil = require('./pragma');

/**
* @template {(_: object) => any} T
* @param {T} fn
* @returns {T}
*/
function memoize(fn) {
const cache = new WeakMap();
// @ts-ignore
return function memoizedFn(arg) {
const cachedValue = cache.get(arg);
if (cachedValue !== undefined) {
return cachedValue;
}
const val = fn(arg);
cache.set(arg, val);
return val;
};
}

const getPragma = memoize(pragmaUtil.getFromContext);

/**
* Check if the node is explicitly declared as a descendant of a React Component
* @param {any} node
* @param {Context} context
* @returns {boolean}
*/
function isExplicitComponent(node, context) {
const sourceCode = context.getSourceCode();
let comment;
// Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes.
// Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27
// eslint-disable-next-line no-warning-comments
// FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented.
try {
comment = sourceCode.getJSDocComment(node);
} catch (err) {
comment = null;
}

if (comment === null) {
return false;
}

let commentAst;
try {
commentAst = doctrine.parse(comment.value, {
unwrap: true,
tags: ['extends', 'augments'],
});
} catch (err) {
// handle a bug in the archived `doctrine`, see #2596
return false;
}

const relevantTags = commentAst.tags.filter(
(tag) =>
tag.name === 'React.Component' || tag.name === 'React.PureComponent',
);

return relevantTags.length > 0;
}

/**
* @param {ASTNode} node
* @param {Context} context
* @returns {boolean}
*/
function isES6Component(node, context) {
const pragma = getPragma(context);
if (isExplicitComponent(node, context)) {
return true;
}

if (!node.superClass) {
return false;
}
if (node.superClass.type === 'MemberExpression') {
return (
node.superClass.object.name === pragma &&
/^(Pure)?Component$/.test(node.superClass.property.name)
);
}
if (node.superClass.type === 'Identifier') {
return /^(Pure)?Component$/.test(node.superClass.name);
}
return false;
}

module.exports = {
isES6Component,
isExplicitComponent,
};
78 changes: 78 additions & 0 deletions packages/eslint-plugin/lib/utilities/pragma.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copied from eslint-plugin-react's lib/util/pragma.js
// Because we don't want to reach deep into that packages's internals
// https://github.com/jsx-eslint/eslint-plugin-react/blob/18de0a653122c4ffd586a0269a7355c15ef05e23/lib/util/pragma.js

/**
* @fileoverview Utility functions for React pragma configuration
* @author Yannick Croissant
*/

const JSX_ANNOTATION_REGEX = /@jsx\s+([^\s]+)/;
// Does not check for reserved keywords or unicode characters
const JS_IDENTIFIER_REGEX = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/;

/**
* @param {Context} context
* @returns {string}
*/
function getCreateClassFromContext(context) {
let pragma = 'createReactClass';
// .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings)
if (context.settings.react && context.settings.react.createClass) {
pragma = context.settings.react.createClass;
}
if (!JS_IDENTIFIER_REGEX.test(pragma)) {
throw new Error(
`createClass pragma ${pragma} is not a valid function name`,
);
}
return pragma;
}

/**
* @param {Context} context
* @returns {string}
*/
function getFragmentFromContext(context) {
let pragma = 'Fragment';
// .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings)
if (context.settings.react && context.settings.react.fragment) {
pragma = context.settings.react.fragment;
}
if (!JS_IDENTIFIER_REGEX.test(pragma)) {
throw new Error(`Fragment pragma ${pragma} is not a valid identifier`);
}
return pragma;
}

/**
* @param {Context} context
* @returns {string}
*/
function getFromContext(context) {
let pragma = 'React';

const sourceCode = context.getSourceCode();
const pragmaNode = sourceCode
.getAllComments()
.find((node) => JSX_ANNOTATION_REGEX.test(node.value));

if (pragmaNode) {
const matches = JSX_ANNOTATION_REGEX.exec(pragmaNode.value);
pragma = matches[1].split('.')[0];
// .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings)
} else if (context.settings.react && context.settings.react.pragma) {
pragma = context.settings.react.pragma;
}

if (!JS_IDENTIFIER_REGEX.test(pragma)) {
throw new Error(`React pragma ${pragma} is not a valid identifier`);
}
return pragma;
}

module.exports = {
getCreateClassFromContext,
getFragmentFromContext,
getFromContext,
};
3 changes: 2 additions & 1 deletion packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@typescript-eslint/parser": "^5.4.0",
"change-case": "^4.1.2",
"common-tags": "^1.8.2",
"doctrine": "^2.1.0",
"eslint-config-prettier": "^8.3.0",
"eslint-module-utils": "^2.7.1",
"eslint-plugin-eslint-comments": "^3.2.0",
Expand All @@ -50,7 +51,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.27.1",
"eslint-plugin-react": "^7.30.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-sort-class-members": "^1.14.0",
"jsx-ast-utils": "^3.2.1",
Expand Down
Loading

0 comments on commit 3fd2ebf

Please sign in to comment.