Initial commit

This commit is contained in:
Alex 2023-07-19 23:40:39 -04:00
commit fd67e90cbc
24 changed files with 7998 additions and 0 deletions

62
.eslintrc Normal file
View File

@ -0,0 +1,62 @@
{
"root": true,
"env": {
"node": true,
"browser": true,
"es6": true
},
"extends": ["eslint:recommended", "prettier"],
"parserOptions": {
"sourceType": "module",
"ecmaVersion": "latest"
},
"rules": {
"no-restricted-imports": [
"warn",
{
"paths": [
"array-includes",
"array.prototype.flat",
"array.prototype.flatmap",
"array.prototype.tosorted",
"object.entries",
"object.fromentries",
"object.hasown",
"object.values",
"string.prototype.matchall",
"has"
]
}
],
"arrow-body-style": ["error", "as-needed"],
"class-methods-use-this": [
"warn",
{ "exceptMethods": ["toString", "shouldComponentUpdate"] }
],
"complexity": ["warn", { "max": 100 }],
"curly": ["error", "multi-line", "consistent"],
"eqeqeq": ["error", "smart"],
"no-async-promise-executor": "off",
"no-case-declarations": "off",
"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",
"object-shorthand": ["error", "always", { "ignoreConstructors": true }],
"one-var": ["error", { "var": "never", "let": "never" }],
"prefer-const": ["error", { "destructuring": "all" }],
"prefer-destructuring": [
"warn",
{ "AssignmentExpression": { "array": false, "object": false } }
],
"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 }]
}
}

139
.gitignore vendored Normal file
View File

@ -0,0 +1,139 @@
eslint-plugin-import
eslint-plugin-jsx-a11y
eslint-plugin-react
jsx-ast-utils
react
dist/**/*.js
dist/**/*.js.map
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
repl.ts

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
registry http://raspberrypi.local:4873
always-auth=true

12
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"editor.formatOnSave": true,
"eslint.runtime": "node",
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"**/__mocks__": true,
"**/__tests__": true,
"**/tests": true
}
}

