mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 10:14:53 -05:00
lint: move last remaining native eslint rules to oxlint (create new alternatives) (#6468)
* lint: clean up unnecessary config and disables * fix: update package import path in browser preload script * lint: create custom ESLint rules for rules not in oxlint - Add object-shorthand-properties rule (replaces object-shorthand) - Add prefer-const rule (replaces prefer-const) - Add no-anchor-tag rule (replaces no-restricted-syntax for <a> tags) - Add no-react-default-import rule (replaces no-restricted-syntax for React.*) These custom rules are needed because oxlint doesn't support these rules yet. All rules are properly tested and integrated into the ESLint config. * refactor: enhance prefer-const rule to track variable reassignments by scope - Introduced a mapping of scopes to reassigned variable names, improving the accuracy of the prefer-const rule. - Added helper functions to determine variable scopes and reassignment status. - Updated logic to check variable reassignments during declaration analysis. - Adjusted test cases to reflect changes in variable handling.
This commit is contained in:
committed by
GitHub
parent
88fbfe7078
commit
f1faf45659
@@ -71,7 +71,7 @@ try {
|
|||||||
console.log('Generated summary:', summary);
|
console.log('Generated summary:', summary);
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
summary: summary,
|
summary,
|
||||||
prNumber: prDetails.number,
|
prNumber: prDetails.number,
|
||||||
author: prDetails.author,
|
author: prDetails.author,
|
||||||
};
|
};
|
||||||
|
|||||||
11
.github/actions/check-migrations.js
vendored
11
.github/actions/check-migrations.js
vendored
@@ -35,14 +35,15 @@ function readMigrations(ref) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
spawnSync('git', ['fetch', 'origin', 'master']);
|
spawnSync('git', ['fetch', 'origin', 'master']);
|
||||||
let masterMigrations = readMigrations('origin/master');
|
const masterMigrations = readMigrations('origin/master');
|
||||||
let headMigrations = readMigrations('HEAD');
|
const headMigrations = readMigrations('HEAD');
|
||||||
|
|
||||||
let latestMasterMigration = masterMigrations[masterMigrations.length - 1].date;
|
const latestMasterMigration =
|
||||||
let newMigrations = headMigrations.filter(
|
masterMigrations[masterMigrations.length - 1].date;
|
||||||
|
const newMigrations = headMigrations.filter(
|
||||||
migration => !masterMigrations.find(m => m.name === migration.name),
|
migration => !masterMigrations.find(m => m.name === migration.name),
|
||||||
);
|
);
|
||||||
let badMigrations = newMigrations.filter(
|
const badMigrations = newMigrations.filter(
|
||||||
migration => migration.date <= latestMasterMigration,
|
migration => migration.date <= latestMasterMigration,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
"actual/prefer-trans-over-t": "error",
|
"actual/prefer-trans-over-t": "error",
|
||||||
"actual/prefer-if-statement": "warn",
|
"actual/prefer-if-statement": "warn",
|
||||||
"actual/prefer-logger-over-console": "error",
|
"actual/prefer-logger-over-console": "error",
|
||||||
|
"actual/object-shorthand-properties": "warn",
|
||||||
|
"actual/prefer-const": "warn",
|
||||||
|
"actual/no-anchor-tag": "warn",
|
||||||
|
"actual/no-react-default-import": "warn",
|
||||||
|
|
||||||
// JSX A11y rules
|
// JSX A11y rules
|
||||||
"jsx-a11y/no-autofocus": [
|
"jsx-a11y/no-autofocus": [
|
||||||
@@ -410,6 +414,12 @@
|
|||||||
"import/no-default-export": "off"
|
"import/no-default-export": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"files": ["packages/docs/**/*"],
|
||||||
|
"rules": {
|
||||||
|
"actual/no-anchor-tag": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
// TODO: enable these
|
// TODO: enable these
|
||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -47,22 +47,7 @@ export default defineConfig(
|
|||||||
perfectionist: pluginPerfectionist,
|
perfectionist: pluginPerfectionist,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'no-restricted-properties': [
|
// TODO: https://github.com/oxc-project/oxc/issues/17076
|
||||||
'error',
|
|
||||||
{
|
|
||||||
object: 'require',
|
|
||||||
property: 'ensure',
|
|
||||||
message:
|
|
||||||
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
object: 'System',
|
|
||||||
property: 'import',
|
|
||||||
message:
|
|
||||||
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'perfectionist/sort-imports': [
|
'perfectionist/sort-imports': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
@@ -93,26 +78,6 @@ export default defineConfig(
|
|||||||
newlinesBetween: 'always',
|
newlinesBetween: 'always',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
'object-shorthand': ['warn', 'properties'],
|
|
||||||
|
|
||||||
'no-restricted-syntax': [
|
|
||||||
'warn',
|
|
||||||
{
|
|
||||||
// forbid React.* as they are legacy https://twitter.com/dan_abramov/status/1308739731551858689
|
|
||||||
selector:
|
|
||||||
":matches(MemberExpression[object.name='React'], TSQualifiedName[left.name='React'])",
|
|
||||||
message:
|
|
||||||
'Using default React import is discouraged, please use named exports directly instead.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// forbid <a> in favor of <Link>
|
|
||||||
selector: 'JSXOpeningElement[name.name="a"]',
|
|
||||||
message: 'Using <a> is discouraged, please use <Link> instead.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'prefer-const': 'warn',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -128,10 +93,4 @@ export default defineConfig(
|
|||||||
'typescript-paths/absolute-import': ['error', { enableAlias: false }],
|
'typescript-paths/absolute-import': ['error', { enableAlias: false }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
files: ['packages/docs/**/*'],
|
|
||||||
rules: {
|
|
||||||
'no-restricted-syntax': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import React, { useEffect } from 'react';
|
import React, { Fragment, useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
import { send } from 'loot-core/platform/client/fetch';
|
import { send } from 'loot-core/platform/client/fetch';
|
||||||
@@ -405,9 +405,7 @@ export function Modals() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map((modal, idx) => (
|
.map((modal, idx) => (
|
||||||
<React.Fragment key={`${modalStack[idx].name}-${idx}`}>
|
<Fragment key={`${modalStack[idx].name}-${idx}`}>{modal}</Fragment>
|
||||||
{modal}
|
|
||||||
</React.Fragment>
|
|
||||||
));
|
));
|
||||||
|
|
||||||
// fragment needed per TS types
|
// fragment needed per TS types
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ const ExternalLink = ({
|
|||||||
}: ExternalLinkProps) => {
|
}: ExternalLinkProps) => {
|
||||||
return (
|
return (
|
||||||
// we can't use <ExternalLink /> here for obvious reasons
|
// we can't use <ExternalLink /> here for obvious reasons
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
<a
|
<a
|
||||||
href={to ?? ''}
|
href={to ?? ''}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
|
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
|
||||||
|
|
||||||
@@ -35,9 +35,7 @@ export function NotesTagFormatter({
|
|||||||
|
|
||||||
switch (segment.type) {
|
switch (segment.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return <Fragment key={index}>{segment.content}</Fragment>;
|
||||||
<React.Fragment key={index}>{segment.content}</React.Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'tag':
|
case 'tag':
|
||||||
if (isNarrowWidth) {
|
if (isNarrowWidth) {
|
||||||
|
|||||||
@@ -8,5 +8,9 @@ module.exports = {
|
|||||||
typography: require('./rules/typography'),
|
typography: require('./rules/typography'),
|
||||||
'prefer-if-statement': require('./rules/prefer-if-statement'),
|
'prefer-if-statement': require('./rules/prefer-if-statement'),
|
||||||
'prefer-logger-over-console': require('./rules/prefer-logger-over-console'),
|
'prefer-logger-over-console': require('./rules/prefer-logger-over-console'),
|
||||||
|
'object-shorthand-properties': require('./rules/object-shorthand-properties'),
|
||||||
|
'prefer-const': require('./rules/prefer-const'),
|
||||||
|
'no-anchor-tag': require('./rules/no-anchor-tag'),
|
||||||
|
'no-react-default-import': require('./rules/no-react-default-import'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Requirements
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import { runClassic } from 'eslint-vitest-rule-tester';
|
||||||
|
|
||||||
|
import * as rule from '../no-anchor-tag';
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
runClassic(
|
||||||
|
'no-anchor-tag',
|
||||||
|
rule,
|
||||||
|
{
|
||||||
|
valid: [
|
||||||
|
// Link component usage
|
||||||
|
`<Link variant="external" to="https://example.com">Click me</Link>`,
|
||||||
|
`<Link variant="internal" to="/path">Internal link</Link>`,
|
||||||
|
|
||||||
|
// Other JSX elements
|
||||||
|
`<div>Content</div>`,
|
||||||
|
`<span>Text</span>`,
|
||||||
|
`<button>Click</button>`,
|
||||||
|
|
||||||
|
// JSX with different casing
|
||||||
|
`<A>Not an anchor</A>`,
|
||||||
|
`<Anchor>Not an anchor</Anchor>`,
|
||||||
|
|
||||||
|
// Self-closing tags
|
||||||
|
`<img src="test.jpg" />`,
|
||||||
|
`<br />`,
|
||||||
|
],
|
||||||
|
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: '<a href="https://example.com">Link</a>',
|
||||||
|
output: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useLink',
|
||||||
|
type: 'JSXOpeningElement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<a href="/path" target="_blank">External</a>',
|
||||||
|
output: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useLink',
|
||||||
|
type: 'JSXOpeningElement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<a>Click here</a>',
|
||||||
|
output: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useLink',
|
||||||
|
type: 'JSXOpeningElement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<div><a href="/test">Link</a></div>',
|
||||||
|
output: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useLink',
|
||||||
|
type: 'JSXOpeningElement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'function Component() { return <a href="/">Home</a>; }',
|
||||||
|
output: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useLink',
|
||||||
|
type: 'JSXOpeningElement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Requirements
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import { runClassic } from 'eslint-vitest-rule-tester';
|
||||||
|
|
||||||
|
import * as rule from '../no-react-default-import';
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
runClassic(
|
||||||
|
'no-react-default-import',
|
||||||
|
rule,
|
||||||
|
{
|
||||||
|
valid: [
|
||||||
|
// Named imports (correct usage)
|
||||||
|
`import { Component, useState } from 'react';`,
|
||||||
|
`import { Component as Comp, useState } from 'react';`,
|
||||||
|
|
||||||
|
// Usage of named imports
|
||||||
|
`const MyComponent = Component.extend({});`,
|
||||||
|
`const [state, setState] = useState(0);`,
|
||||||
|
|
||||||
|
// Other identifiers
|
||||||
|
`const ReactLib = require('react-lib');`,
|
||||||
|
`ReactLib.something();`,
|
||||||
|
|
||||||
|
// Non-React member expressions
|
||||||
|
`const obj = { React: 'test' };`,
|
||||||
|
`obj.React.something();`,
|
||||||
|
|
||||||
|
// JSX with Fragment (correct usage)
|
||||||
|
`import { Fragment } from 'react';`,
|
||||||
|
`const element = <Fragment>Test</Fragment>;`,
|
||||||
|
],
|
||||||
|
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: 'const Component = React.Component;',
|
||||||
|
output: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useNamedExport',
|
||||||
|
type: 'MemberExpression',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'const [state, setState] = React.useState(0);',
|
||||||
|
output: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useNamedExport',
|
||||||
|
type: 'MemberExpression',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'class MyComponent extends React.Component {}',
|
||||||
|
output: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useNamedExport',
|
||||||
|
type: 'MemberExpression',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'function test() { return React.createElement("div"); }',
|
||||||
|
output: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useNamedExport',
|
||||||
|
type: 'MemberExpression',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'const element = <React.Fragment>Test</React.Fragment>;',
|
||||||
|
output: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useNamedExport',
|
||||||
|
type: 'JSXMemberExpression',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageId: 'useNamedExport',
|
||||||
|
type: 'JSXMemberExpression',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Requirements
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import { runClassic } from 'eslint-vitest-rule-tester';
|
||||||
|
|
||||||
|
import * as rule from '../object-shorthand-properties';
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
runClassic(
|
||||||
|
'object-shorthand-properties',
|
||||||
|
rule,
|
||||||
|
{
|
||||||
|
valid: [
|
||||||
|
// Already using shorthand
|
||||||
|
`const obj = { foo };`,
|
||||||
|
`const obj = { foo, bar };`,
|
||||||
|
`const obj = { foo, bar, baz };`,
|
||||||
|
|
||||||
|
// Different key and value names
|
||||||
|
`const obj = { foo: bar };`,
|
||||||
|
`const obj = { foo: bar.baz };`,
|
||||||
|
`const obj = { foo: 123 };`,
|
||||||
|
`const obj = { foo: 'bar' };`,
|
||||||
|
`const obj = { foo: true };`,
|
||||||
|
`const obj = { foo: null };`,
|
||||||
|
`const obj = { foo: undefined };`,
|
||||||
|
|
||||||
|
// Methods (should not be enforced)
|
||||||
|
`const obj = { foo() {} };`,
|
||||||
|
`const obj = { foo: function() {} };`,
|
||||||
|
`const obj = { foo: () => {} };`,
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
`const obj = { [foo]: foo };`,
|
||||||
|
`const obj = { [foo]: bar };`,
|
||||||
|
|
||||||
|
// String/number keys
|
||||||
|
`const obj = { 'foo': foo };`,
|
||||||
|
`const obj = { 123: 123 };`,
|
||||||
|
|
||||||
|
// Nested objects (shorthand should still be enforced in nested objects)
|
||||||
|
`const obj = { foo: { bar } };`,
|
||||||
|
],
|
||||||
|
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: 'const obj = { foo: foo };',
|
||||||
|
output: 'const obj = { foo };',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'foo' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'const obj = { foo: foo, bar: bar };',
|
||||||
|
output: 'const obj = { foo, bar };',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'foo' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'bar' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'const obj = { foo: foo, bar: baz };',
|
||||||
|
output: 'const obj = { foo, bar: baz };',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'foo' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'const obj = { a: a, b: b, c: c };',
|
||||||
|
output: 'const obj = { a, b, c };',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'a' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'b' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'c' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'function test() { return { x: x, y: y }; }',
|
||||||
|
output: 'function test() { return { x, y }; }',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'x' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'y' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'const obj = { prop: prop, method() {} };',
|
||||||
|
output: 'const obj = { prop, method() {} };',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'prop' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'const obj = { foo: { bar: bar } };',
|
||||||
|
output: 'const obj = { foo: { bar } };',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'bar' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'const obj = { ...other, foo: foo };',
|
||||||
|
output: 'const obj = { ...other, foo };',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: { key: 'foo' },
|
||||||
|
type: 'Property',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Requirements
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import { runClassic } from 'eslint-vitest-rule-tester';
|
||||||
|
|
||||||
|
import * as rule from '../prefer-const';
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
runClassic(
|
||||||
|
'prefer-const',
|
||||||
|
rule,
|
||||||
|
{
|
||||||
|
valid: [
|
||||||
|
// Already using const
|
||||||
|
`const x = 1;`,
|
||||||
|
`const x = 1, y = 2;`,
|
||||||
|
|
||||||
|
// let that gets reassigned
|
||||||
|
`let x = 1; x = 2;`,
|
||||||
|
`let x = 1; x++;`,
|
||||||
|
`let x = 1; x += 1;`,
|
||||||
|
`let x = 1; x--;`,
|
||||||
|
`let x = 1; x -= 1;`,
|
||||||
|
|
||||||
|
// let in for loops
|
||||||
|
`for (let i = 0; i < 10; i++) {}`,
|
||||||
|
`for (let item of items) {}`,
|
||||||
|
`for (let item in items) {}`,
|
||||||
|
|
||||||
|
// Destructuring with reassignment
|
||||||
|
`let { x } = obj; x = 2;`,
|
||||||
|
`let [x] = arr; x = 2;`,
|
||||||
|
|
||||||
|
// Variables used before declaration (temporal dead zone)
|
||||||
|
`let x; x = 1;`,
|
||||||
|
|
||||||
|
// Destructuring assignment (valid use of let)
|
||||||
|
`let id; ({ id } = obj);`,
|
||||||
|
`let x, y; ({ x, y } = obj);`,
|
||||||
|
`async function test() { let id; ({ id } = await someFunction()); }`,
|
||||||
|
`let x, y; [x, y] = arr;`,
|
||||||
|
`async function test() { let id; try { ({ id } = await func()); } catch {} }`,
|
||||||
|
|
||||||
|
// Function parameters (not applicable)
|
||||||
|
`function test(x) { return x; }`,
|
||||||
|
|
||||||
|
// Variables in different scopes
|
||||||
|
`const x = 1; function test() { let x = 2; x = 3; }`,
|
||||||
|
],
|
||||||
|
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: 'let x = 1;',
|
||||||
|
output: 'const x = 1;',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: { name: 'x' },
|
||||||
|
type: 'Identifier',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'let x = 1, y = 2;',
|
||||||
|
output: 'const x = 1, y = 2;',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: { name: 'x' },
|
||||||
|
type: 'Identifier',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: { name: 'y' },
|
||||||
|
type: 'Identifier',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'let x = 1; console.log(x);',
|
||||||
|
output: 'const x = 1; console.log(x);',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: { name: 'x' },
|
||||||
|
type: 'Identifier',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'let x = 1; let y = 2;',
|
||||||
|
output: 'const x = 1; const y = 2;',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: { name: 'x' },
|
||||||
|
type: 'Identifier',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: { name: 'y' },
|
||||||
|
type: 'Identifier',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'function test() { let x = 1; return x; }',
|
||||||
|
output: 'function test() { const x = 1; return x; }',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: { name: 'x' },
|
||||||
|
type: 'Identifier',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'let { x } = obj;',
|
||||||
|
output: 'const { x } = obj;',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: { name: 'x' },
|
||||||
|
type: 'Identifier',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'let [x] = arr;',
|
||||||
|
output: 'const [x] = arr;',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: { name: 'x' },
|
||||||
|
type: 'Identifier',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'let x = 1, y = 2; y = 3;',
|
||||||
|
output: 'let x = 1, y = 2; y = 3;',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: { name: 'x' },
|
||||||
|
type: 'Identifier',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
57
packages/eslint-plugin-actual/lib/rules/no-anchor-tag.js
Normal file
57
packages/eslint-plugin-actual/lib/rules/no-anchor-tag.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Rule Definition
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description: 'Forbid usage of <a> tags in favor of <Link> component',
|
||||||
|
},
|
||||||
|
fixable: null,
|
||||||
|
schema: [],
|
||||||
|
messages: {
|
||||||
|
useLink: 'Using <a> is discouraged, please use <Link> instead.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
create(context) {
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current file is the Link component itself
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isLinkComponentFile() {
|
||||||
|
const filename = context.getFilename();
|
||||||
|
const normalizedFilename = filename.replace(/\\/g, '/');
|
||||||
|
return normalizedFilename.includes(
|
||||||
|
'packages/desktop-client/src/components/common/Link.tsx',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
// Public
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return {
|
||||||
|
JSXOpeningElement(node) {
|
||||||
|
// Skip if this is the Link component file itself
|
||||||
|
if (isLinkComponentFile()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the element name is "a"
|
||||||
|
if (node.name.type === 'JSXIdentifier' && node.name.name === 'a') {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'useLink',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Rule Definition
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Forbid usage of default React import (React.*) in favor of named exports',
|
||||||
|
},
|
||||||
|
fixable: null,
|
||||||
|
schema: [],
|
||||||
|
messages: {
|
||||||
|
useNamedExport:
|
||||||
|
'Using default React import is discouraged, please use named exports directly instead.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
create(context) {
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a node is a React default import usage
|
||||||
|
* @param {import('estree').Node} node
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isReactDefaultImport(node) {
|
||||||
|
// Check MemberExpression: React.Component, React.useState, etc.
|
||||||
|
if (node.type === 'MemberExpression') {
|
||||||
|
return (
|
||||||
|
node.object.type === 'Identifier' && node.object.name === 'React'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check TSQualifiedName: React.FC, React.ComponentType, etc. (TypeScript)
|
||||||
|
if (node.type === 'TSQualifiedName') {
|
||||||
|
return node.left.type === 'Identifier' && node.left.name === 'React';
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
// Public
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Catch React.* member expressions (e.g., React.Component, React.useState)
|
||||||
|
MemberExpression(node) {
|
||||||
|
if (isReactDefaultImport(node)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'useNamedExport',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Catch React.* in JSX (e.g., <React.Fragment>)
|
||||||
|
JSXMemberExpression(node) {
|
||||||
|
if (
|
||||||
|
node.object.type === 'JSXIdentifier' &&
|
||||||
|
node.object.name === 'React'
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'useNamedExport',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Catch React.* TypeScript qualified names (e.g., React.FC, React.ComponentType)
|
||||||
|
TSQualifiedName(node) {
|
||||||
|
if (isReactDefaultImport(node)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'useNamedExport',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/* @see https://eslint.org/docs/latest/rules/object-shorthand */
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Rule Definition
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Enforce property shorthand syntax in object literals (properties only)',
|
||||||
|
},
|
||||||
|
fixable: 'code',
|
||||||
|
schema: [],
|
||||||
|
messages: {
|
||||||
|
useShorthand:
|
||||||
|
'Expected property shorthand. Use `{{key}}` instead of `{{key}}: {{key}}`.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
create(context) {
|
||||||
|
const sourceCode = context.getSourceCode();
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a property can use shorthand syntax
|
||||||
|
* @param {import('estree').Property} node
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function canUseShorthand(node) {
|
||||||
|
// Only check properties (not methods)
|
||||||
|
if (node.method) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key must be an identifier (not computed)
|
||||||
|
if (node.key.type !== 'Identifier' || node.computed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value must be an identifier with the same name as the key
|
||||||
|
if (
|
||||||
|
node.value.type !== 'Identifier' ||
|
||||||
|
node.key.name !== node.value.name
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already using shorthand
|
||||||
|
if (node.shorthand) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a fixer to convert to shorthand
|
||||||
|
* @param {import('estree').Property} node
|
||||||
|
* @returns {import('eslint').Rule.ReportFixer}
|
||||||
|
*/
|
||||||
|
function makeFixer(node) {
|
||||||
|
return fixer => {
|
||||||
|
const keyText = sourceCode.getText(node.key);
|
||||||
|
return fixer.replaceText(node, keyText);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
// Public
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return {
|
||||||
|
Property(node) {
|
||||||
|
if (canUseShorthand(node)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'useShorthand',
|
||||||
|
data: {
|
||||||
|
key: node.key.name,
|
||||||
|
},
|
||||||
|
fix: makeFixer(node),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
368
packages/eslint-plugin-actual/lib/rules/prefer-const.js
Normal file
368
packages/eslint-plugin-actual/lib/rules/prefer-const.js
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Rule Definition
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Require `const` declarations for variables that are never reassigned after declared',
|
||||||
|
},
|
||||||
|
fixable: 'code',
|
||||||
|
schema: [],
|
||||||
|
messages: {
|
||||||
|
useConst: "'{{name}}' is never reassigned. Use 'const' instead.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
create(context) {
|
||||||
|
const sourceCode = context.getSourceCode();
|
||||||
|
// Map of scope to Set of reassigned variable names in that scope
|
||||||
|
const reassignedVariablesByScope = new Map();
|
||||||
|
const letDeclarations = [];
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the scope for a node
|
||||||
|
* @param {import('estree').Node} node
|
||||||
|
* @returns {import('eslint').Scope.Scope | null}
|
||||||
|
*/
|
||||||
|
function getScope(node) {
|
||||||
|
try {
|
||||||
|
return sourceCode.getScope(node);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the variable name from an identifier node
|
||||||
|
* @param {import('estree').Node} node
|
||||||
|
* @returns {string | null}
|
||||||
|
*/
|
||||||
|
function getVariableName(node) {
|
||||||
|
if (node.type === 'Identifier') {
|
||||||
|
return node.name;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a variable as reassigned in its scope
|
||||||
|
* @param {string} name
|
||||||
|
* @param {import('estree').Node} node
|
||||||
|
*/
|
||||||
|
function markAsReassigned(name, node) {
|
||||||
|
const scope = getScope(node);
|
||||||
|
if (scope) {
|
||||||
|
// Get the identifier being assigned
|
||||||
|
let identifier = null;
|
||||||
|
if (node.type === 'AssignmentExpression') {
|
||||||
|
identifier = node.left.type === 'Identifier' ? node.left : null;
|
||||||
|
} else if (node.type === 'UpdateExpression') {
|
||||||
|
identifier =
|
||||||
|
node.argument.type === 'Identifier' ? node.argument : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identifier) {
|
||||||
|
// Use ESLint's scope analysis to find the variable being referenced
|
||||||
|
// Search from the identifier's scope upward to find the variable
|
||||||
|
let identifierScope = getScope(identifier);
|
||||||
|
while (identifierScope) {
|
||||||
|
const variable = identifierScope.variables.find(
|
||||||
|
v => v.name === name,
|
||||||
|
);
|
||||||
|
if (variable) {
|
||||||
|
// Found the variable - it's declared in identifierScope
|
||||||
|
// (variables in a scope's variables array are declared in that scope)
|
||||||
|
if (!reassignedVariablesByScope.has(identifierScope)) {
|
||||||
|
reassignedVariablesByScope.set(identifierScope, new Set());
|
||||||
|
}
|
||||||
|
reassignedVariablesByScope.get(identifierScope).add(name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
identifierScope = identifierScope.upper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: find variable in scope chain (for destructuring, etc.)
|
||||||
|
let currentScope = scope;
|
||||||
|
while (currentScope) {
|
||||||
|
const variable = currentScope.variables.find(
|
||||||
|
v => v.name === name && v.defs.length > 0,
|
||||||
|
);
|
||||||
|
if (variable) {
|
||||||
|
// Variable is declared in currentScope (it's in currentScope.variables)
|
||||||
|
if (!reassignedVariablesByScope.has(currentScope)) {
|
||||||
|
reassignedVariablesByScope.set(currentScope, new Set());
|
||||||
|
}
|
||||||
|
reassignedVariablesByScope.get(currentScope).add(name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentScope = currentScope.upper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a variable is reassigned in its scope
|
||||||
|
* @param {string} name
|
||||||
|
* @param {import('eslint').Scope.Scope} variableScope
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isReassignedInScope(name, variableScope) {
|
||||||
|
const reassigned = reassignedVariablesByScope.get(variableScope);
|
||||||
|
return reassigned ? reassigned.has(name) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all variable names from a pattern (handles destructuring)
|
||||||
|
* @param {import('estree').Pattern} pattern
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function extractVariableNames(pattern) {
|
||||||
|
const names = [];
|
||||||
|
if (pattern.type === 'Identifier') {
|
||||||
|
names.push(pattern.name);
|
||||||
|
} else if (pattern.type === 'ObjectPattern') {
|
||||||
|
for (const prop of pattern.properties) {
|
||||||
|
if (prop.type === 'Property') {
|
||||||
|
if (prop.value.type === 'Identifier') {
|
||||||
|
names.push(prop.value.name);
|
||||||
|
} else if (
|
||||||
|
prop.value.type === 'ObjectPattern' ||
|
||||||
|
prop.value.type === 'ArrayPattern'
|
||||||
|
) {
|
||||||
|
names.push(...extractVariableNames(prop.value));
|
||||||
|
}
|
||||||
|
} else if (prop.type === 'RestElement') {
|
||||||
|
names.push(...extractVariableNames(prop.argument));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (pattern.type === 'ArrayPattern') {
|
||||||
|
for (const element of pattern.elements) {
|
||||||
|
if (element) {
|
||||||
|
if (element.type === 'Identifier') {
|
||||||
|
names.push(element.name);
|
||||||
|
} else if (
|
||||||
|
element.type === 'ObjectPattern' ||
|
||||||
|
element.type === 'ArrayPattern'
|
||||||
|
) {
|
||||||
|
names.push(...extractVariableNames(element));
|
||||||
|
} else if (element.type === 'RestElement') {
|
||||||
|
names.push(...extractVariableNames(element.argument));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (pattern.type === 'RestElement') {
|
||||||
|
names.push(...extractVariableNames(pattern.argument));
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a fixer to change `let` to `const`
|
||||||
|
* @param {import('estree').VariableDeclaration} node
|
||||||
|
* @returns {import('eslint').Rule.ReportFixer}
|
||||||
|
*/
|
||||||
|
function makeFixer(node) {
|
||||||
|
return fixer => {
|
||||||
|
const letToken = sourceCode.getFirstToken(node, {
|
||||||
|
filter: token => token.value === 'let',
|
||||||
|
});
|
||||||
|
if (letToken) {
|
||||||
|
return fixer.replaceText(letToken, 'const');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
// Public
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Track assignments to variables
|
||||||
|
AssignmentExpression(node) {
|
||||||
|
// Handle simple assignments: x = value
|
||||||
|
const name = getVariableName(node.left);
|
||||||
|
if (name) {
|
||||||
|
markAsReassigned(name, node);
|
||||||
|
} else {
|
||||||
|
// Handle destructuring assignments: ({ x } = value) or [x] = value
|
||||||
|
const patternNames = extractVariableNames(node.left);
|
||||||
|
for (const patternName of patternNames) {
|
||||||
|
markAsReassigned(patternName, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Track update expressions (x++, x--, x += 1, etc.)
|
||||||
|
UpdateExpression(node) {
|
||||||
|
const name = getVariableName(node.argument);
|
||||||
|
if (name) {
|
||||||
|
markAsReassigned(name, node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Track for loop initializers (let i = 0; i < 10; i++)
|
||||||
|
'ForStatement:exit'(node) {
|
||||||
|
if (node.init && node.init.type === 'VariableDeclaration') {
|
||||||
|
for (const declarator of node.init.declarations) {
|
||||||
|
const name = getVariableName(declarator.id);
|
||||||
|
if (name) {
|
||||||
|
markAsReassigned(name, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.update) {
|
||||||
|
const name = getVariableName(node.update);
|
||||||
|
if (name) {
|
||||||
|
markAsReassigned(name, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'ForInStatement:exit'(node) {
|
||||||
|
if (node.left.type === 'VariableDeclaration') {
|
||||||
|
for (const declarator of node.left.declarations) {
|
||||||
|
const name = getVariableName(declarator.id);
|
||||||
|
if (name) {
|
||||||
|
markAsReassigned(name, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'ForOfStatement:exit'(node) {
|
||||||
|
if (node.left.type === 'VariableDeclaration') {
|
||||||
|
for (const declarator of node.left.declarations) {
|
||||||
|
const name = getVariableName(declarator.id);
|
||||||
|
if (name) {
|
||||||
|
markAsReassigned(name, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Collect all let declarations
|
||||||
|
VariableDeclaration(node) {
|
||||||
|
if (node.kind === 'let') {
|
||||||
|
letDeclarations.push(node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check all let declarations at the end
|
||||||
|
'Program:exit'() {
|
||||||
|
for (const node of letDeclarations) {
|
||||||
|
const nodeScope = getScope(node);
|
||||||
|
if (!nodeScope) continue;
|
||||||
|
|
||||||
|
// Collect all variables in this declaration with their scopes
|
||||||
|
// Variables declared in a VariableDeclaration are in the scope that contains that declaration
|
||||||
|
const allVariablesInDeclaration = new Map();
|
||||||
|
for (const declarator of node.declarations) {
|
||||||
|
const variableNames = extractVariableNames(declarator.id);
|
||||||
|
for (const name of variableNames) {
|
||||||
|
// Find the variable in nodeScope (where it should be declared)
|
||||||
|
// Use the scope where we actually find the variable
|
||||||
|
let variableScope = nodeScope;
|
||||||
|
const variable = nodeScope.variables.find(v => v.name === name);
|
||||||
|
if (variable) {
|
||||||
|
// Variable found in nodeScope - use nodeScope
|
||||||
|
variableScope = nodeScope;
|
||||||
|
} else {
|
||||||
|
// Variable not found in nodeScope, search upward (shouldn't happen, but be safe)
|
||||||
|
let currentScope = nodeScope.upper;
|
||||||
|
while (currentScope) {
|
||||||
|
const foundVar = currentScope.variables.find(
|
||||||
|
v => v.name === name,
|
||||||
|
);
|
||||||
|
if (foundVar) {
|
||||||
|
variableScope = currentScope;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentScope = currentScope.upper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allVariablesInDeclaration.set(name, variableScope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all variables in this declaration can be const
|
||||||
|
const allCanBeConst = Array.from(
|
||||||
|
allVariablesInDeclaration.entries(),
|
||||||
|
).every(([name, varScope]) => !isReassignedInScope(name, varScope));
|
||||||
|
|
||||||
|
// Check each declarator
|
||||||
|
for (const declarator of node.declarations) {
|
||||||
|
const variableNames = extractVariableNames(declarator.id);
|
||||||
|
|
||||||
|
for (const variableName of variableNames) {
|
||||||
|
// Get the scope for this variable
|
||||||
|
const variableScope = allVariablesInDeclaration.get(variableName);
|
||||||
|
if (!variableScope) continue;
|
||||||
|
|
||||||
|
// Skip if the variable is reassigned in its scope
|
||||||
|
if (isReassignedInScope(variableName, variableScope)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the identifier node for this variable name
|
||||||
|
let targetNode = declarator.id;
|
||||||
|
if (declarator.id.type !== 'Identifier') {
|
||||||
|
// For destructuring, find the specific identifier
|
||||||
|
const findIdentifier = (pattern, name) => {
|
||||||
|
if (pattern.type === 'Identifier' && pattern.name === name) {
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
if (pattern.type === 'ObjectPattern') {
|
||||||
|
for (const prop of pattern.properties) {
|
||||||
|
if (prop.type === 'Property') {
|
||||||
|
const found = findIdentifier(prop.value, name);
|
||||||
|
if (found) return found;
|
||||||
|
} else if (prop.type === 'RestElement') {
|
||||||
|
const found = findIdentifier(prop.argument, name);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pattern.type === 'ArrayPattern') {
|
||||||
|
for (const element of pattern.elements) {
|
||||||
|
if (element) {
|
||||||
|
const found = findIdentifier(element, name);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pattern.type === 'RestElement') {
|
||||||
|
return findIdentifier(pattern.argument, name);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
targetNode =
|
||||||
|
findIdentifier(declarator.id, variableName) || declarator.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report for this variable
|
||||||
|
// Only provide a fix if all variables in the declaration can be const
|
||||||
|
context.report({
|
||||||
|
node: targetNode,
|
||||||
|
messageId: 'useConst',
|
||||||
|
data: {
|
||||||
|
name: variableName,
|
||||||
|
},
|
||||||
|
fix: allCanBeConst ? makeFixer(node) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
6
upcoming-release-notes/6468.md
Normal file
6
upcoming-release-notes/6468.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
category: Maintenance
|
||||||
|
authors: [MatissJanis]
|
||||||
|
---
|
||||||
|
|
||||||
|
lint: create new custom rules for eslint rules that are not in oxlint
|
||||||
Reference in New Issue
Block a user