Update
This commit is contained in:
parent
77444efbf2
commit
3debdb9e74
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,7 +2,7 @@ eslint-plugin-import
|
|||||||
eslint-plugin-jsx-a11y
|
eslint-plugin-jsx-a11y
|
||||||
eslint-plugin-react
|
eslint-plugin-react
|
||||||
jsx-ast-utils
|
jsx-ast-utils
|
||||||
react
|
/react
|
||||||
|
|
||||||
dist/**/*.js
|
dist/**/*.js
|
||||||
dist/**/*.js.map
|
dist/**/*.js.map
|
||||||
|
19
build.sh
19
build.sh
@ -1,9 +1,16 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
rm dist/*.js
|
./src/build-local-rules.ts
|
||||||
mkdir -p dist
|
|
||||||
./esbuild.ts
|
./esbuild.ts
|
||||||
|
npx tsc ./src/local/index.ts --outdir ./dist/local --target ESNext --module CommonJS >/dev/null
|
||||||
|
npx tsc ./src/basic.ts --outdir ./dist --declaration --target ESNext --module CommonJS >/dev/null
|
||||||
|
|
||||||
# Test
|
type() {
|
||||||
DEST=$HOME/Git/archive/node_modules
|
npx dts-bundle-generator "./eslint-plugin-$1/$2" \
|
||||||
rm -rf "$DEST/@proteria/eslint-rules"
|
-o "./dist/$1/index.d.ts" \
|
||||||
cp -r dist "$DEST/@proteria/eslint-rules"
|
--project "./eslint-plugin-$1/tsconfig.json" \
|
||||||
|
--no-check
|
||||||
|
}
|
||||||
|
|
||||||
|
# type import src/index.js
|
||||||
|
# type jsx-a11y src/index.js
|
||||||
|
# type react index.js
|
||||||
|
2
dist/basic.d.ts
vendored
Normal file
2
dist/basic.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { ESLintConfig } from 'eslint-define-config';
|
||||||
|
export declare function extendConfig({ plugins, settings, rules, ...config }: ESLintConfig): ESLintConfig;
|
6
dist/package.json
vendored
6
dist/package.json
vendored
@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@aet/eslint-rules",
|
"name": "@aet/eslint-rules",
|
||||||
"version": "0.0.1-beta.11",
|
"version": "0.0.1-beta.16",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"eslint-add-alias": "./addAlias.js"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint": "^8.44.0",
|
"@types/eslint": "^8.44.0",
|
||||||
"aria-query": "^5.3.0",
|
"aria-query": "^5.3.0",
|
||||||
@ -17,6 +20,7 @@
|
|||||||
"eslint-import-resolver-node": "^0.3.7",
|
"eslint-import-resolver-node": "^0.3.7",
|
||||||
"eslint-module-utils": "^2.8.0",
|
"eslint-module-utils": "^2.8.0",
|
||||||
"estraverse": "^5.3.0",
|
"estraverse": "^5.3.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"is-core-module": "^2.12.1",
|
"is-core-module": "^2.12.1",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"language-tags": "^1.0.8",
|
"language-tags": "^1.0.8",
|
||||||
|
12
dist/react-hooks/index.d.ts
vendored
Normal file
12
dist/react-hooks/index.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import type { Linter, Rule } from 'eslint';
|
||||||
|
|
||||||
|
export const __EXPERIMENTAL__: false;
|
||||||
|
|
||||||
|
export const configs: {
|
||||||
|
recommended: Linter.BaseConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rules: {
|
||||||
|
'rules-of-hooks': Rule.RuleModule;
|
||||||
|
'exhaustive-deps': Rule.RuleModule;
|
||||||
|
};
|
233
esbuild.ts
233
esbuild.ts
@ -1,14 +1,212 @@
|
|||||||
#!/usr/bin/env -S node -r esbin
|
#!/usr/bin/env -S node -r esbin
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve, extname, relative } from 'node:path';
|
||||||
|
import { isBuiltin } from 'node:module';
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
import { resolve } from 'path';
|
import type { Loader, Plugin } from 'esbuild';
|
||||||
import { babelPlugin } from './src/babel';
|
import * as babel from '@babel/core';
|
||||||
|
import { memoize } from 'lodash';
|
||||||
|
import { gray, green } from 'picocolors';
|
||||||
|
import type { types as t } from '@babel/core';
|
||||||
|
import { dependencies } from './dist/package.json';
|
||||||
|
import { createMacro, type MacroHandler } from 'babel-plugin-macros';
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const ENV = process.env.NODE_ENV || 'development';
|
const ENV = process.env.NODE_ENV || 'development';
|
||||||
const PROD = ENV === 'production';
|
const PROD = ENV === 'production';
|
||||||
|
|
||||||
|
class HandlerMap {
|
||||||
|
map = new Map<string, MacroHandler>();
|
||||||
|
|
||||||
|
set(names: string | string[], handler: MacroHandler) {
|
||||||
|
names = Array.isArray(names) ? names : [names];
|
||||||
|
const macro = createMacro(handler);
|
||||||
|
for (const name of names) {
|
||||||
|
this.map.set(name, macro);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get keys() {
|
||||||
|
return Array.from(this.map.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvePath = (module: string) => module;
|
||||||
|
require = (module: string) => this.map.get(module);
|
||||||
|
isMacrosName = (module: string) => this.map.has(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = new HandlerMap()
|
||||||
|
.set(
|
||||||
|
'object.assign',
|
||||||
|
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('assign'))),
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
['object-values', 'object.values'],
|
||||||
|
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('values'))),
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
'object.fromentries',
|
||||||
|
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('fromEntries'))),
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
'object.entries',
|
||||||
|
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('entries'))),
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
'has',
|
||||||
|
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
'array-includes',
|
||||||
|
proto(t => t.identifier('includes')),
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
'array.prototype.flatmap',
|
||||||
|
proto(t => t.identifier('flatMap')),
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
'array.prototype.flat',
|
||||||
|
proto(t => t.identifier('flat')),
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
'array.prototype.findlastindex',
|
||||||
|
proto(t => t.identifier('findLastIndex')),
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
'array.prototype.tosorted',
|
||||||
|
proto(t => t.identifier('toSorted')),
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
'string.prototype.matchall',
|
||||||
|
proto(t => t.identifier('matchAll')),
|
||||||
|
);
|
||||||
|
|
||||||
|
function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler {
|
||||||
|
return ({ references, babel: { types: t } }) => {
|
||||||
|
references.default.forEach(referencePath => {
|
||||||
|
referencePath.replaceWith(getReplacement(t));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
|
||||||
|
return ({ references, babel: { types: t } }) => {
|
||||||
|
references.default.forEach(referencePath => {
|
||||||
|
const { parent, parentPath } = referencePath;
|
||||||
|
assert(t.isCallExpression(parent));
|
||||||
|
const [callee, ...rest] = parent.arguments;
|
||||||
|
parentPath!.replaceWith(
|
||||||
|
t.callExpression(
|
||||||
|
t.memberExpression(callee as t.Expression, getProperty(t)),
|
||||||
|
rest,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const babelPlugin: Plugin = {
|
||||||
|
name: 'babel',
|
||||||
|
setup(build) {
|
||||||
|
const { keys, ...macroOptions } = map;
|
||||||
|
|
||||||
|
build.onLoad({ filter: /\.[jt]sx?$/ }, args => {
|
||||||
|
const { path } = args;
|
||||||
|
if (path.includes('node_modules/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = readFileSync(path, 'utf-8')
|
||||||
|
.replaceAll("require('object.hasown/polyfill')()", 'Object.hasOwn')
|
||||||
|
.replaceAll("require('object.fromentries/polyfill')()", 'Object.fromEntries');
|
||||||
|
|
||||||
|
if (
|
||||||
|
path.includes('eslint-plugin-import/src/rules/') ||
|
||||||
|
path.includes('eslint-plugin-import/config/')
|
||||||
|
) {
|
||||||
|
source = source.replace('\nmodule.exports = {', '\nexport default {');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFlow = source.includes('@flow');
|
||||||
|
const loader = extname(path).slice(1) as Loader;
|
||||||
|
|
||||||
|
if (!isFlow && !keys.some(key => source.includes(key))) {
|
||||||
|
return { contents: source, loader };
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = babel.transformSync(source, {
|
||||||
|
filename: path,
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
parserOpts: {
|
||||||
|
plugins: [isFlow ? 'flow' : 'typescript'],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
isFlow && '@babel/plugin-transform-flow-strip-types',
|
||||||
|
['babel-plugin-macros', macroOptions],
|
||||||
|
].filter(Boolean),
|
||||||
|
})!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: res.code!,
|
||||||
|
loader,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Array<T> {
|
||||||
|
filter(
|
||||||
|
predicate: BooleanConstructor,
|
||||||
|
): Exclude<T, null | undefined | false | '' | 0>[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = memoize(console.log);
|
||||||
|
|
||||||
|
const plugins: Plugin[] = [
|
||||||
|
babelPlugin,
|
||||||
|
{
|
||||||
|
name: 'alias',
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /^jsx-ast-utils$/ }, () => ({
|
||||||
|
path: resolve('./jsx-ast-utils/src/index.js'),
|
||||||
|
}));
|
||||||
|
build.onResolve({ filter: /^jsx-ast-utils\/.+$/ }, ({ path }) => ({
|
||||||
|
path: resolve('./jsx-ast-utils/', path.slice('jsx-ast-utils/'.length)) + '.js',
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
plugins.push({
|
||||||
|
name: 'deps-check',
|
||||||
|
setup(build) {
|
||||||
|
const declared = new Set(Object.keys(dependencies));
|
||||||
|
|
||||||
|
build.onResolve({ filter: /^.*$/ }, ({ path, importer }) => {
|
||||||
|
if (
|
||||||
|
!path.startsWith('./') &&
|
||||||
|
!path.startsWith('../') &&
|
||||||
|
!isBuiltin(path) &&
|
||||||
|
path !== 'eslint' &&
|
||||||
|
!path.startsWith('eslint/') &&
|
||||||
|
!path.startsWith('eslint-module-utils/') &&
|
||||||
|
!declared.has(path)
|
||||||
|
) {
|
||||||
|
log(green(path), gray('from'), './' + relative(process.cwd(), importer));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function main(entry: string, outfile: string) {
|
async function main(entry: string, outfile: string) {
|
||||||
const context = await esbuild.context({
|
await esbuild.build({
|
||||||
entryPoints: [entry],
|
entryPoints: [entry],
|
||||||
outfile,
|
outfile,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
@ -16,37 +214,18 @@ async function main(entry: string, outfile: string) {
|
|||||||
platform: 'node',
|
platform: 'node',
|
||||||
packages: 'external',
|
packages: 'external',
|
||||||
sourcemap: 'linked',
|
sourcemap: 'linked',
|
||||||
plugins: [
|
plugins,
|
||||||
babelPlugin,
|
|
||||||
{
|
|
||||||
name: 'alias',
|
|
||||||
setup(build) {
|
|
||||||
build.onResolve({ filter: /^jsx-ast-utils$/ }, () => ({
|
|
||||||
path: resolve('./jsx-ast-utils/src/index.js'),
|
|
||||||
}));
|
|
||||||
build.onResolve({ filter: /^jsx-ast-utils\/.+$/ }, ({ path }) => ({
|
|
||||||
path:
|
|
||||||
resolve('./jsx-ast-utils/', path.slice('jsx-ast-utils/'.length)) + '.js',
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
define: {},
|
define: {},
|
||||||
banner: {
|
banner: {
|
||||||
js: '/* eslint-disable */',
|
js: '/* eslint-disable */',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await context.rebuild();
|
|
||||||
|
|
||||||
if (args.includes('-w') || args.includes('--watch')) {
|
|
||||||
await context.watch();
|
|
||||||
} else {
|
|
||||||
await context.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main('./eslint-plugin-react/index.js', './dist/react/index.js');
|
main('./eslint-plugin-react/index.js', './dist/react/index.js');
|
||||||
main('./eslint-plugin-import/src/index.js', './dist/import/index.js');
|
main('./eslint-plugin-import/src/index.js', './dist/import/index.js');
|
||||||
main('./eslint-plugin-jsx-a11y/src/index.js', './dist/jsx-a11y/index.js');
|
main('./eslint-plugin-jsx-a11y/src/index.js', './dist/jsx-a11y/index.js');
|
||||||
main('./src/ensureRedirect.ts', './dist/ensureRedirect.js');
|
main('./eslint-plugin-react-hooks/index.ts', './dist/react-hooks/index.js');
|
||||||
|
main('./eslint-plugin-n/lib/index.js', './dist/n/index.js');
|
||||||
|
main('./src/addAlias.ts', './dist/addAlias.js');
|
||||||
|
main('./src/rules/index.ts', './dist/rules/index.js');
|
||||||
|
@ -6,10 +6,43 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
||||||
|
import type { Rule, Scope } from 'eslint';
|
||||||
|
import type {
|
||||||
|
FunctionDeclaration,
|
||||||
|
CallExpression,
|
||||||
|
Expression,
|
||||||
|
Super,
|
||||||
|
Node,
|
||||||
|
ArrowFunctionExpression,
|
||||||
|
FunctionExpression,
|
||||||
|
SpreadElement,
|
||||||
|
Identifier,
|
||||||
|
VariableDeclarator,
|
||||||
|
MemberExpression,
|
||||||
|
ChainExpression,
|
||||||
|
Pattern,
|
||||||
|
OptionalMemberExpression,
|
||||||
|
} from 'estree';
|
||||||
|
import type { FromSchema } from 'json-schema-to-ts';
|
||||||
|
import { __EXPERIMENTAL__ } from './index';
|
||||||
|
|
||||||
'use strict';
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
enableDangerousAutofixThisMayCauseInfiniteLoops: false,
|
||||||
|
properties: {
|
||||||
|
additionalHooks: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
enableDangerousAutofixThisMayCauseInfiniteLoops: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default {
|
type Config = FromSchema<typeof schema>;
|
||||||
|
|
||||||
|
const rule: Rule.RuleModule = {
|
||||||
meta: {
|
meta: {
|
||||||
type: 'suggestion',
|
type: 'suggestion',
|
||||||
docs: {
|
docs: {
|
||||||
@ -20,41 +53,24 @@ export default {
|
|||||||
},
|
},
|
||||||
fixable: 'code',
|
fixable: 'code',
|
||||||
hasSuggestions: true,
|
hasSuggestions: true,
|
||||||
schema: [
|
schema: [schema],
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
enableDangerousAutofixThisMayCauseInfiniteLoops: false,
|
|
||||||
properties: {
|
|
||||||
additionalHooks: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
enableDangerousAutofixThisMayCauseInfiniteLoops: {
|
|
||||||
type: 'boolean',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
create(context) {
|
create(context): Rule.RuleListener {
|
||||||
|
const contextOptions = (context.options[0] || {}) as Config;
|
||||||
// Parse the `additionalHooks` regex.
|
// Parse the `additionalHooks` regex.
|
||||||
const additionalHooks =
|
const additionalHooks = contextOptions?.additionalHooks
|
||||||
context.options && context.options[0] && context.options[0].additionalHooks
|
? new RegExp(context.options[0].additionalHooks)
|
||||||
? new RegExp(context.options[0].additionalHooks)
|
: undefined;
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const enableDangerousAutofixThisMayCauseInfiniteLoops =
|
const enableDangerousAutofixThisMayCauseInfiniteLoops =
|
||||||
(context.options &&
|
contextOptions?.enableDangerousAutofixThisMayCauseInfiniteLoops || false;
|
||||||
context.options[0] &&
|
|
||||||
context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops) ||
|
|
||||||
false;
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
additionalHooks,
|
additionalHooks,
|
||||||
enableDangerousAutofixThisMayCauseInfiniteLoops,
|
enableDangerousAutofixThisMayCauseInfiniteLoops,
|
||||||
};
|
};
|
||||||
|
|
||||||
function reportProblem(problem) {
|
function reportProblem(problem: Rule.ReportDescriptor): void {
|
||||||
if (enableDangerousAutofixThisMayCauseInfiniteLoops) {
|
if (enableDangerousAutofixThisMayCauseInfiniteLoops) {
|
||||||
// Used to enable legacy behavior. Dangerous.
|
// Used to enable legacy behavior. Dangerous.
|
||||||
// Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension).
|
// Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension).
|
||||||
@ -65,20 +81,23 @@ export default {
|
|||||||
context.report(problem);
|
context.report(problem);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scopeManager = context.getSourceCode().scopeManager;
|
const scopeManager = context.sourceCode.scopeManager;
|
||||||
|
|
||||||
// Should be shared between visitors.
|
// Should be shared between visitors.
|
||||||
const setStateCallSites = new WeakMap();
|
const setStateCallSites = new WeakMap<Expression, Pattern>();
|
||||||
const stateVariables = new WeakSet();
|
const stateVariables = new WeakSet<Identifier>();
|
||||||
const stableKnownValueCache = new WeakMap();
|
const stableKnownValueCache = new WeakMap<Scope.Variable, boolean>();
|
||||||
const functionWithoutCapturedValueCache = new WeakMap();
|
const functionWithoutCapturedValueCache = new WeakMap<Scope.Variable, boolean>();
|
||||||
const useEffectEventVariables = new WeakSet();
|
const useEffectEventVariables = new WeakSet();
|
||||||
function memoizeWithWeakMap(fn, map) {
|
function memoizeWithWeakMap<T extends object, R>(
|
||||||
|
fn: (v: T) => R,
|
||||||
|
map: WeakMap<T, R>,
|
||||||
|
): (arg: T) => R {
|
||||||
return function (arg) {
|
return function (arg) {
|
||||||
if (map.has(arg)) {
|
if (map.has(arg)) {
|
||||||
// to verify cache hits:
|
// to verify cache hits:
|
||||||
// console.log(arg.name)
|
// console.log(arg.name)
|
||||||
return map.get(arg);
|
return map.get(arg)!;
|
||||||
}
|
}
|
||||||
const result = fn(arg);
|
const result = fn(arg);
|
||||||
map.set(arg, result);
|
map.set(arg, result);
|
||||||
@ -89,12 +108,12 @@ export default {
|
|||||||
* Visitor for both function expressions and arrow function expressions.
|
* Visitor for both function expressions and arrow function expressions.
|
||||||
*/
|
*/
|
||||||
function visitFunctionWithDependencies(
|
function visitFunctionWithDependencies(
|
||||||
node,
|
node: ArrowFunctionExpression | FunctionExpression | FunctionDeclaration,
|
||||||
declaredDependenciesNode,
|
declaredDependenciesNode: SpreadElement | Expression,
|
||||||
reactiveHook,
|
reactiveHook: Super | Expression,
|
||||||
reactiveHookName,
|
reactiveHookName: string,
|
||||||
isEffect,
|
isEffect: boolean,
|
||||||
) {
|
): void {
|
||||||
if (isEffect && node.async) {
|
if (isEffect && node.async) {
|
||||||
reportProblem({
|
reportProblem({
|
||||||
node: node,
|
node: node,
|
||||||
@ -114,7 +133,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the current scope.
|
// Get the current scope.
|
||||||
const scope = scopeManager.acquire(node);
|
const scope = scopeManager.acquire(node)!;
|
||||||
|
|
||||||
// Find all our "pure scopes". On every re-render of a component these
|
// Find all our "pure scopes". On every re-render of a component these
|
||||||
// pure scopes may have changes to the variables declared within. So all
|
// pure scopes may have changes to the variables declared within. So all
|
||||||
@ -124,8 +143,8 @@ export default {
|
|||||||
// According to the rules of React you can't read a mutable value in pure
|
// According to the rules of React you can't read a mutable value in pure
|
||||||
// scope. We can't enforce this in a lint so we trust that all variables
|
// scope. We can't enforce this in a lint so we trust that all variables
|
||||||
// declared outside of pure scope are indeed frozen.
|
// declared outside of pure scope are indeed frozen.
|
||||||
const pureScopes = new Set();
|
const pureScopes = new Set<Scope.Scope>();
|
||||||
let componentScope = null;
|
let componentScope: Scope.Scope;
|
||||||
{
|
{
|
||||||
let currentScope = scope.upper;
|
let currentScope = scope.upper;
|
||||||
while (currentScope) {
|
while (currentScope) {
|
||||||
@ -159,7 +178,7 @@ export default {
|
|||||||
// const onStuff = useEffectEvent(() => {})
|
// const onStuff = useEffectEvent(() => {})
|
||||||
// ^^^ true for this reference
|
// ^^^ true for this reference
|
||||||
// False for everything else.
|
// False for everything else.
|
||||||
function isStableKnownHookValue(resolved) {
|
function isStableKnownHookValue(resolved: Scope.Variable): boolean {
|
||||||
if (!isArray(resolved.defs)) {
|
if (!isArray(resolved.defs)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -171,7 +190,7 @@ export default {
|
|||||||
if (def.node.type !== 'VariableDeclarator') {
|
if (def.node.type !== 'VariableDeclarator') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let init = def.node.init;
|
let init = (def.node as VariableDeclarator).init;
|
||||||
if (init == null) {
|
if (init == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -206,10 +225,11 @@ export default {
|
|||||||
if (init.type !== 'CallExpression') {
|
if (init.type !== 'CallExpression') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let callee = init.callee;
|
let callee: Node = init.callee;
|
||||||
// Step into `= React.something` initializer.
|
// Step into `= React.something` initializer.
|
||||||
if (
|
if (
|
||||||
callee.type === 'MemberExpression' &&
|
callee.type === 'MemberExpression' &&
|
||||||
|
callee.object.type === 'Identifier' &&
|
||||||
callee.object.name === 'React' &&
|
callee.object.name === 'React' &&
|
||||||
callee.property != null &&
|
callee.property != null &&
|
||||||
!callee.computed
|
!callee.computed
|
||||||
@ -219,14 +239,14 @@ export default {
|
|||||||
if (callee.type !== 'Identifier') {
|
if (callee.type !== 'Identifier') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const id = def.node.id;
|
const id = (def.node as VariableDeclarator).id;
|
||||||
const { name } = callee;
|
const { name } = callee;
|
||||||
if (name === 'useRef' && id.type === 'Identifier') {
|
if (name === 'useRef' && id.type === 'Identifier') {
|
||||||
// useRef() return value is stable.
|
// useRef() return value is stable.
|
||||||
return true;
|
return true;
|
||||||
} else if (isUseEffectEventIdentifier(callee) && id.type === 'Identifier') {
|
} else if (isUseEffectEventIdentifier(callee) && id.type === 'Identifier') {
|
||||||
for (const ref of resolved.references) {
|
for (const ref of resolved.references) {
|
||||||
if (ref !== id) {
|
if (ref.identifier !== id) {
|
||||||
useEffectEventVariables.add(ref.identifier);
|
useEffectEventVariables.add(ref.identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,7 +271,7 @@ export default {
|
|||||||
if (writeCount > 1) {
|
if (writeCount > 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
setStateCallSites.set(references[i].identifier, id.elements[0]);
|
setStateCallSites.set(references[i].identifier, id.elements[0]!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Setter is stable.
|
// Setter is stable.
|
||||||
@ -286,7 +306,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Some are just functions that don't reference anything dynamic.
|
// Some are just functions that don't reference anything dynamic.
|
||||||
function isFunctionWithoutCapturedValues(resolved) {
|
function isFunctionWithoutCapturedValues(resolved: Scope.Variable): boolean {
|
||||||
if (!isArray(resolved.defs)) {
|
if (!isArray(resolved.defs)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -353,32 +373,33 @@ export default {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// These are usually mistaken. Collect them.
|
// These are usually mistaken. Collect them.
|
||||||
const currentRefsInEffectCleanup = new Map();
|
const currentRefsInEffectCleanup = new Map<
|
||||||
|
string,
|
||||||
|
{ reference: Scope.Reference; dependencyNode: Identifier }
|
||||||
|
>();
|
||||||
|
|
||||||
// Is this reference inside a cleanup function for this effect node?
|
// Is this reference inside a cleanup function for this effect node?
|
||||||
// We can check by traversing scopes upwards from the reference, and checking
|
// We can check by traversing scopes upwards from the reference, and checking
|
||||||
// if the last "return () => " we encounter is located directly inside the effect.
|
// if the last "return () => " we encounter is located directly inside the effect.
|
||||||
function isInsideEffectCleanup(reference) {
|
function isInsideEffectCleanup(reference: Scope.Reference): boolean {
|
||||||
let curScope = reference.from;
|
let curScope: Scope.Scope = reference.from;
|
||||||
let isInReturnedFunction = false;
|
let isInReturnedFunction = false;
|
||||||
while (curScope.block !== node) {
|
while (curScope.block !== node) {
|
||||||
if (curScope.type === 'function') {
|
if (curScope.type === 'function') {
|
||||||
isInReturnedFunction =
|
isInReturnedFunction = curScope.block.parent?.type === 'ReturnStatement';
|
||||||
curScope.block.parent != null &&
|
|
||||||
curScope.block.parent.type === 'ReturnStatement';
|
|
||||||
}
|
}
|
||||||
curScope = curScope.upper;
|
curScope = curScope.upper!;
|
||||||
}
|
}
|
||||||
return isInReturnedFunction;
|
return isInReturnedFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get dependencies from all our resolved references in pure scopes.
|
// Get dependencies from all our resolved references in pure scopes.
|
||||||
// Key is dependency string, value is whether it's stable.
|
// Key is dependency string, value is whether it's stable.
|
||||||
const dependencies = new Map();
|
const dependencies = new Map<string, Dependencies>();
|
||||||
const optionalChains = new Map();
|
const optionalChains = new Map<string, boolean>();
|
||||||
gatherDependenciesRecursively(scope);
|
gatherDependenciesRecursively(scope);
|
||||||
|
|
||||||
function gatherDependenciesRecursively(currentScope) {
|
function gatherDependenciesRecursively(currentScope: Scope.Scope): void {
|
||||||
for (const reference of currentScope.references) {
|
for (const reference of currentScope.references) {
|
||||||
// If this reference is not resolved or it is not declared in a pure
|
// If this reference is not resolved or it is not declared in a pure
|
||||||
// scope then we don't care about this reference.
|
// scope then we don't care about this reference.
|
||||||
@ -391,9 +412,9 @@ export default {
|
|||||||
|
|
||||||
// Narrow the scope of a dependency if it is, say, a member expression.
|
// Narrow the scope of a dependency if it is, say, a member expression.
|
||||||
// Then normalize the narrowed dependency.
|
// Then normalize the narrowed dependency.
|
||||||
const referenceNode = fastFindReferenceWithParent(node, reference.identifier);
|
const referenceNode = fastFindReferenceWithParent(node, reference.identifier)!;
|
||||||
const dependencyNode = getDependency(referenceNode);
|
const dependencyNode: Node = getDependency(referenceNode);
|
||||||
const dependency = analyzePropertyChain(dependencyNode, optionalChains);
|
const dependency: string = analyzePropertyChain(dependencyNode, optionalChains);
|
||||||
|
|
||||||
// Accessing ref.current inside effect cleanup is bad.
|
// Accessing ref.current inside effect cleanup is bad.
|
||||||
if (
|
if (
|
||||||
@ -401,11 +422,11 @@ export default {
|
|||||||
isEffect &&
|
isEffect &&
|
||||||
// ... and this look like accessing .current...
|
// ... and this look like accessing .current...
|
||||||
dependencyNode.type === 'Identifier' &&
|
dependencyNode.type === 'Identifier' &&
|
||||||
(dependencyNode.parent.type === 'MemberExpression' ||
|
(dependencyNode.parent!.type === 'MemberExpression' ||
|
||||||
dependencyNode.parent.type === 'OptionalMemberExpression') &&
|
dependencyNode.parent!.type === 'OptionalMemberExpression') &&
|
||||||
!dependencyNode.parent.computed &&
|
!dependencyNode.parent!.computed &&
|
||||||
dependencyNode.parent.property.type === 'Identifier' &&
|
dependencyNode.parent!.property.type === 'Identifier' &&
|
||||||
dependencyNode.parent.property.name === 'current' &&
|
dependencyNode.parent!.property.name === 'current' &&
|
||||||
// ...in a cleanup function or below...
|
// ...in a cleanup function or below...
|
||||||
isInsideEffectCleanup(reference)
|
isInsideEffectCleanup(reference)
|
||||||
) {
|
) {
|
||||||
@ -416,8 +437,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
dependencyNode.parent.type === 'TSTypeQuery' ||
|
dependencyNode.parent!.type === 'TSTypeQuery' ||
|
||||||
dependencyNode.parent.type === 'TSTypeReference'
|
dependencyNode.parent!.type === 'TSTypeReference'
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -438,7 +459,7 @@ export default {
|
|||||||
// Add the dependency to a map so we can make sure it is referenced
|
// Add the dependency to a map so we can make sure it is referenced
|
||||||
// again in our dependencies array. Remember whether it's stable.
|
// again in our dependencies array. Remember whether it's stable.
|
||||||
if (!dependencies.has(dependency)) {
|
if (!dependencies.has(dependency)) {
|
||||||
const resolved = reference.resolved;
|
const resolved: Scope.Variable = reference.resolved;
|
||||||
const isStable =
|
const isStable =
|
||||||
memoizedIsStableKnownHookValue(resolved) ||
|
memoizedIsStableKnownHookValue(resolved) ||
|
||||||
memoizedIsFunctionWithoutCapturedValues(resolved);
|
memoizedIsFunctionWithoutCapturedValues(resolved);
|
||||||
@ -447,7 +468,7 @@ export default {
|
|||||||
references: [reference],
|
references: [reference],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dependencies.get(dependency).references.push(reference);
|
dependencies.get(dependency)!.references.push(reference);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,7 +479,7 @@ export default {
|
|||||||
|
|
||||||
// Warn about accessing .current in cleanup effects.
|
// Warn about accessing .current in cleanup effects.
|
||||||
currentRefsInEffectCleanup.forEach(({ reference, dependencyNode }, dependency) => {
|
currentRefsInEffectCleanup.forEach(({ reference, dependencyNode }, dependency) => {
|
||||||
const references = reference.resolved.references;
|
const references: Scope.Reference[] = reference.resolved!.references;
|
||||||
// Is React managing this ref or us?
|
// Is React managing this ref or us?
|
||||||
// Let's see if we can find a .current assignment.
|
// Let's see if we can find a .current assignment.
|
||||||
let foundCurrentAssignment = false;
|
let foundCurrentAssignment = false;
|
||||||
@ -474,8 +495,8 @@ export default {
|
|||||||
parent.property.type === 'Identifier' &&
|
parent.property.type === 'Identifier' &&
|
||||||
parent.property.name === 'current' &&
|
parent.property.name === 'current' &&
|
||||||
// ref.current = <something>
|
// ref.current = <something>
|
||||||
parent.parent.type === 'AssignmentExpression' &&
|
parent.parent!.type === 'AssignmentExpression' &&
|
||||||
parent.parent.left === parent
|
parent.parent!.left === parent
|
||||||
) {
|
) {
|
||||||
foundCurrentAssignment = true;
|
foundCurrentAssignment = true;
|
||||||
break;
|
break;
|
||||||
@ -486,7 +507,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reportProblem({
|
reportProblem({
|
||||||
node: dependencyNode.parent.property,
|
node: (dependencyNode.parent as MemberExpression).property,
|
||||||
message:
|
message:
|
||||||
`The ref value '${dependency}.current' will likely have ` +
|
`The ref value '${dependency}.current' will likely have ` +
|
||||||
`changed by the time this effect cleanup function runs. If ` +
|
`changed by the time this effect cleanup function runs. If ` +
|
||||||
@ -498,8 +519,8 @@ export default {
|
|||||||
|
|
||||||
// Warn about assigning to variables in the outer scope.
|
// Warn about assigning to variables in the outer scope.
|
||||||
// Those are usually bugs.
|
// Those are usually bugs.
|
||||||
const staleAssignments = new Set();
|
const staleAssignments = new Set<string>();
|
||||||
function reportStaleAssignment(writeExpr, key) {
|
function reportStaleAssignment(writeExpr: Node, key: string): void {
|
||||||
if (staleAssignments.has(key)) {
|
if (staleAssignments.has(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -517,7 +538,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remember which deps are stable and report bad usage first.
|
// Remember which deps are stable and report bad usage first.
|
||||||
const stableDependencies = new Set();
|
const stableDependencies = new Set<string>();
|
||||||
dependencies.forEach(({ isStable, references }, key) => {
|
dependencies.forEach(({ isStable, references }, key) => {
|
||||||
if (isStable) {
|
if (isStable) {
|
||||||
stableDependencies.add(key);
|
stableDependencies.add(key);
|
||||||
@ -537,8 +558,8 @@ export default {
|
|||||||
if (!declaredDependenciesNode) {
|
if (!declaredDependenciesNode) {
|
||||||
// Check if there are any top-level setState() calls.
|
// Check if there are any top-level setState() calls.
|
||||||
// Those tend to lead to infinite loops.
|
// Those tend to lead to infinite loops.
|
||||||
let setStateInsideEffectWithoutDeps = null;
|
let setStateInsideEffectWithoutDeps: string | null = null;
|
||||||
dependencies.forEach(({ isStable, references }, key) => {
|
dependencies.forEach(({ references }, key) => {
|
||||||
if (setStateInsideEffectWithoutDeps) {
|
if (setStateInsideEffectWithoutDeps) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -548,14 +569,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const id = reference.identifier;
|
const id = reference.identifier;
|
||||||
const isSetState = setStateCallSites.has(id);
|
const isSetState: boolean = setStateCallSites.has(id);
|
||||||
if (!isSetState) {
|
if (!isSetState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fnScope = reference.from;
|
let fnScope: Scope.Scope = reference.from;
|
||||||
while (fnScope.type !== 'function') {
|
while (fnScope.type !== 'function') {
|
||||||
fnScope = fnScope.upper;
|
fnScope = fnScope.upper!;
|
||||||
}
|
}
|
||||||
const isDirectlyInsideEffect = fnScope.block === node;
|
const isDirectlyInsideEffect = fnScope.block === node;
|
||||||
if (isDirectlyInsideEffect) {
|
if (isDirectlyInsideEffect) {
|
||||||
@ -564,6 +585,7 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (setStateInsideEffectWithoutDeps) {
|
if (setStateInsideEffectWithoutDeps) {
|
||||||
const { suggestedDependencies } = collectRecommendations({
|
const { suggestedDependencies } = collectRecommendations({
|
||||||
dependencies,
|
dependencies,
|
||||||
@ -596,8 +618,8 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const declaredDependencies = [];
|
const declaredDependencies: DeclaredDependency[] = [];
|
||||||
const externalDependencies = new Set();
|
const externalDependencies = new Set<string>();
|
||||||
if (declaredDependenciesNode.type !== 'ArrayExpression') {
|
if (declaredDependenciesNode.type !== 'ArrayExpression') {
|
||||||
// If the declared dependencies are not an array expression then we
|
// If the declared dependencies are not an array expression then we
|
||||||
// can't verify that the user provided the correct dependencies. Tell
|
// can't verify that the user provided the correct dependencies. Tell
|
||||||
@ -640,7 +662,7 @@ export default {
|
|||||||
declaredDependencyNode,
|
declaredDependencyNode,
|
||||||
)}\``,
|
)}\``,
|
||||||
fix(fixer) {
|
fix(fixer) {
|
||||||
return fixer.removeRange(declaredDependencyNode.range);
|
return fixer.removeRange(declaredDependencyNode.range!);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -648,13 +670,13 @@ export default {
|
|||||||
}
|
}
|
||||||
// Try to normalize the declared dependency. If we can't then an error
|
// Try to normalize the declared dependency. If we can't then an error
|
||||||
// will be thrown. We will catch that error and report an error.
|
// will be thrown. We will catch that error and report an error.
|
||||||
let declaredDependency;
|
let declaredDependency: string;
|
||||||
try {
|
try {
|
||||||
declaredDependency = analyzePropertyChain(declaredDependencyNode, null);
|
declaredDependency = analyzePropertyChain(declaredDependencyNode, null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (/Unsupported node type/.test(error.message)) {
|
if (/Unsupported node type/.test(error.message)) {
|
||||||
if (declaredDependencyNode.type === 'Literal') {
|
if (declaredDependencyNode.type === 'Literal') {
|
||||||
if (dependencies.has(declaredDependencyNode.value)) {
|
if (dependencies.has(declaredDependencyNode.value as any)) {
|
||||||
reportProblem({
|
reportProblem({
|
||||||
node: declaredDependencyNode,
|
node: declaredDependencyNode,
|
||||||
message:
|
message:
|
||||||
@ -686,13 +708,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let maybeID = declaredDependencyNode;
|
let maybeID: Expression | Super = declaredDependencyNode;
|
||||||
while (
|
while (
|
||||||
maybeID.type === 'MemberExpression' ||
|
maybeID.type === 'MemberExpression' ||
|
||||||
maybeID.type === 'OptionalMemberExpression' ||
|
maybeID.type === 'OptionalMemberExpression' ||
|
||||||
maybeID.type === 'ChainExpression'
|
maybeID.type === 'ChainExpression'
|
||||||
) {
|
) {
|
||||||
maybeID = maybeID.object || maybeID.expression.object;
|
maybeID =
|
||||||
|
(maybeID as MemberExpression | OptionalMemberExpression).object ||
|
||||||
|
((maybeID as ChainExpression).expression as MemberExpression).object;
|
||||||
}
|
}
|
||||||
const isDeclaredInComponent = !componentScope.through.some(
|
const isDeclaredInComponent = !componentScope.through.some(
|
||||||
ref => ref.identifier === maybeID,
|
ref => ref.identifier === maybeID,
|
||||||
@ -758,10 +782,12 @@ export default {
|
|||||||
|
|
||||||
const message =
|
const message =
|
||||||
`The '${construction.name.name}' ${depType} ${causation} the dependencies of ` +
|
`The '${construction.name.name}' ${depType} ${causation} the dependencies of ` +
|
||||||
`${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc.start.line}) ` +
|
`${reactiveHookName} Hook (at line ${
|
||||||
|
declaredDependenciesNode.loc!.start.line
|
||||||
|
}) ` +
|
||||||
`change on every render. ${advice}`;
|
`change on every render. ${advice}`;
|
||||||
|
|
||||||
let suggest;
|
let suggest: Rule.SuggestionReportDescriptor[] | undefined;
|
||||||
// Only handle the simple case of variable assignments.
|
// Only handle the simple case of variable assignments.
|
||||||
// Wrapping function declarations can mess up hoisting.
|
// Wrapping function declarations can mess up hoisting.
|
||||||
if (
|
if (
|
||||||
@ -782,17 +808,18 @@ export default {
|
|||||||
: ['useCallback(', ')'];
|
: ['useCallback(', ')'];
|
||||||
return [
|
return [
|
||||||
// TODO: also add an import?
|
// TODO: also add an import?
|
||||||
fixer.insertTextBefore(construction.node.init, before),
|
fixer.insertTextBefore(construction.node.init!, before),
|
||||||
// TODO: ideally we'd gather deps here but it would require
|
// TODO: ideally we'd gather deps here but it would require
|
||||||
// restructuring the rule code. This will cause a new lint
|
// restructuring the rule code. This will cause a new lint
|
||||||
// error to appear immediately for useCallback. Note we're
|
// error to appear immediately for useCallback. Note we're
|
||||||
// not adding [] because would that changes semantics.
|
// not adding [] because would that changes semantics.
|
||||||
fixer.insertTextAfter(construction.node.init, after),
|
fixer.insertTextAfter(construction.node.init!, after),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: What if the function needs to change on every render anyway?
|
// TODO: What if the function needs to change on every render anyway?
|
||||||
// Should we suggest removing effect deps as an appropriate fix too?
|
// Should we suggest removing effect deps as an appropriate fix too?
|
||||||
reportProblem({
|
reportProblem({
|
||||||
@ -811,7 +838,7 @@ export default {
|
|||||||
// in some extra deduplication. We can't do this
|
// in some extra deduplication. We can't do this
|
||||||
// for effects though because those have legit
|
// for effects though because those have legit
|
||||||
// use cases for over-specifying deps.
|
// use cases for over-specifying deps.
|
||||||
if (!isEffect && missingDependencies.size > 0) {
|
if (!isEffect && missingDependencies.size) {
|
||||||
suggestedDeps = collectRecommendations({
|
suggestedDeps = collectRecommendations({
|
||||||
dependencies,
|
dependencies,
|
||||||
declaredDependencies: [], // Pretend we don't know
|
declaredDependencies: [], // Pretend we don't know
|
||||||
@ -822,7 +849,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Alphabetize the suggestions, but only if deps were already alphabetized.
|
// Alphabetize the suggestions, but only if deps were already alphabetized.
|
||||||
function areDeclaredDepsAlphabetized() {
|
function areDeclaredDepsAlphabetized(): boolean {
|
||||||
if (declaredDependencies.length === 0) {
|
if (declaredDependencies.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -830,6 +857,7 @@ export default {
|
|||||||
const sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
|
const sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
|
||||||
return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
|
return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (areDeclaredDepsAlphabetized()) {
|
if (areDeclaredDepsAlphabetized()) {
|
||||||
suggestedDeps.sort();
|
suggestedDeps.sort();
|
||||||
}
|
}
|
||||||
@ -838,7 +866,7 @@ export default {
|
|||||||
// This function is the last step before printing a dependency, so now is a good time to
|
// This function is the last step before printing a dependency, so now is a good time to
|
||||||
// check whether any members in our path are always used as optional-only. In that case,
|
// check whether any members in our path are always used as optional-only. In that case,
|
||||||
// we will use ?. instead of . to concatenate those parts of the path.
|
// we will use ?. instead of . to concatenate those parts of the path.
|
||||||
function formatDependency(path) {
|
function formatDependency(path: string): string {
|
||||||
const members = path.split('.');
|
const members = path.split('.');
|
||||||
let finalPath = '';
|
let finalPath = '';
|
||||||
for (let i = 0; i < members.length; i++) {
|
for (let i = 0; i < members.length; i++) {
|
||||||
@ -852,7 +880,12 @@ export default {
|
|||||||
return finalPath;
|
return finalPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWarningMessage(deps, singlePrefix, label, fixVerb) {
|
function getWarningMessage(
|
||||||
|
deps: Set<string>,
|
||||||
|
singlePrefix: string,
|
||||||
|
label: string,
|
||||||
|
fixVerb: string,
|
||||||
|
): string | null {
|
||||||
if (deps.size === 0) {
|
if (deps.size === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -875,7 +908,7 @@ export default {
|
|||||||
|
|
||||||
let extraWarning = '';
|
let extraWarning = '';
|
||||||
if (unnecessaryDependencies.size > 0) {
|
if (unnecessaryDependencies.size > 0) {
|
||||||
let badRef = null;
|
let badRef: string | null = null;
|
||||||
Array.from(unnecessaryDependencies.keys()).forEach(key => {
|
Array.from(unnecessaryDependencies.keys()).forEach(key => {
|
||||||
if (badRef !== null) {
|
if (badRef !== null) {
|
||||||
return;
|
return;
|
||||||
@ -908,7 +941,7 @@ export default {
|
|||||||
if (propDep == null) {
|
if (propDep == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const refs = propDep.references;
|
const refs: Scope.Reference[] = propDep.references;
|
||||||
if (!Array.isArray(refs)) {
|
if (!Array.isArray(refs)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -942,17 +975,17 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!extraWarning && missingDependencies.size > 0) {
|
if (!extraWarning && missingDependencies.size) {
|
||||||
// See if the user is trying to avoid specifying a callable prop.
|
// See if the user is trying to avoid specifying a callable prop.
|
||||||
// This usually means they're unaware of useCallback.
|
// This usually means they're unaware of useCallback.
|
||||||
let missingCallbackDep = null;
|
let missingCallbackDep: string | null = null;
|
||||||
missingDependencies.forEach(missingDep => {
|
missingDependencies.forEach(missingDep => {
|
||||||
if (missingCallbackDep) {
|
if (missingCallbackDep) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Is this a variable from top scope?
|
// Is this a variable from top scope?
|
||||||
const topScopeRef = componentScope.set.get(missingDep);
|
const topScopeRef = componentScope.set.get(missingDep);
|
||||||
const usedDep = dependencies.get(missingDep);
|
const usedDep = dependencies.get(missingDep)!;
|
||||||
if (usedDep.references[0].resolved !== topScopeRef) {
|
if (usedDep.references[0].resolved !== topScopeRef) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -963,9 +996,9 @@ export default {
|
|||||||
}
|
}
|
||||||
// Was it called in at least one case? Then it's a function.
|
// Was it called in at least one case? Then it's a function.
|
||||||
let isFunctionCall = false;
|
let isFunctionCall = false;
|
||||||
let id;
|
let id: Identifier;
|
||||||
for (let i = 0; i < usedDep.references.length; i++) {
|
for (let i = 0; i < usedDep.references.length; i++) {
|
||||||
id = usedDep.references[i].identifier;
|
id = usedDep.references[i].identifier as Identifier;
|
||||||
if (
|
if (
|
||||||
id != null &&
|
id != null &&
|
||||||
id.parent != null &&
|
id.parent != null &&
|
||||||
@ -994,37 +1027,41 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!extraWarning && missingDependencies.size > 0) {
|
if (!extraWarning && missingDependencies.size > 0) {
|
||||||
let setStateRecommendation = null;
|
let setStateRecommendation: {
|
||||||
missingDependencies.forEach(missingDep => {
|
missingDep: string;
|
||||||
|
setter: string;
|
||||||
|
form: 'updater' | 'inlineReducer' | 'reducer';
|
||||||
|
} | null = null;
|
||||||
|
for (const missingDep of missingDependencies) {
|
||||||
if (setStateRecommendation !== null) {
|
if (setStateRecommendation !== null) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
const usedDep = dependencies.get(missingDep);
|
const usedDep = dependencies.get(missingDep)!;
|
||||||
const references = usedDep.references;
|
const references = usedDep.references;
|
||||||
let id;
|
let id: Identifier;
|
||||||
let maybeCall;
|
let maybeCall: Node | null;
|
||||||
for (let i = 0; i < references.length; i++) {
|
for (let i = 0; i < references.length; i++) {
|
||||||
id = references[i].identifier;
|
id = references[i].identifier as Identifier;
|
||||||
maybeCall = id.parent;
|
maybeCall = id.parent!;
|
||||||
// Try to see if we have setState(someExpr(missingDep)).
|
// Try to see if we have setState(someExpr(missingDep)).
|
||||||
while (maybeCall != null && maybeCall !== componentScope.block) {
|
while (maybeCall != null && maybeCall !== componentScope.block) {
|
||||||
if (maybeCall.type === 'CallExpression') {
|
if (maybeCall.type === 'CallExpression') {
|
||||||
const correspondingStateVariable = setStateCallSites.get(
|
const correspondingStateVariable = setStateCallSites.get(
|
||||||
maybeCall.callee,
|
maybeCall.callee as Expression,
|
||||||
);
|
);
|
||||||
if (correspondingStateVariable != null) {
|
if (correspondingStateVariable != null) {
|
||||||
if (correspondingStateVariable.name === missingDep) {
|
if ((correspondingStateVariable as Identifier).name === missingDep) {
|
||||||
// setCount(count + 1)
|
// setCount(count + 1)
|
||||||
setStateRecommendation = {
|
setStateRecommendation = {
|
||||||
missingDep,
|
missingDep,
|
||||||
setter: maybeCall.callee.name,
|
setter: (maybeCall.callee as Identifier).name,
|
||||||
form: 'updater',
|
form: 'updater',
|
||||||
};
|
};
|
||||||
} else if (stateVariables.has(id)) {
|
} else if (stateVariables.has(id)) {
|
||||||
// setCount(count + increment)
|
// setCount(count + increment)
|
||||||
setStateRecommendation = {
|
setStateRecommendation = {
|
||||||
missingDep,
|
missingDep,
|
||||||
setter: maybeCall.callee.name,
|
setter: (maybeCall.callee as Identifier).name,
|
||||||
form: 'reducer',
|
form: 'reducer',
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -1037,7 +1074,7 @@ export default {
|
|||||||
if (def != null && def.type === 'Parameter') {
|
if (def != null && def.type === 'Parameter') {
|
||||||
setStateRecommendation = {
|
setStateRecommendation = {
|
||||||
missingDep,
|
missingDep,
|
||||||
setter: maybeCall.callee.name,
|
setter: (maybeCall.callee as Identifier).name,
|
||||||
form: 'inlineReducer',
|
form: 'inlineReducer',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1046,13 +1083,14 @@ export default {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
maybeCall = maybeCall.parent;
|
maybeCall = maybeCall.parent!;
|
||||||
}
|
}
|
||||||
if (setStateRecommendation !== null) {
|
if (setStateRecommendation !== null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
if (setStateRecommendation !== null) {
|
if (setStateRecommendation !== null) {
|
||||||
switch (setStateRecommendation.form) {
|
switch (setStateRecommendation.form) {
|
||||||
case 'reducer':
|
case 'reducer':
|
||||||
@ -1110,15 +1148,16 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function visitCallExpression(node) {
|
function visitCallExpression(node: CallExpression): void {
|
||||||
const callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
|
const callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
|
||||||
if (callbackIndex === -1) {
|
if (callbackIndex === -1) {
|
||||||
// Not a React Hook call that needs deps.
|
// Not a React Hook call that needs deps.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const callback = node.arguments[callbackIndex];
|
const callback = node.arguments[callbackIndex];
|
||||||
const reactiveHook = node.callee;
|
const reactiveHook = node.callee as Identifier | MemberExpression;
|
||||||
const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
|
const reactiveHookName = (getNodeWithoutReactNamespace(reactiveHook) as Identifier)
|
||||||
|
.name;
|
||||||
const declaredDependenciesNode = node.arguments[callbackIndex + 1];
|
const declaredDependenciesNode = node.arguments[callbackIndex + 1];
|
||||||
const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
|
const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
|
||||||
|
|
||||||
@ -1172,8 +1211,8 @@ export default {
|
|||||||
// The function passed as a callback is not written inline.
|
// The function passed as a callback is not written inline.
|
||||||
// But perhaps it's in the dependencies array?
|
// But perhaps it's in the dependencies array?
|
||||||
if (
|
if (
|
||||||
declaredDependenciesNode.elements &&
|
declaredDependenciesNode.type === 'ArrayExpression' &&
|
||||||
declaredDependenciesNode.elements.some(
|
declaredDependenciesNode.elements?.some(
|
||||||
el => el && el.type === 'Identifier' && el.name === callback.name,
|
el => el && el.type === 'Identifier' && el.name === callback.name,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@ -1266,6 +1305,27 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
isStable: boolean;
|
||||||
|
references: Scope.Reference[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeclaredDependency {
|
||||||
|
key: string;
|
||||||
|
node: Expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DepTree {
|
||||||
|
/** True if used in code */
|
||||||
|
isUsed: boolean;
|
||||||
|
/** True if specified in deps */
|
||||||
|
isSatisfiedRecursively: boolean;
|
||||||
|
/** True if something deeper is used by code */
|
||||||
|
isSubtreeUsed: boolean;
|
||||||
|
/** Nodes for properties */
|
||||||
|
children: Map<string, DepTree>;
|
||||||
|
}
|
||||||
|
|
||||||
// The meat of the logic.
|
// The meat of the logic.
|
||||||
function collectRecommendations({
|
function collectRecommendations({
|
||||||
dependencies,
|
dependencies,
|
||||||
@ -1273,7 +1333,18 @@ function collectRecommendations({
|
|||||||
stableDependencies,
|
stableDependencies,
|
||||||
externalDependencies,
|
externalDependencies,
|
||||||
isEffect,
|
isEffect,
|
||||||
}) {
|
}: {
|
||||||
|
dependencies: Map<string, Dependencies>;
|
||||||
|
declaredDependencies: DeclaredDependency[];
|
||||||
|
stableDependencies: Set<string>;
|
||||||
|
externalDependencies: Set<string>;
|
||||||
|
isEffect: boolean;
|
||||||
|
}): {
|
||||||
|
suggestedDependencies: string[];
|
||||||
|
unnecessaryDependencies: Set<string>;
|
||||||
|
duplicateDependencies: Set<string>;
|
||||||
|
missingDependencies: Set<string>;
|
||||||
|
} {
|
||||||
// Our primary data structure.
|
// Our primary data structure.
|
||||||
// It is a logical representation of property chains:
|
// It is a logical representation of property chains:
|
||||||
// `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz`
|
// `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz`
|
||||||
@ -1284,12 +1355,13 @@ function collectRecommendations({
|
|||||||
// and the nodes that were *declared* as deps. Then we will
|
// and the nodes that were *declared* as deps. Then we will
|
||||||
// traverse it to learn which deps are missing or unnecessary.
|
// traverse it to learn which deps are missing or unnecessary.
|
||||||
const depTree = createDepTree();
|
const depTree = createDepTree();
|
||||||
function createDepTree() {
|
|
||||||
|
function createDepTree(): DepTree {
|
||||||
return {
|
return {
|
||||||
isUsed: false, // True if used in code
|
isUsed: false,
|
||||||
isSatisfiedRecursively: false, // True if specified in deps
|
isSatisfiedRecursively: false,
|
||||||
isSubtreeUsed: false, // True if something deeper is used by code
|
isSubtreeUsed: false,
|
||||||
children: new Map(), // Nodes for properties
|
children: new Map<string, never>(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1315,7 +1387,7 @@ function collectRecommendations({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Tree manipulation helpers.
|
// Tree manipulation helpers.
|
||||||
function getOrCreateNodeByPath(rootNode, path) {
|
function getOrCreateNodeByPath(rootNode: DepTree, path: string): DepTree {
|
||||||
const keys = path.split('.');
|
const keys = path.split('.');
|
||||||
let node = rootNode;
|
let node = rootNode;
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
@ -1328,7 +1400,12 @@ function collectRecommendations({
|
|||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
function markAllParentsByPath(rootNode, path, fn) {
|
|
||||||
|
function markAllParentsByPath(
|
||||||
|
rootNode: DepTree,
|
||||||
|
path: string,
|
||||||
|
fn: (depTree: DepTree) => void,
|
||||||
|
): void {
|
||||||
const keys = path.split('.');
|
const keys = path.split('.');
|
||||||
let node = rootNode;
|
let node = rootNode;
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
@ -1342,10 +1419,15 @@ function collectRecommendations({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now we can learn which dependencies are missing or necessary.
|
// Now we can learn which dependencies are missing or necessary.
|
||||||
const missingDependencies = new Set();
|
const missingDependencies = new Set<string>();
|
||||||
const satisfyingDependencies = new Set();
|
const satisfyingDependencies = new Set<string>();
|
||||||
scanTreeRecursively(depTree, missingDependencies, satisfyingDependencies, key => key);
|
scanTreeRecursively(depTree, missingDependencies, satisfyingDependencies, key => key);
|
||||||
function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) {
|
function scanTreeRecursively(
|
||||||
|
node: DepTree,
|
||||||
|
missingPaths: Set<string>,
|
||||||
|
satisfyingPaths: Set<string>,
|
||||||
|
keyToPath: (key: string) => string,
|
||||||
|
): void {
|
||||||
node.children.forEach((child, key) => {
|
node.children.forEach((child, key) => {
|
||||||
const path = keyToPath(key);
|
const path = keyToPath(key);
|
||||||
if (child.isSatisfiedRecursively) {
|
if (child.isSatisfiedRecursively) {
|
||||||
@ -1375,13 +1457,13 @@ function collectRecommendations({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collect suggestions in the order they were originally specified.
|
// Collect suggestions in the order they were originally specified.
|
||||||
const suggestedDependencies = [];
|
const suggestedDependencies: string[] = [];
|
||||||
const unnecessaryDependencies = new Set();
|
const unnecessaryDependencies = new Set<string>();
|
||||||
const duplicateDependencies = new Set();
|
const duplicateDependencies = new Set<string>();
|
||||||
declaredDependencies.forEach(({ key }) => {
|
declaredDependencies.forEach(({ key }) => {
|
||||||
// Does this declared dep satisfy a real need?
|
// Does this declared dep satisfy a real need?
|
||||||
if (satisfyingDependencies.has(key)) {
|
if (satisfyingDependencies.has(key)) {
|
||||||
if (suggestedDependencies.indexOf(key) === -1) {
|
if (!suggestedDependencies.includes(key)) {
|
||||||
// Good one.
|
// Good one.
|
||||||
suggestedDependencies.push(key);
|
suggestedDependencies.push(key);
|
||||||
} else {
|
} else {
|
||||||
@ -1419,7 +1501,7 @@ function collectRecommendations({
|
|||||||
|
|
||||||
// If the node will result in constructing a referentially unique value, return
|
// If the node will result in constructing a referentially unique value, return
|
||||||
// its human readable type name, else return null.
|
// its human readable type name, else return null.
|
||||||
function getConstructionExpressionType(node) {
|
function getConstructionExpressionType(node: Node) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'ObjectExpression':
|
case 'ObjectExpression':
|
||||||
return 'object';
|
return 'object';
|
||||||
@ -1477,6 +1559,11 @@ function scanForConstructions({
|
|||||||
declaredDependenciesNode,
|
declaredDependenciesNode,
|
||||||
componentScope,
|
componentScope,
|
||||||
scope,
|
scope,
|
||||||
|
}: {
|
||||||
|
declaredDependencies: DeclaredDependency[];
|
||||||
|
declaredDependenciesNode: Node;
|
||||||
|
componentScope: Scope.Scope;
|
||||||
|
scope: Scope.Scope;
|
||||||
}) {
|
}) {
|
||||||
const constructions = declaredDependencies
|
const constructions = declaredDependencies
|
||||||
.map(({ key }) => {
|
.map(({ key }) => {
|
||||||
@ -1502,23 +1589,23 @@ function scanForConstructions({
|
|||||||
) {
|
) {
|
||||||
const constantExpressionType = getConstructionExpressionType(node.node.init);
|
const constantExpressionType = getConstructionExpressionType(node.node.init);
|
||||||
if (constantExpressionType != null) {
|
if (constantExpressionType != null) {
|
||||||
return [ref, constantExpressionType];
|
return [ref, constantExpressionType] as const;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// function handleChange() {}
|
// function handleChange() {}
|
||||||
if (node.type === 'FunctionName' && node.node.type === 'FunctionDeclaration') {
|
if (node.type === 'FunctionName' && node.node.type === 'FunctionDeclaration') {
|
||||||
return [ref, 'function'];
|
return [ref, 'function'] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
// class Foo {}
|
// class Foo {}
|
||||||
if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') {
|
if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') {
|
||||||
return [ref, 'class'];
|
return [ref, 'class'] as const;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
function isUsedOutsideOfHook(ref) {
|
function isUsedOutsideOfHook(ref: Scope.Variable): boolean {
|
||||||
let foundWriteExpr = false;
|
let foundWriteExpr = false;
|
||||||
for (let i = 0; i < ref.references.length; i++) {
|
for (let i = 0; i < ref.references.length; i++) {
|
||||||
const reference = ref.references[i];
|
const reference = ref.references[i];
|
||||||
@ -1534,7 +1621,7 @@ function scanForConstructions({
|
|||||||
}
|
}
|
||||||
let currentScope = reference.from;
|
let currentScope = reference.from;
|
||||||
while (currentScope !== scope && currentScope != null) {
|
while (currentScope !== scope && currentScope != null) {
|
||||||
currentScope = currentScope.upper;
|
currentScope = currentScope.upper!;
|
||||||
}
|
}
|
||||||
if (currentScope !== scope) {
|
if (currentScope !== scope) {
|
||||||
// This reference is outside the Hook callback.
|
// This reference is outside the Hook callback.
|
||||||
@ -1561,21 +1648,22 @@ function scanForConstructions({
|
|||||||
* props.foo.(bar) => (props).foo.bar
|
* props.foo.(bar) => (props).foo.bar
|
||||||
* props.foo.bar.(baz) => (props).foo.bar.baz
|
* props.foo.bar.(baz) => (props).foo.bar.baz
|
||||||
*/
|
*/
|
||||||
function getDependency(node) {
|
function getDependency(node: Node): Node {
|
||||||
|
const parent = node.parent!;
|
||||||
if (
|
if (
|
||||||
(node.parent.type === 'MemberExpression' ||
|
(parent.type === 'MemberExpression' || parent.type === 'OptionalMemberExpression') &&
|
||||||
node.parent.type === 'OptionalMemberExpression') &&
|
parent.object === node &&
|
||||||
node.parent.object === node &&
|
parent.property.type === 'Identifier' &&
|
||||||
node.parent.property.name !== 'current' &&
|
parent.property.name !== 'current' &&
|
||||||
!node.parent.computed &&
|
!parent.computed &&
|
||||||
!(
|
!(
|
||||||
node.parent.parent != null &&
|
parent.parent != null &&
|
||||||
(node.parent.parent.type === 'CallExpression' ||
|
(parent.parent.type === 'CallExpression' ||
|
||||||
node.parent.parent.type === 'OptionalCallExpression') &&
|
parent.parent.type === 'OptionalCallExpression') &&
|
||||||
node.parent.parent.callee === node.parent
|
parent.parent.callee === parent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return getDependency(node.parent);
|
return getDependency(parent);
|
||||||
} else if (
|
} else if (
|
||||||
// Note: we don't check OptionalMemberExpression because it can't be LHS.
|
// Note: we don't check OptionalMemberExpression because it can't be LHS.
|
||||||
node.type === 'MemberExpression' &&
|
node.type === 'MemberExpression' &&
|
||||||
@ -1595,9 +1683,13 @@ function getDependency(node) {
|
|||||||
* It just means there is an optional member somewhere inside.
|
* It just means there is an optional member somewhere inside.
|
||||||
* This particular node might still represent a required member, so check .optional field.
|
* This particular node might still represent a required member, so check .optional field.
|
||||||
*/
|
*/
|
||||||
function markNode(node, optionalChains, result) {
|
function markNode(
|
||||||
|
node: Node,
|
||||||
|
optionalChains: Map<string, boolean> | null,
|
||||||
|
result: string,
|
||||||
|
): void {
|
||||||
if (optionalChains) {
|
if (optionalChains) {
|
||||||
if (node.optional) {
|
if ((node as OptionalMemberExpression).optional) {
|
||||||
// We only want to consider it optional if *all* usages were optional.
|
// We only want to consider it optional if *all* usages were optional.
|
||||||
if (!optionalChains.has(result)) {
|
if (!optionalChains.has(result)) {
|
||||||
// Mark as (maybe) optional. If there's a required usage, this will be overridden.
|
// Mark as (maybe) optional. If there's a required usage, this will be overridden.
|
||||||
@ -1617,7 +1709,10 @@ function markNode(node, optionalChains, result) {
|
|||||||
* foo.bar(.)baz -> 'foo.bar.baz'
|
* foo.bar(.)baz -> 'foo.bar.baz'
|
||||||
* Otherwise throw.
|
* Otherwise throw.
|
||||||
*/
|
*/
|
||||||
function analyzePropertyChain(node, optionalChains) {
|
function analyzePropertyChain(
|
||||||
|
node: Node,
|
||||||
|
optionalChains: Map<string, boolean> | null,
|
||||||
|
): string {
|
||||||
if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
|
if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
|
||||||
const result = node.name;
|
const result = node.name;
|
||||||
if (optionalChains) {
|
if (optionalChains) {
|
||||||
@ -1654,7 +1749,7 @@ function analyzePropertyChain(node, optionalChains) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNodeWithoutReactNamespace(node, options) {
|
function getNodeWithoutReactNamespace(node: Identifier | MemberExpression) {
|
||||||
if (
|
if (
|
||||||
node.type === 'MemberExpression' &&
|
node.type === 'MemberExpression' &&
|
||||||
node.object.type === 'Identifier' &&
|
node.object.type === 'Identifier' &&
|
||||||
@ -1672,7 +1767,13 @@ function getNodeWithoutReactNamespace(node, options) {
|
|||||||
// 0 for useEffect/useMemo/useCallback(fn).
|
// 0 for useEffect/useMemo/useCallback(fn).
|
||||||
// 1 for useImperativeHandle(ref, fn).
|
// 1 for useImperativeHandle(ref, fn).
|
||||||
// For additionally configured Hooks, assume that they're like useEffect (0).
|
// For additionally configured Hooks, assume that they're like useEffect (0).
|
||||||
function getReactiveHookCallbackIndex(calleeNode, options) {
|
function getReactiveHookCallbackIndex(
|
||||||
|
calleeNode: Expression | Super,
|
||||||
|
options: {
|
||||||
|
additionalHooks?: RegExp;
|
||||||
|
enableDangerousAutofixThisMayCauseInfiniteLoops: boolean;
|
||||||
|
},
|
||||||
|
): 0 | 1 | -1 {
|
||||||
const node = getNodeWithoutReactNamespace(calleeNode);
|
const node = getNodeWithoutReactNamespace(calleeNode);
|
||||||
if (node.type !== 'Identifier') {
|
if (node.type !== 'Identifier') {
|
||||||
return -1;
|
return -1;
|
||||||
@ -1718,12 +1819,12 @@ function getReactiveHookCallbackIndex(calleeNode, options) {
|
|||||||
* - optimized by only searching nodes with a range surrounding our target node
|
* - optimized by only searching nodes with a range surrounding our target node
|
||||||
* - agnostic to AST node types, it looks for `{ type: string, ... }`
|
* - agnostic to AST node types, it looks for `{ type: string, ... }`
|
||||||
*/
|
*/
|
||||||
function fastFindReferenceWithParent(start, target) {
|
function fastFindReferenceWithParent(start: Node, target: Node): Node | null {
|
||||||
const queue = [start];
|
const queue = [start];
|
||||||
let item = null;
|
let item: Node;
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
item = queue.shift();
|
item = queue.shift()!;
|
||||||
|
|
||||||
if (isSameIdentifier(item, target)) {
|
if (isSameIdentifier(item, target)) {
|
||||||
return item;
|
return item;
|
||||||
@ -1754,7 +1855,7 @@ function fastFindReferenceWithParent(start, target) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinEnglish(arr) {
|
function joinEnglish(arr: string[]): string {
|
||||||
let s = '';
|
let s = '';
|
||||||
for (let i = 0; i < arr.length; i++) {
|
for (let i = 0; i < arr.length; i++) {
|
||||||
s += arr[i];
|
s += arr[i];
|
||||||
@ -1769,7 +1870,7 @@ function joinEnglish(arr) {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNodeLike(val) {
|
function isNodeLike(val: any): boolean {
|
||||||
return (
|
return (
|
||||||
typeof val === 'object' &&
|
typeof val === 'object' &&
|
||||||
val !== null &&
|
val !== null &&
|
||||||
@ -1778,23 +1879,25 @@ function isNodeLike(val) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSameIdentifier(a, b) {
|
function isSameIdentifier(a: Node, b: Node): boolean {
|
||||||
return (
|
return (
|
||||||
(a.type === 'Identifier' || a.type === 'JSXIdentifier') &&
|
(a.type === 'Identifier' || a.type === 'JSXIdentifier') &&
|
||||||
a.type === b.type &&
|
a.type === b.type &&
|
||||||
a.name === b.name &&
|
a.name === b.name &&
|
||||||
a.range[0] === b.range[0] &&
|
a.range![0] === b.range![0] &&
|
||||||
a.range[1] === b.range[1]
|
a.range![1] === b.range![1]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAncestorNodeOf(a, b) {
|
function isAncestorNodeOf(a: Node, b: Node): boolean {
|
||||||
return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
|
return a.range![0] <= b.range![0] && a.range![1] >= b.range![1];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUseEffectEventIdentifier(node) {
|
function isUseEffectEventIdentifier(node: Node): boolean {
|
||||||
if (__EXPERIMENTAL__) {
|
if (__EXPERIMENTAL__) {
|
||||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default rule;
|
||||||
|
@ -7,15 +7,23 @@
|
|||||||
|
|
||||||
/* global BigInt */
|
/* global BigInt */
|
||||||
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
||||||
|
import type { Rule, Scope } from 'eslint';
|
||||||
'use strict';
|
import type {
|
||||||
|
CallExpression,
|
||||||
|
Expression,
|
||||||
|
Super,
|
||||||
|
Node,
|
||||||
|
Identifier,
|
||||||
|
BaseFunction,
|
||||||
|
} from 'estree';
|
||||||
|
import { __EXPERIMENTAL__ } from './index';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
||||||
* character to exclude identifiers like "user".
|
* character to exclude identifiers like "user".
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function isHookName(s) {
|
function isHookName(s: string) {
|
||||||
if (__EXPERIMENTAL__) {
|
if (__EXPERIMENTAL__) {
|
||||||
return s === 'use' || /^use[A-Z0-9]/.test(s);
|
return s === 'use' || /^use[A-Z0-9]/.test(s);
|
||||||
}
|
}
|
||||||
@ -26,8 +34,7 @@ function isHookName(s) {
|
|||||||
* We consider hooks to be a hook name identifier or a member expression
|
* We consider hooks to be a hook name identifier or a member expression
|
||||||
* containing a hook name.
|
* containing a hook name.
|
||||||
*/
|
*/
|
||||||
|
function isHook(node: Node) {
|
||||||
function isHook(node) {
|
|
||||||
if (node.type === 'Identifier') {
|
if (node.type === 'Identifier') {
|
||||||
return isHookName(node.name);
|
return isHookName(node.name);
|
||||||
} else if (
|
} else if (
|
||||||
@ -48,16 +55,16 @@ function isHook(node) {
|
|||||||
* always start with an uppercase letter.
|
* always start with an uppercase letter.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function isComponentName(node) {
|
function isComponentName(node: Node) {
|
||||||
return node.type === 'Identifier' && /^[A-Z]/.test(node.name);
|
return node.type === 'Identifier' && /^[A-Z]/.test(node.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isReactFunction(node, functionName) {
|
function isReactFunction(node: Expression | Super, functionName: string) {
|
||||||
return (
|
return (
|
||||||
node.name === functionName ||
|
(node as Identifier).name === functionName ||
|
||||||
(node.type === 'MemberExpression' &&
|
(node.type === 'MemberExpression' &&
|
||||||
node.object.name === 'React' &&
|
(node.object as Identifier).name === 'React' &&
|
||||||
node.property.name === functionName)
|
(node.property as Identifier).name === functionName)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,12 +72,10 @@ function isReactFunction(node, functionName) {
|
|||||||
* Checks if the node is a callback argument of forwardRef. This render function
|
* Checks if the node is a callback argument of forwardRef. This render function
|
||||||
* should follow the rules of hooks.
|
* should follow the rules of hooks.
|
||||||
*/
|
*/
|
||||||
|
function isForwardRefCallback(node: Rule.Node) {
|
||||||
function isForwardRefCallback(node) {
|
|
||||||
return !!(
|
return !!(
|
||||||
node.parent &&
|
(node.parent as CallExpression)?.callee &&
|
||||||
node.parent.callee &&
|
isReactFunction((node.parent as CallExpression).callee, 'forwardRef')
|
||||||
isReactFunction(node.parent.callee, 'forwardRef')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,15 +84,14 @@ function isForwardRefCallback(node) {
|
|||||||
* functional component should follow the rules of hooks.
|
* functional component should follow the rules of hooks.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function isMemoCallback(node) {
|
function isMemoCallback(node: Rule.Node) {
|
||||||
return !!(
|
return !!(
|
||||||
node.parent &&
|
(node.parent as CallExpression)?.callee &&
|
||||||
node.parent.callee &&
|
isReactFunction((node.parent as CallExpression).callee, 'memo')
|
||||||
isReactFunction(node.parent.callee, 'memo')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInsideComponentOrHook(node) {
|
function isInsideComponentOrHook(node: Rule.Node) {
|
||||||
while (node) {
|
while (node) {
|
||||||
const functionName = getFunctionName(node);
|
const functionName = getFunctionName(node);
|
||||||
if (functionName) {
|
if (functionName) {
|
||||||
@ -103,21 +107,21 @@ function isInsideComponentOrHook(node) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUseEffectEventIdentifier(node) {
|
function isUseEffectEventIdentifier(node: Node) {
|
||||||
if (__EXPERIMENTAL__) {
|
if (__EXPERIMENTAL__) {
|
||||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUseIdentifier(node) {
|
function isUseIdentifier(node: Node) {
|
||||||
if (__EXPERIMENTAL__) {
|
if (__EXPERIMENTAL__) {
|
||||||
return node.type === 'Identifier' && node.name === 'use';
|
return node.type === 'Identifier' && node.name === 'use';
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const rule: Rule.RuleModule = {
|
||||||
meta: {
|
meta: {
|
||||||
type: 'problem',
|
type: 'problem',
|
||||||
docs: {
|
docs: {
|
||||||
@ -127,17 +131,20 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
create(context) {
|
create(context) {
|
||||||
let lastEffect = null;
|
let lastEffect: CallExpression | null = null;
|
||||||
const codePathReactHooksMapStack = [];
|
const codePathReactHooksMapStack: Map<
|
||||||
const codePathSegmentStack = [];
|
Rule.CodePathSegment,
|
||||||
const useEffectEventFunctions = new WeakSet();
|
(Expression | Super)[]
|
||||||
|
>[] = [];
|
||||||
|
const codePathSegmentStack: Rule.CodePathSegment[] = [];
|
||||||
|
const useEffectEventFunctions = new WeakSet<Identifier>();
|
||||||
|
|
||||||
// For a given scope, iterate through the references and add all useEffectEvent definitions. We can
|
// For a given scope, iterate through the references and add all useEffectEvent definitions. We can
|
||||||
// do this in non-Program nodes because we can rely on the assumption that useEffectEvent functions
|
// do this in non-Program nodes because we can rely on the assumption that useEffectEvent functions
|
||||||
// can only be declared within a component or hook at its top level.
|
// can only be declared within a component or hook at its top level.
|
||||||
function recordAllUseEffectEventFunctions(scope) {
|
function recordAllUseEffectEventFunctions(scope: Scope.Scope) {
|
||||||
for (const reference of scope.references) {
|
for (const reference of scope.references) {
|
||||||
const parent = reference.identifier.parent;
|
const parent = reference.identifier.parent!;
|
||||||
if (
|
if (
|
||||||
parent.type === 'VariableDeclarator' &&
|
parent.type === 'VariableDeclarator' &&
|
||||||
parent.init &&
|
parent.init &&
|
||||||
@ -145,7 +152,7 @@ export default {
|
|||||||
parent.init.callee &&
|
parent.init.callee &&
|
||||||
isUseEffectEventIdentifier(parent.init.callee)
|
isUseEffectEventIdentifier(parent.init.callee)
|
||||||
) {
|
) {
|
||||||
for (const ref of reference.resolved.references) {
|
for (const ref of reference.resolved!.references) {
|
||||||
if (ref !== reference) {
|
if (ref !== reference) {
|
||||||
useEffectEventFunctions.add(ref.identifier);
|
useEffectEventFunctions.add(ref.identifier);
|
||||||
}
|
}
|
||||||
@ -167,7 +174,7 @@ export default {
|
|||||||
// Everything is ok if all React Hooks are both reachable from the initial
|
// Everything is ok if all React Hooks are both reachable from the initial
|
||||||
// segment and reachable from every final segment.
|
// segment and reachable from every final segment.
|
||||||
onCodePathEnd(codePath, codePathNode) {
|
onCodePathEnd(codePath, codePathNode) {
|
||||||
const reactHooksMap = codePathReactHooksMapStack.pop();
|
const reactHooksMap = codePathReactHooksMapStack.pop()!;
|
||||||
if (reactHooksMap.size === 0) {
|
if (reactHooksMap.size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -197,7 +204,10 @@ export default {
|
|||||||
* Populates `cyclic` with cyclic segments.
|
* Populates `cyclic` with cyclic segments.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function countPathsFromStart(segment, pathHistory) {
|
function countPathsFromStart(
|
||||||
|
segment: Rule.CodePathSegment,
|
||||||
|
pathHistory?: Set<string>,
|
||||||
|
) {
|
||||||
const { cache } = countPathsFromStart;
|
const { cache } = countPathsFromStart;
|
||||||
let paths = cache.get(segment.id);
|
let paths = cache.get(segment.id);
|
||||||
const pathList = new Set(pathHistory);
|
const pathList = new Set(pathHistory);
|
||||||
@ -211,7 +221,7 @@ export default {
|
|||||||
cyclic.add(cyclicSegment);
|
cyclic.add(cyclicSegment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BigInt('0');
|
return 0n;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the current segment to pathList
|
// add the current segment to pathList
|
||||||
@ -223,11 +233,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (codePath.thrownSegments.includes(segment)) {
|
if (codePath.thrownSegments.includes(segment)) {
|
||||||
paths = BigInt('0');
|
paths = 0n;
|
||||||
} else if (segment.prevSegments.length === 0) {
|
} else if (segment.prevSegments.length === 0) {
|
||||||
paths = BigInt('1');
|
paths = 1n;
|
||||||
} else {
|
} else {
|
||||||
paths = BigInt('0');
|
paths = 0n;
|
||||||
for (const prevSegment of segment.prevSegments) {
|
for (const prevSegment of segment.prevSegments) {
|
||||||
paths += countPathsFromStart(prevSegment, pathList);
|
paths += countPathsFromStart(prevSegment, pathList);
|
||||||
}
|
}
|
||||||
@ -266,7 +276,10 @@ export default {
|
|||||||
* Populates `cyclic` with cyclic segments.
|
* Populates `cyclic` with cyclic segments.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function countPathsToEnd(segment, pathHistory) {
|
function countPathsToEnd(
|
||||||
|
segment: Rule.CodePathSegment,
|
||||||
|
pathHistory?: Set<string>,
|
||||||
|
): bigint {
|
||||||
const { cache } = countPathsToEnd;
|
const { cache } = countPathsToEnd;
|
||||||
let paths = cache.get(segment.id);
|
let paths = cache.get(segment.id);
|
||||||
const pathList = new Set(pathHistory);
|
const pathList = new Set(pathHistory);
|
||||||
@ -280,7 +293,7 @@ export default {
|
|||||||
cyclic.add(cyclicSegment);
|
cyclic.add(cyclicSegment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BigInt('0');
|
return 0n;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the current segment to pathList
|
// add the current segment to pathList
|
||||||
@ -292,11 +305,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (codePath.thrownSegments.includes(segment)) {
|
if (codePath.thrownSegments.includes(segment)) {
|
||||||
paths = BigInt('0');
|
paths = 0n;
|
||||||
} else if (segment.nextSegments.length === 0) {
|
} else if (segment.nextSegments.length === 0) {
|
||||||
paths = BigInt('1');
|
paths = 1n;
|
||||||
} else {
|
} else {
|
||||||
paths = BigInt('0');
|
paths = 0n;
|
||||||
for (const nextSegment of segment.nextSegments) {
|
for (const nextSegment of segment.nextSegments) {
|
||||||
paths += countPathsToEnd(nextSegment, pathList);
|
paths += countPathsToEnd(nextSegment, pathList);
|
||||||
}
|
}
|
||||||
@ -328,7 +341,7 @@ export default {
|
|||||||
* so we would return that.
|
* so we would return that.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function shortestPathLengthToStart(segment) {
|
function shortestPathLengthToStart(segment: Rule.CodePathSegment): number {
|
||||||
const { cache } = shortestPathLengthToStart;
|
const { cache } = shortestPathLengthToStart;
|
||||||
let length = cache.get(segment.id);
|
let length = cache.get(segment.id);
|
||||||
|
|
||||||
@ -361,9 +374,9 @@ export default {
|
|||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
countPathsFromStart.cache = new Map();
|
countPathsFromStart.cache = new Map<string, bigint>();
|
||||||
countPathsToEnd.cache = new Map();
|
countPathsToEnd.cache = new Map<string, bigint>();
|
||||||
shortestPathLengthToStart.cache = new Map();
|
shortestPathLengthToStart.cache = new Map<string, number | null>();
|
||||||
|
|
||||||
// Count all code paths to the end of our component/hook. Also primes
|
// Count all code paths to the end of our component/hook. Also primes
|
||||||
// the `countPathsToEnd` cache.
|
// the `countPathsToEnd` cache.
|
||||||
@ -480,7 +493,7 @@ export default {
|
|||||||
// called in.
|
// called in.
|
||||||
if (isDirectlyInsideComponentOrHook) {
|
if (isDirectlyInsideComponentOrHook) {
|
||||||
// Report an error if the hook is called inside an async function.
|
// Report an error if the hook is called inside an async function.
|
||||||
const isAsyncFunction = codePathNode.async;
|
const isAsyncFunction = (codePathNode as BaseFunction).async;
|
||||||
if (isAsyncFunction) {
|
if (isAsyncFunction) {
|
||||||
context.report({
|
context.report({
|
||||||
node: hook,
|
node: hook,
|
||||||
@ -565,8 +578,8 @@ export default {
|
|||||||
if (isHook(node.callee)) {
|
if (isHook(node.callee)) {
|
||||||
// Add the hook node to a map keyed by the code path segment. We will
|
// Add the hook node to a map keyed by the code path segment. We will
|
||||||
// do full code path analysis at the end of our code path.
|
// do full code path analysis at the end of our code path.
|
||||||
const reactHooksMap = last(codePathReactHooksMapStack);
|
const reactHooksMap = codePathReactHooksMapStack.at(-1)!;
|
||||||
const codePathSegment = last(codePathSegmentStack);
|
const codePathSegment = codePathSegmentStack.at(-1)!;
|
||||||
let reactHooks = reactHooksMap.get(codePathSegment);
|
let reactHooks = reactHooksMap.get(codePathSegment);
|
||||||
if (!reactHooks) {
|
if (!reactHooks) {
|
||||||
reactHooks = [];
|
reactHooks = [];
|
||||||
@ -637,8 +650,8 @@ export default {
|
|||||||
* where JS gives anonymous function expressions names. We roughly detect the
|
* where JS gives anonymous function expressions names. We roughly detect the
|
||||||
* same AST nodes with some exceptions to better fit our use case.
|
* same AST nodes with some exceptions to better fit our use case.
|
||||||
*/
|
*/
|
||||||
|
function getFunctionName(node: Node) {
|
||||||
function getFunctionName(node) {
|
const parent = node.parent!;
|
||||||
if (
|
if (
|
||||||
node.type === 'FunctionDeclaration' ||
|
node.type === 'FunctionDeclaration' ||
|
||||||
(node.type === 'FunctionExpression' && node.id)
|
(node.type === 'FunctionExpression' && node.id)
|
||||||
@ -653,24 +666,20 @@ function getFunctionName(node) {
|
|||||||
node.type === 'FunctionExpression' ||
|
node.type === 'FunctionExpression' ||
|
||||||
node.type === 'ArrowFunctionExpression'
|
node.type === 'ArrowFunctionExpression'
|
||||||
) {
|
) {
|
||||||
if (node.parent.type === 'VariableDeclarator' && node.parent.init === node) {
|
if (parent.type === 'VariableDeclarator' && parent.init === node) {
|
||||||
// const useHook = () => {};
|
// const useHook = () => {};
|
||||||
return node.parent.id;
|
return parent.id;
|
||||||
} else if (
|
} else if (
|
||||||
node.parent.type === 'AssignmentExpression' &&
|
parent.type === 'AssignmentExpression' &&
|
||||||
node.parent.right === node &&
|
parent.right === node &&
|
||||||
node.parent.operator === '='
|
parent.operator === '='
|
||||||
) {
|
) {
|
||||||
// useHook = () => {};
|
// useHook = () => {};
|
||||||
return node.parent.left;
|
return parent.left;
|
||||||
} else if (
|
} else if (parent.type === 'Property' && parent.value === node && !parent.computed) {
|
||||||
node.parent.type === 'Property' &&
|
|
||||||
node.parent.value === node &&
|
|
||||||
!node.parent.computed
|
|
||||||
) {
|
|
||||||
// {useHook: () => {}}
|
// {useHook: () => {}}
|
||||||
// {useHook() {}}
|
// {useHook() {}}
|
||||||
return node.parent.key;
|
return parent.key;
|
||||||
|
|
||||||
// NOTE: We could also support `ClassProperty` and `MethodDefinition`
|
// NOTE: We could also support `ClassProperty` and `MethodDefinition`
|
||||||
// here to be pedantic. However, hooks in a class are an anti-pattern. So
|
// here to be pedantic. However, hooks in a class are an anti-pattern. So
|
||||||
@ -679,16 +688,16 @@ function getFunctionName(node) {
|
|||||||
// class {useHook = () => {}}
|
// class {useHook = () => {}}
|
||||||
// class {useHook() {}}
|
// class {useHook() {}}
|
||||||
} else if (
|
} else if (
|
||||||
node.parent.type === 'AssignmentPattern' &&
|
parent.type === 'AssignmentPattern' &&
|
||||||
node.parent.right === node &&
|
parent.right === node &&
|
||||||
!node.parent.computed
|
!parent.computed
|
||||||
) {
|
) {
|
||||||
// const {useHook = () => {}} = {};
|
// const {useHook = () => {}} = {};
|
||||||
// ({useHook = () => {}} = {});
|
// ({useHook = () => {}} = {});
|
||||||
//
|
//
|
||||||
// Kinda clowny, but we'd said we'd follow spec convention for
|
// Kinda clowny, but we'd said we'd follow spec convention for
|
||||||
// `IsAnonymousFunctionDefinition()` usage.
|
// `IsAnonymousFunctionDefinition()` usage.
|
||||||
return node.parent.left;
|
return parent.left;
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -697,10 +706,4 @@ function getFunctionName(node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export default rule;
|
||||||
* Convenience function for peeking the last item in a stack.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function last(array) {
|
|
||||||
return array[array.length - 1];
|
|
||||||
}
|
|
||||||
|
@ -4,10 +4,12 @@
|
|||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
import type { Linter } from 'eslint';
|
||||||
import RulesOfHooks from './RulesOfHooks';
|
import RulesOfHooks from './RulesOfHooks';
|
||||||
import ExhaustiveDeps from './ExhaustiveDeps';
|
import ExhaustiveDeps from './ExhaustiveDeps';
|
||||||
|
|
||||||
|
export const __EXPERIMENTAL__ = false;
|
||||||
|
|
||||||
export const configs = {
|
export const configs = {
|
||||||
recommended: {
|
recommended: {
|
||||||
plugins: ['react-hooks'],
|
plugins: ['react-hooks'],
|
||||||
@ -15,7 +17,7 @@ export const configs = {
|
|||||||
'react-hooks/rules-of-hooks': 'error',
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
'react-hooks/exhaustive-deps': 'warn',
|
'react-hooks/exhaustive-deps': 'warn',
|
||||||
},
|
},
|
||||||
},
|
} as Linter.BaseConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rules = {
|
export const rules = {
|
||||||
|
12
eslint-plugin-react-hooks/package.json
Normal file
12
eslint-plugin-react-hooks/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"upstream": {
|
||||||
|
"version": 1,
|
||||||
|
"sources": {
|
||||||
|
"main": {
|
||||||
|
"repository": "git@github.com:facebook/react.git",
|
||||||
|
"commit": "899cb95f52cc83ab5ca1eb1e268c909d3f0961e7",
|
||||||
|
"branch": "main"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
eslint-plugin-react-hooks/types.d.ts
vendored
Normal file
65
eslint-plugin-react-hooks/types.d.ts
vendored
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { BaseNode } from 'estree';
|
||||||
|
|
||||||
|
declare module 'eslint' {
|
||||||
|
namespace Rule {
|
||||||
|
interface RuleContext {
|
||||||
|
getSource(node: BaseNode): string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'estree' {
|
||||||
|
interface BaseNodeWithoutComments {
|
||||||
|
parent?: Node;
|
||||||
|
}
|
||||||
|
interface NodeMap {
|
||||||
|
OptionalCallExpression: OptionalCallExpression;
|
||||||
|
OptionalMemberExpression: OptionalMemberExpression;
|
||||||
|
TSAsExpression: TSAsExpression;
|
||||||
|
TSTypeQuery: TSTypeQuery;
|
||||||
|
TSTypeReference: TSTypeReference;
|
||||||
|
TypeCastExpression: TypeCastExpression;
|
||||||
|
}
|
||||||
|
interface ExpressionMap {
|
||||||
|
OptionalCallExpression: OptionalCallExpression;
|
||||||
|
OptionalMemberExpression: OptionalMemberExpression;
|
||||||
|
TSAsExpression: TSAsExpression;
|
||||||
|
TypeCastExpression: TypeCastExpression;
|
||||||
|
}
|
||||||
|
interface AssignmentPattern {
|
||||||
|
computed?: boolean;
|
||||||
|
}
|
||||||
|
interface ChainExpression {
|
||||||
|
computed?: boolean;
|
||||||
|
}
|
||||||
|
interface TypeCastExpression extends BaseNode {
|
||||||
|
type: 'TypeCastExpression';
|
||||||
|
expression: Expression;
|
||||||
|
}
|
||||||
|
interface TSAsExpression extends BaseNode {
|
||||||
|
type: 'TSAsExpression';
|
||||||
|
expression: Expression;
|
||||||
|
}
|
||||||
|
interface TSTypeQuery extends BaseNode {
|
||||||
|
type: 'TSTypeQuery';
|
||||||
|
}
|
||||||
|
interface TSTypeReference extends BaseNode {
|
||||||
|
type: 'TSTypeReference';
|
||||||
|
}
|
||||||
|
/** @deprecated flow only */
|
||||||
|
interface TypeParameter extends BaseNode {
|
||||||
|
type: 'TypeParameter';
|
||||||
|
}
|
||||||
|
interface OptionalMemberExpression extends BaseNode {
|
||||||
|
type: 'OptionalMemberExpression';
|
||||||
|
object: Expression | Super;
|
||||||
|
property: Expression | PrivateIdentifier;
|
||||||
|
computed: boolean;
|
||||||
|
optional: boolean;
|
||||||
|
}
|
||||||
|
interface OptionalCallExpression extends BaseNode {
|
||||||
|
type: 'OptionalCallExpression';
|
||||||
|
callee: Expression | Super;
|
||||||
|
arguments: (Expression | SpreadElement)[];
|
||||||
|
}
|
||||||
|
}
|
14
package.json
14
package.json
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"name": "@aet/eslint-configs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "./esbuild.ts",
|
"build": "./esbuild.ts",
|
||||||
"check-import": "for js in dist/*.js; do cat $js | grep 'require('; done"
|
"check-import": "for js in dist/*.js; do cat $js | grep 'require('; done"
|
||||||
@ -10,17 +11,28 @@
|
|||||||
"@babel/preset-env": "^7.22.9",
|
"@babel/preset-env": "^7.22.9",
|
||||||
"@types/babel-plugin-macros": "^3.1.0",
|
"@types/babel-plugin-macros": "^3.1.0",
|
||||||
"@types/babel__core": "^7.20.1",
|
"@types/babel__core": "^7.20.1",
|
||||||
|
"@types/eslint": "^8.44.0",
|
||||||
|
"@types/estree": "^1.0.1",
|
||||||
|
"@types/estree-jsx": "^1.0.0",
|
||||||
|
"@types/lodash": "^4.14.195",
|
||||||
"@types/node": "^20.4.2",
|
"@types/node": "^20.4.2",
|
||||||
|
"@typescript-eslint/types": "^6.1.0",
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
|
"dts-bundle-generator": "^8.0.1",
|
||||||
"esbin": "0.0.1-beta.1",
|
"esbin": "0.0.1-beta.1",
|
||||||
"esbuild": "0.18.14",
|
"esbuild": "0.18.14",
|
||||||
"esbuild-plugin-alias": "^0.2.1",
|
"esbuild-plugin-alias": "^0.2.1",
|
||||||
"esbuild-register": "3.4.2",
|
"esbuild-register": "3.4.2",
|
||||||
"eslint": "8.45.0",
|
"eslint": "8.45.0",
|
||||||
"eslint-config-prettier": "8.8.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
|
"eslint-define-config": "^1.21.0",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"glob": "^10.3.3",
|
"glob": "^10.3.3",
|
||||||
"prettier": "^3.0.0"
|
"json-schema-to-ts": "^2.9.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
|
@ -277,6 +277,116 @@ index 709a4744..00000000
|
|||||||
- },
|
- },
|
||||||
- ],
|
- ],
|
||||||
-}
|
-}
|
||||||
|
diff --git a/config/electron.js b/config/electron.js
|
||||||
|
index f98ff061..0f3aa51d 100644
|
||||||
|
--- a/config/electron.js
|
||||||
|
+++ b/config/electron.js
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Default settings for Electron applications.
|
||||||
|
*/
|
||||||
|
-module.exports = {
|
||||||
|
+export default {
|
||||||
|
settings: {
|
||||||
|
'import/core-modules': ['electron'],
|
||||||
|
},
|
||||||
|
diff --git a/config/errors.js b/config/errors.js
|
||||||
|
index 127c29a0..b46a4c0b 100644
|
||||||
|
--- a/config/errors.js
|
||||||
|
+++ b/config/errors.js
|
||||||
|
@@ -1,9 +1,8 @@
|
||||||
|
/**
|
||||||
|
* unopinionated config. just the things that are necessarily runtime errors
|
||||||
|
* waiting to happen.
|
||||||
|
- * @type {Object}
|
||||||
|
*/
|
||||||
|
-module.exports = {
|
||||||
|
+export default {
|
||||||
|
plugins: ['import'],
|
||||||
|
rules: { 'import/no-unresolved': 2,
|
||||||
|
'import/named': 2,
|
||||||
|
diff --git a/config/react-native.js b/config/react-native.js
|
||||||
|
index a1aa0ee5..97bdf0cf 100644
|
||||||
|
--- a/config/react-native.js
|
||||||
|
+++ b/config/react-native.js
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
/**
|
||||||
|
* - adds platform extensions to Node resolver
|
||||||
|
*/
|
||||||
|
-module.exports = {
|
||||||
|
+export default {
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
node: {
|
||||||
|
diff --git a/config/react.js b/config/react.js
|
||||||
|
index 68555512..8e090a83 100644
|
||||||
|
--- a/config/react.js
|
||||||
|
+++ b/config/react.js
|
||||||
|
@@ -5,7 +5,7 @@
|
||||||
|
* define jsnext:main and have JSX internally, you may run into problems
|
||||||
|
* if you don't enable these settings at the top level.
|
||||||
|
*/
|
||||||
|
-module.exports = {
|
||||||
|
+export default {
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
'import/extensions': ['.js', '.jsx'],
|
||||||
|
diff --git a/config/recommended.js b/config/recommended.js
|
||||||
|
index 8e7ca9fd..9ced8146 100644
|
||||||
|
--- a/config/recommended.js
|
||||||
|
+++ b/config/recommended.js
|
||||||
|
@@ -1,8 +1,7 @@
|
||||||
|
/**
|
||||||
|
* The basics.
|
||||||
|
- * @type {Object}
|
||||||
|
*/
|
||||||
|
-module.exports = {
|
||||||
|
+export default {
|
||||||
|
plugins: ['import'],
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
diff --git a/config/stage-0.js b/config/stage-0.js
|
||||||
|
index 42419123..01ebeeb8 100644
|
||||||
|
--- a/config/stage-0.js
|
||||||
|
+++ b/config/stage-0.js
|
||||||
|
@@ -2,9 +2,8 @@
|
||||||
|
* Rules in progress.
|
||||||
|
*
|
||||||
|
* Do not expect these to adhere to semver across releases.
|
||||||
|
- * @type {Object}
|
||||||
|
*/
|
||||||
|
-module.exports = {
|
||||||
|
+export default {
|
||||||
|
plugins: ['import'],
|
||||||
|
rules: {
|
||||||
|
'import/no-deprecated': 1,
|
||||||
|
diff --git a/config/typescript.js b/config/typescript.js
|
||||||
|
index 9fd789db..c277b6c5 100644
|
||||||
|
--- a/config/typescript.js
|
||||||
|
+++ b/config/typescript.js
|
||||||
|
@@ -7,7 +7,7 @@
|
||||||
|
// `.ts`/`.tsx`/`.js`/`.jsx` implementation.
|
||||||
|
const allExtensions = ['.ts', '.tsx', '.js', '.jsx'];
|
||||||
|
|
||||||
|
-module.exports = {
|
||||||
|
+export default {
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
'import/extensions': allExtensions,
|
||||||
|
diff --git a/config/warnings.js b/config/warnings.js
|
||||||
|
index 5d74143b..ffa27d8d 100644
|
||||||
|
--- a/config/warnings.js
|
||||||
|
+++ b/config/warnings.js
|
||||||
|
@@ -1,8 +1,7 @@
|
||||||
|
/**
|
||||||
|
* more opinionated config.
|
||||||
|
- * @type {Object}
|
||||||
|
*/
|
||||||
|
-module.exports = {
|
||||||
|
+export default {
|
||||||
|
plugins: ['import'],
|
||||||
|
rules: {
|
||||||
|
'import/no-named-as-default': 1,
|
||||||
diff --git a/scripts/resolverDirectories.js b/scripts/resolverDirectories.js
|
diff --git a/scripts/resolverDirectories.js b/scripts/resolverDirectories.js
|
||||||
index f0c03a3c..a7cadb55 100644
|
index f0c03a3c..a7cadb55 100644
|
||||||
--- a/scripts/resolverDirectories.js
|
--- a/scripts/resolverDirectories.js
|
||||||
@ -302,10 +412,10 @@ index 92b838c0..ccb13ba0 100644
|
|||||||
return `${repoUrl}/blob/${commitish}/docs/rules/${ruleName}.md`;
|
return `${repoUrl}/blob/${commitish}/docs/rules/${ruleName}.md`;
|
||||||
}
|
}
|
||||||
diff --git a/src/index.js b/src/index.js
|
diff --git a/src/index.js b/src/index.js
|
||||||
index feafba90..84992bef 100644
|
index feafba90..9a464041 100644
|
||||||
--- a/src/index.js
|
--- a/src/index.js
|
||||||
+++ b/src/index.js
|
+++ b/src/index.js
|
||||||
@@ -1,71 +1,132 @@
|
@@ -1,71 +1,135 @@
|
||||||
-export const rules = {
|
-export const rules = {
|
||||||
- 'no-unresolved': require('./rules/no-unresolved'),
|
- 'no-unresolved': require('./rules/no-unresolved'),
|
||||||
- named: require('./rules/named'),
|
- named: require('./rules/named'),
|
||||||
@ -383,6 +493,9 @@ index feafba90..84992bef 100644
|
|||||||
- 'no-named-as-default-member': require('./rules/no-named-as-default-member'),
|
- 'no-named-as-default-member': require('./rules/no-named-as-default-member'),
|
||||||
- 'no-anonymous-default-export': require('./rules/no-anonymous-default-export'),
|
- 'no-anonymous-default-export': require('./rules/no-anonymous-default-export'),
|
||||||
- 'no-unused-modules': require('./rules/no-unused-modules'),
|
- 'no-unused-modules': require('./rules/no-unused-modules'),
|
||||||
|
+/**
|
||||||
|
+ * @type {Readonly<import('eslint').Linter.RulesRecord>}
|
||||||
|
+ */
|
||||||
+export const rules = /*#__PURE__*/ kebabCase({
|
+export const rules = /*#__PURE__*/ kebabCase({
|
||||||
+ noUnresolved,
|
+ noUnresolved,
|
||||||
+ named,
|
+ named,
|
||||||
@ -464,9 +577,8 @@ index feafba90..84992bef 100644
|
|||||||
+ importsFirst,
|
+ importsFirst,
|
||||||
+});
|
+});
|
||||||
|
|
||||||
-export const configs = {
|
export const configs = {
|
||||||
- recommended: require('../config/recommended'),
|
- recommended: require('../config/recommended'),
|
||||||
+export const configs = /*#__PURE__*/ kebabCase({
|
|
||||||
+ recommended,
|
+ recommended,
|
||||||
|
|
||||||
- errors: require('../config/errors'),
|
- errors: require('../config/errors'),
|
||||||
@ -483,12 +595,11 @@ index feafba90..84992bef 100644
|
|||||||
- 'react-native': require('../config/react-native'),
|
- 'react-native': require('../config/react-native'),
|
||||||
- electron: require('../config/electron'),
|
- electron: require('../config/electron'),
|
||||||
- typescript: require('../config/typescript'),
|
- typescript: require('../config/typescript'),
|
||||||
-};
|
|
||||||
+ react,
|
+ react,
|
||||||
+ reactNative,
|
+ 'react-native': reactNative,
|
||||||
+ electron,
|
+ electron,
|
||||||
+ typescript,
|
+ typescript,
|
||||||
+});
|
};
|
||||||
+
|
+
|
||||||
+function kebabCase(obj) {
|
+function kebabCase(obj) {
|
||||||
+ return Object.fromEntries(
|
+ return Object.fromEntries(
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
diff --git a/src/index.js b/src/index.js
|
diff --git a/src/index.js b/src/index.js
|
||||||
index 7b931fe..f7c1f91 100644
|
index 7b931fe..eaea267 100644
|
||||||
--- a/src/index.js
|
--- a/src/index.js
|
||||||
+++ b/src/index.js
|
+++ b/src/index.js
|
||||||
@@ -1,47 +1,87 @@
|
@@ -1,296 +1,344 @@
|
||||||
/* eslint-disable global-require */
|
/* eslint-disable global-require */
|
||||||
+// @ts-check
|
+// @ts-check
|
||||||
+import accessibleEmoji from './rules/accessible-emoji';
|
+import accessibleEmoji from './rules/accessible-emoji';
|
||||||
@ -45,7 +45,7 @@ index 7b931fe..f7c1f91 100644
|
|||||||
+import scope from './rules/scope';
|
+import scope from './rules/scope';
|
||||||
+import tabindexNoPositive from './rules/tabindex-no-positive';
|
+import tabindexNoPositive from './rules/tabindex-no-positive';
|
||||||
|
|
||||||
module.exports = {
|
-module.exports = {
|
||||||
- rules: {
|
- rules: {
|
||||||
- 'accessible-emoji': require('./rules/accessible-emoji'),
|
- 'accessible-emoji': require('./rules/accessible-emoji'),
|
||||||
- 'alt-text': require('./rules/alt-text'),
|
- 'alt-text': require('./rules/alt-text'),
|
||||||
@ -87,58 +87,532 @@ index 7b931fe..f7c1f91 100644
|
|||||||
- scope: require('./rules/scope'),
|
- scope: require('./rules/scope'),
|
||||||
- 'tabindex-no-positive': require('./rules/tabindex-no-positive'),
|
- 'tabindex-no-positive': require('./rules/tabindex-no-positive'),
|
||||||
- },
|
- },
|
||||||
+ rules: kebabCase({
|
- configs: {
|
||||||
+ accessibleEmoji,
|
- recommended: {
|
||||||
+ altText,
|
- plugins: [
|
||||||
+ anchorAmbiguousText,
|
- 'jsx-a11y',
|
||||||
+ anchorHasContent,
|
+export const rules = kebabCase({
|
||||||
+ anchorIsValid,
|
+ accessibleEmoji,
|
||||||
+ ariaActivedescendantHasTabindex,
|
+ altText,
|
||||||
+ ariaProps,
|
+ anchorAmbiguousText,
|
||||||
+ ariaProptypes,
|
+ anchorHasContent,
|
||||||
+ ariaRole,
|
+ anchorIsValid,
|
||||||
+ ariaUnsupportedElements,
|
+ ariaActivedescendantHasTabindex,
|
||||||
+ autocompleteValid,
|
+ ariaProps,
|
||||||
+ clickEventsHaveKeyEvents,
|
+ ariaProptypes,
|
||||||
+ controlHasAssociatedLabel,
|
+ ariaRole,
|
||||||
+ headingHasContent,
|
+ ariaUnsupportedElements,
|
||||||
+ htmlHasLang,
|
+ autocompleteValid,
|
||||||
+ iframeHasTitle,
|
+ clickEventsHaveKeyEvents,
|
||||||
+ imgRedundantAlt,
|
+ controlHasAssociatedLabel,
|
||||||
+ interactiveSupportsFocus,
|
+ headingHasContent,
|
||||||
+ labelHasAssociatedControl,
|
+ htmlHasLang,
|
||||||
+ labelHasFor,
|
+ iframeHasTitle,
|
||||||
+ lang,
|
+ imgRedundantAlt,
|
||||||
+ mediaHasCaption,
|
+ interactiveSupportsFocus,
|
||||||
+ mouseEventsHaveKeyEvents,
|
+ labelHasAssociatedControl,
|
||||||
+ noAccessKey,
|
+ labelHasFor,
|
||||||
+ noAriaHiddenOnFocusable,
|
+ lang,
|
||||||
+ noAutofocus,
|
+ mediaHasCaption,
|
||||||
+ noDistractingElements,
|
+ mouseEventsHaveKeyEvents,
|
||||||
+ noInteractiveElementToNoninteractiveRole,
|
+ noAccessKey,
|
||||||
+ noNoninteractiveElementInteractions,
|
+ noAriaHiddenOnFocusable,
|
||||||
+ noNoninteractiveElementToInteractiveRole,
|
+ noAutofocus,
|
||||||
+ noNoninteractiveTabindex,
|
+ noDistractingElements,
|
||||||
+ noOnChange,
|
+ noInteractiveElementToNoninteractiveRole,
|
||||||
+ noRedundantRoles,
|
+ noNoninteractiveElementInteractions,
|
||||||
+ noStaticElementInteractions,
|
+ noNoninteractiveElementToInteractiveRole,
|
||||||
+ preferTagOverRole,
|
+ noNoninteractiveTabindex,
|
||||||
+ roleHasRequiredAriaProps,
|
+ noOnChange,
|
||||||
+ roleSupportsAriaProps,
|
+ noRedundantRoles,
|
||||||
+ scope,
|
+ noStaticElementInteractions,
|
||||||
+ tabindexNoPositive,
|
+ preferTagOverRole,
|
||||||
+ }),
|
+ roleHasRequiredAriaProps,
|
||||||
configs: {
|
+ roleSupportsAriaProps,
|
||||||
recommended: {
|
+ scope,
|
||||||
plugins: [
|
+ tabindexNoPositive,
|
||||||
@@ -294,3 +334,9 @@ module.exports = {
|
+});
|
||||||
|
+export const configs = {
|
||||||
|
+ recommended: {
|
||||||
|
+ plugins: [
|
||||||
|
+ 'jsx-a11y',
|
||||||
|
+ ],
|
||||||
|
+ parserOptions: {
|
||||||
|
+ ecmaFeatures: {
|
||||||
|
+ jsx: true,
|
||||||
|
+ },
|
||||||
|
+ },
|
||||||
|
+ rules: {
|
||||||
|
+ 'jsx-a11y/alt-text': 'error',
|
||||||
|
+ 'jsx-a11y/anchor-ambiguous-text': 'off', // TODO: error
|
||||||
|
+ 'jsx-a11y/anchor-has-content': 'error',
|
||||||
|
+ 'jsx-a11y/anchor-is-valid': 'error',
|
||||||
|
+ 'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
|
||||||
|
+ 'jsx-a11y/aria-props': 'error',
|
||||||
|
+ 'jsx-a11y/aria-proptypes': 'error',
|
||||||
|
+ 'jsx-a11y/aria-role': 'error',
|
||||||
|
+ 'jsx-a11y/aria-unsupported-elements': 'error',
|
||||||
|
+ 'jsx-a11y/autocomplete-valid': 'error',
|
||||||
|
+ 'jsx-a11y/click-events-have-key-events': 'error',
|
||||||
|
+ 'jsx-a11y/control-has-associated-label': ['off', {
|
||||||
|
+ ignoreElements: [
|
||||||
|
+ 'audio',
|
||||||
|
+ 'canvas',
|
||||||
|
+ 'embed',
|
||||||
|
+ 'input',
|
||||||
|
+ 'textarea',
|
||||||
|
+ 'tr',
|
||||||
|
+ 'video',
|
||||||
|
+ ],
|
||||||
|
+ ignoreRoles: [
|
||||||
|
+ 'grid',
|
||||||
|
+ 'listbox',
|
||||||
|
+ 'menu',
|
||||||
|
+ 'menubar',
|
||||||
|
+ 'radiogroup',
|
||||||
|
+ 'row',
|
||||||
|
+ 'tablist',
|
||||||
|
+ 'toolbar',
|
||||||
|
+ 'tree',
|
||||||
|
+ 'treegrid',
|
||||||
|
+ ],
|
||||||
|
+ includeRoles: [
|
||||||
|
+ 'alert',
|
||||||
|
+ 'dialog',
|
||||||
|
+ ],
|
||||||
|
+ }],
|
||||||
|
+ 'jsx-a11y/heading-has-content': 'error',
|
||||||
|
+ 'jsx-a11y/html-has-lang': 'error',
|
||||||
|
+ 'jsx-a11y/iframe-has-title': 'error',
|
||||||
|
+ 'jsx-a11y/img-redundant-alt': 'error',
|
||||||
|
+ 'jsx-a11y/interactive-supports-focus': [
|
||||||
|
+ 'error',
|
||||||
|
+ {
|
||||||
|
+ tabbable: [
|
||||||
|
+ 'button',
|
||||||
|
+ 'checkbox',
|
||||||
|
+ 'link',
|
||||||
|
+ 'searchbox',
|
||||||
|
+ 'spinbutton',
|
||||||
|
+ 'switch',
|
||||||
|
+ 'textbox',
|
||||||
|
+ ],
|
||||||
|
+ },
|
||||||
|
],
|
||||||
|
- parserOptions: {
|
||||||
|
- ecmaFeatures: {
|
||||||
|
- jsx: true,
|
||||||
|
+ 'jsx-a11y/label-has-associated-control': 'error',
|
||||||
|
+ 'jsx-a11y/label-has-for': 'off',
|
||||||
|
+ 'jsx-a11y/media-has-caption': 'error',
|
||||||
|
+ 'jsx-a11y/mouse-events-have-key-events': 'error',
|
||||||
|
+ 'jsx-a11y/no-access-key': 'error',
|
||||||
|
+ 'jsx-a11y/no-autofocus': 'error',
|
||||||
|
+ 'jsx-a11y/no-distracting-elements': 'error',
|
||||||
|
+ 'jsx-a11y/no-interactive-element-to-noninteractive-role': [
|
||||||
|
+ 'error',
|
||||||
|
+ {
|
||||||
|
+ tr: ['none', 'presentation'],
|
||||||
|
+ canvas: ['img'],
|
||||||
|
},
|
||||||
|
- },
|
||||||
|
- rules: {
|
||||||
|
- 'jsx-a11y/alt-text': 'error',
|
||||||
|
- 'jsx-a11y/anchor-ambiguous-text': 'off', // TODO: error
|
||||||
|
- 'jsx-a11y/anchor-has-content': 'error',
|
||||||
|
- 'jsx-a11y/anchor-is-valid': 'error',
|
||||||
|
- 'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
|
||||||
|
- 'jsx-a11y/aria-props': 'error',
|
||||||
|
- 'jsx-a11y/aria-proptypes': 'error',
|
||||||
|
- 'jsx-a11y/aria-role': 'error',
|
||||||
|
- 'jsx-a11y/aria-unsupported-elements': 'error',
|
||||||
|
- 'jsx-a11y/autocomplete-valid': 'error',
|
||||||
|
- 'jsx-a11y/click-events-have-key-events': 'error',
|
||||||
|
- 'jsx-a11y/control-has-associated-label': ['off', {
|
||||||
|
- ignoreElements: [
|
||||||
|
- 'audio',
|
||||||
|
- 'canvas',
|
||||||
|
- 'embed',
|
||||||
|
- 'input',
|
||||||
|
- 'textarea',
|
||||||
|
- 'tr',
|
||||||
|
- 'video',
|
||||||
|
+ ],
|
||||||
|
+ 'jsx-a11y/no-noninteractive-element-interactions': [
|
||||||
|
+ 'error',
|
||||||
|
+ {
|
||||||
|
+ handlers: [
|
||||||
|
+ 'onClick',
|
||||||
|
+ 'onError',
|
||||||
|
+ 'onLoad',
|
||||||
|
+ 'onMouseDown',
|
||||||
|
+ 'onMouseUp',
|
||||||
|
+ 'onKeyPress',
|
||||||
|
+ 'onKeyDown',
|
||||||
|
+ 'onKeyUp',
|
||||||
|
],
|
||||||
|
- ignoreRoles: [
|
||||||
|
- 'grid',
|
||||||
|
+ alert: ['onKeyUp', 'onKeyDown', 'onKeyPress'],
|
||||||
|
+ body: ['onError', 'onLoad'],
|
||||||
|
+ dialog: ['onKeyUp', 'onKeyDown', 'onKeyPress'],
|
||||||
|
+ iframe: ['onError', 'onLoad'],
|
||||||
|
+ img: ['onError', 'onLoad'],
|
||||||
|
+ },
|
||||||
|
+ ],
|
||||||
|
+ 'jsx-a11y/no-noninteractive-element-to-interactive-role': [
|
||||||
|
+ 'error',
|
||||||
|
+ {
|
||||||
|
+ ul: [
|
||||||
|
'listbox',
|
||||||
|
'menu',
|
||||||
|
'menubar',
|
||||||
|
'radiogroup',
|
||||||
|
- 'row',
|
||||||
|
'tablist',
|
||||||
|
- 'toolbar',
|
||||||
|
'tree',
|
||||||
|
'treegrid',
|
||||||
|
],
|
||||||
|
- includeRoles: [
|
||||||
|
- 'alert',
|
||||||
|
- 'dialog',
|
||||||
|
- ],
|
||||||
|
- }],
|
||||||
|
- 'jsx-a11y/heading-has-content': 'error',
|
||||||
|
- 'jsx-a11y/html-has-lang': 'error',
|
||||||
|
- 'jsx-a11y/iframe-has-title': 'error',
|
||||||
|
- 'jsx-a11y/img-redundant-alt': 'error',
|
||||||
|
- 'jsx-a11y/interactive-supports-focus': [
|
||||||
|
- 'error',
|
||||||
|
- {
|
||||||
|
- tabbable: [
|
||||||
|
- 'button',
|
||||||
|
- 'checkbox',
|
||||||
|
- 'link',
|
||||||
|
- 'searchbox',
|
||||||
|
- 'spinbutton',
|
||||||
|
- 'switch',
|
||||||
|
- 'textbox',
|
||||||
|
- ],
|
||||||
|
- },
|
||||||
|
- ],
|
||||||
|
- 'jsx-a11y/label-has-associated-control': 'error',
|
||||||
|
- 'jsx-a11y/label-has-for': 'off',
|
||||||
|
- 'jsx-a11y/media-has-caption': 'error',
|
||||||
|
- 'jsx-a11y/mouse-events-have-key-events': 'error',
|
||||||
|
- 'jsx-a11y/no-access-key': 'error',
|
||||||
|
- 'jsx-a11y/no-autofocus': 'error',
|
||||||
|
- 'jsx-a11y/no-distracting-elements': 'error',
|
||||||
|
- 'jsx-a11y/no-interactive-element-to-noninteractive-role': [
|
||||||
|
- 'error',
|
||||||
|
- {
|
||||||
|
- tr: ['none', 'presentation'],
|
||||||
|
- canvas: ['img'],
|
||||||
|
- },
|
||||||
|
- ],
|
||||||
|
- 'jsx-a11y/no-noninteractive-element-interactions': [
|
||||||
|
- 'error',
|
||||||
|
- {
|
||||||
|
- handlers: [
|
||||||
|
- 'onClick',
|
||||||
|
- 'onError',
|
||||||
|
- 'onLoad',
|
||||||
|
- 'onMouseDown',
|
||||||
|
- 'onMouseUp',
|
||||||
|
- 'onKeyPress',
|
||||||
|
- 'onKeyDown',
|
||||||
|
- 'onKeyUp',
|
||||||
|
- ],
|
||||||
|
- alert: ['onKeyUp', 'onKeyDown', 'onKeyPress'],
|
||||||
|
- body: ['onError', 'onLoad'],
|
||||||
|
- dialog: ['onKeyUp', 'onKeyDown', 'onKeyPress'],
|
||||||
|
- iframe: ['onError', 'onLoad'],
|
||||||
|
- img: ['onError', 'onLoad'],
|
||||||
|
- },
|
||||||
|
- ],
|
||||||
|
- 'jsx-a11y/no-noninteractive-element-to-interactive-role': [
|
||||||
|
- 'error',
|
||||||
|
- {
|
||||||
|
- ul: [
|
||||||
|
- 'listbox',
|
||||||
|
- 'menu',
|
||||||
|
- 'menubar',
|
||||||
|
- 'radiogroup',
|
||||||
|
- 'tablist',
|
||||||
|
- 'tree',
|
||||||
|
- 'treegrid',
|
||||||
|
- ],
|
||||||
|
- ol: [
|
||||||
|
- 'listbox',
|
||||||
|
- 'menu',
|
||||||
|
- 'menubar',
|
||||||
|
- 'radiogroup',
|
||||||
|
- 'tablist',
|
||||||
|
- 'tree',
|
||||||
|
- 'treegrid',
|
||||||
|
- ],
|
||||||
|
- li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
|
||||||
|
- table: ['grid'],
|
||||||
|
- td: ['gridcell'],
|
||||||
|
- fieldset: ['radiogroup', 'presentation'],
|
||||||
|
- },
|
||||||
|
- ],
|
||||||
|
- 'jsx-a11y/no-noninteractive-tabindex': [
|
||||||
|
- 'error',
|
||||||
|
- {
|
||||||
|
- tags: [],
|
||||||
|
- roles: ['tabpanel'],
|
||||||
|
- allowExpressionValues: true,
|
||||||
|
- },
|
||||||
|
- ],
|
||||||
|
- 'jsx-a11y/no-redundant-roles': 'error',
|
||||||
|
- 'jsx-a11y/no-static-element-interactions': [
|
||||||
|
- 'error',
|
||||||
|
- {
|
||||||
|
- allowExpressionValues: true,
|
||||||
|
- handlers: [
|
||||||
|
- 'onClick',
|
||||||
|
- 'onMouseDown',
|
||||||
|
- 'onMouseUp',
|
||||||
|
- 'onKeyPress',
|
||||||
|
- 'onKeyDown',
|
||||||
|
- 'onKeyUp',
|
||||||
|
- ],
|
||||||
|
- },
|
||||||
|
- ],
|
||||||
|
- 'jsx-a11y/role-has-required-aria-props': 'error',
|
||||||
|
- 'jsx-a11y/role-supports-aria-props': 'error',
|
||||||
|
- 'jsx-a11y/scope': 'error',
|
||||||
|
- 'jsx-a11y/tabindex-no-positive': 'error',
|
||||||
|
- },
|
||||||
|
- },
|
||||||
|
- strict: {
|
||||||
|
- plugins: [
|
||||||
|
- 'jsx-a11y',
|
||||||
|
- ],
|
||||||
|
- parserOptions: {
|
||||||
|
- ecmaFeatures: {
|
||||||
|
- jsx: true,
|
||||||
|
- },
|
||||||
|
- },
|
||||||
|
- rules: {
|
||||||
|
- 'jsx-a11y/alt-text': 'error',
|
||||||
|
- 'jsx-a11y/anchor-has-content': 'error',
|
||||||
|
- 'jsx-a11y/anchor-is-valid': 'error',
|
||||||
|
- 'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
|
||||||
|
- 'jsx-a11y/aria-props': 'error',
|
||||||
|
- 'jsx-a11y/aria-proptypes': 'error',
|
||||||
|
- 'jsx-a11y/aria-role': 'error',
|
||||||
|
- 'jsx-a11y/aria-unsupported-elements': 'error',
|
||||||
|
- 'jsx-a11y/autocomplete-valid': 'error',
|
||||||
|
- 'jsx-a11y/click-events-have-key-events': 'error',
|
||||||
|
- 'jsx-a11y/control-has-associated-label': ['off', {
|
||||||
|
- ignoreElements: [
|
||||||
|
- 'audio',
|
||||||
|
- 'canvas',
|
||||||
|
- 'embed',
|
||||||
|
- 'input',
|
||||||
|
- 'textarea',
|
||||||
|
- 'tr',
|
||||||
|
- 'video',
|
||||||
|
- ],
|
||||||
|
- ignoreRoles: [
|
||||||
|
- 'grid',
|
||||||
|
+ ol: [
|
||||||
|
'listbox',
|
||||||
|
'menu',
|
||||||
|
'menubar',
|
||||||
|
'radiogroup',
|
||||||
|
- 'row',
|
||||||
|
'tablist',
|
||||||
|
- 'toolbar',
|
||||||
|
'tree',
|
||||||
|
'treegrid',
|
||||||
|
],
|
||||||
|
- includeRoles: [
|
||||||
|
- 'alert',
|
||||||
|
- 'dialog',
|
||||||
|
+ li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
|
||||||
|
+ table: ['grid'],
|
||||||
|
+ td: ['gridcell'],
|
||||||
|
+ fieldset: ['radiogroup', 'presentation'],
|
||||||
|
+ },
|
||||||
|
+ ],
|
||||||
|
+ 'jsx-a11y/no-noninteractive-tabindex': [
|
||||||
|
+ 'error',
|
||||||
|
+ {
|
||||||
|
+ tags: [],
|
||||||
|
+ roles: ['tabpanel'],
|
||||||
|
+ allowExpressionValues: true,
|
||||||
|
+ },
|
||||||
|
+ ],
|
||||||
|
+ 'jsx-a11y/no-redundant-roles': 'error',
|
||||||
|
+ 'jsx-a11y/no-static-element-interactions': [
|
||||||
|
+ 'error',
|
||||||
|
+ {
|
||||||
|
+ allowExpressionValues: true,
|
||||||
|
+ handlers: [
|
||||||
|
+ 'onClick',
|
||||||
|
+ 'onMouseDown',
|
||||||
|
+ 'onMouseUp',
|
||||||
|
+ 'onKeyPress',
|
||||||
|
+ 'onKeyDown',
|
||||||
|
+ 'onKeyUp',
|
||||||
|
],
|
||||||
|
- }],
|
||||||
|
- 'jsx-a11y/heading-has-content': 'error',
|
||||||
|
- 'jsx-a11y/html-has-lang': 'error',
|
||||||
|
- 'jsx-a11y/iframe-has-title': 'error',
|
||||||
|
- 'jsx-a11y/img-redundant-alt': 'error',
|
||||||
|
- 'jsx-a11y/interactive-supports-focus': [
|
||||||
|
- 'error',
|
||||||
|
- {
|
||||||
|
- tabbable: [
|
||||||
|
- 'button',
|
||||||
|
- 'checkbox',
|
||||||
|
- 'link',
|
||||||
|
- 'progressbar',
|
||||||
|
- 'searchbox',
|
||||||
|
- 'slider',
|
||||||
|
- 'spinbutton',
|
||||||
|
- 'switch',
|
||||||
|
- 'textbox',
|
||||||
|
- ],
|
||||||
|
- },
|
||||||
|
+ },
|
||||||
|
+ ],
|
||||||
|
+ 'jsx-a11y/role-has-required-aria-props': 'error',
|
||||||
|
+ 'jsx-a11y/role-supports-aria-props': 'error',
|
||||||
|
+ 'jsx-a11y/scope': 'error',
|
||||||
|
+ 'jsx-a11y/tabindex-no-positive': 'error',
|
||||||
|
+ },
|
||||||
|
+ },
|
||||||
|
+ strict: {
|
||||||
|
+ plugins: [
|
||||||
|
+ 'jsx-a11y',
|
||||||
|
+ ],
|
||||||
|
+ parserOptions: {
|
||||||
|
+ ecmaFeatures: {
|
||||||
|
+ jsx: true,
|
||||||
|
+ },
|
||||||
|
+ },
|
||||||
|
+ rules: {
|
||||||
|
+ 'jsx-a11y/alt-text': 'error',
|
||||||
|
+ 'jsx-a11y/anchor-has-content': 'error',
|
||||||
|
+ 'jsx-a11y/anchor-is-valid': 'error',
|
||||||
|
+ 'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
|
||||||
|
+ 'jsx-a11y/aria-props': 'error',
|
||||||
|
+ 'jsx-a11y/aria-proptypes': 'error',
|
||||||
|
+ 'jsx-a11y/aria-role': 'error',
|
||||||
|
+ 'jsx-a11y/aria-unsupported-elements': 'error',
|
||||||
|
+ 'jsx-a11y/autocomplete-valid': 'error',
|
||||||
|
+ 'jsx-a11y/click-events-have-key-events': 'error',
|
||||||
|
+ 'jsx-a11y/control-has-associated-label': ['off', {
|
||||||
|
+ ignoreElements: [
|
||||||
|
+ 'audio',
|
||||||
|
+ 'canvas',
|
||||||
|
+ 'embed',
|
||||||
|
+ 'input',
|
||||||
|
+ 'textarea',
|
||||||
|
+ 'tr',
|
||||||
|
+ 'video',
|
||||||
|
],
|
||||||
|
- 'jsx-a11y/label-has-for': 'off',
|
||||||
|
- 'jsx-a11y/label-has-associated-control': 'error',
|
||||||
|
- 'jsx-a11y/media-has-caption': 'error',
|
||||||
|
- 'jsx-a11y/mouse-events-have-key-events': 'error',
|
||||||
|
- 'jsx-a11y/no-access-key': 'error',
|
||||||
|
- 'jsx-a11y/no-autofocus': 'error',
|
||||||
|
- 'jsx-a11y/no-distracting-elements': 'error',
|
||||||
|
- 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
|
||||||
|
- 'jsx-a11y/no-noninteractive-element-interactions': [
|
||||||
|
- 'error',
|
||||||
|
- {
|
||||||
|
- body: ['onError', 'onLoad'],
|
||||||
|
- iframe: ['onError', 'onLoad'],
|
||||||
|
- img: ['onError', 'onLoad'],
|
||||||
|
- },
|
||||||
|
+ ignoreRoles: [
|
||||||
|
+ 'grid',
|
||||||
|
+ 'listbox',
|
||||||
|
+ 'menu',
|
||||||
|
+ 'menubar',
|
||||||
|
+ 'radiogroup',
|
||||||
|
+ 'row',
|
||||||
|
+ 'tablist',
|
||||||
|
+ 'toolbar',
|
||||||
|
+ 'tree',
|
||||||
|
+ 'treegrid',
|
||||||
|
],
|
||||||
|
- 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error',
|
||||||
|
- 'jsx-a11y/no-noninteractive-tabindex': 'error',
|
||||||
|
- 'jsx-a11y/no-redundant-roles': 'error',
|
||||||
|
- 'jsx-a11y/no-static-element-interactions': 'error',
|
||||||
|
- 'jsx-a11y/role-has-required-aria-props': 'error',
|
||||||
|
- 'jsx-a11y/role-supports-aria-props': 'error',
|
||||||
|
- 'jsx-a11y/scope': 'error',
|
||||||
|
- 'jsx-a11y/tabindex-no-positive': 'error',
|
||||||
|
- },
|
||||||
|
+ includeRoles: [
|
||||||
|
+ 'alert',
|
||||||
|
+ 'dialog',
|
||||||
|
+ ],
|
||||||
|
+ }],
|
||||||
|
+ 'jsx-a11y/heading-has-content': 'error',
|
||||||
|
+ 'jsx-a11y/html-has-lang': 'error',
|
||||||
|
+ 'jsx-a11y/iframe-has-title': 'error',
|
||||||
|
+ 'jsx-a11y/img-redundant-alt': 'error',
|
||||||
|
+ 'jsx-a11y/interactive-supports-focus': [
|
||||||
|
+ 'error',
|
||||||
|
+ {
|
||||||
|
+ tabbable: [
|
||||||
|
+ 'button',
|
||||||
|
+ 'checkbox',
|
||||||
|
+ 'link',
|
||||||
|
+ 'progressbar',
|
||||||
|
+ 'searchbox',
|
||||||
|
+ 'slider',
|
||||||
|
+ 'spinbutton',
|
||||||
|
+ 'switch',
|
||||||
|
+ 'textbox',
|
||||||
|
+ ],
|
||||||
|
+ },
|
||||||
|
+ ],
|
||||||
|
+ 'jsx-a11y/label-has-for': 'off',
|
||||||
|
+ 'jsx-a11y/label-has-associated-control': 'error',
|
||||||
|
+ 'jsx-a11y/media-has-caption': 'error',
|
||||||
|
+ 'jsx-a11y/mouse-events-have-key-events': 'error',
|
||||||
|
+ 'jsx-a11y/no-access-key': 'error',
|
||||||
|
+ 'jsx-a11y/no-autofocus': 'error',
|
||||||
|
+ 'jsx-a11y/no-distracting-elements': 'error',
|
||||||
|
+ 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
|
||||||
|
+ 'jsx-a11y/no-noninteractive-element-interactions': [
|
||||||
|
+ 'error',
|
||||||
|
+ {
|
||||||
|
+ body: ['onError', 'onLoad'],
|
||||||
|
+ iframe: ['onError', 'onLoad'],
|
||||||
|
+ img: ['onError', 'onLoad'],
|
||||||
|
+ },
|
||||||
|
+ ],
|
||||||
|
+ 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error',
|
||||||
|
+ 'jsx-a11y/no-noninteractive-tabindex': 'error',
|
||||||
|
+ 'jsx-a11y/no-redundant-roles': 'error',
|
||||||
|
+ 'jsx-a11y/no-static-element-interactions': 'error',
|
||||||
|
+ 'jsx-a11y/role-has-required-aria-props': 'error',
|
||||||
|
+ 'jsx-a11y/role-supports-aria-props': 'error',
|
||||||
|
+ 'jsx-a11y/scope': 'error',
|
||||||
|
+ 'jsx-a11y/tabindex-no-positive': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
+
|
+
|
||||||
|
+/** @param {object} obj */
|
||||||
+function kebabCase(obj) {
|
+function kebabCase(obj) {
|
||||||
+ return Object.fromEntries(
|
+ return Object.fromEntries(
|
||||||
+ Object.entries(obj).map(([key, value]) => [key.replace(/([A-Z])/g, '-$1').toLowerCase(), value])
|
+ Object.entries(obj).map(([key, value]) => [
|
||||||
+ )
|
+ key.replace(/([A-Z])/g, '-$1').toLowerCase(),
|
||||||
|
+ value,
|
||||||
|
+ ]),
|
||||||
|
+ );
|
||||||
+}
|
+}
|
||||||
\ No newline at end of file
|
|
||||||
|
25
patch/eslint-plugin-n.patch
Normal file
25
patch/eslint-plugin-n.patch
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
diff --git a/lib/index.js b/lib/index.js
|
||||||
|
index 341c86d..3fb26d1 100644
|
||||||
|
--- a/lib/index.js
|
||||||
|
+++ b/lib/index.js
|
||||||
|
@@ -1,15 +1,16 @@
|
||||||
|
/* DON'T EDIT THIS FILE. This is generated by 'scripts/update-lib-index.js' */
|
||||||
|
"use strict"
|
||||||
|
|
||||||
|
-const pkg = require("../package.json")
|
||||||
|
+import { name, version } from "../package.json"
|
||||||
|
+import recommendedModule from "./configs/recommended-module"
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
- name: pkg.name,
|
||||||
|
- version: pkg.version,
|
||||||
|
+ name,
|
||||||
|
+ version,
|
||||||
|
},
|
||||||
|
configs: {
|
||||||
|
- "recommended-module": require("./configs/recommended-module"),
|
||||||
|
+ "recommended-module": recommendedModule,
|
||||||
|
"recommended-script": require("./configs/recommended-script"),
|
||||||
|
get recommended() {
|
||||||
|
return require("./configs/recommended")()
|
@ -87,12 +87,12 @@ index 4991f200..00000000
|
|||||||
- ],
|
- ],
|
||||||
-}
|
-}
|
||||||
diff --git a/index.js b/index.js
|
diff --git a/index.js b/index.js
|
||||||
index 4140c6c8..792ceb4f 100644
|
index 4140c6c8..03e623af 100644
|
||||||
--- a/index.js
|
--- a/index.js
|
||||||
+++ b/index.js
|
+++ b/index.js
|
||||||
@@ -1,15 +1,13 @@
|
@@ -1,31 +1,25 @@
|
||||||
'use strict';
|
-'use strict';
|
||||||
|
-
|
||||||
-const configAll = require('./configs/all');
|
-const configAll = require('./configs/all');
|
||||||
-const configRecommended = require('./configs/recommended');
|
-const configRecommended = require('./configs/recommended');
|
||||||
-const configRuntime = require('./configs/jsx-runtime');
|
-const configRuntime = require('./configs/jsx-runtime');
|
||||||
@ -102,16 +102,47 @@ index 4140c6c8..792ceb4f 100644
|
|||||||
+import configRecommended from './configs/recommended';
|
+import configRecommended from './configs/recommended';
|
||||||
+import configRuntime from './configs/jsx-runtime';
|
+import configRuntime from './configs/jsx-runtime';
|
||||||
+import { name } from './package.json';
|
+import { name } from './package.json';
|
||||||
+import allRules from './lib/rules';
|
+export { default as rules } from './lib/rules';
|
||||||
|
|
||||||
// for legacy config system
|
// for legacy config system
|
||||||
-const plugins = [
|
-const plugins = [
|
||||||
- 'react',
|
- 'react',
|
||||||
-];
|
-];
|
||||||
+const plugins = [name];
|
+const plugins = [name];
|
||||||
|
+
|
||||||
|
+export const deprecatedRules = configAll.plugins.react.deprecatedRules;
|
||||||
|
|
||||||
module.exports = {
|
-module.exports = {
|
||||||
deprecatedRules: configAll.plugins.react.deprecatedRules,
|
- deprecatedRules: configAll.plugins.react.deprecatedRules,
|
||||||
|
- rules: allRules,
|
||||||
|
- configs: {
|
||||||
|
- recommended: Object.assign({}, configRecommended, {
|
||||||
|
- parserOptions: configRecommended.languageOptions.parserOptions,
|
||||||
|
- plugins,
|
||||||
|
- }),
|
||||||
|
- all: Object.assign({}, configAll, {
|
||||||
|
- parserOptions: configAll.languageOptions.parserOptions,
|
||||||
|
- plugins,
|
||||||
|
- }),
|
||||||
|
- 'jsx-runtime': Object.assign({}, configRuntime, {
|
||||||
|
- parserOptions: configRuntime.languageOptions.parserOptions,
|
||||||
|
- plugins,
|
||||||
|
- }),
|
||||||
|
- },
|
||||||
|
+export const configs = {
|
||||||
|
+ recommended: Object.assign({}, configRecommended, {
|
||||||
|
+ parserOptions: configRecommended.languageOptions.parserOptions,
|
||||||
|
+ plugins,
|
||||||
|
+ }),
|
||||||
|
+ all: Object.assign({}, configAll, {
|
||||||
|
+ parserOptions: configAll.languageOptions.parserOptions,
|
||||||
|
+ plugins,
|
||||||
|
+ }),
|
||||||
|
+ 'jsx-runtime': Object.assign({}, configRuntime, {
|
||||||
|
+ parserOptions: configRuntime.languageOptions.parserOptions,
|
||||||
|
+ plugins,
|
||||||
|
+ }),
|
||||||
|
};
|
||||||
diff --git a/lib/rules/button-has-type.js b/lib/rules/button-has-type.js
|
diff --git a/lib/rules/button-has-type.js b/lib/rules/button-has-type.js
|
||||||
index 204a33c4..01d992c2 100644
|
index 204a33c4..01d992c2 100644
|
||||||
--- a/lib/rules/button-has-type.js
|
--- a/lib/rules/button-has-type.js
|
||||||
@ -253,7 +284,7 @@ index 55073bfe..efc07af1 100644
|
|||||||
const astUtil = require('./ast');
|
const astUtil = require('./ast');
|
||||||
const isCreateElement = require('./isCreateElement');
|
const isCreateElement = require('./isCreateElement');
|
||||||
diff --git a/package.json b/package.json
|
diff --git a/package.json b/package.json
|
||||||
index cb736434..a97113c0 100644
|
index b1fa86fa..758b2177 100644
|
||||||
--- a/package.json
|
--- a/package.json
|
||||||
+++ b/package.json
|
+++ b/package.json
|
||||||
@@ -25,21 +25,13 @@
|
@@ -25,21 +25,13 @@
|
||||||
@ -273,12 +304,12 @@ index cb736434..a97113c0 100644
|
|||||||
- "object.values": "^1.1.6",
|
- "object.values": "^1.1.6",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"resolve": "^2.0.0-next.4",
|
"resolve": "^2.0.0-next.4",
|
||||||
- "semver": "^6.3.0",
|
- "semver": "^6.3.1",
|
||||||
- "string.prototype.matchall": "^4.0.8"
|
- "string.prototype.matchall": "^4.0.8"
|
||||||
+ "semver": "^6.3.0"
|
+ "semver": "^6.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.21.0",
|
"@babel/core": "^7.22.9",
|
||||||
diff --git a/tsconfig.json b/tsconfig.json
|
diff --git a/tsconfig.json b/tsconfig.json
|
||||||
deleted file mode 100644
|
deleted file mode 100644
|
||||||
index 39187b7f..00000000
|
index 39187b7f..00000000
|
||||||
|
155
pnpm-lock.yaml
generated
155
pnpm-lock.yaml
generated
@ -20,12 +20,30 @@ devDependencies:
|
|||||||
'@types/babel__core':
|
'@types/babel__core':
|
||||||
specifier: ^7.20.1
|
specifier: ^7.20.1
|
||||||
version: 7.20.1
|
version: 7.20.1
|
||||||
|
'@types/eslint':
|
||||||
|
specifier: ^8.44.0
|
||||||
|
version: 8.44.0
|
||||||
|
'@types/estree':
|
||||||
|
specifier: ^1.0.1
|
||||||
|
version: 1.0.1
|
||||||
|
'@types/estree-jsx':
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0
|
||||||
|
'@types/lodash':
|
||||||
|
specifier: ^4.14.195
|
||||||
|
version: 4.14.195
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.4.2
|
specifier: ^20.4.2
|
||||||
version: 20.4.2
|
version: 20.4.2
|
||||||
|
'@typescript-eslint/types':
|
||||||
|
specifier: ^6.1.0
|
||||||
|
version: 6.1.0
|
||||||
babel-plugin-macros:
|
babel-plugin-macros:
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
|
dts-bundle-generator:
|
||||||
|
specifier: ^8.0.1
|
||||||
|
version: 8.0.1
|
||||||
esbin:
|
esbin:
|
||||||
specifier: 0.0.1-beta.1
|
specifier: 0.0.1-beta.1
|
||||||
version: 0.0.1-beta.1(esbuild@0.18.14)
|
version: 0.0.1-beta.1(esbuild@0.18.14)
|
||||||
@ -44,15 +62,30 @@ devDependencies:
|
|||||||
eslint-config-prettier:
|
eslint-config-prettier:
|
||||||
specifier: 8.8.0
|
specifier: 8.8.0
|
||||||
version: 8.8.0(eslint@8.45.0)
|
version: 8.8.0(eslint@8.45.0)
|
||||||
|
eslint-define-config:
|
||||||
|
specifier: ^1.21.0
|
||||||
|
version: 1.21.0
|
||||||
eslint-plugin-import:
|
eslint-plugin-import:
|
||||||
specifier: ^2.27.5
|
specifier: ^2.27.5
|
||||||
version: 2.27.5(eslint@8.45.0)
|
version: 2.27.5(eslint@8.45.0)
|
||||||
glob:
|
glob:
|
||||||
specifier: ^10.3.3
|
specifier: ^10.3.3
|
||||||
version: 10.3.3
|
version: 10.3.3
|
||||||
|
json-schema-to-ts:
|
||||||
|
specifier: ^2.9.1
|
||||||
|
version: 2.9.1
|
||||||
|
lodash:
|
||||||
|
specifier: ^4.17.21
|
||||||
|
version: 4.17.21
|
||||||
|
picocolors:
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
|
typescript:
|
||||||
|
specifier: 5.1.6
|
||||||
|
version: 5.1.6
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -1690,10 +1723,35 @@ packages:
|
|||||||
'@babel/types': 7.21.5
|
'@babel/types': 7.21.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/eslint@8.44.0:
|
||||||
|
resolution: {integrity: sha512-gsF+c/0XOguWgaOgvFs+xnnRqt9GwgTvIks36WpE6ueeI4KCEHHd8K/CKHqhOqrJKsYH8m27kRzQEvWXAwXUTw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.1
|
||||||
|
'@types/json-schema': 7.0.12
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/estree-jsx@1.0.0:
|
||||||
|
resolution: {integrity: sha512-3qvGd0z8F2ENTGr/GG1yViqfiKmRfrXVx5sJyHGFu3z7m5g5utCQtGp/g29JnjflhtQJBv1WDQukHiT58xPcYQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/estree@1.0.1:
|
||||||
|
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/json-schema@7.0.12:
|
||||||
|
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/json5@0.0.29:
|
/@types/json5@0.0.29:
|
||||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/lodash@4.14.195:
|
||||||
|
resolution: {integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/node@20.4.2:
|
/@types/node@20.4.2:
|
||||||
resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==}
|
resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -1702,6 +1760,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@typescript-eslint/types@6.1.0:
|
||||||
|
resolution: {integrity: sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==}
|
||||||
|
engines: {node: ^16.0.0 || >=18.0.0}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/acorn-jsx@5.3.2(acorn@8.10.0):
|
/acorn-jsx@5.3.2(acorn@8.10.0):
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1911,6 +1974,15 @@ packages:
|
|||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cliui@8.0.1:
|
||||||
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 7.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/color-convert@1.9.3:
|
/color-convert@1.9.3:
|
||||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2015,6 +2087,15 @@ packages:
|
|||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/dts-bundle-generator@8.0.1:
|
||||||
|
resolution: {integrity: sha512-9JVw78/OXdKfq+RUrmpLm6WAUJp+aOUGEHimVqIlOEH2VugRt1I8CVIoQZlirWZko+/SVZkNgpWCyZubUuzzPA==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
typescript: 5.1.6
|
||||||
|
yargs: 17.7.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eastasianwidth@0.2.0:
|
/eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2186,6 +2267,11 @@ packages:
|
|||||||
eslint: 8.45.0
|
eslint: 8.45.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-define-config@1.21.0:
|
||||||
|
resolution: {integrity: sha512-OKfreV19Nw4yK4UX1CDkv5FXWdzeF+VSROsO28DVi1BrzqOD4a3U71LJqEhcupK65MoLXxARQ0pSg8bDvNPONA==}
|
||||||
|
engines: {node: ^16.13.0 || >=18.0.0, npm: '>=7.0.0', pnpm: '>= 8.6.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-import-resolver-node@0.3.7:
|
/eslint-import-resolver-node@0.3.7:
|
||||||
resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==}
|
resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2248,7 +2334,7 @@ packages:
|
|||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
object.values: 1.1.6
|
object.values: 1.1.6
|
||||||
resolve: 1.22.2
|
resolve: 1.22.2
|
||||||
semver: 6.3.0
|
semver: 6.3.1
|
||||||
tsconfig-paths: 3.14.2
|
tsconfig-paths: 3.14.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- eslint-import-resolver-typescript
|
- eslint-import-resolver-typescript
|
||||||
@ -2434,6 +2520,11 @@ packages:
|
|||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/get-caller-file@2.0.5:
|
||||||
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/get-intrinsic@1.2.1:
|
/get-intrinsic@1.2.1:
|
||||||
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
|
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2465,7 +2556,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.1.1
|
foreground-child: 3.1.1
|
||||||
jackspeak: 2.2.1
|
jackspeak: 2.2.1
|
||||||
minimatch: 9.0.1
|
minimatch: 9.0.3
|
||||||
minipass: 6.0.2
|
minipass: 6.0.2
|
||||||
path-scurry: 1.10.1
|
path-scurry: 1.10.1
|
||||||
dev: true
|
dev: true
|
||||||
@ -2759,6 +2850,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/json-schema-to-ts@2.9.1:
|
||||||
|
resolution: {integrity: sha512-8MNpRGERlCUWYeJwsWkMrJ0MWzBz49dfqpG+n9viiIlP4othaahbiaNQZuBzmPxRLUhOv1QJMCzW5WE8nHFGIQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.21.5
|
||||||
|
'@types/json-schema': 7.0.12
|
||||||
|
ts-algebra: 1.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/json-schema-traverse@0.4.1:
|
/json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2807,6 +2907,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash@4.17.21:
|
||||||
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/lru-cache@5.1.1:
|
/lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2824,8 +2928,8 @@ packages:
|
|||||||
brace-expansion: 1.1.11
|
brace-expansion: 1.1.11
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/minimatch@9.0.1:
|
/minimatch@9.0.3:
|
||||||
resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==}
|
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.1
|
brace-expansion: 2.0.1
|
||||||
@ -3039,6 +3143,11 @@ packages:
|
|||||||
jsesc: 0.5.0
|
jsesc: 0.5.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/require-directory@2.1.1:
|
||||||
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/resolve-from@4.0.0:
|
/resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -3079,11 +3188,6 @@ packages:
|
|||||||
is-regex: 1.1.4
|
is-regex: 1.1.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/semver@6.3.0:
|
|
||||||
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
|
|
||||||
hasBin: true
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/semver@6.3.1:
|
/semver@6.3.1:
|
||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -3226,6 +3330,10 @@ packages:
|
|||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ts-algebra@1.2.0:
|
||||||
|
resolution: {integrity: sha512-kMuJJd8B2N/swCvIvn1hIFcIOrLGbWl9m/J6O3kHx9VRaevh00nvgjPiEGaRee7DRaAczMYR2uwWvXU22VFltw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tsconfig-paths@3.14.2:
|
/tsconfig-paths@3.14.2:
|
||||||
resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==}
|
resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3264,6 +3372,12 @@ packages:
|
|||||||
is-typed-array: 1.1.10
|
is-typed-array: 1.1.10
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/typescript@5.1.6:
|
||||||
|
resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/unbox-primitive@1.0.2:
|
/unbox-primitive@1.0.2:
|
||||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3365,6 +3479,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/y18n@5.0.8:
|
||||||
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/yallist@3.1.1:
|
/yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -3374,6 +3493,24 @@ packages:
|
|||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/yargs-parser@21.1.1:
|
||||||
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/yargs@17.7.2:
|
||||||
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
cliui: 8.0.1
|
||||||
|
escalade: 3.1.1
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
require-directory: 2.1.1
|
||||||
|
string-width: 4.2.3
|
||||||
|
y18n: 5.0.8
|
||||||
|
yargs-parser: 21.1.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/yocto-queue@0.1.0:
|
/yocto-queue@0.1.0:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -6,4 +6,5 @@ sync() (
|
|||||||
sync eslint-plugin-import
|
sync eslint-plugin-import
|
||||||
sync eslint-plugin-jsx-a11y
|
sync eslint-plugin-jsx-a11y
|
||||||
sync eslint-plugin-react
|
sync eslint-plugin-react
|
||||||
|
sync eslint-plugin-n
|
||||||
sync jsx-ast-utils
|
sync jsx-ast-utils
|
||||||
|
20
src/addAlias.ts
Normal file
20
src/addAlias.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { name } from '../dist/package.json';
|
||||||
|
|
||||||
|
const pkgPath = resolve(process.cwd(), 'package.json');
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||||
|
|
||||||
|
pkg.devDependencies ??= {};
|
||||||
|
|
||||||
|
Object.assign(pkg.devDependencies, {
|
||||||
|
'eslint-plugin-import': `file:./node_modules/${name}/import`,
|
||||||
|
'eslint-plugin-jsx-a11y': `file:./node_modules/${name}/jsx-a11y`,
|
||||||
|
'eslint-plugin-local': `file:./node_modules/${name}/local`,
|
||||||
|
'eslint-plugin-rules': `file:./node_modules/${name}/rules`,
|
||||||
|
'eslint-plugin-react': `file:./node_modules/${name}/react`,
|
||||||
|
'eslint-plugin-react-hooks': `file:./node_modules/${name}/react-hooks`,
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
156
src/babel.ts
156
src/babel.ts
@ -1,156 +0,0 @@
|
|||||||
import assert from 'node:assert';
|
|
||||||
import { readFileSync } from 'node:fs';
|
|
||||||
import { extname } from 'node:path';
|
|
||||||
import * as babel from '@babel/core';
|
|
||||||
import type { types as t } from '@babel/core';
|
|
||||||
import type { Loader, Plugin } from 'esbuild';
|
|
||||||
import { createMacro, type MacroHandler } from 'babel-plugin-macros';
|
|
||||||
|
|
||||||
class HandlerMap {
|
|
||||||
map = new Map<string, MacroHandler>();
|
|
||||||
|
|
||||||
set(names: string | string[], handler: MacroHandler) {
|
|
||||||
names = Array.isArray(names) ? names : [names];
|
|
||||||
const macro = createMacro(handler);
|
|
||||||
for (const name of names) {
|
|
||||||
this.map.set(name, macro);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
get keys() {
|
|
||||||
return Array.from(this.map.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvePath = (module: string) => module;
|
|
||||||
require = (module: string) => this.map.get(module);
|
|
||||||
isMacrosName = (module: string) => this.map.has(module);
|
|
||||||
}
|
|
||||||
|
|
||||||
const map = new HandlerMap()
|
|
||||||
.set(
|
|
||||||
'object.assign',
|
|
||||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('assign'))),
|
|
||||||
)
|
|
||||||
.set(
|
|
||||||
['object-values', 'object.values'],
|
|
||||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('values'))),
|
|
||||||
)
|
|
||||||
.set(
|
|
||||||
'object.fromentries',
|
|
||||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('fromEntries'))),
|
|
||||||
)
|
|
||||||
.set(
|
|
||||||
'object.entries',
|
|
||||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('entries'))),
|
|
||||||
)
|
|
||||||
.set(
|
|
||||||
'has',
|
|
||||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
|
|
||||||
)
|
|
||||||
.set(
|
|
||||||
'array-includes',
|
|
||||||
proto(t => t.identifier('includes')),
|
|
||||||
)
|
|
||||||
.set(
|
|
||||||
'array.prototype.flatmap',
|
|
||||||
proto(t => t.identifier('flatMap')),
|
|
||||||
)
|
|
||||||
.set(
|
|
||||||
'array.prototype.flat',
|
|
||||||
proto(t => t.identifier('flat')),
|
|
||||||
)
|
|
||||||
.set(
|
|
||||||
'array.prototype.findlastindex',
|
|
||||||
proto(t => t.identifier('findLastIndex')),
|
|
||||||
)
|
|
||||||
.set(
|
|
||||||
'array.prototype.tosorted',
|
|
||||||
proto(t => t.identifier('toSorted')),
|
|
||||||
)
|
|
||||||
.set(
|
|
||||||
'string.prototype.matchall',
|
|
||||||
proto(t => t.identifier('matchAll')),
|
|
||||||
);
|
|
||||||
|
|
||||||
function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler {
|
|
||||||
return ({ references, babel: { types: t } }) => {
|
|
||||||
references.default.forEach(referencePath => {
|
|
||||||
referencePath.replaceWith(getReplacement(t));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
|
|
||||||
return ({ references, babel: { types: t } }) => {
|
|
||||||
references.default.forEach(referencePath => {
|
|
||||||
const { parent, parentPath } = referencePath;
|
|
||||||
assert(t.isCallExpression(parent));
|
|
||||||
const [callee, ...rest] = parent.arguments;
|
|
||||||
parentPath!.replaceWith(
|
|
||||||
t.callExpression(
|
|
||||||
t.memberExpression(callee as t.Expression, getProperty(t)),
|
|
||||||
rest,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const babelPlugin: Plugin = {
|
|
||||||
name: 'babel',
|
|
||||||
setup(build) {
|
|
||||||
const { keys, ...macroOptions } = map;
|
|
||||||
|
|
||||||
build.onLoad({ filter: /\.[jt]sx?$/ }, args => {
|
|
||||||
const { path } = args;
|
|
||||||
if (path.includes('node_modules/')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let source = readFileSync(path, 'utf-8')
|
|
||||||
.replaceAll("require('object.hasown/polyfill')()", 'Object.hasOwn')
|
|
||||||
.replaceAll("require('object.fromentries/polyfill')()", 'Object.fromEntries');
|
|
||||||
|
|
||||||
if (
|
|
||||||
path.includes('eslint-plugin-import/src/rules/') ||
|
|
||||||
path.includes('eslint-plugin-import/config/')
|
|
||||||
) {
|
|
||||||
source = source.replace('\nmodule.exports = {', '\nexport default {');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFlow = source.includes('@flow');
|
|
||||||
const loader = extname(path).slice(1) as Loader;
|
|
||||||
|
|
||||||
if (!isFlow && !keys.some(key => source.includes(key))) {
|
|
||||||
return { contents: source, loader };
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = babel.transformSync(source, {
|
|
||||||
filename: path,
|
|
||||||
babelrc: false,
|
|
||||||
configFile: false,
|
|
||||||
parserOpts: {
|
|
||||||
plugins: [isFlow ? 'flow' : 'typescript'],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
isFlow && '@babel/plugin-transform-flow-strip-types',
|
|
||||||
['babel-plugin-macros', macroOptions],
|
|
||||||
].filter(Boolean),
|
|
||||||
})!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
contents: res.code!,
|
|
||||||
loader,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Array<T> {
|
|
||||||
filter(
|
|
||||||
predicate: BooleanConstructor,
|
|
||||||
): Exclude<T, null | undefined | false | '' | 0>[];
|
|
||||||
}
|
|
||||||
}
|
|
131
src/basic.ts
Normal file
131
src/basic.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// @ts-check
|
||||||
|
import type { ESLintConfig } from 'eslint-define-config';
|
||||||
|
|
||||||
|
export function extendConfig({
|
||||||
|
plugins,
|
||||||
|
settings,
|
||||||
|
rules,
|
||||||
|
...config
|
||||||
|
}: ESLintConfig): ESLintConfig {
|
||||||
|
return {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['@typescript-eslint', 'import', ...(plugins ?? [])],
|
||||||
|
env: { node: true, browser: true },
|
||||||
|
reportUnusedDisableDirectives: true,
|
||||||
|
parserOptions: { project: ['./tsconfig.json'] },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'prettier',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:import/errors',
|
||||||
|
'plugin:import/typescript',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:jsx-a11y/recommended',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
'import/parsers': {
|
||||||
|
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
'import/core-modules': ['node:test'],
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
...settings,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-duplicate-imports': 'error',
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
name: 'crypto',
|
||||||
|
importNames: ['webcrypto'],
|
||||||
|
message: 'Use global `crypto` instead',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-restricted-globals': ['error', 'event', 'name'],
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'@typescript-eslint/consistent-type-imports': [
|
||||||
|
'error',
|
||||||
|
{ disallowTypeAnnotations: false, fixStyle: 'inline-type-imports' },
|
||||||
|
],
|
||||||
|
'@typescript-eslint/ban-types': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
extendDefaults: false,
|
||||||
|
types: {
|
||||||
|
String: { message: 'Use string instead', fixWith: 'string' },
|
||||||
|
Number: { message: 'Use number instead', fixWith: 'number' },
|
||||||
|
Boolean: { message: 'Use boolean instead', fixWith: 'boolean' },
|
||||||
|
Symbol: { message: 'Use symbol instead', fixWith: 'symbol' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-namespace': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-use-before-define': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'@typescript-eslint/triple-slash-reference': 'off',
|
||||||
|
'@typescript-eslint/no-empty-interface': 'off',
|
||||||
|
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{ ignoreRestSiblings: true, varsIgnorePattern: '^_' },
|
||||||
|
],
|
||||||
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
complexity: ['warn', { max: 100 }],
|
||||||
|
curly: ['error', 'multi-line', 'consistent'],
|
||||||
|
eqeqeq: ['error', 'smart'],
|
||||||
|
'no-async-promise-executor': 'off',
|
||||||
|
'no-case-declarations': 'off',
|
||||||
|
'no-console': 'warn',
|
||||||
|
'no-constant-condition': ['error', { checkLoops: false }],
|
||||||
|
'no-debugger': 'off',
|
||||||
|
'no-empty': ['error', { allowEmptyCatch: true }],
|
||||||
|
'no-inner-declarations': 'off',
|
||||||
|
'no-lonely-if': 'error',
|
||||||
|
'no-template-curly-in-string': 'error',
|
||||||
|
'no-var': 'error',
|
||||||
|
'import/export': 'off',
|
||||||
|
'import/order': ['error', { groups: ['builtin', 'external'] }],
|
||||||
|
'object-shorthand': ['error', 'always', { ignoreConstructors: true }],
|
||||||
|
'one-var': ['error', { var: 'never', let: 'never' }],
|
||||||
|
'prefer-arrow-callback': 'error',
|
||||||
|
'prefer-const': ['error', { destructuring: 'all' }],
|
||||||
|
'prefer-destructuring': [
|
||||||
|
'warn',
|
||||||
|
{ AssignmentExpression: { array: false, object: false } },
|
||||||
|
],
|
||||||
|
'prefer-object-spread': 'error',
|
||||||
|
'prefer-rest-params': 'warn',
|
||||||
|
'prefer-spread': 'warn',
|
||||||
|
'quote-props': ['error', 'as-needed'],
|
||||||
|
'spaced-comment': ['error', 'always', { markers: ['/'] }],
|
||||||
|
'sort-imports': ['warn', { ignoreDeclarationSort: true }],
|
||||||
|
yoda: ['error', 'never', { exceptRange: true }],
|
||||||
|
'react/display-name': 'off',
|
||||||
|
'react/no-children-prop': 'error',
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/no-unknown-property': ['error', { ignore: ['css'] }],
|
||||||
|
...rules,
|
||||||
|
},
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
}
|
20
src/build-local-rules.ts
Executable file
20
src/build-local-rules.ts
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env -S node -r esbin
|
||||||
|
import { readdirSync, writeFileSync } from 'node:fs';
|
||||||
|
import { camelCase } from 'lodash';
|
||||||
|
|
||||||
|
const files = readdirSync('./src/rules')
|
||||||
|
.filter(file => file.endsWith('.ts'))
|
||||||
|
.filter(file => file !== 'index.ts')
|
||||||
|
.map(file => file.slice(0, -3));
|
||||||
|
|
||||||
|
const entryFile = `
|
||||||
|
import type { Rule } from 'eslint';
|
||||||
|
|
||||||
|
${files.map(file => `import ${camelCase(file)} from "./${file}"`).join(';\n')}
|
||||||
|
|
||||||
|
export const rules: Record<string, Rule.RuleModule> = {
|
||||||
|
${files.map(file => `"${file}": ${camelCase(file)}`).join(',\n ')}
|
||||||
|
};
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
writeFileSync('./src/rules/index.ts', entryFile);
|
29
src/local/index.ts
Normal file
29
src/local/index.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import type { ESLint } from 'eslint';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import { resolve, basename, extname } from 'node:path';
|
||||||
|
|
||||||
|
function tryRequire(candidates: string[]) {
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
try {
|
||||||
|
require(candidate);
|
||||||
|
return;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tryRequire(['esbin', 'esbuild-register', 'ts-node/register/transpile-only']);
|
||||||
|
|
||||||
|
const folders = resolve(process.cwd(), 'eslint-local-rules');
|
||||||
|
const files = fs.readdirSync(folders);
|
||||||
|
|
||||||
|
const plugin: ESLint.Plugin = {
|
||||||
|
rules: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const name = basename(file, extname(file));
|
||||||
|
const module = require(resolve(folders, file));
|
||||||
|
plugin.rules![name] = module.default ?? module;
|
||||||
|
}
|
||||||
|
|
||||||
|
export = plugin;
|
9
src/rules/index.ts
Normal file
9
src/rules/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { Rule } from 'eslint';
|
||||||
|
|
||||||
|
import noImportDot from "./no-import-dot";
|
||||||
|
import requireNodePrefix from "./require-node-prefix"
|
||||||
|
|
||||||
|
export const rules: Record<string, Rule.RuleModule> = {
|
||||||
|
"no-import-dot": noImportDot,
|
||||||
|
"require-node-prefix": requireNodePrefix
|
||||||
|
};
|
@ -1,32 +0,0 @@
|
|||||||
import type { Rule } from "eslint";
|
|
||||||
|
|
||||||
const rule: Rule.RuleModule = {
|
|
||||||
meta: {
|
|
||||||
type: "problem",
|
|
||||||
docs: {
|
|
||||||
description: "Disallow direct usage of `new PrismaClient()`",
|
|
||||||
category: "Best Practices",
|
|
||||||
recommended: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
create(context) {
|
|
||||||
// Check if the file is the target file where the import is allowed
|
|
||||||
if (context.filename.endsWith("src/utils/db.ts")) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
NewExpression(node) {
|
|
||||||
if (node.callee.type === "Identifier" && node.callee.name === "PrismaClient") {
|
|
||||||
context.report({
|
|
||||||
node,
|
|
||||||
message:
|
|
||||||
"Avoid direct usage of `new PrismaClient()`. Import from `src/utils/db.ts` instead.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default rule;
|
|
@ -1,33 +0,0 @@
|
|||||||
import type { Rule } from "eslint";
|
|
||||||
|
|
||||||
const rule: Rule.RuleModule = {
|
|
||||||
meta: {
|
|
||||||
type: "problem",
|
|
||||||
docs: {
|
|
||||||
description: "Disallow importing webcrypto from node:crypto and crypto modules",
|
|
||||||
category: "Best Practices",
|
|
||||||
recommended: true,
|
|
||||||
},
|
|
||||||
schema: [],
|
|
||||||
},
|
|
||||||
create: context => ({
|
|
||||||
ImportDeclaration(node) {
|
|
||||||
const importedSource = node.source.value as string;
|
|
||||||
const importedSpecifier = node.specifiers[0];
|
|
||||||
|
|
||||||
if (
|
|
||||||
(importedSource === "crypto" || importedSource === "node:crypto") &&
|
|
||||||
importedSpecifier.type === "ImportSpecifier" &&
|
|
||||||
importedSpecifier.local.name === "webcrypto"
|
|
||||||
) {
|
|
||||||
context.report({
|
|
||||||
node: importedSpecifier,
|
|
||||||
message:
|
|
||||||
"Do not import 'webcrypto' from 'crypto' or 'node:crypto'. Use the global variable 'crypto' instead.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default rule;
|
|
@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
"allowArbitraryExtensions": true,
|
"allowArbitraryExtensions": true,
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "node",
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user