9
build.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
rm dist/*.js
mkdir -p dist
./esbuild.ts
# Test
DEST=$HOME/Git/archive/node_modules
rm -rf "$DEST/@proteria/eslint-rules"
cp -r dist "$DEST/@proteria/eslint-rules"

9
dist/README.md vendored Normal file
View File

@ -0,0 +1,9 @@
# eslint-rules
Custom build of various ESLint rule libraries with little dependency as possible.
## Includes
- eslint-plugin-import
- eslint-plugin-jsx-a11y
- eslint-plugin-react

30
dist/package.json vendored Normal file
View File

@ -0,0 +1,30 @@
{
"name": "@aet/eslint-rules",
"version": "0.0.1-beta.11",
"license": "UNLICENSED",
"publishConfig": {
"registry": "http://raspberrypi.local:4873"
},
"peerDependencies": {
"typescript": "^5.1.6"
},
"dependencies": {
"aria-query": "^5.3.0",
"axe-core": "4.7.2",
"axobject-query": "^3.2.1",
"damerau-levenshtein": "1.0.8",
"debug": "^4.3.4",
"doctrine": "^3.0.0",
"eslint-import-resolver-node": "^0.3.7",
"eslint-module-utils": "^2.8.0",
"estraverse": "^5.3.0",
"is-core-module": "^2.12.1",
"is-glob": "^4.0.3",
"language-tags": "^1.0.8",
"minimatch": "^9.0.3",
"prop-types": "^15.8.1",
"resolve": "^2.0.0-next.4",
"semver": "^7.5.4",
"tsconfig-paths": "^4.2.0"
}
}

52
esbuild.ts Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env -S node -r esbin
import esbuild from 'esbuild';
import { resolve } from 'path';
import { babelPlugin } from './src/babel';
const args = process.argv.slice(2);
const ENV = process.env.NODE_ENV || 'development';
const PROD = ENV === 'production';
async function main(entry: string, outfile: string) {
const context = await esbuild.context({
entryPoints: [entry],
outfile,
bundle: true,
minify: PROD,
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',
}));
},
},
],
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');

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,706 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* global BigInt */
/* eslint-disable no-for-of-loops/no-for-of-loops */
'use strict';
/**
* Catch all identifiers that begin with "use" followed by an uppercase Latin
* character to exclude identifiers like "user".
*/
function isHookName(s) {
if (__EXPERIMENTAL__) {
return s === 'use' || /^use[A-Z0-9]/.test(s);
}
return /^use[A-Z0-9]/.test(s);
}
/**
* We consider hooks to be a hook name identifier or a member expression
* containing a hook name.
*/
function isHook(node) {
if (node.type === 'Identifier') {
return isHookName(node.name);
} else if (
node.type === 'MemberExpression' &&
!node.computed &&
isHook(node.property)
) {
const obj = node.object;
const isPascalCaseNameSpace = /^[A-Z].*/;
return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);
} else {
return false;
}
}
/**
* Checks if the node is a React component name. React component names must
* always start with an uppercase letter.
*/
function isComponentName(node) {
return node.type === 'Identifier' && /^[A-Z]/.test(node.name);
}
function isReactFunction(node, functionName) {
return (
node.name === functionName ||
(node.type === 'MemberExpression' &&
node.object.name === 'React' &&
node.property.name === functionName)
);
}
/**
* Checks if the node is a callback argument of forwardRef. This render function
* should follow the rules of hooks.
*/
function isForwardRefCallback(node) {
return !!(
node.parent &&
node.parent.callee &&
isReactFunction(node.parent.callee, 'forwardRef')
);
}
/**
* Checks if the node is a callback argument of React.memo. This anonymous
* functional component should follow the rules of hooks.
*/
function isMemoCallback(node) {
return !!(
node.parent &&
node.parent.callee &&
isReactFunction(node.parent.callee, 'memo')
);
}
function isInsideComponentOrHook(node) {
while (node) {
const functionName = getFunctionName(node);
if (functionName) {
if (isComponentName(functionName) || isHook(functionName)) {
return true;
}
}
if (isForwardRefCallback(node) || isMemoCallback(node)) {
return true;
}
node = node.parent;
}
return false;
}
function isUseEffectEventIdentifier(node) {
if (__EXPERIMENTAL__) {
return node.type === 'Identifier' && node.name === 'useEffectEvent';
}
return false;
}
function isUseIdentifier(node) {
if (__EXPERIMENTAL__) {
return node.type === 'Identifier' && node.name === 'use';
}
return false;
}
export default {
meta: {
type: 'problem',
docs: {
description: 'enforces the Rules of Hooks',
recommended: true,
url: 'https://reactjs.org/docs/hooks-rules.html',
},
},
create(context) {
let lastEffect = null;
const codePathReactHooksMapStack = [];
const codePathSegmentStack = [];
const useEffectEventFunctions = new WeakSet();
// 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) {
for (const reference of scope.references) {
const parent = reference.identifier.parent;
if (
parent.type === 'VariableDeclarator' &&
parent.init &&
parent.init.type === 'CallExpression' &&
parent.init.callee &&
isUseEffectEventIdentifier(parent.init.callee)
) {
for (const ref of reference.resolved.references) {
if (ref !== reference) {
useEffectEventFunctions.add(ref.identifier);
}
}
}
}
}
return {
// Maintain code segment path stack as we traverse.
onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
onCodePathSegmentEnd: () => codePathSegmentStack.pop(),
// Maintain code path stack as we traverse.
onCodePathStart: () => codePathReactHooksMapStack.push(new Map()),
// Process our code path.
//
// 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();
if (reactHooksMap.size === 0) {
return;
}
// All of the segments which are cyclic are recorded in this set.
const cyclic = new Set();
/**
* Count the number of code paths from the start of the function to this
* segment. For example:
*
* ```js
* function MyComponent() {
* if (condition) {
* // Segment 1
* } else {
* // Segment 2
* }
* // Segment 3
* }
* ```
*
* Segments 1 and 2 have one path to the beginning of `MyComponent` and
* segment 3 has two paths to the beginning of `MyComponent` since we
* could have either taken the path of segment 1 or segment 2.
*
* Populates `cyclic` with cyclic segments.
*/
function countPathsFromStart(segment, pathHistory) {
const { cache } = countPathsFromStart;
let paths = cache.get(segment.id);
const pathList = new Set(pathHistory);
// If `pathList` includes the current segment then we've found a cycle!
// We need to fill `cyclic` with all segments inside cycle
if (pathList.has(segment.id)) {
const pathArray = [...pathList];
const cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
for (const cyclicSegment of cyclicSegments) {
cyclic.add(cyclicSegment);
}
return BigInt('0');
}
// add the current segment to pathList
pathList.add(segment.id);
// We have a cached `paths`. Return it.
if (paths !== undefined) {
return paths;
}
if (codePath.thrownSegments.includes(segment)) {
paths = BigInt('0');
} else if (segment.prevSegments.length === 0) {
paths = BigInt('1');
} else {
paths = BigInt('0');
for (const prevSegment of segment.prevSegments) {
paths += countPathsFromStart(prevSegment, pathList);
}
}
// If our segment is reachable then there should be at least one path
// to it from the start of our code path.
if (segment.reachable && paths === BigInt('0')) {
cache.delete(segment.id);
} else {
cache.set(segment.id, paths);
}
return paths;
}
/**
* Count the number of code paths from this segment to the end of the
* function. For example:
*
* ```js
* function MyComponent() {
* // Segment 1
* if (condition) {
* // Segment 2
* } else {
* // Segment 3
* }
* }
* ```
*
* Segments 2 and 3 have one path to the end of `MyComponent` and
* segment 1 has two paths to the end of `MyComponent` since we could
* either take the path of segment 1 or segment 2.
*
* Populates `cyclic` with cyclic segments.
*/
function countPathsToEnd(segment, pathHistory) {
const { cache } = countPathsToEnd;
let paths = cache.get(segment.id);
const pathList = new Set(pathHistory);
// If `pathList` includes the current segment then we've found a cycle!
// We need to fill `cyclic` with all segments inside cycle
if (pathList.has(segment.id)) {
const pathArray = Array.from(pathList);
const cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
for (const cyclicSegment of cyclicSegments) {
cyclic.add(cyclicSegment);
}
return BigInt('0');
}
// add the current segment to pathList
pathList.add(segment.id);
// We have a cached `paths`. Return it.
if (paths !== undefined) {
return paths;
}
if (codePath.thrownSegments.includes(segment)) {
paths = BigInt('0');
} else if (segment.nextSegments.length === 0) {
paths = BigInt('1');
} else {
paths = BigInt('0');
for (const nextSegment of segment.nextSegments) {
paths += countPathsToEnd(nextSegment, pathList);
}
}
cache.set(segment.id, paths);
return paths;
}
/**
* Gets the shortest path length to the start of a code path.
* For example:
*
* ```js
* function MyComponent() {
* if (condition) {
* // Segment 1
* }
* // Segment 2
* }
* ```
*
* There is only one path from segment 1 to the code path start. Its
* length is one so that is the shortest path.
*
* There are two paths from segment 2 to the code path start. One
* through segment 1 with a length of two and another directly to the
* start with a length of one. The shortest path has a length of one
* so we would return that.
*/
function shortestPathLengthToStart(segment) {
const { cache } = shortestPathLengthToStart;
let length = cache.get(segment.id);
// If `length` is null then we found a cycle! Return infinity since
// the shortest path is definitely not the one where we looped.
if (length === null) {
return Infinity;
}
// We have a cached `length`. Return it.
if (length !== undefined) {
return length;
}
// Compute `length` and cache it. Guarding against cycles.
cache.set(segment.id, null);
if (segment.prevSegments.length === 0) {
length = 1;
} else {
length = Infinity;
for (const prevSegment of segment.prevSegments) {
const prevLength = shortestPathLengthToStart(prevSegment);
if (prevLength < length) {
length = prevLength;
}
}
length += 1;
}
cache.set(segment.id, length);
return length;
}
countPathsFromStart.cache = new Map();
countPathsToEnd.cache = new Map();
shortestPathLengthToStart.cache = new Map();
// Count all code paths to the end of our component/hook. Also primes
// the `countPathsToEnd` cache.
const allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment);
// Gets the function name for our code path. If the function name is
// `undefined` then we know either that we have an anonymous function
// expression or our code path is not in a function. In both cases we
// will want to error since neither are React function components or
// hook functions - unless it is an anonymous function argument to
// forwardRef or memo.
const codePathFunctionName = getFunctionName(codePathNode);
// This is a valid code path for React hooks if we are directly in a React
// function component or we are in a hook function.
const isSomewhereInsideComponentOrHook = isInsideComponentOrHook(codePathNode);
const isDirectlyInsideComponentOrHook = codePathFunctionName
? isComponentName(codePathFunctionName) || isHook(codePathFunctionName)
: isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode);
// Compute the earliest finalizer level using information from the
// cache. We expect all reachable final segments to have a cache entry
// after calling `visitSegment()`.
let shortestFinalPathLength = Infinity;
for (const finalSegment of codePath.finalSegments) {
if (!finalSegment.reachable) {
continue;
}
const length = shortestPathLengthToStart(finalSegment);
if (length < shortestFinalPathLength) {
shortestFinalPathLength = length;
}
}
// Make sure all React Hooks pass our lint invariants. Log warnings
// if not.
for (const [segment, reactHooks] of reactHooksMap) {
// NOTE: We could report here that the hook is not reachable, but
// that would be redundant with more general "no unreachable"
// lint rules.
if (!segment.reachable) {
continue;
}
// If there are any final segments with a shorter path to start then
// we possibly have an early return.
//
// If our segment is a final segment itself then siblings could
// possibly be early returns.
const possiblyHasEarlyReturn =
segment.nextSegments.length === 0
? shortestFinalPathLength <= shortestPathLengthToStart(segment)
: shortestFinalPathLength < shortestPathLengthToStart(segment);
// Count all the paths from the start of our code path to the end of
// our code path that go _through_ this segment. The critical piece
// of this is _through_. If we just call `countPathsToEnd(segment)`
// then we neglect that we may have gone through multiple paths to get
// to this point! Consider:
//
// ```js
// function MyComponent() {
// if (a) {
// // Segment 1
// } else {
// // Segment 2
// }
// // Segment 3
// if (b) {
// // Segment 4
// } else {
// // Segment 5
// }
// }
// ```
//
// In this component we have four code paths:
//
// 1. `a = true; b = true`
// 2. `a = true; b = false`
// 3. `a = false; b = true`
// 4. `a = false; b = false`
//
// From segment 3 there are two code paths to the end through segment
// 4 and segment 5. However, we took two paths to get here through
// segment 1 and segment 2.
//
// If we multiply the paths from start (two) by the paths to end (two)
// for segment 3 we get four. Which is our desired count.
const pathsFromStartToEnd =
countPathsFromStart(segment) * countPathsToEnd(segment);
// Is this hook a part of a cyclic segment?
const cycled = cyclic.has(segment.id);
for (const hook of reactHooks) {
// Report an error if a hook may be called more then once.
// `use(...)` can be called in loops.
if (cycled && !isUseIdentifier(hook)) {
context.report({
node: hook,
message:
`React Hook "${context.getSource(hook)}" may be executed ` +
'more than once. Possibly because it is called in a loop. ' +
'React Hooks must be called in the exact same order in ' +
'every component render.',
});
}
// If this is not a valid code path for React hooks then we need to
// log a warning for every hook in this code path.
//
// Pick a special message depending on the scope this hook was
// called in.
if (isDirectlyInsideComponentOrHook) {
// Report an error if the hook is called inside an async function.
const isAsyncFunction = codePathNode.async;
if (isAsyncFunction) {
context.report({
node: hook,
message:
`React Hook "${context.getSource(hook)}" cannot be ` +
'called in an async function.',
});
}
// Report an error if a hook does not reach all finalizing code
// path segments.
//
// Special case when we think there might be an early return.
if (
!cycled &&
pathsFromStartToEnd !== allPathsFromStartToEnd &&
!isUseIdentifier(hook) // `use(...)` can be called conditionally.
) {
const message =
`React Hook "${context.getSource(hook)}" is called ` +
'conditionally. React Hooks must be called in the exact ' +
'same order in every component render.' +
(possiblyHasEarlyReturn
? ' Did you accidentally call a React Hook after an' +
' early return?'
: '');
context.report({ node: hook, message });
}
} else if (
codePathNode.parent &&
(codePathNode.parent.type === 'MethodDefinition' ||
codePathNode.parent.type === 'ClassProperty') &&
codePathNode.parent.value === codePathNode
) {
// Custom message for hooks inside a class
const message =
`React Hook "${context.getSource(hook)}" cannot be called ` +
'in a class component. React Hooks must be called in a ' +
'React function component or a custom React Hook function.';
context.report({ node: hook, message });
} else if (codePathFunctionName) {
// Custom message if we found an invalid function name.
const message =
`React Hook "${context.getSource(hook)}" is called in ` +
`function "${context.getSource(codePathFunctionName)}" ` +
'that is neither a React function component nor a custom ' +
'React Hook function.' +
' React component names must start with an uppercase letter.' +
' React Hook names must start with the word "use".';
context.report({ node: hook, message });
} else if (codePathNode.type === 'Program') {
// These are dangerous if you have inline requires enabled.
const message =
`React Hook "${context.getSource(hook)}" cannot be called ` +
'at the top level. React Hooks must be called in a ' +
'React function component or a custom React Hook function.';
context.report({ node: hook, message });
} else {
// Assume in all other cases the user called a hook in some
// random function callback. This should usually be true for
// anonymous function expressions. Hopefully this is clarifying
// enough in the common case that the incorrect message in
// uncommon cases doesn't matter.
// `use(...)` can be called in callbacks.
if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
const message =
`React Hook "${context.getSource(hook)}" cannot be called ` +
'inside a callback. React Hooks must be called in a ' +
'React function component or a custom React Hook function.';
context.report({ node: hook, message });
}
}
}
}
},
// Missed opportunity...We could visit all `Identifier`s instead of all
// `CallExpression`s and check that _every use_ of a hook name is valid.
// But that gets complicated and enters type-system territory, so we're
// only being strict about hook calls for now.
CallExpression(node) {
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);
let reactHooks = reactHooksMap.get(codePathSegment);
if (!reactHooks) {
reactHooks = [];
reactHooksMap.set(codePathSegment, reactHooks);
}
reactHooks.push(node.callee);
}
// useEffectEvent: useEffectEvent functions can be passed by reference within useEffect as well as in
// another useEffectEvent
if (
node.callee.type === 'Identifier' &&
(node.callee.name === 'useEffect' || isUseEffectEventIdentifier(node.callee)) &&
node.arguments.length > 0
) {
// Denote that we have traversed into a useEffect call, and stash the CallExpr for
// comparison later when we exit
lastEffect = node;
}
},
Identifier(node) {
// This identifier resolves to a useEffectEvent function, but isn't being referenced in an
// effect or another event function. It isn't being called either.
if (
lastEffect == null &&
useEffectEventFunctions.has(node) &&
node.parent.type !== 'CallExpression'
) {
context.report({
node,
message:
`\`${context.getSource(
node,
)}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
'the same component. They cannot be assigned to variables or passed down.',
});
}
},
'CallExpression:exit'(node) {
if (node === lastEffect) {
lastEffect = null;
}
},
FunctionDeclaration(node) {
// function MyComponent() { const onClick = useEffectEvent(...) }
if (isInsideComponentOrHook(node)) {
recordAllUseEffectEventFunctions(context.getScope());
}
},
ArrowFunctionExpression(node) {
// const MyComponent = () => { const onClick = useEffectEvent(...) }
if (isInsideComponentOrHook(node)) {
recordAllUseEffectEventFunctions(context.getScope());
}
},
};
},
};
/**
* Gets the static name of a function AST node. For function declarations it is
* easy. For anonymous function expressions it is much harder. If you search for
* `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
* 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) {
if (
node.type === 'FunctionDeclaration' ||
(node.type === 'FunctionExpression' && node.id)
) {
// function useHook() {}
// const whatever = function useHook() {};
//
// Function declaration or function expression names win over any
// assignment statements or other renames.
return node.id;
} else if (
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
) {
if (node.parent.type === 'VariableDeclarator' && node.parent.init === node) {
// const useHook = () => {};
return node.parent.id;
} else if (
node.parent.type === 'AssignmentExpression' &&
node.parent.right === node &&
node.parent.operator === '='
) {
// useHook = () => {};
return node.parent.left;
} else if (
node.parent.type === 'Property' &&
node.parent.value === node &&
!node.parent.computed
) {
// {useHook: () => {}}
// {useHook() {}}
return node.parent.key;
// NOTE: We could also support `ClassProperty` and `MethodDefinition`
// here to be pedantic. However, hooks in a class are an anti-pattern. So
// we don't allow it to error early.
//
// class {useHook = () => {}}
// class {useHook() {}}
} else if (
node.parent.type === 'AssignmentPattern' &&
node.parent.right === node &&
!node.parent.computed
) {
// const {useHook = () => {}} = {};
// ({useHook = () => {}} = {});
//
// Kinda clowny, but we'd said we'd follow spec convention for
// `IsAnonymousFunctionDefinition()` usage.
return node.parent.left;
} else {
return undefined;
}
} else {
return undefined;
}
}
/**
* Convenience function for peeking the last item in a stack.
*/
function last(array) {
return array[array.length - 1];
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import RulesOfHooks from './RulesOfHooks';
import ExhaustiveDeps from './ExhaustiveDeps';
export const configs = {
recommended: {
plugins: ['react-hooks'],
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
},
};
export const rules = {
'rules-of-hooks': RulesOfHooks,
'exhaustive-deps': ExhaustiveDeps,
};

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"scripts": {
"build": "./esbuild.ts",
"check-import": "for js in dist/*.js; do cat $js | grep 'require('; done"
},
"private": true,
"devDependencies": {
"@babel/core": "^7.22.9",
"@babel/plugin-transform-flow-strip-types": "^7.22.5",
"@babel/preset-env": "^7.22.9",
"@types/babel-plugin-macros": "^3.1.0",
"@types/babel__core": "^7.20.1",
"@types/node": "^20.4.2",
"babel-plugin-macros": "^3.1.0",
"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-plugin-import": "^2.27.5",
"glob": "^10.3.3",
"prettier": "^3.0.0"
},
"prettier": {
"arrowParens": "avoid",
"tabWidth": 2,
"printWidth": 90,
"semi": true,
"singleQuote": true,
"trailingComma": "all"
}
}

View File

@ -0,0 +1,498 @@
diff --git a/.babelrc b/.babelrc
index 883c03b7..0111d616 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,18 +1,11 @@
{
- "presets": ["airbnb"],
- "sourceMaps": "inline",
- "retainLines": true,
+ "presets": [["@babel/preset-env", { "targets": { "node": 16 } }]],
"env": {
"test": {
- "plugins": [
- "istanbul",
- ["module-resolver", { "root": ["./src/"] }],
- ]
+ "plugins": ["istanbul", ["module-resolver", { "root": ["./src/"] }]]
},
"testCompiled": {
- "plugins": [
- ["module-resolver", { "root": ["./lib/"] }],
- ]
+ "plugins": [["module-resolver", { "root": ["./lib/"] }]]
}
}
}
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 709a4744..00000000
--- a/.eslintrc
+++ /dev/null
@@ -1,247 +0,0 @@
-{
- "root": true,
- "plugins": [
- "eslint-plugin",
- "import",
- ],
- "extends": [
- "eslint:recommended",
- "plugin:eslint-plugin/recommended",
- "plugin:import/recommended",
- ],
- "env": {
- "node": true,
- "es6": true,
- "es2017": true,
- },
- "parserOptions": {
- "sourceType": "module",
- "ecmaVersion": 2020,
- },
- "rules": {
- "arrow-body-style": [2, "as-needed"],
- "arrow-parens": [2, "always"],
- "arrow-spacing": [2, { "before": true, "after": true }],
- "block-spacing": [2, "always"],
- "brace-style": [2, "1tbs", { "allowSingleLine": true }],
- "comma-dangle": ["error", {
- "arrays": "always-multiline",
- "objects": "always-multiline",
- "imports": "always-multiline",
- "exports": "always-multiline",
- "functions": "always-multiline",
- }],
- "comma-spacing": [2, { "before": false, "after": true }],
- "comma-style": [2, "last"],
- "computed-property-spacing": [2, "never"],
- "curly": [2, "all"],
- "default-case": [2, { "commentPattern": "(?:)" }],
- "default-case-last": [2],
- "default-param-last": [2],
- "dot-location": [2, "property"],
- "dot-notation": [2, { "allowKeywords": true, "allowPattern": "throws" }],
- "eol-last": [2, "always"],
- "eqeqeq": [2, "allow-null"],
- "for-direction": [2],
- "function-call-argument-newline": [2, "consistent"],
- "func-call-spacing": [2, "never"],
- "implicit-arrow-linebreak": [2, "beside"],
- "indent": [2, 2, {
- "SwitchCase": 1,
- "VariableDeclarator": 1,
- "outerIIFEBody": 1,
- "FunctionDeclaration": {
- "parameters": 1,
- "body": 1
- },
- "FunctionExpression": {
- "parameters": 1,
- "body": 1
- },
- "CallExpression": {
- "arguments": 1
- },
- "ArrayExpression": 1,
- "ObjectExpression": 1,
- "ImportDeclaration": 1,
- "flatTernaryExpressions": false,
- }],
- "jsx-quotes": [2, "prefer-double"],
- "key-spacing": [2, {
- "beforeColon": false,
- "afterColon": true,
- "mode": "strict",
- }],
- "keyword-spacing": ["error", {
- "before": true,
- "after": true,
- "overrides": {
- "return": { "after": true },
- "throw": { "after": true },
- "case": { "after": true }
- }
- }],
- "linebreak-style": [2, "unix"],
- "lines-around-directive": [2, {
- "before": "always",
- "after": "always",
- }],
- "max-len": 0,
- "new-parens": 2,
- "no-array-constructor": 2,
- "no-compare-neg-zero": 2,
- "no-cond-assign": [2, "always"],
- "no-extra-parens": 2,
- "no-multiple-empty-lines": [2, { "max": 1, "maxEOF": 1, "maxBOF": 0 }],
- "no-return-assign": [2, "always"],
- "no-trailing-spaces": 2,
- "no-var": 2,
- "object-curly-spacing": [2, "always"],
- "object-shorthand": ["error", "always", {
- "ignoreConstructors": false,
- "avoidQuotes": false,
- "avoidExplicitReturnArrows": true,
- }],
- "one-var": [2, "never"],
- "operator-linebreak": [2, "none", {
- "overrides": {
- "?": "before",
- ":": "before",
- "&&": "before",
- "||": "before",
- },
- }],
- "prefer-const": 2,
- "prefer-object-spread": 2,
- "prefer-rest-params": 2,
- "prefer-template": 2,
- "quote-props": [2, "as-needed", { "keywords": false }],
- "quotes": [2, "single", {
- "allowTemplateLiterals": true,
- "avoidEscape": true,
- }],
- "rest-spread-spacing": [2, "never"],
- "semi": [2, "always"],
- "semi-spacing": [2, { "before": false, "after": true }],
- "semi-style": [2, "last"],
- "space-before-blocks": [2, { "functions": "always", "keywords": "always", "classes": "always" }],
- "space-before-function-paren": ["error", {
- "anonymous": "always",
- "named": "never",
- "asyncArrow": "always",
- }],
- "space-in-parens": [2, "never"],
- "space-infix-ops": [2],
- "space-unary-ops": [2, { "words": true, "nonwords": false }],
- "switch-colon-spacing": [2, { "after": true, "before": false }],
- "template-curly-spacing": [2, "never"],
- "template-tag-spacing": [2, "never"],
- "unicode-bom": [2, "never"],
- "use-isnan": [2, { "enforceForSwitchCase": true }],
- "valid-typeof": [2],
- "wrap-iife": [2, "outside", { "functionPrototypeMethods": true }],
- "wrap-regex": [2],
- "yield-star-spacing": [2, { "before": false, "after": true }],
- "yoda": [2, "never", { "exceptRange": true, "onlyEquality": false }],
-
- "eslint-plugin/consistent-output": [
- "error",
- "always",
- ],
- "eslint-plugin/meta-property-ordering": "error",
- "eslint-plugin/no-deprecated-context-methods": "error",
- "eslint-plugin/no-deprecated-report-api": "off",
- "eslint-plugin/prefer-replace-text": "error",
- "eslint-plugin/report-message-format": "error",
- "eslint-plugin/require-meta-docs-description": ["error", { "pattern": "^(Enforce|Ensure|Prefer|Forbid).+\\.$" }],
- "eslint-plugin/require-meta-schema": "error",
- "eslint-plugin/require-meta-type": "error",
-
- // dog fooding
- "import/no-extraneous-dependencies": ["error", {
- "devDependencies": [
- "tests/**",
- "resolvers/*/test/**",
- "scripts/**"
- ],
- "optionalDependencies": false,
- "peerDependencies": true,
- "bundledDependencies": false,
- }],
- "import/unambiguous": "off",
- },
-
- "settings": {
- "import/resolver": {
- "node": {
- "paths": [
- "src",
- ],
- },
- },
- },
-
- "overrides": [
- {
- "files": "scripts/**",
- "rules": {
- "no-console": "off",
- },
- },
- {
- "files": "resolvers/**",
- "env": {
- "es6": false,
- },
- },
- {
- "files": "resolvers/webpack/**",
- "rules": {
- "no-console": 1,
- "prefer-template": 0,
- "prefer-object-spread": 0,
- "prefer-rest-params": 0,
- },
- "env": {
- "es6": true,
- },
- },
- {
- "files": [
- "resolvers/*/test/**/*",
- ],
- "env": {
- "mocha": true,
- "es6": false
- },
- },
- {
- "files": "utils/**",
- "parserOptions": {
- "ecmaVersion": 6,
- },
- "rules": {
- "comma-dangle": ["error", {
- "arrays": "always-multiline",
- "objects": "always-multiline",
- "imports": "always-multiline",
- "exports": "always-multiline",
- "functions": "never"
- }],
- "prefer-object-spread": "off",
- "prefer-template": "off",
- "no-console": 1,
- },
- },
- {
- "files": "tests/**",
- "env": {
- "mocha": true,
- },
- "rules": {
- "max-len": 0,
- "import/default": 0,
- },
- },
- ],
-}
diff --git a/scripts/resolverDirectories.js b/scripts/resolverDirectories.js
index f0c03a3c..a7cadb55 100644
--- a/scripts/resolverDirectories.js
+++ b/scripts/resolverDirectories.js
@@ -1,3 +1,3 @@
-import glob from 'glob';
+import { globSync } from 'glob';
-export default glob.sync('./resolvers/*/');
+export default globSync('./resolvers/*/');
diff --git a/src/docsUrl.js b/src/docsUrl.js
index 92b838c0..ccb13ba0 100644
--- a/src/docsUrl.js
+++ b/src/docsUrl.js
@@ -1,7 +1,7 @@
-import pkg from '../package.json';
+import { version } from '../package.json';
const repoUrl = 'https://github.com/import-js/eslint-plugin-import';
-export default function docsUrl(ruleName, commitish = `v${pkg.version}`) {
+export default function docsUrl(ruleName, commitish = `v${version}`) {
return `${repoUrl}/blob/${commitish}/docs/rules/${ruleName}.md`;
}
diff --git a/src/index.js b/src/index.js
index feafba90..84992bef 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,71 +1,132 @@
-export const rules = {
- 'no-unresolved': require('./rules/no-unresolved'),
- named: require('./rules/named'),
- default: require('./rules/default'),
- namespace: require('./rules/namespace'),
- 'no-namespace': require('./rules/no-namespace'),
- export: require('./rules/export'),
- 'no-mutable-exports': require('./rules/no-mutable-exports'),
- extensions: require('./rules/extensions'),
- 'no-restricted-paths': require('./rules/no-restricted-paths'),
- 'no-internal-modules': require('./rules/no-internal-modules'),
- 'group-exports': require('./rules/group-exports'),
- 'no-relative-packages': require('./rules/no-relative-packages'),
- 'no-relative-parent-imports': require('./rules/no-relative-parent-imports'),
- 'consistent-type-specifier-style': require('./rules/consistent-type-specifier-style'),
+/* eslint-disable spaced-comment */
+import noUnresolved from './rules/no-unresolved';
+import named from './rules/named';
+import defaultRule from './rules/default';
+import namespace from './rules/namespace';
+import noNamespace from './rules/no-namespace';
+import exportRule from './rules/export';
+import noMutableExports from './rules/no-mutable-exports';
+import extensions from './rules/extensions';
+import noRestrictedPaths from './rules/no-restricted-paths';
+import noInternalModules from './rules/no-internal-modules';
+import groupExports from './rules/group-exports';
+import noRelativePackages from './rules/no-relative-packages';
+import noRelativeParentImports from './rules/no-relative-parent-imports';
+import consistentTypeSpecifierStyle from './rules/consistent-type-specifier-style';
+import noSelfImport from './rules/no-self-import';
+import noCycle from './rules/no-cycle';
+import noNamedDefault from './rules/no-named-default';
+import noNamedAsDefault from './rules/no-named-as-default';
+import noNamedAsDefaultMember from './rules/no-named-as-default-member';
+import noAnonymousDefaultExport from './rules/no-anonymous-default-export';
+import noUnusedModules from './rules/no-unused-modules';
+import noCommonjs from './rules/no-commonjs';
+import noAmd from './rules/no-amd';
+import noDuplicates from './rules/no-duplicates';
+import first from './rules/first';
+import maxDependencies from './rules/max-dependencies';
+import noExtraneousDependencies from './rules/no-extraneous-dependencies';
+import noAbsolutePath from './rules/no-absolute-path';
+import noNodejsModules from './rules/no-nodejs-modules';
+import noWebpackLoaderSyntax from './rules/no-webpack-loader-syntax';
+import order from './rules/order';
+import newlineAfterImport from './rules/newline-after-import';
+import preferDefaultExport from './rules/prefer-default-export';
+import noDefaultExport from './rules/no-default-export';
+import noNamedExport from './rules/no-named-export';
+import noDynamicRequire from './rules/no-dynamic-require';
+import unambiguous from './rules/unambiguous';
+import noUnassignedImport from './rules/no-unassigned-import';
+import noUselessPathSegments from './rules/no-useless-path-segments';
+import dynamicImportChunkname from './rules/dynamic-import-chunkname';
+import noImportModuleExports from './rules/no-import-module-exports';
+import noEmptyNamedBlocks from './rules/no-empty-named-blocks';
+import exportsLast from './rules/exports-last';
+import noDeprecated from './rules/no-deprecated';
+import importsFirst from './rules/imports-first';
+import recommended from '../config/recommended';
+import errors from '../config/errors';
+import warnings from '../config/warnings';
+import stage0 from '../config/stage-0';
+import react from '../config/react';
+import reactNative from '../config/react-native';
+import electron from '../config/electron';
+import typescript from '../config/typescript';
- 'no-self-import': require('./rules/no-self-import'),
- 'no-cycle': require('./rules/no-cycle'),
- 'no-named-default': require('./rules/no-named-default'),
- 'no-named-as-default': require('./rules/no-named-as-default'),
- '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'),
+export const rules = /*#__PURE__*/ kebabCase({
+ noUnresolved,
+ named,
+ default: defaultRule,
+ namespace,
+ noNamespace,
+ export: exportRule,
+ noMutableExports,
+ extensions,
+ noRestrictedPaths,
+ noInternalModules,
+ groupExports,
+ noRelativePackages,
+ noRelativeParentImports,
+ consistentTypeSpecifierStyle,
- 'no-commonjs': require('./rules/no-commonjs'),
- 'no-amd': require('./rules/no-amd'),
- 'no-duplicates': require('./rules/no-duplicates'),
- first: require('./rules/first'),
- 'max-dependencies': require('./rules/max-dependencies'),
- 'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'),
- 'no-absolute-path': require('./rules/no-absolute-path'),
- 'no-nodejs-modules': require('./rules/no-nodejs-modules'),
- 'no-webpack-loader-syntax': require('./rules/no-webpack-loader-syntax'),
- order: require('./rules/order'),
- 'newline-after-import': require('./rules/newline-after-import'),
- 'prefer-default-export': require('./rules/prefer-default-export'),
- 'no-default-export': require('./rules/no-default-export'),
- 'no-named-export': require('./rules/no-named-export'),
- 'no-dynamic-require': require('./rules/no-dynamic-require'),
- unambiguous: require('./rules/unambiguous'),
- 'no-unassigned-import': require('./rules/no-unassigned-import'),
- 'no-useless-path-segments': require('./rules/no-useless-path-segments'),
- 'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
- 'no-import-module-exports': require('./rules/no-import-module-exports'),
- 'no-empty-named-blocks': require('./rules/no-empty-named-blocks'),
+ noSelfImport,
+ noCycle,
+ noNamedDefault,
+ noNamedAsDefault,
+ noNamedAsDefaultMember,
+ noAnonymousDefaultExport,
+ noUnusedModules,
+
+ noCommonjs,
+ noAmd,
+ noDuplicates,
+ first,
+ maxDependencies,
+ noExtraneousDependencies,
+ noAbsolutePath,
+ noNodejsModules,
+ noWebpackLoaderSyntax,
+ order,
+ newlineAfterImport,
+ preferDefaultExport,
+ noDefaultExport,
+ noNamedExport,
+ noDynamicRequire,
+ unambiguous,
+ noUnassignedImport,
+ noUselessPathSegments,
+ dynamicImportChunkname,
+ noImportModuleExports,
+ noEmptyNamedBlocks,
// export
- 'exports-last': require('./rules/exports-last'),
+ exportsLast,
// metadata-based
- 'no-deprecated': require('./rules/no-deprecated'),
+ noDeprecated,
// deprecated aliases to rules
- 'imports-first': require('./rules/imports-first'),
-};
+ importsFirst,
+});
-export const configs = {
- recommended: require('../config/recommended'),
+export const configs = /*#__PURE__*/ kebabCase({
+ recommended,
- errors: require('../config/errors'),
- warnings: require('../config/warnings'),
+ errors,
+ warnings,
// shhhh... work in progress "secret" rules
- 'stage-0': require('../config/stage-0'),
+ 'stage-0': stage0,
// useful stuff for folks using various environments
- react: require('../config/react'),
- 'react-native': require('../config/react-native'),
- electron: require('../config/electron'),
- typescript: require('../config/typescript'),
-};
+ react,
+ reactNative,
+ electron,
+ typescript,
+});
+
+function kebabCase(obj) {
+ return Object.fromEntries(
+ Object.entries(obj).map(([key, value]) => [key.replace(/([A-Z])/g, '-$1').toLowerCase(), value])
+ )
+}
\ No newline at end of file

View File

@ -0,0 +1,144 @@
diff --git a/src/index.js b/src/index.js
index 7b931fe..f7c1f91 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,47 +1,87 @@
/* eslint-disable global-require */
+// @ts-check
+import accessibleEmoji from './rules/accessible-emoji';
+import altText from './rules/alt-text';
+import anchorAmbiguousText from './rules/anchor-ambiguous-text';
+import anchorHasContent from './rules/anchor-has-content';
+import anchorIsValid from './rules/anchor-is-valid';
+import ariaActivedescendantHasTabindex from './rules/aria-activedescendant-has-tabindex';
+import ariaProps from './rules/aria-props';
+import ariaProptypes from './rules/aria-proptypes';
+import ariaRole from './rules/aria-role';
+import ariaUnsupportedElements from './rules/aria-unsupported-elements';
+import autocompleteValid from './rules/autocomplete-valid';
+import clickEventsHaveKeyEvents from './rules/click-events-have-key-events';
+import controlHasAssociatedLabel from './rules/control-has-associated-label';
+import headingHasContent from './rules/heading-has-content';
+import htmlHasLang from './rules/html-has-lang';
+import iframeHasTitle from './rules/iframe-has-title';
+import imgRedundantAlt from './rules/img-redundant-alt';
+import interactiveSupportsFocus from './rules/interactive-supports-focus';
+import labelHasAssociatedControl from './rules/label-has-associated-control';
+import labelHasFor from './rules/label-has-for';
+import lang from './rules/lang';
+import mediaHasCaption from './rules/media-has-caption';
+import mouseEventsHaveKeyEvents from './rules/mouse-events-have-key-events';
+import noAriaHiddenOnFocusable from './rules/no-aria-hidden-on-focusable';
+import noAccessKey from './rules/no-access-key';
+import noAutofocus from './rules/no-autofocus';
+import noDistractingElements from './rules/no-distracting-elements';
+import noInteractiveElementToNoninteractiveRole from './rules/no-interactive-element-to-noninteractive-role';
+import noNoninteractiveElementInteractions from './rules/no-noninteractive-element-interactions';
+import noNoninteractiveElementToInteractiveRole from './rules/no-noninteractive-element-to-interactive-role';
+import noNoninteractiveTabindex from './rules/no-noninteractive-tabindex';
+import noOnChange from './rules/no-onchange';
+import noRedundantRoles from './rules/no-redundant-roles';
+import noStaticElementInteractions from './rules/no-static-element-interactions';
+import preferTagOverRole from './rules/prefer-tag-over-role';
+import roleHasRequiredAriaProps from './rules/role-has-required-aria-props';
+import roleSupportsAriaProps from './rules/role-supports-aria-props';
+import scope from './rules/scope';
+import tabindexNoPositive from './rules/tabindex-no-positive';
module.exports = {
- rules: {
- 'accessible-emoji': require('./rules/accessible-emoji'),
- 'alt-text': require('./rules/alt-text'),
- 'anchor-ambiguous-text': require('./rules/anchor-ambiguous-text'),
- 'anchor-has-content': require('./rules/anchor-has-content'),
- 'anchor-is-valid': require('./rules/anchor-is-valid'),
- 'aria-activedescendant-has-tabindex': require('./rules/aria-activedescendant-has-tabindex'),
- 'aria-props': require('./rules/aria-props'),
- 'aria-proptypes': require('./rules/aria-proptypes'),
- 'aria-role': require('./rules/aria-role'),
- 'aria-unsupported-elements': require('./rules/aria-unsupported-elements'),
- 'autocomplete-valid': require('./rules/autocomplete-valid'),
- 'click-events-have-key-events': require('./rules/click-events-have-key-events'),
- 'control-has-associated-label': require('./rules/control-has-associated-label'),
- 'heading-has-content': require('./rules/heading-has-content'),
- 'html-has-lang': require('./rules/html-has-lang'),
- 'iframe-has-title': require('./rules/iframe-has-title'),
- 'img-redundant-alt': require('./rules/img-redundant-alt'),
- 'interactive-supports-focus': require('./rules/interactive-supports-focus'),
- 'label-has-associated-control': require('./rules/label-has-associated-control'),
- 'label-has-for': require('./rules/label-has-for'),
- lang: require('./rules/lang'),
- 'media-has-caption': require('./rules/media-has-caption'),
- 'mouse-events-have-key-events': require('./rules/mouse-events-have-key-events'),
- 'no-access-key': require('./rules/no-access-key'),
- 'no-aria-hidden-on-focusable': require('./rules/no-aria-hidden-on-focusable'),
- 'no-autofocus': require('./rules/no-autofocus'),
- 'no-distracting-elements': require('./rules/no-distracting-elements'),
- 'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'),
- 'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'),
- 'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'),
- 'no-noninteractive-tabindex': require('./rules/no-noninteractive-tabindex'),
- 'no-onchange': require('./rules/no-onchange'),
- 'no-redundant-roles': require('./rules/no-redundant-roles'),
- 'no-static-element-interactions': require('./rules/no-static-element-interactions'),
- 'prefer-tag-over-role': require('./rules/prefer-tag-over-role'),
- 'role-has-required-aria-props': require('./rules/role-has-required-aria-props'),
- 'role-supports-aria-props': require('./rules/role-supports-aria-props'),
- 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 = {
},
},
};
+
+function kebabCase(obj) {
+ return Object.fromEntries(
+ Object.entries(obj).map(([key, value]) => [key.replace(/([A-Z])/g, '-$1').toLowerCase(), value])
+ )
+}
\ No newline at end of file

