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-react
|
||||
jsx-ast-utils
|
||||
react
|
||||
/react
|
||||
|
||||
dist/**/*.js
|
||||
dist/**/*.js.map
|
||||
|
19
build.sh
19
build.sh
@ -1,9 +1,16 @@
|
||||
#!/bin/bash
|
||||
rm dist/*.js
|
||||
mkdir -p dist
|
||||
./src/build-local-rules.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
|
||||
DEST=$HOME/Git/archive/node_modules
|
||||
rm -rf "$DEST/@proteria/eslint-rules"
|
||||
cp -r dist "$DEST/@proteria/eslint-rules"
|
||||
type() {
|
||||
npx dts-bundle-generator "./eslint-plugin-$1/$2" \
|
||||
-o "./dist/$1/index.d.ts" \
|
||||
--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",
|
||||
"version": "0.0.1-beta.11",
|
||||
"version": "0.0.1-beta.16",
|
||||
"license": "UNLICENSED",
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"bin": {
|
||||
"eslint-add-alias": "./addAlias.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/eslint": "^8.44.0",
|
||||
"aria-query": "^5.3.0",
|
||||
@ -17,6 +20,7 @@
|
||||
"eslint-import-resolver-node": "^0.3.7",
|
||||
"eslint-module-utils": "^2.8.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"is-core-module": "^2.12.1",
|
||||
"is-glob": "^4.0.3",
|
||||
"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
|
||||
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 { resolve } from 'path';
|
||||
import { babelPlugin } from './src/babel';
|
||||
import type { Loader, Plugin } from 'esbuild';
|
||||
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 ENV = process.env.NODE_ENV || 'development';
|
||||
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) {
|
||||
const context = await esbuild.context({
|
||||
await esbuild.build({
|
||||
entryPoints: [entry],
|
||||
outfile,
|
||||
bundle: true,
|
||||
@ -16,37 +214,18 @@ async function main(entry: string, outfile: string) {
|
||||
platform: 'node',
|
||||
packages: 'external',
|
||||
sourcemap: 'linked',
|
||||
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',
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins,
|
||||
define: {},
|
||||
banner: {
|
||||
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-import/src/index.js', './dist/import/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 */
|
||||
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: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
@ -20,41 +53,24 @@ export default {
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
enableDangerousAutofixThisMayCauseInfiniteLoops: false,
|
||||
properties: {
|
||||
additionalHooks: {
|
||||
type: 'string',
|
||||
},
|
||||
enableDangerousAutofixThisMayCauseInfiniteLoops: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
schema: [schema],
|
||||
},
|
||||
create(context) {
|
||||
create(context): Rule.RuleListener {
|
||||
const contextOptions = (context.options[0] || {}) as Config;
|
||||
// Parse the `additionalHooks` regex.
|
||||
const additionalHooks =
|
||||
context.options && context.options[0] && context.options[0].additionalHooks
|
||||
? new RegExp(context.options[0].additionalHooks)
|
||||
: undefined;
|
||||
const additionalHooks = contextOptions?.additionalHooks
|
||||
? new RegExp(context.options[0].additionalHooks)
|
||||
: undefined;
|
||||
|
||||
const enableDangerousAutofixThisMayCauseInfiniteLoops =
|
||||
(context.options &&
|
||||
context.options[0] &&
|
||||
context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops) ||
|
||||
false;
|
||||
contextOptions?.enableDangerousAutofixThisMayCauseInfiniteLoops || false;
|
||||
|
||||
const options = {
|
||||
additionalHooks,
|
||||
enableDangerousAutofixThisMayCauseInfiniteLoops,
|
||||
};
|
||||
|
||||
function reportProblem(problem) {
|
||||
function reportProblem(problem: Rule.ReportDescriptor): void {
|
||||
if (enableDangerousAutofixThisMayCauseInfiniteLoops) {
|
||||
// Used to enable legacy behavior. Dangerous.
|
||||
// Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension).
|
||||
@ -65,20 +81,23 @@ export default {
|
||||
context.report(problem);
|
||||
}
|
||||
|
||||
const scopeManager = context.getSourceCode().scopeManager;
|
||||
const scopeManager = context.sourceCode.scopeManager;
|
||||
|
||||
// Should be shared between visitors.
|
||||
const setStateCallSites = new WeakMap();
|
||||
const stateVariables = new WeakSet();
|
||||
const stableKnownValueCache = new WeakMap();
|
||||
const functionWithoutCapturedValueCache = new WeakMap();
|
||||
const setStateCallSites = new WeakMap<Expression, Pattern>();
|
||||
const stateVariables = new WeakSet<Identifier>();
|
||||
const stableKnownValueCache = new WeakMap<Scope.Variable, boolean>();
|
||||
const functionWithoutCapturedValueCache = new WeakMap<Scope.Variable, boolean>();
|
||||
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) {
|
||||
if (map.has(arg)) {
|
||||
// to verify cache hits:
|
||||
// console.log(arg.name)
|
||||
return map.get(arg);
|
||||
return map.get(arg)!;
|
||||
}
|
||||
const result = fn(arg);
|
||||
map.set(arg, result);
|
||||
@ -89,12 +108,12 @@ export default {
|
||||
* Visitor for both function expressions and arrow function expressions.
|
||||
*/
|
||||
function visitFunctionWithDependencies(
|
||||
node,
|
||||
declaredDependenciesNode,
|
||||
reactiveHook,
|
||||
reactiveHookName,
|
||||
isEffect,
|
||||
) {
|
||||
node: ArrowFunctionExpression | FunctionExpression | FunctionDeclaration,
|
||||
declaredDependenciesNode: SpreadElement | Expression,
|
||||
reactiveHook: Super | Expression,
|
||||
reactiveHookName: string,
|
||||
isEffect: boolean,
|
||||
): void {
|
||||
if (isEffect && node.async) {
|
||||
reportProblem({
|
||||
node: node,
|
||||
@ -114,7 +133,7 @@ export default {
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// scope. We can't enforce this in a lint so we trust that all variables
|
||||
// declared outside of pure scope are indeed frozen.
|
||||
const pureScopes = new Set();
|
||||
let componentScope = null;
|
||||
const pureScopes = new Set<Scope.Scope>();
|
||||
let componentScope: Scope.Scope;
|
||||
{
|
||||
let currentScope = scope.upper;
|
||||
while (currentScope) {
|
||||
@ -159,7 +178,7 @@ export default {
|
||||
// const onStuff = useEffectEvent(() => {})
|
||||
// ^^^ true for this reference
|
||||
// False for everything else.
|
||||
function isStableKnownHookValue(resolved) {
|
||||
function isStableKnownHookValue(resolved: Scope.Variable): boolean {
|
||||
if (!isArray(resolved.defs)) {
|
||||
return false;
|
||||
}
|
||||
@ -171,7 +190,7 @@ export default {
|
||||
if (def.node.type !== 'VariableDeclarator') {
|
||||
return false;
|
||||
}
|
||||
let init = def.node.init;
|
||||
let init = (def.node as VariableDeclarator).init;
|
||||
if (init == null) {
|
||||
return false;
|
||||
}
|
||||
@ -206,10 +225,11 @@ export default {
|
||||
if (init.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
let callee = init.callee;
|
||||
let callee: Node = init.callee;
|
||||
// Step into `= React.something` initializer.
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.object.type === 'Identifier' &&
|
||||
callee.object.name === 'React' &&
|
||||
callee.property != null &&
|
||||
!callee.computed
|
||||
@ -219,14 +239,14 @@ export default {
|
||||
if (callee.type !== 'Identifier') {
|
||||
return false;
|
||||
}
|
||||
const id = def.node.id;
|
||||
const id = (def.node as VariableDeclarator).id;
|
||||
const { name } = callee;
|
||||
if (name === 'useRef' && id.type === 'Identifier') {
|
||||
// useRef() return value is stable.
|
||||
return true;
|
||||
} else if (isUseEffectEventIdentifier(callee) && id.type === 'Identifier') {
|
||||
for (const ref of resolved.references) {
|
||||
if (ref !== id) {
|
||||
if (ref.identifier !== id) {
|
||||
useEffectEventVariables.add(ref.identifier);
|
||||
}
|
||||
}
|
||||
@ -251,7 +271,7 @@ export default {
|
||||
if (writeCount > 1) {
|
||||
return false;
|
||||
}
|
||||
setStateCallSites.set(references[i].identifier, id.elements[0]);
|
||||
setStateCallSites.set(references[i].identifier, id.elements[0]!);
|
||||
}
|
||||
}
|
||||
// Setter is stable.
|
||||
@ -286,7 +306,7 @@ export default {
|
||||
}
|
||||
|
||||
// Some are just functions that don't reference anything dynamic.
|
||||
function isFunctionWithoutCapturedValues(resolved) {
|
||||
function isFunctionWithoutCapturedValues(resolved: Scope.Variable): boolean {
|
||||
if (!isArray(resolved.defs)) {
|
||||
return false;
|
||||
}
|
||||
@ -353,32 +373,33 @@ export default {
|
||||
);
|
||||
|
||||
// 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?
|
||||
// We can check by traversing scopes upwards from the reference, and checking
|
||||
// if the last "return () => " we encounter is located directly inside the effect.
|
||||
function isInsideEffectCleanup(reference) {
|
||||
let curScope = reference.from;
|
||||
function isInsideEffectCleanup(reference: Scope.Reference): boolean {
|
||||
let curScope: Scope.Scope = reference.from;
|
||||
let isInReturnedFunction = false;
|
||||
while (curScope.block !== node) {
|
||||
if (curScope.type === 'function') {
|
||||
isInReturnedFunction =
|
||||
curScope.block.parent != null &&
|
||||
curScope.block.parent.type === 'ReturnStatement';
|
||||
isInReturnedFunction = curScope.block.parent?.type === 'ReturnStatement';
|
||||
}
|
||||
curScope = curScope.upper;
|
||||
curScope = curScope.upper!;
|
||||
}
|
||||
return isInReturnedFunction;
|
||||
}
|
||||
|
||||
// Get dependencies from all our resolved references in pure scopes.
|
||||
// Key is dependency string, value is whether it's stable.
|
||||
const dependencies = new Map();
|
||||
const optionalChains = new Map();
|
||||
const dependencies = new Map<string, Dependencies>();
|
||||
const optionalChains = new Map<string, boolean>();
|
||||
gatherDependenciesRecursively(scope);
|
||||
|
||||
function gatherDependenciesRecursively(currentScope) {
|
||||
function gatherDependenciesRecursively(currentScope: Scope.Scope): void {
|
||||
for (const reference of currentScope.references) {
|
||||
// If this reference is not resolved or it is not declared in a pure
|
||||
// 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.
|
||||
// Then normalize the narrowed dependency.
|
||||
const referenceNode = fastFindReferenceWithParent(node, reference.identifier);
|
||||
const dependencyNode = getDependency(referenceNode);
|
||||
const dependency = analyzePropertyChain(dependencyNode, optionalChains);
|
||||
const referenceNode = fastFindReferenceWithParent(node, reference.identifier)!;
|
||||
const dependencyNode: Node = getDependency(referenceNode);
|
||||
const dependency: string = analyzePropertyChain(dependencyNode, optionalChains);
|
||||
|
||||
// Accessing ref.current inside effect cleanup is bad.
|
||||
if (
|
||||
@ -401,11 +422,11 @@ export default {
|
||||
isEffect &&
|
||||
// ... and this look like accessing .current...
|
||||
dependencyNode.type === 'Identifier' &&
|
||||
(dependencyNode.parent.type === 'MemberExpression' ||
|
||||
dependencyNode.parent.type === 'OptionalMemberExpression') &&
|
||||
!dependencyNode.parent.computed &&
|
||||
dependencyNode.parent.property.type === 'Identifier' &&
|
||||
dependencyNode.parent.property.name === 'current' &&
|
||||
(dependencyNode.parent!.type === 'MemberExpression' ||
|
||||
dependencyNode.parent!.type === 'OptionalMemberExpression') &&
|
||||
!dependencyNode.parent!.computed &&
|
||||
dependencyNode.parent!.property.type === 'Identifier' &&
|
||||
dependencyNode.parent!.property.name === 'current' &&
|
||||
// ...in a cleanup function or below...
|
||||
isInsideEffectCleanup(reference)
|
||||
) {
|
||||
@ -416,8 +437,8 @@ export default {
|
||||
}
|
||||
|
||||
if (
|
||||
dependencyNode.parent.type === 'TSTypeQuery' ||
|
||||
dependencyNode.parent.type === 'TSTypeReference'
|
||||
dependencyNode.parent!.type === 'TSTypeQuery' ||
|
||||
dependencyNode.parent!.type === 'TSTypeReference'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@ -438,7 +459,7 @@ export default {
|
||||
// Add the dependency to a map so we can make sure it is referenced
|
||||
// again in our dependencies array. Remember whether it's stable.
|
||||
if (!dependencies.has(dependency)) {
|
||||
const resolved = reference.resolved;
|
||||
const resolved: Scope.Variable = reference.resolved;
|
||||
const isStable =
|
||||
memoizedIsStableKnownHookValue(resolved) ||
|
||||
memoizedIsFunctionWithoutCapturedValues(resolved);
|
||||
@ -447,7 +468,7 @@ export default {
|
||||
references: [reference],
|
||||
});
|
||||
} 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.
|
||||
currentRefsInEffectCleanup.forEach(({ reference, dependencyNode }, dependency) => {
|
||||
const references = reference.resolved.references;
|
||||
const references: Scope.Reference[] = reference.resolved!.references;
|
||||
// Is React managing this ref or us?
|
||||
// Let's see if we can find a .current assignment.
|
||||
let foundCurrentAssignment = false;
|
||||
@ -474,8 +495,8 @@ export default {
|
||||
parent.property.type === 'Identifier' &&
|
||||
parent.property.name === 'current' &&
|
||||
// ref.current = <something>
|
||||
parent.parent.type === 'AssignmentExpression' &&
|
||||
parent.parent.left === parent
|
||||
parent.parent!.type === 'AssignmentExpression' &&
|
||||
parent.parent!.left === parent
|
||||
) {
|
||||
foundCurrentAssignment = true;
|
||||
break;
|
||||
@ -486,7 +507,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
reportProblem({
|
||||
node: dependencyNode.parent.property,
|
||||
node: (dependencyNode.parent as MemberExpression).property,
|
||||
message:
|
||||
`The ref value '${dependency}.current' will likely have ` +
|
||||
`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.
|
||||
// Those are usually bugs.
|
||||
const staleAssignments = new Set();
|
||||
function reportStaleAssignment(writeExpr, key) {
|
||||
const staleAssignments = new Set<string>();
|
||||
function reportStaleAssignment(writeExpr: Node, key: string): void {
|
||||
if (staleAssignments.has(key)) {
|
||||
return;
|
||||
}
|
||||
@ -517,7 +538,7 @@ export default {
|
||||
}
|
||||
|
||||
// Remember which deps are stable and report bad usage first.
|
||||
const stableDependencies = new Set();
|
||||
const stableDependencies = new Set<string>();
|
||||
dependencies.forEach(({ isStable, references }, key) => {
|
||||
if (isStable) {
|
||||
stableDependencies.add(key);
|
||||
@ -537,8 +558,8 @@ export default {
|
||||
if (!declaredDependenciesNode) {
|
||||
// Check if there are any top-level setState() calls.
|
||||
// Those tend to lead to infinite loops.
|
||||
let setStateInsideEffectWithoutDeps = null;
|
||||
dependencies.forEach(({ isStable, references }, key) => {
|
||||
let setStateInsideEffectWithoutDeps: string | null = null;
|
||||
dependencies.forEach(({ references }, key) => {
|
||||
if (setStateInsideEffectWithoutDeps) {
|
||||
return;
|
||||
}
|
||||
@ -548,14 +569,14 @@ export default {
|
||||
}
|
||||
|
||||
const id = reference.identifier;
|
||||
const isSetState = setStateCallSites.has(id);
|
||||
const isSetState: boolean = setStateCallSites.has(id);
|
||||
if (!isSetState) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fnScope = reference.from;
|
||||
let fnScope: Scope.Scope = reference.from;
|
||||
while (fnScope.type !== 'function') {
|
||||
fnScope = fnScope.upper;
|
||||
fnScope = fnScope.upper!;
|
||||
}
|
||||
const isDirectlyInsideEffect = fnScope.block === node;
|
||||
if (isDirectlyInsideEffect) {
|
||||
@ -564,6 +585,7 @@ export default {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (setStateInsideEffectWithoutDeps) {
|
||||
const { suggestedDependencies } = collectRecommendations({
|
||||
dependencies,
|
||||
@ -596,8 +618,8 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const declaredDependencies = [];
|
||||
const externalDependencies = new Set();
|
||||
const declaredDependencies: DeclaredDependency[] = [];
|
||||
const externalDependencies = new Set<string>();
|
||||
if (declaredDependenciesNode.type !== 'ArrayExpression') {
|
||||
// If the declared dependencies are not an array expression then we
|
||||
// can't verify that the user provided the correct dependencies. Tell
|
||||
@ -640,7 +662,7 @@ export default {
|
||||
declaredDependencyNode,
|
||||
)}\``,
|
||||
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
|
||||
// will be thrown. We will catch that error and report an error.
|
||||
let declaredDependency;
|
||||
let declaredDependency: string;
|
||||
try {
|
||||
declaredDependency = analyzePropertyChain(declaredDependencyNode, null);
|
||||
} catch (error) {
|
||||
if (/Unsupported node type/.test(error.message)) {
|
||||
if (declaredDependencyNode.type === 'Literal') {
|
||||
if (dependencies.has(declaredDependencyNode.value)) {
|
||||
if (dependencies.has(declaredDependencyNode.value as any)) {
|
||||
reportProblem({
|
||||
node: declaredDependencyNode,
|
||||
message:
|
||||
@ -686,13 +708,15 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
let maybeID = declaredDependencyNode;
|
||||
let maybeID: Expression | Super = declaredDependencyNode;
|
||||
while (
|
||||
maybeID.type === 'MemberExpression' ||
|
||||
maybeID.type === 'OptionalMemberExpression' ||
|
||||
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(
|
||||
ref => ref.identifier === maybeID,
|
||||
@ -758,10 +782,12 @@ export default {
|
||||
|
||||
const message =
|
||||
`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}`;
|
||||
|
||||
let suggest;
|
||||
let suggest: Rule.SuggestionReportDescriptor[] | undefined;
|
||||
// Only handle the simple case of variable assignments.
|
||||
// Wrapping function declarations can mess up hoisting.
|
||||
if (
|
||||
@ -782,17 +808,18 @@ export default {
|
||||
: ['useCallback(', ')'];
|
||||
return [
|
||||
// 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
|
||||
// restructuring the rule code. This will cause a new lint
|
||||
// error to appear immediately for useCallback. Note we're
|
||||
// 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?
|
||||
// Should we suggest removing effect deps as an appropriate fix too?
|
||||
reportProblem({
|
||||
@ -811,7 +838,7 @@ export default {
|
||||
// in some extra deduplication. We can't do this
|
||||
// for effects though because those have legit
|
||||
// use cases for over-specifying deps.
|
||||
if (!isEffect && missingDependencies.size > 0) {
|
||||
if (!isEffect && missingDependencies.size) {
|
||||
suggestedDeps = collectRecommendations({
|
||||
dependencies,
|
||||
declaredDependencies: [], // Pretend we don't know
|
||||
@ -822,7 +849,7 @@ export default {
|
||||
}
|
||||
|
||||
// Alphabetize the suggestions, but only if deps were already alphabetized.
|
||||
function areDeclaredDepsAlphabetized() {
|
||||
function areDeclaredDepsAlphabetized(): boolean {
|
||||
if (declaredDependencies.length === 0) {
|
||||
return true;
|
||||
}
|
||||
@ -830,6 +857,7 @@ export default {
|
||||
const sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
|
||||
return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
|
||||
}
|
||||
|
||||
if (areDeclaredDepsAlphabetized()) {
|
||||
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
|
||||
// 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.
|
||||
function formatDependency(path) {
|
||||
function formatDependency(path: string): string {
|
||||
const members = path.split('.');
|
||||
let finalPath = '';
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
@ -852,7 +880,12 @@ export default {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@ -875,7 +908,7 @@ export default {
|
||||
|
||||
let extraWarning = '';
|
||||
if (unnecessaryDependencies.size > 0) {
|
||||
let badRef = null;
|
||||
let badRef: string | null = null;
|
||||
Array.from(unnecessaryDependencies.keys()).forEach(key => {
|
||||
if (badRef !== null) {
|
||||
return;
|
||||
@ -908,7 +941,7 @@ export default {
|
||||
if (propDep == null) {
|
||||
return;
|
||||
}
|
||||
const refs = propDep.references;
|
||||
const refs: Scope.Reference[] = propDep.references;
|
||||
if (!Array.isArray(refs)) {
|
||||
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.
|
||||
// This usually means they're unaware of useCallback.
|
||||
let missingCallbackDep = null;
|
||||
let missingCallbackDep: string | null = null;
|
||||
missingDependencies.forEach(missingDep => {
|
||||
if (missingCallbackDep) {
|
||||
return;
|
||||
}
|
||||
// Is this a variable from top scope?
|
||||
const topScopeRef = componentScope.set.get(missingDep);
|
||||
const usedDep = dependencies.get(missingDep);
|
||||
const usedDep = dependencies.get(missingDep)!;
|
||||
if (usedDep.references[0].resolved !== topScopeRef) {
|
||||
return;
|
||||
}
|
||||
@ -963,9 +996,9 @@ export default {
|
||||
}
|
||||
// Was it called in at least one case? Then it's a function.
|
||||
let isFunctionCall = false;
|
||||
let id;
|
||||
let id: Identifier;
|
||||
for (let i = 0; i < usedDep.references.length; i++) {
|
||||
id = usedDep.references[i].identifier;
|
||||
id = usedDep.references[i].identifier as Identifier;
|
||||
if (
|
||||
id != null &&
|
||||
id.parent != null &&
|
||||
@ -994,37 +1027,41 @@ export default {
|
||||
}
|
||||
|
||||
if (!extraWarning && missingDependencies.size > 0) {
|
||||
let setStateRecommendation = null;
|
||||
missingDependencies.forEach(missingDep => {
|
||||
let setStateRecommendation: {
|
||||
missingDep: string;
|
||||
setter: string;
|
||||
form: 'updater' | 'inlineReducer' | 'reducer';
|
||||
} | null = null;
|
||||
for (const missingDep of missingDependencies) {
|
||||
if (setStateRecommendation !== null) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
const usedDep = dependencies.get(missingDep);
|
||||
const usedDep = dependencies.get(missingDep)!;
|
||||
const references = usedDep.references;
|
||||
let id;
|
||||
let maybeCall;
|
||||
let id: Identifier;
|
||||
let maybeCall: Node | null;
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
id = references[i].identifier;
|
||||
maybeCall = id.parent;
|
||||
id = references[i].identifier as Identifier;
|
||||
maybeCall = id.parent!;
|
||||
// Try to see if we have setState(someExpr(missingDep)).
|
||||
while (maybeCall != null && maybeCall !== componentScope.block) {
|
||||
if (maybeCall.type === 'CallExpression') {
|
||||
const correspondingStateVariable = setStateCallSites.get(
|
||||
maybeCall.callee,
|
||||
maybeCall.callee as Expression,
|
||||
);
|
||||
if (correspondingStateVariable != null) {
|
||||
if (correspondingStateVariable.name === missingDep) {
|
||||
if ((correspondingStateVariable as Identifier).name === missingDep) {
|
||||
// setCount(count + 1)
|
||||
setStateRecommendation = {
|
||||
missingDep,
|
||||
setter: maybeCall.callee.name,
|
||||
setter: (maybeCall.callee as Identifier).name,
|
||||
form: 'updater',
|
||||
};
|
||||
} else if (stateVariables.has(id)) {
|
||||
// setCount(count + increment)
|
||||
setStateRecommendation = {
|
||||
missingDep,
|
||||
setter: maybeCall.callee.name,
|
||||
setter: (maybeCall.callee as Identifier).name,
|
||||
form: 'reducer',
|
||||
};
|
||||
} else {
|
||||
@ -1037,7 +1074,7 @@ export default {
|
||||
if (def != null && def.type === 'Parameter') {
|
||||
setStateRecommendation = {
|
||||
missingDep,
|
||||
setter: maybeCall.callee.name,
|
||||
setter: (maybeCall.callee as Identifier).name,
|
||||
form: 'inlineReducer',
|
||||
};
|
||||
}
|
||||
@ -1046,13 +1083,14 @@ export default {
|
||||
break;
|
||||
}
|
||||
}
|
||||
maybeCall = maybeCall.parent;
|
||||
maybeCall = maybeCall.parent!;
|
||||
}
|
||||
if (setStateRecommendation !== null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (setStateRecommendation !== null) {
|
||||
switch (setStateRecommendation.form) {
|
||||
case 'reducer':
|
||||
@ -1110,15 +1148,16 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
function visitCallExpression(node) {
|
||||
function visitCallExpression(node: CallExpression): void {
|
||||
const callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
|
||||
if (callbackIndex === -1) {
|
||||
// Not a React Hook call that needs deps.
|
||||
return;
|
||||
}
|
||||
const callback = node.arguments[callbackIndex];
|
||||
const reactiveHook = node.callee;
|
||||
const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
|
||||
const reactiveHook = node.callee as Identifier | MemberExpression;
|
||||
const reactiveHookName = (getNodeWithoutReactNamespace(reactiveHook) as Identifier)
|
||||
.name;
|
||||
const declaredDependenciesNode = node.arguments[callbackIndex + 1];
|
||||
const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
|
||||
|
||||
@ -1172,8 +1211,8 @@ export default {
|
||||
// The function passed as a callback is not written inline.
|
||||
// But perhaps it's in the dependencies array?
|
||||
if (
|
||||
declaredDependenciesNode.elements &&
|
||||
declaredDependenciesNode.elements.some(
|
||||
declaredDependenciesNode.type === 'ArrayExpression' &&
|
||||
declaredDependenciesNode.elements?.some(
|
||||
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.
|
||||
function collectRecommendations({
|
||||
dependencies,
|
||||
@ -1273,7 +1333,18 @@ function collectRecommendations({
|
||||
stableDependencies,
|
||||
externalDependencies,
|
||||
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.
|
||||
// It is a logical representation of property chains:
|
||||
// `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
|
||||
// traverse it to learn which deps are missing or unnecessary.
|
||||
const depTree = createDepTree();
|
||||
function createDepTree() {
|
||||
|
||||
function createDepTree(): DepTree {
|
||||
return {
|
||||
isUsed: false, // True if used in code
|
||||
isSatisfiedRecursively: false, // True if specified in deps
|
||||
isSubtreeUsed: false, // True if something deeper is used by code
|
||||
children: new Map(), // Nodes for properties
|
||||
isUsed: false,
|
||||
isSatisfiedRecursively: false,
|
||||
isSubtreeUsed: false,
|
||||
children: new Map<string, never>(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1315,7 +1387,7 @@ function collectRecommendations({
|
||||
});
|
||||
|
||||
// Tree manipulation helpers.
|
||||
function getOrCreateNodeByPath(rootNode, path) {
|
||||
function getOrCreateNodeByPath(rootNode: DepTree, path: string): DepTree {
|
||||
const keys = path.split('.');
|
||||
let node = rootNode;
|
||||
for (const key of keys) {
|
||||
@ -1328,7 +1400,12 @@ function collectRecommendations({
|
||||
}
|
||||
return node;
|
||||
}
|
||||
function markAllParentsByPath(rootNode, path, fn) {
|
||||
|
||||
function markAllParentsByPath(
|
||||
rootNode: DepTree,
|
||||
path: string,
|
||||
fn: (depTree: DepTree) => void,
|
||||
): void {
|
||||
const keys = path.split('.');
|
||||
let node = rootNode;
|
||||
for (const key of keys) {
|
||||
@ -1342,10 +1419,15 @@ function collectRecommendations({
|
||||
}
|
||||
|
||||
// Now we can learn which dependencies are missing or necessary.
|
||||
const missingDependencies = new Set();
|
||||
const satisfyingDependencies = new Set();
|
||||
const missingDependencies = new Set<string>();
|
||||
const satisfyingDependencies = new Set<string>();
|
||||
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) => {
|
||||
const path = keyToPath(key);
|
||||
if (child.isSatisfiedRecursively) {
|
||||
@ -1375,13 +1457,13 @@ function collectRecommendations({
|
||||
}
|
||||
|
||||
// Collect suggestions in the order they were originally specified.
|
||||
const suggestedDependencies = [];
|
||||
const unnecessaryDependencies = new Set();
|
||||
const duplicateDependencies = new Set();
|
||||
const suggestedDependencies: string[] = [];
|
||||
const unnecessaryDependencies = new Set<string>();
|
||||
const duplicateDependencies = new Set<string>();
|
||||
declaredDependencies.forEach(({ key }) => {
|
||||
// Does this declared dep satisfy a real need?
|
||||
if (satisfyingDependencies.has(key)) {
|
||||
if (suggestedDependencies.indexOf(key) === -1) {
|
||||
if (!suggestedDependencies.includes(key)) {
|
||||
// Good one.
|
||||
suggestedDependencies.push(key);
|
||||
} else {
|
||||
@ -1419,7 +1501,7 @@ function collectRecommendations({
|
||||
|
||||
// If the node will result in constructing a referentially unique value, return
|
||||
// its human readable type name, else return null.
|
||||
function getConstructionExpressionType(node) {
|
||||
function getConstructionExpressionType(node: Node) {
|
||||
switch (node.type) {
|
||||
case 'ObjectExpression':
|
||||
return 'object';
|
||||
@ -1477,6 +1559,11 @@ function scanForConstructions({
|
||||
declaredDependenciesNode,
|
||||
componentScope,
|
||||
scope,
|
||||
}: {
|
||||
declaredDependencies: DeclaredDependency[];
|
||||
declaredDependenciesNode: Node;
|
||||
componentScope: Scope.Scope;
|
||||
scope: Scope.Scope;
|
||||
}) {
|
||||
const constructions = declaredDependencies
|
||||
.map(({ key }) => {
|
||||
@ -1502,23 +1589,23 @@ function scanForConstructions({
|
||||
) {
|
||||
const constantExpressionType = getConstructionExpressionType(node.node.init);
|
||||
if (constantExpressionType != null) {
|
||||
return [ref, constantExpressionType];
|
||||
return [ref, constantExpressionType] as const;
|
||||
}
|
||||
}
|
||||
// function handleChange() {}
|
||||
if (node.type === 'FunctionName' && node.node.type === 'FunctionDeclaration') {
|
||||
return [ref, 'function'];
|
||||
return [ref, 'function'] as const;
|
||||
}
|
||||
|
||||
// class Foo {}
|
||||
if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') {
|
||||
return [ref, 'class'];
|
||||
return [ref, 'class'] as const;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
function isUsedOutsideOfHook(ref) {
|
||||
function isUsedOutsideOfHook(ref: Scope.Variable): boolean {
|
||||
let foundWriteExpr = false;
|
||||
for (let i = 0; i < ref.references.length; i++) {
|
||||
const reference = ref.references[i];
|
||||
@ -1534,7 +1621,7 @@ function scanForConstructions({
|
||||
}
|
||||
let currentScope = reference.from;
|
||||
while (currentScope !== scope && currentScope != null) {
|
||||
currentScope = currentScope.upper;
|
||||
currentScope = currentScope.upper!;
|
||||
}
|
||||
if (currentScope !== scope) {
|
||||
// This reference is outside the Hook callback.
|
||||
@ -1561,21 +1648,22 @@ function scanForConstructions({
|
||||
* props.foo.(bar) => (props).foo.bar
|
||||
* props.foo.bar.(baz) => (props).foo.bar.baz
|
||||
*/
|
||||
function getDependency(node) {
|
||||
function getDependency(node: Node): Node {
|
||||
const parent = node.parent!;
|
||||
if (
|
||||
(node.parent.type === 'MemberExpression' ||
|
||||
node.parent.type === 'OptionalMemberExpression') &&
|
||||
node.parent.object === node &&
|
||||
node.parent.property.name !== 'current' &&
|
||||
!node.parent.computed &&
|
||||
(parent.type === 'MemberExpression' || parent.type === 'OptionalMemberExpression') &&
|
||||
parent.object === node &&
|
||||
parent.property.type === 'Identifier' &&
|
||||
parent.property.name !== 'current' &&
|
||||
!parent.computed &&
|
||||
!(
|
||||
node.parent.parent != null &&
|
||||
(node.parent.parent.type === 'CallExpression' ||
|
||||
node.parent.parent.type === 'OptionalCallExpression') &&
|
||||
node.parent.parent.callee === node.parent
|
||||
parent.parent != null &&
|
||||
(parent.parent.type === 'CallExpression' ||
|
||||
parent.parent.type === 'OptionalCallExpression') &&
|
||||
parent.parent.callee === parent
|
||||
)
|
||||
) {
|
||||
return getDependency(node.parent);
|
||||
return getDependency(parent);
|
||||
} else if (
|
||||
// Note: we don't check OptionalMemberExpression because it can't be LHS.
|
||||
node.type === 'MemberExpression' &&
|
||||
@ -1595,9 +1683,13 @@ function getDependency(node) {
|
||||
* It just means there is an optional member somewhere inside.
|
||||
* 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 (node.optional) {
|
||||
if ((node as OptionalMemberExpression).optional) {
|
||||
// We only want to consider it optional if *all* usages were optional.
|
||||
if (!optionalChains.has(result)) {
|
||||
// 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'
|
||||
* Otherwise throw.
|
||||
*/
|
||||
function analyzePropertyChain(node, optionalChains) {
|
||||
function analyzePropertyChain(
|
||||
node: Node,
|
||||
optionalChains: Map<string, boolean> | null,
|
||||
): string {
|
||||
if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
|
||||
const result = node.name;
|
||||
if (optionalChains) {
|
||||
@ -1654,7 +1749,7 @@ function analyzePropertyChain(node, optionalChains) {
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeWithoutReactNamespace(node, options) {
|
||||
function getNodeWithoutReactNamespace(node: Identifier | MemberExpression) {
|
||||
if (
|
||||
node.type === 'MemberExpression' &&
|
||||
node.object.type === 'Identifier' &&
|
||||
@ -1672,7 +1767,13 @@ function getNodeWithoutReactNamespace(node, options) {
|
||||
// 0 for useEffect/useMemo/useCallback(fn).
|
||||
// 1 for useImperativeHandle(ref, fn).
|
||||
// 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);
|
||||
if (node.type !== 'Identifier') {
|
||||
return -1;
|
||||
@ -1718,12 +1819,12 @@ function getReactiveHookCallbackIndex(calleeNode, options) {
|
||||
* - optimized by only searching nodes with a range surrounding our target node
|
||||
* - 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];
|
||||
let item = null;
|
||||
let item: Node;
|
||||
|
||||
while (queue.length) {
|
||||
item = queue.shift();
|
||||
item = queue.shift()!;
|
||||
|
||||
if (isSameIdentifier(item, target)) {
|
||||
return item;
|
||||
@ -1754,7 +1855,7 @@ function fastFindReferenceWithParent(start, target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function joinEnglish(arr) {
|
||||
function joinEnglish(arr: string[]): string {
|
||||
let s = '';
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
s += arr[i];
|
||||
@ -1769,7 +1870,7 @@ function joinEnglish(arr) {
|
||||
return s;
|
||||
}
|
||||
|
||||
function isNodeLike(val) {
|
||||
function isNodeLike(val: any): boolean {
|
||||
return (
|
||||
typeof val === 'object' &&
|
||||
val !== null &&
|
||||
@ -1778,23 +1879,25 @@ function isNodeLike(val) {
|
||||
);
|
||||
}
|
||||
|
||||
function isSameIdentifier(a, b) {
|
||||
function isSameIdentifier(a: Node, b: Node): boolean {
|
||||
return (
|
||||
(a.type === 'Identifier' || a.type === 'JSXIdentifier') &&
|
||||
a.type === b.type &&
|
||||
a.name === b.name &&
|
||||
a.range[0] === b.range[0] &&
|
||||
a.range[1] === b.range[1]
|
||||
a.range![0] === b.range![0] &&
|
||||
a.range![1] === b.range![1]
|
||||
);
|
||||
}
|
||||
|
||||
function isAncestorNodeOf(a, b) {
|
||||
return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
|
||||
function isAncestorNodeOf(a: Node, b: Node): boolean {
|
||||
return a.range![0] <= b.range![0] && a.range![1] >= b.range![1];
|
||||
}
|
||||
|
||||
function isUseEffectEventIdentifier(node) {
|
||||
function isUseEffectEventIdentifier(node: Node): boolean {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default rule;
|
||||
|
@ -7,15 +7,23 @@
|
||||
|
||||
/* global BigInt */
|
||||
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
||||
|
||||
'use strict';
|
||||
import type { Rule, Scope } from 'eslint';
|
||||
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
|
||||
* character to exclude identifiers like "user".
|
||||
*/
|
||||
|
||||
function isHookName(s) {
|
||||
function isHookName(s: string) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
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
|
||||
* containing a hook name.
|
||||
*/
|
||||
|
||||
function isHook(node) {
|
||||
function isHook(node: Node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return isHookName(node.name);
|
||||
} else if (
|
||||
@ -48,16 +55,16 @@ function isHook(node) {
|
||||
* always start with an uppercase letter.
|
||||
*/
|
||||
|
||||
function isComponentName(node) {
|
||||
function isComponentName(node: Node) {
|
||||
return node.type === 'Identifier' && /^[A-Z]/.test(node.name);
|
||||
}
|
||||
|
||||
function isReactFunction(node, functionName) {
|
||||
function isReactFunction(node: Expression | Super, functionName: string) {
|
||||
return (
|
||||
node.name === functionName ||
|
||||
(node as Identifier).name === functionName ||
|
||||
(node.type === 'MemberExpression' &&
|
||||
node.object.name === 'React' &&
|
||||
node.property.name === functionName)
|
||||
(node.object as Identifier).name === 'React' &&
|
||||
(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
|
||||
* should follow the rules of hooks.
|
||||
*/
|
||||
|
||||
function isForwardRefCallback(node) {
|
||||
function isForwardRefCallback(node: Rule.Node) {
|
||||
return !!(
|
||||
node.parent &&
|
||||
node.parent.callee &&
|
||||
isReactFunction(node.parent.callee, 'forwardRef')
|
||||
(node.parent as CallExpression)?.callee &&
|
||||
isReactFunction((node.parent as CallExpression).callee, 'forwardRef')
|
||||
);
|
||||
}
|
||||
|
||||
@ -79,15 +84,14 @@ function isForwardRefCallback(node) {
|
||||
* functional component should follow the rules of hooks.
|
||||
*/
|
||||
|
||||
function isMemoCallback(node) {
|
||||
function isMemoCallback(node: Rule.Node) {
|
||||
return !!(
|
||||
node.parent &&
|
||||
node.parent.callee &&
|
||||
isReactFunction(node.parent.callee, 'memo')
|
||||
(node.parent as CallExpression)?.callee &&
|
||||
isReactFunction((node.parent as CallExpression).callee, 'memo')
|
||||
);
|
||||
}
|
||||
|
||||
function isInsideComponentOrHook(node) {
|
||||
function isInsideComponentOrHook(node: Rule.Node) {
|
||||
while (node) {
|
||||
const functionName = getFunctionName(node);
|
||||
if (functionName) {
|
||||
@ -103,21 +107,21 @@ function isInsideComponentOrHook(node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUseEffectEventIdentifier(node) {
|
||||
function isUseEffectEventIdentifier(node: Node) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUseIdentifier(node) {
|
||||
function isUseIdentifier(node: Node) {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return node.type === 'Identifier' && node.name === 'use';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
@ -127,17 +131,20 @@ export default {
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
let lastEffect = null;
|
||||
const codePathReactHooksMapStack = [];
|
||||
const codePathSegmentStack = [];
|
||||
const useEffectEventFunctions = new WeakSet();
|
||||
let lastEffect: CallExpression | null = null;
|
||||
const codePathReactHooksMapStack: Map<
|
||||
Rule.CodePathSegment,
|
||||
(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
|
||||
// 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.
|
||||
function recordAllUseEffectEventFunctions(scope) {
|
||||
function recordAllUseEffectEventFunctions(scope: Scope.Scope) {
|
||||
for (const reference of scope.references) {
|
||||
const parent = reference.identifier.parent;
|
||||
const parent = reference.identifier.parent!;
|
||||
if (
|
||||
parent.type === 'VariableDeclarator' &&
|
||||
parent.init &&
|
||||
@ -145,7 +152,7 @@ export default {
|
||||
parent.init.callee &&
|
||||
isUseEffectEventIdentifier(parent.init.callee)
|
||||
) {
|
||||
for (const ref of reference.resolved.references) {
|
||||
for (const ref of reference.resolved!.references) {
|
||||
if (ref !== reference) {
|
||||
useEffectEventFunctions.add(ref.identifier);
|
||||
}
|
||||
@ -167,7 +174,7 @@ export default {
|
||||
// Everything is ok if all React Hooks are both reachable from the initial
|
||||
// segment and reachable from every final segment.
|
||||
onCodePathEnd(codePath, codePathNode) {
|
||||
const reactHooksMap = codePathReactHooksMapStack.pop();
|
||||
const reactHooksMap = codePathReactHooksMapStack.pop()!;
|
||||
if (reactHooksMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
@ -197,7 +204,10 @@ export default {
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsFromStart(segment, pathHistory) {
|
||||
function countPathsFromStart(
|
||||
segment: Rule.CodePathSegment,
|
||||
pathHistory?: Set<string>,
|
||||
) {
|
||||
const { cache } = countPathsFromStart;
|
||||
let paths = cache.get(segment.id);
|
||||
const pathList = new Set(pathHistory);
|
||||
@ -211,7 +221,7 @@ export default {
|
||||
cyclic.add(cyclicSegment);
|
||||
}
|
||||
|
||||
return BigInt('0');
|
||||
return 0n;
|
||||
}
|
||||
|
||||
// add the current segment to pathList
|
||||
@ -223,11 +233,11 @@ export default {
|
||||
}
|
||||
|
||||
if (codePath.thrownSegments.includes(segment)) {
|
||||
paths = BigInt('0');
|
||||
paths = 0n;
|
||||
} else if (segment.prevSegments.length === 0) {
|
||||
paths = BigInt('1');
|
||||
paths = 1n;
|
||||
} else {
|
||||
paths = BigInt('0');
|
||||
paths = 0n;
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
paths += countPathsFromStart(prevSegment, pathList);
|
||||
}
|
||||
@ -266,7 +276,10 @@ export default {
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsToEnd(segment, pathHistory) {
|
||||
function countPathsToEnd(
|
||||
segment: Rule.CodePathSegment,
|
||||
pathHistory?: Set<string>,
|
||||
): bigint {
|
||||
const { cache } = countPathsToEnd;
|
||||
let paths = cache.get(segment.id);
|
||||
const pathList = new Set(pathHistory);
|
||||
@ -280,7 +293,7 @@ export default {
|
||||
cyclic.add(cyclicSegment);
|
||||
}
|
||||
|
||||
return BigInt('0');
|
||||
return 0n;
|
||||
}
|
||||
|
||||
// add the current segment to pathList
|
||||
@ -292,11 +305,11 @@ export default {
|
||||
}
|
||||
|
||||
if (codePath.thrownSegments.includes(segment)) {
|
||||
paths = BigInt('0');
|
||||
paths = 0n;
|
||||
} else if (segment.nextSegments.length === 0) {
|
||||
paths = BigInt('1');
|
||||
paths = 1n;
|
||||
} else {
|
||||
paths = BigInt('0');
|
||||
paths = 0n;
|
||||
for (const nextSegment of segment.nextSegments) {
|
||||
paths += countPathsToEnd(nextSegment, pathList);
|
||||
}
|
||||
@ -328,7 +341,7 @@ export default {
|
||||
* so we would return that.
|
||||
*/
|
||||
|
||||
function shortestPathLengthToStart(segment) {
|
||||
function shortestPathLengthToStart(segment: Rule.CodePathSegment): number {
|
||||
const { cache } = shortestPathLengthToStart;
|
||||
let length = cache.get(segment.id);
|
||||
|
||||
@ -361,9 +374,9 @@ export default {
|
||||
return length;
|
||||
}
|
||||
|
||||
countPathsFromStart.cache = new Map();
|
||||
countPathsToEnd.cache = new Map();
|
||||
shortestPathLengthToStart.cache = new Map();
|
||||
countPathsFromStart.cache = new Map<string, bigint>();
|
||||
countPathsToEnd.cache = new Map<string, bigint>();
|
||||
shortestPathLengthToStart.cache = new Map<string, number | null>();
|
||||
|
||||
// Count all code paths to the end of our component/hook. Also primes
|
||||
// the `countPathsToEnd` cache.
|
||||
@ -480,7 +493,7 @@ export default {
|
||||
// called in.
|
||||
if (isDirectlyInsideComponentOrHook) {
|
||||
// Report an error if the hook is called inside an async function.
|
||||
const isAsyncFunction = codePathNode.async;
|
||||
const isAsyncFunction = (codePathNode as BaseFunction).async;
|
||||
if (isAsyncFunction) {
|
||||
context.report({
|
||||
node: hook,
|
||||
@ -565,8 +578,8 @@ export default {
|
||||
if (isHook(node.callee)) {
|
||||
// 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.
|
||||
const reactHooksMap = last(codePathReactHooksMapStack);
|
||||
const codePathSegment = last(codePathSegmentStack);
|
||||
const reactHooksMap = codePathReactHooksMapStack.at(-1)!;
|
||||
const codePathSegment = codePathSegmentStack.at(-1)!;
|
||||
let reactHooks = reactHooksMap.get(codePathSegment);
|
||||
if (!reactHooks) {
|
||||
reactHooks = [];
|
||||
@ -637,8 +650,8 @@ export default {
|
||||
* where JS gives anonymous function expressions names. We roughly detect the
|
||||
* same AST nodes with some exceptions to better fit our use case.
|
||||
*/
|
||||
|
||||
function getFunctionName(node) {
|
||||
function getFunctionName(node: Node) {
|
||||
const parent = node.parent!;
|
||||
if (
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
(node.type === 'FunctionExpression' && node.id)
|
||||
@ -653,24 +666,20 @@ function getFunctionName(node) {
|
||||
node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
if (node.parent.type === 'VariableDeclarator' && node.parent.init === node) {
|
||||
if (parent.type === 'VariableDeclarator' && parent.init === node) {
|
||||
// const useHook = () => {};
|
||||
return node.parent.id;
|
||||
return parent.id;
|
||||
} else if (
|
||||
node.parent.type === 'AssignmentExpression' &&
|
||||
node.parent.right === node &&
|
||||
node.parent.operator === '='
|
||||
parent.type === 'AssignmentExpression' &&
|
||||
parent.right === node &&
|
||||
parent.operator === '='
|
||||
) {
|
||||
// useHook = () => {};
|
||||
return node.parent.left;
|
||||
} else if (
|
||||
node.parent.type === 'Property' &&
|
||||
node.parent.value === node &&
|
||||
!node.parent.computed
|
||||
) {
|
||||
return parent.left;
|
||||
} else if (parent.type === 'Property' && parent.value === node && !parent.computed) {
|
||||
// {useHook: () => {}}
|
||||
// {useHook() {}}
|
||||
return node.parent.key;
|
||||
return parent.key;
|
||||
|
||||
// NOTE: We could also support `ClassProperty` and `MethodDefinition`
|
||||
// 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() {}}
|
||||
} else if (
|
||||
node.parent.type === 'AssignmentPattern' &&
|
||||
node.parent.right === node &&
|
||||
!node.parent.computed
|
||||
parent.type === 'AssignmentPattern' &&
|
||||
parent.right === node &&
|
||||
!parent.computed
|
||||
) {
|
||||
// const {useHook = () => {}} = {};
|
||||
// ({useHook = () => {}} = {});
|
||||
//
|
||||
// Kinda clowny, but we'd said we'd follow spec convention for
|
||||
// `IsAnonymousFunctionDefinition()` usage.
|
||||
return node.parent.left;
|
||||
return parent.left;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
@ -697,10 +706,4 @@ function getFunctionName(node) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for peeking the last item in a stack.
|
||||
*/
|
||||
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
export default rule;
|
||||
|
@ -4,10 +4,12 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type { Linter } from 'eslint';
|
||||
import RulesOfHooks from './RulesOfHooks';
|
||||
import ExhaustiveDeps from './ExhaustiveDeps';
|
||||
|
||||
export const __EXPERIMENTAL__ = false;
|
||||
|
||||
export const configs = {
|
||||
recommended: {
|
||||
plugins: ['react-hooks'],
|
||||
@ -15,7 +17,7 @@ export const configs = {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
},
|
||||
},
|
||||
} as Linter.BaseConfig,
|
||||
};
|
||||
|
||||
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": {
|
||||
"build": "./esbuild.ts",
|
||||
"check-import": "for js in dist/*.js; do cat $js | grep 'require('; done"
|
||||
@ -10,17 +11,28 @@
|
||||
"@babel/preset-env": "^7.22.9",
|
||||
"@types/babel-plugin-macros": "^3.1.0",
|
||||
"@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",
|
||||
"@typescript-eslint/types": "^6.1.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"dts-bundle-generator": "^8.0.1",
|
||||
"esbin": "0.0.1-beta.1",
|
||||
"esbuild": "0.18.14",
|
||||
"esbuild-plugin-alias": "^0.2.1",
|
||||
"esbuild-register": "3.4.2",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-define-config": "^1.21.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"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": {
|
||||
"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
|
||||
index f0c03a3c..a7cadb55 100644
|
||||
--- a/scripts/resolverDirectories.js
|
||||
@ -302,10 +412,10 @@ index 92b838c0..ccb13ba0 100644
|
||||
return `${repoUrl}/blob/${commitish}/docs/rules/${ruleName}.md`;
|
||||
}
|
||||
diff --git a/src/index.js b/src/index.js
|
||||
index feafba90..84992bef 100644
|
||||
index feafba90..9a464041 100644
|
||||
--- a/src/index.js
|
||||
+++ b/src/index.js
|
||||
@@ -1,71 +1,132 @@
|
||||
@@ -1,71 +1,135 @@
|
||||
-export const rules = {
|
||||
- 'no-unresolved': require('./rules/no-unresolved'),
|
||||
- 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-anonymous-default-export': require('./rules/no-anonymous-default-export'),
|
||||
- 'no-unused-modules': require('./rules/no-unused-modules'),
|
||||
+/**
|
||||
+ * @type {Readonly<import('eslint').Linter.RulesRecord>}
|
||||
+ */
|
||||
+export const rules = /*#__PURE__*/ kebabCase({
|
||||
+ noUnresolved,
|
||||
+ named,
|
||||
@ -464,9 +577,8 @@ index feafba90..84992bef 100644
|
||||
+ importsFirst,
|
||||
+});
|
||||
|
||||
-export const configs = {
|
||||
export const configs = {
|
||||
- recommended: require('../config/recommended'),
|
||||
+export const configs = /*#__PURE__*/ kebabCase({
|
||||
+ recommended,
|
||||
|
||||
- errors: require('../config/errors'),
|
||||
@ -483,12 +595,11 @@ index feafba90..84992bef 100644
|
||||
- 'react-native': require('../config/react-native'),
|
||||
- electron: require('../config/electron'),
|
||||
- typescript: require('../config/typescript'),
|
||||
-};
|
||||
+ react,
|
||||
+ reactNative,
|
||||
+ 'react-native': reactNative,
|
||||
+ electron,
|
||||
+ typescript,
|
||||
+});
|
||||
};
|
||||
+
|
||||
+function kebabCase(obj) {
|
||||
+ return Object.fromEntries(
|
||||
|
@ -1,8 +1,8 @@
|
||||
diff --git a/src/index.js b/src/index.js
|
||||
index 7b931fe..f7c1f91 100644
|
||||
index 7b931fe..eaea267 100644
|
||||
--- a/src/index.js
|
||||
+++ b/src/index.js
|
||||
@@ -1,47 +1,87 @@
|
||||
@@ -1,296 +1,344 @@
|
||||
/* eslint-disable global-require */
|
||||
+// @ts-check
|
||||
+import accessibleEmoji from './rules/accessible-emoji';
|
||||
@ -45,7 +45,7 @@ index 7b931fe..f7c1f91 100644
|
||||
+import scope from './rules/scope';
|
||||
+import tabindexNoPositive from './rules/tabindex-no-positive';
|
||||
|
||||
module.exports = {
|
||||
-module.exports = {
|
||||
- rules: {
|
||||
- 'accessible-emoji': require('./rules/accessible-emoji'),
|
||||
- 'alt-text': require('./rules/alt-text'),
|
||||
@ -87,58 +87,532 @@ index 7b931fe..f7c1f91 100644
|
||||
- scope: require('./rules/scope'),
|
||||
- 'tabindex-no-positive': require('./rules/tabindex-no-positive'),
|
||||
- },
|
||||
+ rules: kebabCase({
|
||||
+ accessibleEmoji,
|
||||
+ altText,
|
||||
+ anchorAmbiguousText,
|
||||
+ anchorHasContent,
|
||||
+ anchorIsValid,
|
||||
+ ariaActivedescendantHasTabindex,
|
||||
+ ariaProps,
|
||||
+ ariaProptypes,
|
||||
+ ariaRole,
|
||||
+ ariaUnsupportedElements,
|
||||
+ autocompleteValid,
|
||||
+ clickEventsHaveKeyEvents,
|
||||
+ controlHasAssociatedLabel,
|
||||
+ headingHasContent,
|
||||
+ htmlHasLang,
|
||||
+ iframeHasTitle,
|
||||
+ imgRedundantAlt,
|
||||
+ interactiveSupportsFocus,
|
||||
+ labelHasAssociatedControl,
|
||||
+ labelHasFor,
|
||||
+ lang,
|
||||
+ mediaHasCaption,
|
||||
+ mouseEventsHaveKeyEvents,
|
||||
+ noAccessKey,
|
||||
+ noAriaHiddenOnFocusable,
|
||||
+ noAutofocus,
|
||||
+ noDistractingElements,
|
||||
+ noInteractiveElementToNoninteractiveRole,
|
||||
+ noNoninteractiveElementInteractions,
|
||||
+ noNoninteractiveElementToInteractiveRole,
|
||||
+ noNoninteractiveTabindex,
|
||||
+ noOnChange,
|
||||
+ noRedundantRoles,
|
||||
+ noStaticElementInteractions,
|
||||
+ preferTagOverRole,
|
||||
+ roleHasRequiredAriaProps,
|
||||
+ roleSupportsAriaProps,
|
||||
+ scope,
|
||||
+ tabindexNoPositive,
|
||||
+ }),
|
||||
configs: {
|
||||
recommended: {
|
||||
plugins: [
|
||||
@@ -294,3 +334,9 @@ module.exports = {
|
||||
- configs: {
|
||||
- recommended: {
|
||||
- plugins: [
|
||||
- 'jsx-a11y',
|
||||
+export const rules = kebabCase({
|
||||
+ accessibleEmoji,
|
||||
+ altText,
|
||||
+ anchorAmbiguousText,
|
||||
+ anchorHasContent,
|
||||
+ anchorIsValid,
|
||||
+ ariaActivedescendantHasTabindex,
|
||||
+ ariaProps,
|
||||
+ ariaProptypes,
|
||||
+ ariaRole,
|
||||
+ ariaUnsupportedElements,
|
||||
+ autocompleteValid,
|
||||
+ clickEventsHaveKeyEvents,
|
||||
+ controlHasAssociatedLabel,
|
||||
+ headingHasContent,
|
||||
+ htmlHasLang,
|
||||
+ iframeHasTitle,
|
||||
+ imgRedundantAlt,
|
||||
+ interactiveSupportsFocus,
|
||||
+ labelHasAssociatedControl,
|
||||
+ labelHasFor,
|
||||
+ lang,
|
||||
+ mediaHasCaption,
|
||||
+ mouseEventsHaveKeyEvents,
|
||||
+ noAccessKey,
|
||||
+ noAriaHiddenOnFocusable,
|
||||
+ noAutofocus,
|
||||
+ noDistractingElements,
|
||||
+ noInteractiveElementToNoninteractiveRole,
|
||||
+ noNoninteractiveElementInteractions,
|
||||
+ noNoninteractiveElementToInteractiveRole,
|
||||
+ noNoninteractiveTabindex,
|
||||
+ noOnChange,
|
||||
+ noRedundantRoles,
|
||||
+ noStaticElementInteractions,
|
||||
+ preferTagOverRole,
|
||||
+ roleHasRequiredAriaProps,
|
||||
+ roleSupportsAriaProps,
|
||||
+ scope,
|
||||
+ tabindexNoPositive,
|
||||
+});
|
||||
+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) {
|
||||
+ 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
|
||||
index 4140c6c8..792ceb4f 100644
|
||||
index 4140c6c8..03e623af 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -1,15 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
-'use strict';
|
||||
-
|
||||
-const configAll = require('./configs/all');
|
||||
-const configRecommended = require('./configs/recommended');
|
||||
-const configRuntime = require('./configs/jsx-runtime');
|
||||
@ -102,16 +102,47 @@ index 4140c6c8..792ceb4f 100644
|
||||
+import configRecommended from './configs/recommended';
|
||||
+import configRuntime from './configs/jsx-runtime';
|
||||
+import { name } from './package.json';
|
||||
+import allRules from './lib/rules';
|
||||
+export { default as rules } from './lib/rules';
|
||||
|
||||
// for legacy config system
|
||||
-const plugins = [
|
||||
- 'react',
|
||||
-];
|
||||
+const plugins = [name];
|
||||
+
|
||||
+export const deprecatedRules = configAll.plugins.react.deprecatedRules;
|
||||
|
||||
module.exports = {
|
||||
deprecatedRules: configAll.plugins.react.deprecatedRules,
|
||||
-module.exports = {
|
||||
- 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
|
||||
index 204a33c4..01d992c2 100644
|
||||
--- a/lib/rules/button-has-type.js
|
||||
@ -253,7 +284,7 @@ index 55073bfe..efc07af1 100644
|
||||
const astUtil = require('./ast');
|
||||
const isCreateElement = require('./isCreateElement');
|
||||
diff --git a/package.json b/package.json
|
||||
index cb736434..a97113c0 100644
|
||||
index b1fa86fa..758b2177 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -25,21 +25,13 @@
|
||||
@ -273,12 +304,12 @@ index cb736434..a97113c0 100644
|
||||
- "object.values": "^1.1.6",
|
||||
"prop-types": "^15.8.1",
|
||||
"resolve": "^2.0.0-next.4",
|
||||
- "semver": "^6.3.0",
|
||||
- "semver": "^6.3.1",
|
||||
- "string.prototype.matchall": "^4.0.8"
|
||||
+ "semver": "^6.3.0"
|
||||
+ "semver": "^6.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/core": "^7.22.9",
|
||||
diff --git a/tsconfig.json b/tsconfig.json
|
||||
deleted file mode 100644
|
||||
index 39187b7f..00000000
|
||||
|
155
pnpm-lock.yaml
generated
155
pnpm-lock.yaml
generated
@ -20,12 +20,30 @@ devDependencies:
|
||||
'@types/babel__core':
|
||||
specifier: ^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':
|
||||
specifier: ^20.4.2
|
||||
version: 20.4.2
|
||||
'@typescript-eslint/types':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
babel-plugin-macros:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
dts-bundle-generator:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
esbin:
|
||||
specifier: 0.0.1-beta.1
|
||||
version: 0.0.1-beta.1(esbuild@0.18.14)
|
||||
@ -44,15 +62,30 @@ devDependencies:
|
||||
eslint-config-prettier:
|
||||
specifier: 8.8.0
|
||||
version: 8.8.0(eslint@8.45.0)
|
||||
eslint-define-config:
|
||||
specifier: ^1.21.0
|
||||
version: 1.21.0
|
||||
eslint-plugin-import:
|
||||
specifier: ^2.27.5
|
||||
version: 2.27.5(eslint@8.45.0)
|
||||
glob:
|
||||
specifier: ^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:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
typescript:
|
||||
specifier: 5.1.6
|
||||
version: 5.1.6
|
||||
|
||||
packages:
|
||||
|
||||
@ -1690,10 +1723,35 @@ packages:
|
||||
'@babel/types': 7.21.5
|
||||
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:
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
dev: true
|
||||
|
||||
/@types/lodash@4.14.195:
|
||||
resolution: {integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==}
|
||||
dev: true
|
||||
|
||||
/@types/node@20.4.2:
|
||||
resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==}
|
||||
dev: true
|
||||
@ -1702,6 +1760,11 @@ packages:
|
||||
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
||||
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):
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@ -1911,6 +1974,15 @@ packages:
|
||||
supports-color: 7.2.0
|
||||
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:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
dependencies:
|
||||
@ -2015,6 +2087,15 @@ packages:
|
||||
esutils: 2.0.3
|
||||
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:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
dev: true
|
||||
@ -2186,6 +2267,11 @@ packages:
|
||||
eslint: 8.45.0
|
||||
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:
|
||||
resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==}
|
||||
dependencies:
|
||||
@ -2248,7 +2334,7 @@ packages:
|
||||
minimatch: 3.1.2
|
||||
object.values: 1.1.6
|
||||
resolve: 1.22.2
|
||||
semver: 6.3.0
|
||||
semver: 6.3.1
|
||||
tsconfig-paths: 3.14.2
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
@ -2434,6 +2520,11 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
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:
|
||||
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
|
||||
dependencies:
|
||||
@ -2465,7 +2556,7 @@ packages:
|
||||
dependencies:
|
||||
foreground-child: 3.1.1
|
||||
jackspeak: 2.2.1
|
||||
minimatch: 9.0.1
|
||||
minimatch: 9.0.3
|
||||
minipass: 6.0.2
|
||||
path-scurry: 1.10.1
|
||||
dev: true
|
||||
@ -2759,6 +2850,15 @@ packages:
|
||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||
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:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
dev: true
|
||||
@ -2807,6 +2907,10 @@ packages:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
dev: true
|
||||
|
||||
/lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
dev: true
|
||||
|
||||
/lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
dependencies:
|
||||
@ -2824,8 +2928,8 @@ packages:
|
||||
brace-expansion: 1.1.11
|
||||
dev: true
|
||||
|
||||
/minimatch@9.0.1:
|
||||
resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==}
|
||||
/minimatch@9.0.3:
|
||||
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
@ -3039,6 +3143,11 @@ packages:
|
||||
jsesc: 0.5.0
|
||||
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:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@ -3079,11 +3188,6 @@ packages:
|
||||
is-regex: 1.1.4
|
||||
dev: true
|
||||
|
||||
/semver@6.3.0:
|
||||
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@ -3226,6 +3330,10 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/ts-algebra@1.2.0:
|
||||
resolution: {integrity: sha512-kMuJJd8B2N/swCvIvn1hIFcIOrLGbWl9m/J6O3kHx9VRaevh00nvgjPiEGaRee7DRaAczMYR2uwWvXU22VFltw==}
|
||||
dev: true
|
||||
|
||||
/tsconfig-paths@3.14.2:
|
||||
resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==}
|
||||
dependencies:
|
||||
@ -3264,6 +3372,12 @@ packages:
|
||||
is-typed-array: 1.1.10
|
||||
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:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
dependencies:
|
||||
@ -3365,6 +3479,11 @@ packages:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
dev: true
|
||||
|
||||
/y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
dev: true
|
||||
@ -3374,6 +3493,24 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
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:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -6,4 +6,5 @@ sync() (
|
||||
sync eslint-plugin-import
|
||||
sync eslint-plugin-jsx-a11y
|
||||
sync eslint-plugin-react
|
||||
sync eslint-plugin-n
|
||||
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": {
|
||||
"allowJs": true,
|
||||
"allowArbitraryExtensions": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"jsx": "react-jsx",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
|
Loading…
x
Reference in New Issue
Block a user