mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 18:20:24 -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);
|
||||
|
||||
const result = {
|
||||
summary: summary,
|
||||
summary,
|
||||
prNumber: prDetails.number,
|
||||
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']);
|
||||
let masterMigrations = readMigrations('origin/master');
|
||||
let headMigrations = readMigrations('HEAD');
|
||||
const masterMigrations = readMigrations('origin/master');
|
||||
const headMigrations = readMigrations('HEAD');
|
||||
|
||||
let latestMasterMigration = masterMigrations[masterMigrations.length - 1].date;
|
||||
let newMigrations = headMigrations.filter(
|
||||
const latestMasterMigration =
|
||||
masterMigrations[masterMigrations.length - 1].date;
|
||||
const newMigrations = headMigrations.filter(
|
||||
migration => !masterMigrations.find(m => m.name === migration.name),
|
||||
);
|
||||
let badMigrations = newMigrations.filter(
|
||||
const badMigrations = newMigrations.filter(
|
||||
migration => migration.date <= latestMasterMigration,
|
||||
);
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
"actual/prefer-trans-over-t": "error",
|
||||
"actual/prefer-if-statement": "warn",
|
||||
"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/no-autofocus": [
|
||||
@@ -410,6 +414,12 @@
|
||||
"import/no-default-export": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/docs/**/*"],
|
||||
"rules": {
|
||||
"actual/no-anchor-tag": "off"
|
||||
}
|
||||
},
|
||||
// TODO: enable these
|
||||
{
|
||||
"files": [
|
||||
|
||||
@@ -47,22 +47,7 @@ export default defineConfig(
|
||||
perfectionist: pluginPerfectionist,
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-properties': [
|
||||
'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',
|
||||
},
|
||||
],
|
||||
|
||||
// TODO: https://github.com/oxc-project/oxc/issues/17076
|
||||
'perfectionist/sort-imports': [
|
||||
'warn',
|
||||
{
|
||||
@@ -93,26 +78,6 @@ export default defineConfig(
|
||||
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 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/docs/**/*'],
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { Fragment, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import { send } from 'loot-core/platform/client/fetch';
|
||||
@@ -405,9 +405,7 @@ export function Modals() {
|
||||
}
|
||||
})
|
||||
.map((modal, idx) => (
|
||||
<React.Fragment key={`${modalStack[idx].name}-${idx}`}>
|
||||
{modal}
|
||||
</React.Fragment>
|
||||
<Fragment key={`${modalStack[idx].name}-${idx}`}>{modal}</Fragment>
|
||||
));
|
||||
|
||||
// fragment needed per TS types
|
||||
|
||||
@@ -52,7 +52,6 @@ const ExternalLink = ({
|
||||
}: ExternalLinkProps) => {
|
||||
return (
|
||||
// we can't use <ExternalLink /> here for obvious reasons
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
<a
|
||||
href={to ?? ''}
|
||||
target="_blank"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
|
||||
|
||||
@@ -35,9 +35,7 @@ export function NotesTagFormatter({
|
||||
|
||||
switch (segment.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<React.Fragment key={index}>{segment.content}</React.Fragment>
|
||||
);
|
||||
return <Fragment key={index}>{segment.content}</Fragment>;
|
||||
|
||||
case 'tag':
|
||||
if (isNarrowWidth) {
|
||||
|
||||
@@ -8,5 +8,9 @@ module.exports = {
|
||||
typography: require('./rules/typography'),
|
||||
'prefer-if-statement': require('./rules/prefer-if-statement'),
|
||||
'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