View File

@ -0,0 +1,310 @@
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 4991f200..00000000
--- a/.eslintrc
+++ /dev/null
@@ -1,82 +0,0 @@
-{
- "root": true,
- "extends": ["airbnb-base", "plugin:eslint-plugin/recommended"],
- "plugins": ["eslint-plugin"],
- "env": {
- "es6": true,
- "node": true
- },
- "parserOptions": {
- "ecmaVersion": 6,
- "ecmaFeatures": {
- "jsx": true
- },
- "sourceType": "script",
- },
- "ignorePatterns": [
- "coverage/",
- ".nyc_output/",
- ],
- "rules": {
- "comma-dangle": [2, "always-multiline"],
- "object-shorthand": [2, "always", {
- "ignoreConstructors": false,
- "avoidQuotes": false, // this is the override vs airbnb
- }],
- "max-len": [2, 120, {
- "ignoreStrings": true,
- "ignoreTemplateLiterals": true,
- "ignoreComments": true,
- }],
- "consistent-return": 0,
-
- "prefer-destructuring": [2, { "array": false, "object": false }, { "enforceForRenamedProperties": false }],
- "prefer-object-spread": 0, // until node 8 is required
- "prefer-rest-params": 0, // until node 6 is required
- "prefer-spread": 0, // until node 6 is required
- "function-call-argument-newline": 1, // TODO: enable
- "function-paren-newline": 0,
- "no-plusplus": [2, {"allowForLoopAfterthoughts": true}],
- "no-param-reassign": 1,
- "no-restricted-syntax": [2, {
- "selector": "ObjectPattern",
- "message": "Object destructuring is not compatible with Node v4"
- }],
- "strict": [2, "safe"],
- "valid-jsdoc": [2, {
- "requireReturn": false,
- "requireParamDescription": false,
- "requireReturnDescription": false,
- }],
-
- "eslint-plugin/consistent-output": 0,
- "eslint-plugin/require-meta-docs-description": [2, { "pattern": "^(Enforce|Require|Disallow)" }],
- "eslint-plugin/require-meta-schema": 0,
- "eslint-plugin/require-meta-type": 0
- },
- "overrides": [
- {
- "files": "tests/**",
- "rules": {
- "no-template-curly-in-string": 1,
- },
- },
- {
- "files": "markdown.config.js",
- "rules": {
- "no-console": 0,
- },
- },
- {
- "files": ".github/workflows/*.js",
- "parserOptions": {
- "ecmaVersion": 2019,
- },
- "rules": {
- "camelcase": 0,
- "no-console": 0,
- "no-restricted-syntax": 0,
- },
- },
- ],
-}
diff --git a/index.js b/index.js
index 4140c6c8..792ceb4f 100644
--- a/index.js
+++ b/index.js
@@ -1,15 +1,13 @@
'use strict';
-const configAll = require('./configs/all');
-const configRecommended = require('./configs/recommended');
-const configRuntime = require('./configs/jsx-runtime');
-
-const allRules = require('./lib/rules');
+import configAll from './configs/all';
+import configRecommended from './configs/recommended';
+import configRuntime from './configs/jsx-runtime';
+import { name } from './package.json';
+import allRules from './lib/rules';
// for legacy config system
-const plugins = [
- 'react',
-];
+const plugins = [name];
module.exports = {
deprecatedRules: configAll.plugins.react.deprecatedRules,
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
+++ b/lib/rules/button-has-type.js
@@ -5,8 +5,7 @@
'use strict';
-const getProp = require('jsx-ast-utils/getProp');
-const getLiteralPropValue = require('jsx-ast-utils/getLiteralPropValue');
+const { getProp, getLiteralPropValue } = require('jsx-ast-utils');
const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');
const report = require('../util/report');
diff --git a/lib/rules/jsx-fragments.js b/lib/rules/jsx-fragments.js
index 38b4dd8b..d0575572 100644
--- a/lib/rules/jsx-fragments.js
+++ b/lib/rules/jsx-fragments.js
@@ -5,7 +5,7 @@
'use strict';
-const elementType = require('jsx-ast-utils/elementType');
+import { elementType } from 'jsx-ast-utils';
const pragmaUtil = require('../util/pragma');
const variableUtil = require('../util/variable');
const testReactVersion = require('../util/version').testReactVersion;
diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js
index 263ed824..e413ef5d 100644
--- a/lib/rules/jsx-key.js
+++ b/lib/rules/jsx-key.js
@@ -5,8 +5,7 @@
'use strict';
-const hasProp = require('jsx-ast-utils/hasProp');
-const propName = require('jsx-ast-utils/propName');
+import { hasProp, propName } from 'jsx-ast-utils';
const values = require('object.values');
const docsUrl = require('../util/docsUrl');
const pragmaUtil = require('../util/pragma');
diff --git a/lib/rules/jsx-no-bind.js b/lib/rules/jsx-no-bind.js
index 0a4c38e6..75a57bd9 100644
--- a/lib/rules/jsx-no-bind.js
+++ b/lib/rules/jsx-no-bind.js
@@ -7,7 +7,7 @@
'use strict';
-const propName = require('jsx-ast-utils/propName');
+import { propName } from 'jsx-ast-utils';
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
diff --git a/lib/rules/jsx-pascal-case.js b/lib/rules/jsx-pascal-case.js
index a1bb4811..db051356 100644
--- a/lib/rules/jsx-pascal-case.js
+++ b/lib/rules/jsx-pascal-case.js
@@ -5,7 +5,7 @@
'use strict';
-const elementType = require('jsx-ast-utils/elementType');
+import { elementType } from 'jsx-ast-utils';
const minimatch = require('minimatch');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
diff --git a/lib/rules/jsx-sort-props.js b/lib/rules/jsx-sort-props.js
index 5de5bee1..8013a8de 100644
--- a/lib/rules/jsx-sort-props.js
+++ b/lib/rules/jsx-sort-props.js
@@ -5,7 +5,7 @@
'use strict';
-const propName = require('jsx-ast-utils/propName');
+import { propName } from 'jsx-ast-utils';
const includes = require('array-includes');
const toSorted = require('array.prototype.tosorted');
diff --git a/lib/rules/no-namespace.js b/lib/rules/no-namespace.js
index 64bbc8d5..b5e9c803 100644
--- a/lib/rules/no-namespace.js
+++ b/lib/rules/no-namespace.js
@@ -5,7 +5,7 @@
'use strict';
-const elementType = require('jsx-ast-utils/elementType');
+import { elementType } from 'jsx-ast-utils';
const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');
const report = require('../util/report');
diff --git a/lib/util/annotations.js b/lib/util/annotations.js
index 60aaef8c..ad8dc0bf 100644
--- a/lib/util/annotations.js
+++ b/lib/util/annotations.js
@@ -27,6 +27,6 @@ function isAnnotatedFunctionPropsDeclaration(node, context) {
return (isAnnotated && (isDestructuredProps || isProps));
}
-module.exports = {
+export {
isAnnotatedFunctionPropsDeclaration,
};
diff --git a/lib/util/ast.js b/lib/util/ast.js
index fd6019a3..3cbc293e 100644
--- a/lib/util/ast.js
+++ b/lib/util/ast.js
@@ -4,7 +4,7 @@
'use strict';
-const estraverse = require('estraverse');
+import estraverse from 'estraverse';
// const pragmaUtil = require('./pragma');
/**
@@ -428,7 +428,7 @@ function isTSTypeParameterInstantiation(node) {
return nodeType === 'TSTypeParameterInstantiation';
}
-module.exports = {
+export {
traverse,
findReturnStatement,
getFirstNodeInLine,
diff --git a/lib/util/jsx.js b/lib/util/jsx.js
index 55073bfe..efc07af1 100644
--- a/lib/util/jsx.js
+++ b/lib/util/jsx.js
@@ -4,7 +4,7 @@
'use strict';
-const elementType = require('jsx-ast-utils/elementType');
+import { elementType } from 'jsx-ast-utils';
const astUtil = require('./ast');
const isCreateElement = require('./isCreateElement');
diff --git a/package.json b/package.json
index cb736434..a97113c0 100644
--- a/package.json
+++ b/package.json
@@ -25,21 +25,13 @@
"homepage": "https://github.com/jsx-eslint/eslint-plugin-react",
"bugs": "https://github.com/jsx-eslint/eslint-plugin-react/issues",
"dependencies": {
- "array-includes": "^3.1.6",
- "array.prototype.flatmap": "^1.3.1",
- "array.prototype.tosorted": "^1.1.1",
"doctrine": "^2.1.0",
"estraverse": "^5.3.0",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^3.1.2",
- "object.entries": "^1.1.6",
- "object.fromentries": "^2.0.6",
- "object.hasown": "^1.1.2",
- "object.values": "^1.1.6",
"prop-types": "^15.8.1",
"resolve": "^2.0.0-next.4",
- "semver": "^6.3.0",
- "string.prototype.matchall": "^4.0.8"
+ "semver": "^6.3.0"
},
"devDependencies": {
"@babel/core": "^7.21.0",
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 39187b7f..00000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
- "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
- // "lib": ["es2015"], /* Specify library files to be included in the compilation. */
- "allowJs": true, /* Allow javascript files to be compiled. */
- "checkJs": true, /* Report errors in .js files. */
- "noEmit": true, /* Do not emit outputs. */
- "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
-
- /* Strict Type-Checking Options */
- // "strict": true, /* Enable all strict type-checking options. */
- "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
- // "strictNullChecks": true, /* Enable strict null checks. */
- // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
- "strictFunctionTypes": true, /* Enable strict checking of function types. */
- "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
- "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
- "alwaysStrict": false, /* Parse in strict mode and emit "use strict" for each source file. */
- "resolveJsonModule": true
- },
- "include": ["lib"],
-}

459
patch/jsx-ast-utils.patch Normal file
View File

@ -0,0 +1,459 @@
diff --git a/elementType.js b/elementType.js
index 96873ac..c7b909a 100644
--- a/elementType.js
+++ b/elementType.js
@@ -1 +1 @@
-module.exports = require('./lib').elementType; // eslint-disable-line import/no-unresolved
+export { elementType as default } from './src';
\ No newline at end of file
diff --git a/eventHandlers.js b/eventHandlers.js
index b5d7f62..208eaca 100644
--- a/eventHandlers.js
+++ b/eventHandlers.js
@@ -1 +1 @@
-module.exports = require('./lib').eventHandlers; // eslint-disable-line import/no-unresolved
+export { eventHandlers as default } from './src';
\ No newline at end of file
diff --git a/eventHandlersByType.js b/eventHandlersByType.js
index b7a0c42..e2757e9 100644
--- a/eventHandlersByType.js
+++ b/eventHandlersByType.js
@@ -1 +1 @@
-module.exports = require('./lib').eventHandlersByType; // eslint-disable-line import/no-unresolved
+export { eventHandlersByType as default } from './src';
\ No newline at end of file
diff --git a/getLiteralPropValue.js b/getLiteralPropValue.js
index efc5c8d..970fc39 100644
--- a/getLiteralPropValue.js
+++ b/getLiteralPropValue.js
@@ -1 +1 @@
-module.exports = require('./lib').getLiteralPropValue; // eslint-disable-line import/no-unresolved
+export { getLiteralPropValue as default } from './src';
\ No newline at end of file
diff --git a/getProp.js b/getProp.js
index e523c2c..4e270e9 100644
--- a/getProp.js
+++ b/getProp.js
@@ -1 +1 @@
-module.exports = require('./lib').getProp; // eslint-disable-line import/no-unresolved
+export { getProp as default } from './src';
\ No newline at end of file
diff --git a/getPropValue.js b/getPropValue.js
index 883a37c..86bf585 100644
--- a/getPropValue.js
+++ b/getPropValue.js
@@ -1 +1 @@
-module.exports = require('./lib').getPropValue; // eslint-disable-line import/no-unresolved
+export { getPropValue as default } from './src';
\ No newline at end of file
diff --git a/hasAnyProp.js b/hasAnyProp.js
index 2a88420..95b3adc 100644
--- a/hasAnyProp.js
+++ b/hasAnyProp.js
@@ -1 +1 @@
-module.exports = require('./lib').hasAnyProp; // eslint-disable-line import/no-unresolved
+export { hasAnyProp as default } from './src';
\ No newline at end of file
diff --git a/hasEveryProp.js b/hasEveryProp.js
index 338f569..e865100 100644
--- a/hasEveryProp.js
+++ b/hasEveryProp.js
@@ -1 +1 @@
-module.exports = require('./lib').hasEveryProp; // eslint-disable-line import/no-unresolved
+export { hasEveryProp } from './src';
\ No newline at end of file
diff --git a/hasProp.js b/hasProp.js
index 0f1cd04..bfed363 100644
--- a/hasProp.js
+++ b/hasProp.js
@@ -1 +1 @@
-module.exports = require('./lib').hasProp; // eslint-disable-line import/no-unresolved
+export { hasProp } from './src';
\ No newline at end of file
diff --git a/package.json b/package.json
index 738d1b3..befb66e 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "jsx-ast-utils",
"version": "3.3.4",
"description": "AST utility module for statically analyzing JSX",
- "main": "lib/index.js",
+ "main": "src/index.js",
"scripts": {
"prepack": "npmignore --auto --commentLines=autogenerated && npm run build",
"prebuild": "rimraf lib",
@@ -62,8 +62,6 @@
},
"license": "MIT",
"dependencies": {
- "array-includes": "^3.1.6",
- "array.prototype.flat": "^1.3.1",
"object.assign": "^4.1.4",
"object.values": "^1.1.6"
},
diff --git a/propName.js b/propName.js
index 361a52b..4424cfe 100644
--- a/propName.js
+++ b/propName.js
@@ -1 +1 @@
-module.exports = require('./lib').propName; // eslint-disable-line import/no-unresolved
+export { propName } from './src';
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index 6814764..565a66e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,7 +5,7 @@ import getProp from './getProp';
import getPropValue, { getLiteralPropValue } from './getPropValue';
import propName from './propName';
-module.exports = {
+export {
hasProp,
hasAnyProp,
hasEveryProp,
diff --git a/src/values/JSXElement.js b/src/values/JSXElement.js
index e5a049f..029c23d 100644
--- a/src/values/JSXElement.js
+++ b/src/values/JSXElement.js
@@ -1,12 +1,11 @@
+import getValue from './index';
+
/**
* Extractor function for a JSXElement type value node.
*
* Returns self-closing element with correct name.
*/
export default function extractValueFromJSXElement(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
-
const Tag = value.openingElement.name.name;
if (value.openingElement.selfClosing) {
return `<${Tag} />`;
diff --git a/src/values/JSXFragment.js b/src/values/JSXFragment.js
index 379137e..90cbc54 100644
--- a/src/values/JSXFragment.js
+++ b/src/values/JSXFragment.js
@@ -1,12 +1,11 @@
+import getValue from './index';
+
/**
* Extractor function for a JSXFragment type value node.
*
* Returns self-closing element with correct name.
*/
export default function extractValueFromJSXFragment(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
-
if (value.children.length === 0) {
return '<></>';
}
diff --git a/src/values/expressions/ArrayExpression.js b/src/values/expressions/ArrayExpression.js
index f333d58..4462c9d 100644
--- a/src/values/expressions/ArrayExpression.js
+++ b/src/values/expressions/ArrayExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for an ArrayExpression type value node.
* An array expression is an expression with [] syntax.
@@ -5,8 +7,6 @@
* @returns - An array of the extracted elements.
*/
export default function extractValueFromArrayExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
return value.elements.map((element) => {
if (element === null) return undefined;
return getValue(element);
diff --git a/src/values/expressions/AssignmentExpression.js b/src/values/expressions/AssignmentExpression.js
index 7a14de5..6fce401 100644
--- a/src/values/expressions/AssignmentExpression.js
+++ b/src/values/expressions/AssignmentExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a AssignmentExpression type value node.
* An assignment expression looks like `x = y` or `x += y` in expression position.
@@ -7,7 +9,5 @@
* @returns - The extracted value converted to correct type.
*/
export default function extractValueFromAssignmentExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
return `${getValue(value.left)} ${value.operator} ${getValue(value.right)}`;
}
diff --git a/src/values/expressions/BinaryExpression.js b/src/values/expressions/BinaryExpression.js
index 5764f3f..7cf9c07 100644
--- a/src/values/expressions/BinaryExpression.js
+++ b/src/values/expressions/BinaryExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a BinaryExpression type value node.
* A binary expression has a left and right side separated by an operator
@@ -7,8 +9,6 @@
* @returns - The extracted value converted to correct type.
*/
export default function extractValueFromBinaryExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
const { operator, left, right } = value;
const leftVal = getValue(left);
const rightVal = getValue(right);
diff --git a/src/values/expressions/BindExpression.js b/src/values/expressions/BindExpression.js
index bc0bf0f..6c10fa4 100644
--- a/src/values/expressions/BindExpression.js
+++ b/src/values/expressions/BindExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a BindExpression type value node.
* A bind expression looks like `::this.foo`
@@ -8,8 +10,6 @@
* @returns - The extracted value converted to correct type.
*/
export default function extractValueFromBindExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
const callee = getValue(value.callee);
// If value.object === null, the callee must be a MemberExpression.
diff --git a/src/values/expressions/CallExpression.js b/src/values/expressions/CallExpression.js
index f225d16..ce7e43d 100644
--- a/src/values/expressions/CallExpression.js
+++ b/src/values/expressions/CallExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a CallExpression type value node.
* A call expression looks like `bar()`
@@ -8,8 +10,6 @@
* @returns - The extracted value converted to correct type.
*/
export default function extractValueFromCallExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
const args = Array.isArray(value.arguments) ? value.arguments.map((x) => getValue(x)).join(', ') : '';
return `${getValue(value.callee)}${value.optional ? '?.' : ''}(${args})`;
}
diff --git a/src/values/expressions/ChainExpression.js b/src/values/expressions/ChainExpression.js
index 1fb00a7..e8b3b75 100644
--- a/src/values/expressions/ChainExpression.js
+++ b/src/values/expressions/ChainExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a ChainExpression type value node.
* A member expression is accessing a property on an object `obj.property`.
@@ -7,7 +9,5 @@
* and maintaing `obj?.property` convention.
*/
export default function extractValueFromChainExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
return getValue(value.expression || value);
}
diff --git a/src/values/expressions/ConditionalExpression.js b/src/values/expressions/ConditionalExpression.js
index 4fd185d..a3308f6 100644
--- a/src/values/expressions/ConditionalExpression.js
+++ b/src/values/expressions/ConditionalExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a ConditionalExpression type value node.
*
@@ -5,8 +7,6 @@
* @returns - The extracted value converted to correct type.
*/
export default function extractValueFromConditionalExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
const {
test,
alternate,
diff --git a/src/values/expressions/LogicalExpression.js b/src/values/expressions/LogicalExpression.js
index 5b65a4e..b5cc573 100644
--- a/src/values/expressions/LogicalExpression.js
+++ b/src/values/expressions/LogicalExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a LogicalExpression type value node.
* A logical expression is `a && b` or `a || b`, so we evaluate both sides
@@ -7,8 +9,6 @@
* @returns - The extracted value converted to correct type.
*/
export default function extractValueFromLogicalExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
const { operator, left, right } = value;
const leftVal = getValue(left);
const rightVal = getValue(right);
diff --git a/src/values/expressions/MemberExpression.js b/src/values/expressions/MemberExpression.js
index 5d48fd5..d0cb7af 100644
--- a/src/values/expressions/MemberExpression.js
+++ b/src/values/expressions/MemberExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a MemberExpression type value node.
* A member expression is accessing a property on an object `obj.property`.
@@ -7,7 +9,5 @@
* and maintaing `obj.property` convention.
*/
export default function extractValueFromMemberExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
return `${getValue(value.object)}${value.optional ? '?.' : '.'}${getValue(value.property)}`;
}
diff --git a/src/values/expressions/ObjectExpression.js b/src/values/expressions/ObjectExpression.js
index 383b569..c1c08a8 100644
--- a/src/values/expressions/ObjectExpression.js
+++ b/src/values/expressions/ObjectExpression.js
@@ -1,4 +1,5 @@
import assign from 'object.assign';
+import getValue from './index';
/**
* Extractor function for an ObjectExpression type value node.
@@ -7,8 +8,6 @@ import assign from 'object.assign';
* @returns - a representation of the object
*/
export default function extractValueFromObjectExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
return value.properties.reduce((obj, property) => {
// Support types: SpreadProperty and ExperimentalSpreadProperty
if (/^(?:Experimental)?Spread(?:Property|Element)$/.test(property.type)) {
diff --git a/src/values/expressions/OptionalCallExpression.js b/src/values/expressions/OptionalCallExpression.js
index bcee760..3a36971 100644
--- a/src/values/expressions/OptionalCallExpression.js
+++ b/src/values/expressions/OptionalCallExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a OptionalCallExpression type value node.
* A member expression is accessing a property on an object `obj.property` and invoking it.
@@ -7,7 +9,5 @@
* and maintaing `obj.property?.()` convention.
*/
export default function extractValueFromOptionalCallExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
return `${getValue(value.callee)}?.(${value.arguments.map((x) => getValue(x)).join(', ')})`;
}
diff --git a/src/values/expressions/OptionalMemberExpression.js b/src/values/expressions/OptionalMemberExpression.js
index 8062eae..d236b04 100644
--- a/src/values/expressions/OptionalMemberExpression.js
+++ b/src/values/expressions/OptionalMemberExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a OptionalMemberExpression type value node.
* A member expression is accessing a property on an object `obj.property`.
@@ -7,7 +9,5 @@
* and maintaing `obj?.property` convention.
*/
export default function extractValueFromOptionalMemberExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
return `${getValue(value.object)}?.${getValue(value.property)}`;
}
diff --git a/src/values/expressions/SequenceExpression.js b/src/values/expressions/SequenceExpression.js
index 129d992..d644730 100644
--- a/src/values/expressions/SequenceExpression.js
+++ b/src/values/expressions/SequenceExpression.js
@@ -1,3 +1,5 @@
+import getValue from './index';
+
/**
* Extractor function for a SequenceExpression type value node.
* A Sequence expression is an object with an attribute named
@@ -7,7 +9,5 @@
* @returns - An array of the extracted elements.
*/
export default function extractValueFromSequenceExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
return value.expressions.map((element) => getValue(element));
}
diff --git a/src/values/expressions/TSNonNullExpression.js b/src/values/expressions/TSNonNullExpression.js
index 79dd1b0..7d74c8f 100644
--- a/src/values/expressions/TSNonNullExpression.js
+++ b/src/values/expressions/TSNonNullExpression.js
@@ -1,5 +1,5 @@
-const extractValueFromThisExpression = require('./ThisExpression').default;
-const extractValueFromCallExpression = require('./CallExpression').default;
+import extractValueFromThisExpression from './ThisExpression';
+import extractValueFromCallExpression from './CallExpression';
function navigate(obj, prop, value) {
if (value.computed) {
diff --git a/src/values/expressions/TypeCastExpression.js b/src/values/expressions/TypeCastExpression.js
index 1afd232..aeda1e5 100644
--- a/src/values/expressions/TypeCastExpression.js
+++ b/src/values/expressions/TypeCastExpression.js
@@ -1,3 +1,4 @@
+import getValue from './index';
/**
* Extractor function for a TypeCastExpression type value node.
* A type cast expression looks like `(this.handleClick: (event: MouseEvent) => void))`
@@ -7,7 +8,5 @@
* @returns - The extracted value converted to correct type.
*/
export default function extractValueFromTypeCastExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
return getValue(value.expression);
}
diff --git a/src/values/expressions/UnaryExpression.js b/src/values/expressions/UnaryExpression.js
index b699722..cc1fa35 100644
--- a/src/values/expressions/UnaryExpression.js
+++ b/src/values/expressions/UnaryExpression.js
@@ -1,3 +1,4 @@
+import getValue from './index';
/**
* Extractor function for a UnaryExpression type value node.
* A unary expression is an expression with a unary operator.
@@ -7,8 +8,6 @@
* @returns - The extracted value converted to correct type.
*/
export default function extractValueFromUnaryExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
const { operator, argument } = value;
switch (operator) {
diff --git a/src/values/expressions/UpdateExpression.js b/src/values/expressions/UpdateExpression.js
index 0e3898c..ca005a2 100644
--- a/src/values/expressions/UpdateExpression.js
+++ b/src/values/expressions/UpdateExpression.js
@@ -1,3 +1,4 @@
+import getValue from './index';
/**
* Extractor function for an UpdateExpression type value node.
* An update expression is an expression with an update operator.
@@ -7,8 +8,6 @@
* @returns - The extracted value converted to correct type.
*/
export default function extractValueFromUpdateExpression(value) {
- // eslint-disable-next-line global-require
- const getValue = require('.').default;
const { operator, argument, prefix } = value;
let val = getValue(argument);

3380
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

9
save_patch.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
sync() (
cd "$1" && git diff > "../patch/$1.patch"
)
sync eslint-plugin-import
sync eslint-plugin-jsx-a11y
sync eslint-plugin-react
sync jsx-ast-utils

156
src/babel.ts Normal file
View File

@ -0,0 +1,156 @@
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>[];
}
}

View File

@ -0,0 +1,39 @@
import type { Rule } from "eslint";
const rule: Rule.RuleModule = {
meta: {
type: "problem",
docs: {
description:
"Bans import from the specifier '.' and '..' and replaces it with '.+/index'",
category: "Best Practices",
recommended: true,
},
fixable: "code",
},
create: context => ({
ImportDeclaration(node) {
if (node.source.value === ".") {
context.report({
node: node.source,
message:
"Importing from the specifier '.' is not allowed. Use './index' instead.",
fix(fixer) {
return fixer.replaceText(node.source, '"./index"');
},
});
} else if (node.source.value === "..") {
context.report({
node: node.source,
message:
"Importing from the specifier '..' is not allowed. Use '../index' instead.",
fix(fixer) {
return fixer.replaceText(node.source, '"../index"');
},
});
}
},
}),
};
export default rule;

View File

@ -0,0 +1,32 @@
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;

View File

@ -0,0 +1,33 @@
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;

View File

@ -0,0 +1,36 @@
// @ts-check
import { builtinModules } from "node:module";
import type { Rule } from "eslint";
const rule: Rule.RuleModule = {
meta: {
type: "problem",
docs: {
description:
"Disallow imports of built-in Node.js modules without the `node:` prefix",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
},
create: context => ({
ImportDeclaration(node) {
const { source } = node;
if (source?.type === "Literal" && typeof source.value === "string") {
const moduleName = source.value;
if (builtinModules.includes(moduleName) && !moduleName.startsWith("node:")) {
context.report({
node: source,
message: `Import of built-in Node.js module "${moduleName}" must use the "node:" prefix.`,
fix: fixer => fixer.replaceText(source, `"node:${moduleName}"`),
});
}
}
},
}),
};
export default rule;

24
tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"allowArbitraryExtensions": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"jsx": "react-jsx",
"module": "commonjs",
"moduleResolution": "bundler",
"noImplicitOverride": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"strict": true,
"stripInternal": true,
"target": "esnext",
"useUnknownInCatchVariables": false
},
"ts-node": {
"transpileOnly": true,
"compilerOptions": {
"module": "commonjs"
}
}
}