From cf83dee7371ffcd512fd5158fad967a3fc7935b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 06:13:15 +0000 Subject: [PATCH 1/4] feat: close guideline coverage gaps for JavaScript and TypeScript Every numbered clause of the pinned guide had a catalog row, but several rows claimed enforcement that the resolved settings did not provide, the guideline policy reversed only one prose/sample conflict, and TypeScript files ran pinned JavaScript rules that misreport TypeScript syntax. Catalog: record a disposition per policy (compatibility and guideline), add a `partial` enforcement level, scope mappings to the base preset, the guideline policy, or an opt-in profile, and record the TypeScript implementation of each replaced rule. Dispositions for 5.2, 10.4, 10.10, 10.11, 16.1, 17.1, 18.1, 18.6, and 7.2 now match actual behavior. Guideline policy: enforce the prose for clauses 5.2, 6.2/18.13, 10.1, 10.2, 10.6, 10.10 (selectable `importGroups: example | prose`), 10.11, 16.1, 17.1, 17.2, 18.1 (JSX), 18.6, 18.7, 22.1, and 22.7. Add rules no-multiline-string-concat, multiline-condition-layout, constant-name, and a lineCommentRuns option for require-docblock. TypeScript: replace pinned rules that report valid TypeScript or ignore TypeScript syntax with TypeScript-aware implementations of the same rules and options (native no-dupe-class-members, no-array-constructor, default-param-last, no-use-before-define; Stylistic comma-dangle, comma-spacing, key-spacing, lines-between-class-members, lines-around-comment, object-curly-spacing, semi, space-before-blocks, space-infix-ops; merge-aware ag-ts/no-redeclare). no-accessors covers abstract, signature, and auto-accessors; unknown-catch covers every catch binding pattern. `--check-tsconfig` verifies clauses 26.1/26.2. Profile: move AdGuard project conventions that the guide does not require (stricter JSDoc, MV2/MV3 imports, import-newlines, boundaries, logger context, @typescript-eslint rules) from the base preset into the opt-in `adguard-projects` profile with accurate provenance. Tests: run every enforced clause's good and bad examples through the complete preset for both policies, require partial and overridden dispositions to be demonstrated, and require an idiomatic TypeScript corpus to pass the full TypeScript preset. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MCeDV2VhbUtLyAP9XRG1zL --- .oxlintrc.json | 2 +- README.md | 6 +- docs/conformance.md | 4 +- docs/coverage.md | 1419 ++++----- packages/oxlint-config/README.md | 47 +- packages/oxlint-config/package.json | 2 + packages/oxlint-config/src/cli.ts | 33 +- packages/oxlint-config/src/compiler.ts | 128 + packages/oxlint-config/src/index.ts | 129 +- packages/oxlint-plugin/README.md | 7 +- packages/oxlint-plugin/package.json | 4 + packages/oxlint-plugin/src/helpers.ts | 77 + packages/oxlint-plugin/src/index.ts | 196 +- packages/oxlint-plugin/src/typescript.ts | 140 + packages/rule-catalog/README.md | 2 +- packages/rule-catalog/src/catalog.json | 2791 +++++++++++++++--- packages/rule-catalog/src/index.ts | 43 +- scripts/generate.ts | 714 ++++- scripts/source-policy.ts | 3 + scripts/write-configs.ts | 4 + tests/behavior.test.ts | 4 +- tests/catalog.test.ts | 8 +- tests/clause-cases.ts | 1081 +++++++ tests/clauses.test.ts | 151 + tests/compiler.test.ts | 129 + tests/config-cli.test.ts | 9 + tests/consumer.test.ts | 12 +- tests/custom-cases.ts | 131 +- tests/custom.test.ts | 8 +- tests/fixtures/typescript/classes.ts | 15 + tests/fixtures/typescript/component.tsx | 20 + tests/fixtures/typescript/declarations.d.ts | 17 + tests/fixtures/typescript/imports.ts | 19 + tests/fixtures/typescript/merging.ts | 47 + tests/fixtures/typescript/namespace-merge.ts | 11 + tests/fixtures/typescript/overloads.ts | 44 + tests/fixtures/typescript/square.ts | 16 + tests/fixtures/typescript/types.ts | 78 + tests/gap-cases.ts | 62 + tests/gaps.test.ts | 21 +- tests/integration.test.ts | 63 +- tests/typescript-preset.test.ts | 107 + tests/typescript-rules.test.ts | 92 + tests/typescript-syntax.test.ts | 3 +- 44 files changed, 6528 insertions(+), 1371 deletions(-) create mode 100644 packages/oxlint-config/src/compiler.ts create mode 100644 packages/oxlint-plugin/src/typescript.ts create mode 100644 tests/clause-cases.ts create mode 100644 tests/clauses.test.ts create mode 100644 tests/compiler.test.ts create mode 100644 tests/fixtures/typescript/classes.ts create mode 100644 tests/fixtures/typescript/component.tsx create mode 100644 tests/fixtures/typescript/declarations.d.ts create mode 100644 tests/fixtures/typescript/imports.ts create mode 100644 tests/fixtures/typescript/merging.ts create mode 100644 tests/fixtures/typescript/namespace-merge.ts create mode 100644 tests/fixtures/typescript/overloads.ts create mode 100644 tests/fixtures/typescript/square.ts create mode 100644 tests/fixtures/typescript/types.ts create mode 100644 tests/typescript-preset.test.ts create mode 100644 tests/typescript-rules.test.ts diff --git a/.oxlintrc.json b/.oxlintrc.json index 2cdf153..6525e6e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,6 +1,6 @@ { "extends": [ - "./packages/oxlint-config/dist/typescript.json", + "./packages/oxlint-config/dist/typescript-adguard-projects.json", "./packages/oxlint-config/dist/node.json" ], "ignorePatterns": [ diff --git a/README.md b/README.md index e9a4182..16aaaa7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AdGuard guidelines for Oxlint -An Oxlint-only implementation of the pinned [AdGuard JavaScript guidelines](docs/reference/Javascript.md), including the complete inherited Airbnb and JSDoc sample configuration. The sample configuration takes precedence when it conflicts with prose. Every one of the 134 numbered clauses has a disposition in the generated [coverage catalog](docs/coverage.md). +An Oxlint-only implementation of the pinned [AdGuard JavaScript guidelines](docs/reference/Javascript.md), including the complete inherited Airbnb and JSDoc sample configuration and the guide's TypeScript section. The default `compatibility` policy gives the sample configuration precedence when it conflicts with prose; the opt-in `guideline` policy enforces the prose instead. Every one of the 134 numbered clauses has a disposition for both policies in the generated [coverage catalog](docs/coverage.md), and `tests/clauses.test.ts` runs each enforced clause's good and bad examples through the complete preset. The pnpm workspace contains three reusable packages: @@ -33,7 +33,7 @@ pnpm exec ag-oxlint-config --language typescript --environment node pnpm exec oxlint --config .oxlintrc.json . ``` -The CLI preserves existing files unless passed `--force`. `--output -` prints JSON. JavaScript/browser/module are the defaults. Use `--source-type script` for browser scripts; `.cjs` and `.cts` retain CommonJS semantics. +The CLI preserves existing files unless passed `--force`. `--output -` prints JSON. JavaScript/browser/module are the defaults. Use `--source-type script` for browser scripts; `.cjs` and `.cts` retain CommonJS semantics. `--policy guideline` enforces guide prose over conflicting sample settings (`--import-groups example|prose` selects the clause 10.10 interpretation), and `--profile adguard-projects` adds conventions shared by AdGuard projects that the guide does not require. `--check-tsconfig` verifies the compiler options of clauses 26.1 and 26.2. The programmatic API works in TypeScript scripts executed with `tsx`: @@ -57,7 +57,7 @@ For compiler checking: } ``` -TypeScript presets preserve documentation requirements while avoiding duplicate JSDoc type annotations. Type checking uses the TypeScript compiler; the default presets require no separate typed lint service. Optional `createConfig({ language: 'typescript', typeAware: true })` enables type-dependent lint rules with `oxlint-tsgolint@7.0.2002`; see [type-aware linting](packages/oxlint-config/README.md#type-aware-linting). Compiler checking remains a separate gate. +TypeScript presets apply the JavaScript rules through TypeScript-aware implementations where the pinned JavaScript ones misreport TypeScript syntax, and preserve documentation requirements while avoiding duplicate JSDoc type annotations. Type checking uses the TypeScript compiler; the default presets require no separate typed lint service. Optional `createConfig({ language: 'typescript', typeAware: true, profile: 'adguard-projects' })` enables type-dependent lint rules with `oxlint-tsgolint@7.0.2002`; see [type-aware linting](packages/oxlint-config/README.md#type-aware-linting). Compiler checking remains a separate gate. ## Scope and maintenance diff --git a/docs/conformance.md b/docs/conformance.md index de7596e..60c31fc 100644 --- a/docs/conformance.md +++ b/docs/conformance.md @@ -12,6 +12,6 @@ The handwritten adapters address standalone scope metadata and comment globals, Vitest coverage includes every handwritten TypeScript runtime file under `packages/*/src`, including files not otherwise imported. Thresholds are 95% for statements, lines, and functions and 90% for branches. Vendored source is assessed through conformance rather than included in that percentage. Repeated CLI executions used to check stability are not counted as distinct corpus scenarios. -For TypeScript files, the preset uses Oxlint's native `no-unused-vars`, `no-shadow`, `no-useless-constructor`, and `no-empty-function` implementations to understand type signatures and constructor parameter properties. It retains inherited settings, explicitly preserves the baseline's `caughtErrors: none` default, and uses Stylistic's TypeScript-capable indentation rule with the inherited four-space setting. These syntax-specific overrides have dedicated cases for all four TypeScript extensions; exact upstream JavaScript conformance continues to use the pinned implementations. Type-only declarations and modern TypeScript binding behavior are governed by these documented overrides rather than an ESLint TypeScript parser or a typed lint service. +The guide applies the JavaScript rules to TypeScript. For TypeScript files, the preset replaces pinned JavaScript implementations that report valid TypeScript or ignore TypeScript syntax with TypeScript-aware implementations of the same rules and options: Oxlint's native `no-unused-vars`, `no-shadow`, `no-useless-constructor`, `no-empty-function`, `no-dupe-class-members`, `no-array-constructor`, `default-param-last`, and `no-use-before-define`; Stylistic's `indent`, `comma-dangle` (extended to enums, type parameters, and tuples as airbnb-typescript does), `comma-spacing`, `key-spacing`, `lines-between-class-members`, `lines-around-comment`, `object-curly-spacing`, `semi`, `space-before-blocks`, and `space-infix-ops`; and `ag-ts/no-redeclare`, which runs the pinned rule but accepts overloads and the declaration merges that TypeScript permits. Import and React settings gain TypeScript extensions and development globs. The generator records each replacement and its reason in the catalog; `tests/typescript-preset.test.ts` requires idiomatic TypeScript to pass the complete preset under both policies and requires each replacement to report TypeScript-only violations. Replacements are limited to rules with an observed difference: TypeScript syntax the pinned rules neither misreport nor check, such as generic call spacing, remains outside the preset. The pinned `import/export` rule reports exported interface merges, as upstream does. Exact upstream JavaScript conformance continues to use the pinned implementations. -The default presets remain syntax-only. `createConfig({ language: 'typescript', typeAware: true })` opts into mappings marked `requiresTypeInfo` using the optional `oxlint-tsgolint@7.0.2002` peer. Currently this adds `typescript/consistent-type-exports` only in the TypeScript file override. A real multi-file project and a packed-package consumer test check both an invalid re-export and its type-only correction. This establishes execution of that rule, not complete Airbnb TypeScript parity, compatibility with a project's nested policies, or full guideline-prose compliance. Compiler checking, declaration emission, and manual review remain separate requirements. +The default presets remain syntax-only. `createConfig({ language: 'typescript', typeAware: true })` opts into mappings marked `requiresTypeInfo` using the optional `oxlint-tsgolint@7.0.2002` peer. Currently this adds `typescript/consistent-type-exports`, part of the `adguard-projects` profile, only in the TypeScript file override. A real multi-file project and a packed-package consumer test check both an invalid re-export and its type-only correction. This establishes execution of that rule, not complete Airbnb TypeScript parity, compatibility with a project's nested policies, or full guideline-prose compliance. Compiler checking, declaration emission, and manual review remain separate requirements. diff --git a/docs/coverage.md b/docs/coverage.md index 4907f8c..30bce8b 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -1,701 +1,730 @@ # Guideline coverage -Generated by `pnpm catalog:generate`. Sample settings and inherited disabled rules take precedence over prose. +Generated by `pnpm catalog:generate`. The default `compatibility` policy gives the sample configuration and its inherited disabled rules precedence over prose. The opt-in `guideline` policy enforces the prose where the two conflict. `partial` marks clauses whose rules enforce only part of the stated requirement. -| Clause | Enforcement | Rules | Interpretation | -| --- | --- | --- | --- | -| [1.1 types--primitives](reference/Javascript.md#types--primitives) | manual | | Educational semantics; supported browser targets must be selected by the consumer. | -| [1.2 types--complex](reference/Javascript.md#types--complex) | manual | | Educational explanation of reference values. | -| [2.1 references--prefer-const](reference/Javascript.md#references--prefer-const) | javascript | `prefer-const`, `no-const-assign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [2.2 references--disallow-var](reference/Javascript.md#references--disallow-var) | javascript | `no-var` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [2.3 references--block-scope](reference/Javascript.md#references--block-scope) | javascript | `no-undef` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [3.1 objects--quoted-props](reference/Javascript.md#objects--quoted-props) | javascript | `quote-props` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [3.2 objects--prototype-builtins](reference/Javascript.md#objects--prototype-builtins) | javascript | `no-prototype-builtins` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [3.3 objects--rest-spread](reference/Javascript.md#objects--rest-spread) | javascript | `prefer-object-spread` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [4.1 es6-array-spreads](reference/Javascript.md#es6-array-spreads) | manual | | Recognizing arbitrary copying algorithms requires semantic intent; reviewed manually. | -| [4.2 arrays--from-iterable](reference/Javascript.md#arrays--from-iterable) | manual | | An unknown JavaScript value may be array-like rather than iterable; no speculative rewrite. | -| [4.3 arrays--from-array-like](reference/Javascript.md#arrays--from-array-like) | manual | | Whether an arbitrary runtime value is array-like requires runtime information. | -| [4.4 arrays--mapping](reference/Javascript.md#arrays--mapping) | custom | `ag/prefer-array-from-map` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [4.5 arrays--callback-return](reference/Javascript.md#arrays--callback-return) | javascript | `array-callback-return` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [5.1 destructuring--object](reference/Javascript.md#destructuring--object) | javascript | `prefer-destructuring` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [5.2 destructuring--array](reference/Javascript.md#destructuring--array) | javascript | `prefer-destructuring` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [5.3 destructuring--object-over-array](reference/Javascript.md#destructuring--object-over-array) | manual | | Distinguishing multiple return values from an intentional array API requires review. | -| [6.1 strings--quotes](reference/Javascript.md#strings--quotes) | javascript | `quotes` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [6.2 strings--line-length](reference/Javascript.md#strings--line-length) | overridden | `max-len`, `no-useless-concat` | Sample precedence: max-len, no-useless-concat retains its inherited/explicit options and severity. | -| [6.3 es6-template-literals](reference/Javascript.md#es6-template-literals) | javascript | `prefer-template`, `template-curly-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [6.4 strings--eval](reference/Javascript.md#strings--eval) | javascript | `no-eval` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [6.5 strings--escaping](reference/Javascript.md#strings--escaping) | javascript | `no-useless-escape` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.1 functions--iife](reference/Javascript.md#functions--iife) | javascript | `wrap-iife` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.2 functions--in-blocks](reference/Javascript.md#functions--in-blocks) | javascript | `no-loop-func` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.3 functions--arguments-shadow](reference/Javascript.md#functions--arguments-shadow) | javascript | `no-shadow-restricted-names` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.4 es6-rest](reference/Javascript.md#es6-rest) | javascript | `prefer-rest-params` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.5 es6-default-parameters](reference/Javascript.md#es6-default-parameters) | javascript | `no-param-reassign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.6 functions--default-side-effects](reference/Javascript.md#functions--default-side-effects) | custom | `ag/no-default-side-effects` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.7 functions--defaults-last](reference/Javascript.md#functions--defaults-last) | javascript | `default-param-last` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.8 functions--constructor](reference/Javascript.md#functions--constructor) | javascript | `no-new-func` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.9 functions--signature-spacing](reference/Javascript.md#functions--signature-spacing) | javascript | `space-before-function-paren`, `space-before-blocks` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.10 functions--mutate-params](reference/Javascript.md#functions--mutate-params) | javascript | `no-param-reassign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.11 functions--reassign-params](reference/Javascript.md#functions--reassign-params) | javascript | `no-param-reassign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.12 functions--spread-vs-apply](reference/Javascript.md#functions--spread-vs-apply) | javascript | `prefer-spread` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.13 functions--signature-invocation-indentation](reference/Javascript.md#functions--signature-invocation-indentation) | javascript | `function-paren-newline` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [8.1 arrows--use-them](reference/Javascript.md#arrows--use-them) | javascript | `prefer-arrow-callback`, `arrow-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [8.2 arrows--one-arg-parens](reference/Javascript.md#arrows--one-arg-parens) | javascript | `arrow-parens` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [8.3 whitespace--implicit-arrow-linebreak](reference/Javascript.md#whitespace--implicit-arrow-linebreak) | javascript | `implicit-arrow-linebreak` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [9.1 constructors--use-class](reference/Javascript.md#constructors--use-class) | custom | `ag/no-prototype-mutation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [9.2 constructors--extends](reference/Javascript.md#constructors--extends) | custom | `ag/no-prototype-mutation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [9.3 constructors--chaining](reference/Javascript.md#constructors--chaining) | manual | | Method chaining is optional API design guidance. | -| [9.4 constructors--no-useless](reference/Javascript.md#constructors--no-useless) | javascript | `no-useless-constructor` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [9.5 classes--methods-use-this](reference/Javascript.md#classes--methods-use-this) | javascript | `class-methods-use-this` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.1 modules--use-them](reference/Javascript.md#modules--use-them) | overridden | `import/no-commonjs`, `import/no-amd` | Sample precedence: import/no-commonjs, import/no-amd retains its inherited/explicit options and severity. | -| [10.2 modules--no-wildcard](reference/Javascript.md#modules--no-wildcard) | overridden | `import/no-namespace` | Sample precedence: import/no-namespace retains its inherited/explicit options and severity. | -| [10.3 modules--no-export-from-import](reference/Javascript.md#modules--no-export-from-import) | custom | `ag/no-direct-reexport` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.4 modules--no-duplicate-imports](reference/Javascript.md#modules--no-duplicate-imports) | overridden | `no-duplicate-imports` | Sample precedence: no-duplicate-imports retains its inherited/explicit options and severity. | -| [10.5 modules--no-mutable-exports](reference/Javascript.md#modules--no-mutable-exports) | javascript | `import/no-mutable-exports` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.6 modules--prefer-named-export](reference/Javascript.md#modules--prefer-named-export) | overridden | `import/prefer-default-export` | Sample precedence: import/prefer-default-export retains its inherited/explicit options and severity. | -| [10.7 modules--imports-first](reference/Javascript.md#modules--imports-first) | javascript | `import/first` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.8 modules--multiline-imports-over-newlines](reference/Javascript.md#modules--multiline-imports-over-newlines) | javascript | `object-curly-newline` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.9 modules--import-extensions](reference/Javascript.md#modules--import-extensions) | javascript | `import/extensions` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.10 modules--import-grouping](reference/Javascript.md#modules--import-grouping) | javascript | `import/order` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.11 modules--import-order](reference/Javascript.md#modules--import-order) | javascript | `import/order` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.12 modules--import-member-order](reference/Javascript.md#modules--import-member-order) | javascript | `import/order`, `sort-imports` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.13 modules--import-node-protocol](reference/Javascript.md#modules--import-node-protocol) | native | `unicorn/prefer-node-protocol` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [11.1 properties--dot](reference/Javascript.md#properties--dot) | javascript | `dot-notation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [11.2 properties--bracket](reference/Javascript.md#properties--bracket) | javascript | `dot-notation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [11.3 es2016-properties--exponentiation-operator](reference/Javascript.md#es2016-properties--exponentiation-operator) | javascript | `no-restricted-properties` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [12.1 variables--const](reference/Javascript.md#variables--const) | javascript | `no-undef`, `prefer-const` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [12.2 variables--one-const](reference/Javascript.md#variables--one-const) | javascript | `one-var` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [12.3 variables--define-where-used](reference/Javascript.md#variables--define-where-used) | manual | | A reasonable declaration location is a human judgment. | -| [12.4 variables--no-chain-assignment](reference/Javascript.md#variables--no-chain-assignment) | javascript | `no-multi-assign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [12.5 variables--unary-increment-decrement](reference/Javascript.md#variables--unary-increment-decrement) | javascript | `no-plusplus` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [12.6 variables--linebreak](reference/Javascript.md#variables--linebreak) | javascript | `max-len`, `operator-linebreak` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [12.7 variables--no-unused-vars](reference/Javascript.md#variables--no-unused-vars) | javascript | `no-unused-vars` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [13.1 hoisting--about](reference/Javascript.md#hoisting--about) | manual | | Educational explanation of hoisting and temporal dead zones. | -| [13.2 hoisting--anon-expressions](reference/Javascript.md#hoisting--anon-expressions) | manual | | Educational explanation of anonymous function hoisting. | -| [13.3 hoisting--named-expresions](reference/Javascript.md#hoisting--named-expresions) | manual | | Educational explanation of named function expression hoisting. | -| [13.4 hoisting--declarations](reference/Javascript.md#hoisting--declarations) | manual | | Educational explanation of function declaration hoisting. | -| [14.1 comparison--eqeqeq](reference/Javascript.md#comparison--eqeqeq) | javascript | `eqeqeq` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [14.2 comparison--if](reference/Javascript.md#comparison--if) | manual | | Educational explanation of boolean coercion. | -| [14.3 comparison--shortcuts](reference/Javascript.md#comparison--shortcuts) | manual | | Requires semantic types for arbitrary JavaScript expressions; review manually. | -| [14.4 comparison--moreinfo](reference/Javascript.md#comparison--moreinfo) | manual | | Further-reading reference, not a code requirement. | -| [14.5 comparison--switch-blocks](reference/Javascript.md#comparison--switch-blocks) | javascript | `no-case-declarations` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [14.6 comparison--nested-ternaries](reference/Javascript.md#comparison--nested-ternaries) | javascript | `no-nested-ternary` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [14.7 comparison--unneeded-ternary](reference/Javascript.md#comparison--unneeded-ternary) | javascript | `no-unneeded-ternary` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [14.8 comparison--no-mixed-operators](reference/Javascript.md#comparison--no-mixed-operators) | javascript | `no-mixed-operators` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [15.1 blocks--braces](reference/Javascript.md#blocks--braces) | javascript | `curly`, `brace-style` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [15.2 blocks--cuddled-elses](reference/Javascript.md#blocks--cuddled-elses) | javascript | `brace-style` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [15.3 blocks--no-else-return](reference/Javascript.md#blocks--no-else-return) | javascript | `no-else-return` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [16.1 control-statements](reference/Javascript.md#control-statements) | javascript | `operator-linebreak` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [16.2 control-statement--value-selection](reference/Javascript.md#control-statement--value-selection) | javascript | `no-unused-expressions` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [17.1 comments--multiline](reference/Javascript.md#comments--multiline) | custom | `ag/require-docblock` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [17.2 comments--singleline](reference/Javascript.md#comments--singleline) | overridden | `line-comment-position`, `lines-around-comment` | Sample precedence: line-comment-position, lines-around-comment retains its inherited/explicit options and severity. | -| [17.3 comments--spaces](reference/Javascript.md#comments--spaces) | javascript | `spaced-comment` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [17.4 comments--actionitems](reference/Javascript.md#comments--actionitems) | manual | | TODO/FIXME classify work urgency; the author annotation is explicitly optional. | -| [17.6 comments-jsdoc](reference/Javascript.md#comments-jsdoc) | javascript | `jsdoc/require-file-overview`, `jsdoc/require-description`, `jsdoc/require-description-complete-sentence`, `jsdoc/require-hyphen-before-param-description`, `jsdoc/require-throws`, `jsdoc/sort-tags` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.1 whitespace--spaces](reference/Javascript.md#whitespace--spaces) | javascript | `indent` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.2 whitespace--before-blocks](reference/Javascript.md#whitespace--before-blocks) | javascript | `space-before-blocks` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.3 whitespace--around-keywords](reference/Javascript.md#whitespace--around-keywords) | javascript | `keyword-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.4 whitespace--infix-ops](reference/Javascript.md#whitespace--infix-ops) | javascript | `space-infix-ops` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.5 whitespace--newline-at-end](reference/Javascript.md#whitespace--newline-at-end) | javascript | `eol-last` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.6 whitespace--chains](reference/Javascript.md#whitespace--chains) | javascript | `newline-per-chained-call`, `no-whitespace-before-property` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.7 whitespace--after-blocks](reference/Javascript.md#whitespace--after-blocks) | overridden | `padding-line-between-statements` | Sample precedence: padding-line-between-statements retains its inherited/explicit options and severity. | -| [18.8 whitespace--padded-blocks](reference/Javascript.md#whitespace--padded-blocks) | javascript | `padded-blocks` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.9 whitespace--no-multiple-blanks](reference/Javascript.md#whitespace--no-multiple-blanks) | javascript | `no-multiple-empty-lines` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.10 whitespace--in-parens](reference/Javascript.md#whitespace--in-parens) | javascript | `space-in-parens` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.11 whitespace--in-brackets](reference/Javascript.md#whitespace--in-brackets) | javascript | `array-bracket-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.12 whitespace--in-braces](reference/Javascript.md#whitespace--in-braces) | javascript | `object-curly-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.13 whitespace--max-len](reference/Javascript.md#whitespace--max-len) | javascript | `max-len` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.14 whitespace--block-spacing](reference/Javascript.md#whitespace--block-spacing) | javascript | `block-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.15 whitespace--comma-spacing](reference/Javascript.md#whitespace--comma-spacing) | javascript | `comma-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.16 whitespace--computed-property-spacing](reference/Javascript.md#whitespace--computed-property-spacing) | javascript | `computed-property-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.17 whitespace--func-call-spacing](reference/Javascript.md#whitespace--func-call-spacing) | javascript | `func-call-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.18 whitespace--key-spacing](reference/Javascript.md#whitespace--key-spacing) | javascript | `key-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.19 whitespace--no-trailing-spaces](reference/Javascript.md#whitespace--no-trailing-spaces) | javascript | `no-trailing-spaces` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.20 whitespace--no-multiple-empty-lines](reference/Javascript.md#whitespace--no-multiple-empty-lines) | javascript | `no-multiple-empty-lines` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [19.1 commas--leading-trailing](reference/Javascript.md#commas--leading-trailing) | javascript | `comma-style` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [19.2 commas--dangling](reference/Javascript.md#commas--dangling) | javascript | `comma-dangle` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [20.1 semicolons--required](reference/Javascript.md#semicolons--required) | javascript | `semi` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [21.1 coercion--explicit](reference/Javascript.md#coercion--explicit) | manual | | Determining where a semantic conversion belongs requires review. | -| [21.2 coercion--strings](reference/Javascript.md#coercion--strings) | javascript | `no-new-wrappers` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [21.3 coercion--numbers](reference/Javascript.md#coercion--numbers) | javascript | `radix`, `no-new-wrappers` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [21.4 coercion--comment-deviations](reference/Javascript.md#coercion--comment-deviations) | overridden | `no-bitwise` | Sample precedence: no-bitwise retains its inherited/explicit options and severity. | -| [21.5 coercion--bitwise](reference/Javascript.md#coercion--bitwise) | manual | | Educational warning about bitwise numeric range. | -| [21.6 coercion--booleans](reference/Javascript.md#coercion--booleans) | javascript | `no-new-wrappers` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [22.1 naming--descriptive](reference/Javascript.md#naming--descriptive) | overridden | `id-length` | Sample precedence: id-length retains its inherited/explicit options and severity. | -| [22.2 naming--camelCase](reference/Javascript.md#naming--camelCase) | javascript | `camelcase` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [22.3 naming--PascalCase](reference/Javascript.md#naming--PascalCase) | javascript | `new-cap` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [22.4 naming--leading-underscore](reference/Javascript.md#naming--leading-underscore) | javascript | `no-underscore-dangle` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [22.5 naming--self-this](reference/Javascript.md#naming--self-this) | native | `unicorn/no-this-assignment` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [22.6 naming--PascalCase-singleton](reference/Javascript.md#naming--PascalCase-singleton) | manual | | Identifying a singleton or function library requires API-level intent. | -| [22.7 naming--Acronyms-and-Initialisms](reference/Javascript.md#naming--Acronyms-and-Initialisms) | manual | | Recognizing domain acronyms requires a project vocabulary. | -| [22.7 naming--constants](reference/Javascript.md#naming--constants) | manual | | The guide distinguishes semantic constants from ordinary const bindings without defining a boundary. | -| [23.1 accessors--not-required](reference/Javascript.md#accessors--not-required) | manual | | Accessor functions are explicitly optional. | -| [23.2 accessors--no-getters-setters](reference/Javascript.md#accessors--no-getters-setters) | custom | `ag/no-accessors` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [23.3 accessors--boolean-prefix](reference/Javascript.md#accessors--boolean-prefix) | manual | | Boolean API intent cannot be inferred reliably for arbitrary JavaScript methods. | -| [23.4 accessors--consistent](reference/Javascript.md#accessors--consistent) | manual | | Consistency of get/set APIs is a design-review requirement. | -| [24.1 standard-library--isnan](reference/Javascript.md#standard-library--isnan) | javascript | `no-restricted-globals` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [24.2 standard-library--isfinite](reference/Javascript.md#standard-library--isfinite) | javascript | `no-restricted-globals` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [25.1 typescript--enum-naming-conventions](reference/Javascript.md#typescript--enum-naming-conventions) | custom | `ag/enum-name` | Enum and member casing is enforced. Singular English names require review; values are unrestricted. | -| [26.1 typescript--tsconfig-strict](reference/Javascript.md#typescript--tsconfig-strict) | compiler | | Enforced by shared tsconfig settings and compiler integration tests. | -| [26.2 typescript--tsconfig-nouncheckedindexedaccess](reference/Javascript.md#typescript--tsconfig-nouncheckedindexedaccess) | compiler | | Enforced by shared tsconfig settings and compiler integration tests. | -| [25.3 typescript--caught-error-type](reference/Javascript.md#typescript--caught-error-type) | custom | `ag/unknown-catch` | Explicit any is rejected; unannotated catches use unknown through strict compiler settings. | +| Clause | Compatibility | Guideline | Rules | Interpretation | +| --- | --- | --- | --- | --- | +| [1.1 types--primitives](reference/Javascript.md#types--primitives) | manual | manual | | Educational semantics; supported browser targets must be selected by the consumer. | +| [1.2 types--complex](reference/Javascript.md#types--complex) | manual | manual | | Educational explanation of reference values. | +| [2.1 references--prefer-const](reference/Javascript.md#references--prefer-const) | javascript | javascript | `prefer-const`, `no-const-assign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [2.2 references--disallow-var](reference/Javascript.md#references--disallow-var) | javascript | javascript | `no-var` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [2.3 references--block-scope](reference/Javascript.md#references--block-scope) | javascript | javascript | `no-undef` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [3.1 objects--quoted-props](reference/Javascript.md#objects--quoted-props) | javascript | javascript | `quote-props` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [3.2 objects--prototype-builtins](reference/Javascript.md#objects--prototype-builtins) | javascript | javascript | `no-prototype-builtins` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [3.3 objects--rest-spread](reference/Javascript.md#objects--rest-spread) | javascript | javascript | `prefer-object-spread` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [4.1 es6-array-spreads](reference/Javascript.md#es6-array-spreads) | manual | manual | | Recognizing arbitrary copying algorithms requires semantic intent; reviewed manually. | +| [4.2 arrays--from-iterable](reference/Javascript.md#arrays--from-iterable) | manual | manual | | An unknown JavaScript value may be array-like rather than iterable; no speculative rewrite. | +| [4.3 arrays--from-array-like](reference/Javascript.md#arrays--from-array-like) | manual | manual | | Whether an arbitrary runtime value is array-like requires runtime information. | +| [4.4 arrays--mapping](reference/Javascript.md#arrays--mapping) | custom | custom | `ag/prefer-array-from-map` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [4.5 arrays--callback-return](reference/Javascript.md#arrays--callback-return) | javascript | javascript | `array-callback-return` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [5.1 destructuring--object](reference/Javascript.md#destructuring--object) | javascript | javascript | `prefer-destructuring` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [5.2 destructuring--array](reference/Javascript.md#destructuring--array) | partial | javascript | `prefer-destructuring` | Inherited prefer-destructuring checks assignments but not declarations such as `const first = arr[0]`; the guideline policy checks both. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [5.3 destructuring--object-over-array](reference/Javascript.md#destructuring--object-over-array) | manual | manual | | Distinguishing multiple return values from an intentional array API requires review. | +| [6.1 strings--quotes](reference/Javascript.md#strings--quotes) | javascript | javascript | `quotes` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [6.2 strings--line-length](reference/Javascript.md#strings--line-length) | overridden | custom | `max-len`, `no-useless-concat`; guideline: `max-len`, `ag/no-multiline-string-concat` | Sample precedence: max-len, no-useless-concat retains its inherited/explicit options and severity. Guideline: max-len ignores strings and template literals; ag/no-multiline-string-concat rejects strings broken across lines with concatenation. | +| [6.3 es6-template-literals](reference/Javascript.md#es6-template-literals) | javascript | javascript | `prefer-template`, `template-curly-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [6.4 strings--eval](reference/Javascript.md#strings--eval) | javascript | javascript | `no-eval` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [6.5 strings--escaping](reference/Javascript.md#strings--escaping) | javascript | javascript | `no-useless-escape` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.1 functions--iife](reference/Javascript.md#functions--iife) | javascript | javascript | `wrap-iife` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.2 functions--in-blocks](reference/Javascript.md#functions--in-blocks) | javascript | javascript | `no-loop-func`, `no-inner-declarations` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.3 functions--arguments-shadow](reference/Javascript.md#functions--arguments-shadow) | javascript | javascript | `no-shadow-restricted-names` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.4 es6-rest](reference/Javascript.md#es6-rest) | javascript | javascript | `prefer-rest-params` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.5 es6-default-parameters](reference/Javascript.md#es6-default-parameters) | javascript | javascript | `no-param-reassign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.6 functions--default-side-effects](reference/Javascript.md#functions--default-side-effects) | custom | custom | `ag/no-default-side-effects` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.7 functions--defaults-last](reference/Javascript.md#functions--defaults-last) | javascript | javascript | `default-param-last` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.8 functions--constructor](reference/Javascript.md#functions--constructor) | javascript | javascript | `no-new-func` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.9 functions--signature-spacing](reference/Javascript.md#functions--signature-spacing) | javascript | javascript | `space-before-function-paren`, `space-before-blocks` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.10 functions--mutate-params](reference/Javascript.md#functions--mutate-params) | javascript | javascript | `no-param-reassign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.11 functions--reassign-params](reference/Javascript.md#functions--reassign-params) | javascript | javascript | `no-param-reassign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.12 functions--spread-vs-apply](reference/Javascript.md#functions--spread-vs-apply) | javascript | javascript | `prefer-spread` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.13 functions--signature-invocation-indentation](reference/Javascript.md#functions--signature-invocation-indentation) | javascript | javascript | `function-paren-newline` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [8.1 arrows--use-them](reference/Javascript.md#arrows--use-them) | javascript | javascript | `prefer-arrow-callback`, `arrow-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [8.2 arrows--one-arg-parens](reference/Javascript.md#arrows--one-arg-parens) | javascript | javascript | `arrow-parens` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [8.3 whitespace--implicit-arrow-linebreak](reference/Javascript.md#whitespace--implicit-arrow-linebreak) | javascript | javascript | `implicit-arrow-linebreak` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [9.1 constructors--use-class](reference/Javascript.md#constructors--use-class) | custom | custom | `ag/no-prototype-mutation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [9.2 constructors--extends](reference/Javascript.md#constructors--extends) | custom | custom | `ag/no-prototype-mutation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [9.3 constructors--chaining](reference/Javascript.md#constructors--chaining) | manual | manual | | Method chaining is optional API design guidance. | +| [9.4 constructors--no-useless](reference/Javascript.md#constructors--no-useless) | javascript | javascript | `no-useless-constructor` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [9.5 classes--methods-use-this](reference/Javascript.md#classes--methods-use-this) | javascript | javascript | `class-methods-use-this` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.1 modules--use-them](reference/Javascript.md#modules--use-them) | overridden | javascript | `import/no-commonjs`, `import/no-amd` | Sample precedence: import/no-commonjs, import/no-amd retains its inherited/explicit options and severity. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.2 modules--no-wildcard](reference/Javascript.md#modules--no-wildcard) | overridden | native | `import/no-namespace` | Sample precedence: import/no-namespace retains its inherited/explicit options and severity. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.3 modules--no-export-from-import](reference/Javascript.md#modules--no-export-from-import) | custom | custom | `ag/no-direct-reexport` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.4 modules--no-duplicate-imports](reference/Javascript.md#modules--no-duplicate-imports) | javascript | javascript | `no-duplicate-imports`, `import/no-duplicates` | Airbnb disables no-duplicate-imports in favor of import/no-duplicates, which enforces the clause. | +| [10.5 modules--no-mutable-exports](reference/Javascript.md#modules--no-mutable-exports) | javascript | javascript | `import/no-mutable-exports` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.6 modules--prefer-named-export](reference/Javascript.md#modules--prefer-named-export) | overridden | native | `import/prefer-default-export`; guideline: `import/no-default-export` | Sample precedence: import/prefer-default-export retains its inherited/explicit options and severity. Guideline: import/prefer-default-export is disabled and import/no-default-export rejects default exports. | +| [10.7 modules--imports-first](reference/Javascript.md#modules--imports-first) | javascript | javascript | `import/first` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.8 modules--multiline-imports-over-newlines](reference/Javascript.md#modules--multiline-imports-over-newlines) | javascript | javascript | `object-curly-newline` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.9 modules--import-extensions](reference/Javascript.md#modules--import-extensions) | javascript | javascript | `import/extensions` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.10 modules--import-grouping](reference/Javascript.md#modules--import-grouping) | partial | javascript | `import/order` | Inherited import/order places built-in, package, and internal imports first, but does not require blank lines between groups or separate parent from sibling imports; use the guideline policy. Guideline: import/order requires blank lines between groups. `importGroups: example` follows the clause example (built-in and package imports share a block); `prose` separates every listed category. | +| [10.11 modules--import-order](reference/Javascript.md#modules--import-order) | overridden | javascript | `import/order` | Inherited import/order options do not alphabetize; the guideline policy orders imports by path. Guideline: import/order alphabetizes imports by path within each group. | +| [10.12 modules--import-member-order](reference/Javascript.md#modules--import-member-order) | javascript | javascript | `import/order`, `sort-imports` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.13 modules--import-node-protocol](reference/Javascript.md#modules--import-node-protocol) | native | native | `unicorn/prefer-node-protocol` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [11.1 properties--dot](reference/Javascript.md#properties--dot) | javascript | javascript | `dot-notation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [11.2 properties--bracket](reference/Javascript.md#properties--bracket) | javascript | javascript | `dot-notation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [11.3 es2016-properties--exponentiation-operator](reference/Javascript.md#es2016-properties--exponentiation-operator) | javascript | javascript | `no-restricted-properties` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [12.1 variables--const](reference/Javascript.md#variables--const) | javascript | javascript | `no-undef`, `prefer-const` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [12.2 variables--one-const](reference/Javascript.md#variables--one-const) | javascript | javascript | `one-var` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [12.3 variables--define-where-used](reference/Javascript.md#variables--define-where-used) | manual | manual | | A reasonable declaration location is a human judgment. | +| [12.4 variables--no-chain-assignment](reference/Javascript.md#variables--no-chain-assignment) | javascript | javascript | `no-multi-assign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [12.5 variables--unary-increment-decrement](reference/Javascript.md#variables--unary-increment-decrement) | javascript | javascript | `no-plusplus` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [12.6 variables--linebreak](reference/Javascript.md#variables--linebreak) | javascript | javascript | `max-len`, `operator-linebreak` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [12.7 variables--no-unused-vars](reference/Javascript.md#variables--no-unused-vars) | javascript | javascript | `no-unused-vars` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [13.1 hoisting--about](reference/Javascript.md#hoisting--about) | manual | manual | | Educational explanation of hoisting and temporal dead zones. | +| [13.2 hoisting--anon-expressions](reference/Javascript.md#hoisting--anon-expressions) | manual | manual | | Educational explanation of anonymous function hoisting. | +| [13.3 hoisting--named-expresions](reference/Javascript.md#hoisting--named-expresions) | manual | manual | | Educational explanation of named function expression hoisting. | +| [13.4 hoisting--declarations](reference/Javascript.md#hoisting--declarations) | manual | manual | | Educational explanation of function declaration hoisting. | +| [14.1 comparison--eqeqeq](reference/Javascript.md#comparison--eqeqeq) | javascript | javascript | `eqeqeq` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [14.2 comparison--if](reference/Javascript.md#comparison--if) | manual | manual | | Educational explanation of boolean coercion. | +| [14.3 comparison--shortcuts](reference/Javascript.md#comparison--shortcuts) | manual | manual | | Requires semantic types for arbitrary JavaScript expressions; review manually. | +| [14.4 comparison--moreinfo](reference/Javascript.md#comparison--moreinfo) | manual | manual | | Further-reading reference, not a code requirement. | +| [14.5 comparison--switch-blocks](reference/Javascript.md#comparison--switch-blocks) | javascript | javascript | `no-case-declarations` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [14.6 comparison--nested-ternaries](reference/Javascript.md#comparison--nested-ternaries) | javascript | javascript | `no-nested-ternary` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [14.7 comparison--unneeded-ternary](reference/Javascript.md#comparison--unneeded-ternary) | javascript | javascript | `no-unneeded-ternary` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [14.8 comparison--no-mixed-operators](reference/Javascript.md#comparison--no-mixed-operators) | javascript | javascript | `no-mixed-operators` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [15.1 blocks--braces](reference/Javascript.md#blocks--braces) | javascript | javascript | `curly`, `brace-style` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [15.2 blocks--cuddled-elses](reference/Javascript.md#blocks--cuddled-elses) | javascript | javascript | `brace-style` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [15.3 blocks--no-else-return](reference/Javascript.md#blocks--no-else-return) | javascript | javascript | `no-else-return` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [16.1 control-statements](reference/Javascript.md#control-statements) | partial | custom | `operator-linebreak`; guideline: `operator-linebreak`, `ag/multiline-condition-layout` | operator-linebreak puts logical operators first; starting the condition on its own line is enforced by the guideline policy. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [16.2 control-statement--value-selection](reference/Javascript.md#control-statement--value-selection) | javascript | javascript | `no-unused-expressions` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [17.1 comments--multiline](reference/Javascript.md#comments--multiline) | partial | custom | `ag/require-docblock` | Multiline /* */ blocks are rejected; runs of // line comments (the clause example) are rejected only by the guideline policy. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [17.2 comments--singleline](reference/Javascript.md#comments--singleline) | overridden | javascript | `line-comment-position`, `lines-around-comment` | Sample precedence: line-comment-position, lines-around-comment retains its inherited/explicit options and severity. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [17.3 comments--spaces](reference/Javascript.md#comments--spaces) | javascript | javascript | `spaced-comment` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [17.4 comments--actionitems](reference/Javascript.md#comments--actionitems) | manual | manual | | TODO/FIXME classify work urgency; the author annotation is explicitly optional. | +| [17.6 comments-jsdoc](reference/Javascript.md#comments-jsdoc) | javascript | javascript | `jsdoc/require-file-overview` | The recommended eslint-plugin-jsdoc configuration plus jsdoc/require-file-overview. | +| [18.1 whitespace--spaces](reference/Javascript.md#whitespace--spaces) | partial | javascript | `indent`, `react/jsx-indent`, `react/jsx-indent-props` | indent requires four spaces; inherited react/jsx-indent and react/jsx-indent-props require two-space JSX, which the guideline policy changes to four. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.2 whitespace--before-blocks](reference/Javascript.md#whitespace--before-blocks) | javascript | javascript | `space-before-blocks` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.3 whitespace--around-keywords](reference/Javascript.md#whitespace--around-keywords) | javascript | javascript | `keyword-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.4 whitespace--infix-ops](reference/Javascript.md#whitespace--infix-ops) | javascript | javascript | `space-infix-ops` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.5 whitespace--newline-at-end](reference/Javascript.md#whitespace--newline-at-end) | javascript | javascript | `eol-last` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.6 whitespace--chains](reference/Javascript.md#whitespace--chains) | partial | javascript | `newline-per-chained-call`, `no-whitespace-before-property` | Inherited newline-per-chained-call allows four calls on one line; the guideline policy allows two. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.7 whitespace--after-blocks](reference/Javascript.md#whitespace--after-blocks) | overridden | partial | `padding-line-between-statements` | Sample precedence: padding-line-between-statements retains its inherited/explicit options and severity. Guideline: padding-line-between-statements requires a blank line after block-like statements; blank lines between multiline members of object and array literals remain reviewable. | +| [18.8 whitespace--padded-blocks](reference/Javascript.md#whitespace--padded-blocks) | javascript | javascript | `padded-blocks` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.9 whitespace--no-multiple-blanks](reference/Javascript.md#whitespace--no-multiple-blanks) | javascript | javascript | `no-multiple-empty-lines` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.10 whitespace--in-parens](reference/Javascript.md#whitespace--in-parens) | javascript | javascript | `space-in-parens` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.11 whitespace--in-brackets](reference/Javascript.md#whitespace--in-brackets) | javascript | javascript | `array-bracket-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.12 whitespace--in-braces](reference/Javascript.md#whitespace--in-braces) | javascript | javascript | `object-curly-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.13 whitespace--max-len](reference/Javascript.md#whitespace--max-len) | javascript | javascript | `max-len` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. Guideline: max-len exempts strings and template literals, as the clause notes. | +| [18.14 whitespace--block-spacing](reference/Javascript.md#whitespace--block-spacing) | javascript | javascript | `block-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.15 whitespace--comma-spacing](reference/Javascript.md#whitespace--comma-spacing) | javascript | javascript | `comma-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.16 whitespace--computed-property-spacing](reference/Javascript.md#whitespace--computed-property-spacing) | javascript | javascript | `computed-property-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.17 whitespace--func-call-spacing](reference/Javascript.md#whitespace--func-call-spacing) | javascript | javascript | `func-call-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.18 whitespace--key-spacing](reference/Javascript.md#whitespace--key-spacing) | javascript | javascript | `key-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.19 whitespace--no-trailing-spaces](reference/Javascript.md#whitespace--no-trailing-spaces) | javascript | javascript | `no-trailing-spaces` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.20 whitespace--no-multiple-empty-lines](reference/Javascript.md#whitespace--no-multiple-empty-lines) | javascript | javascript | `no-multiple-empty-lines` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [19.1 commas--leading-trailing](reference/Javascript.md#commas--leading-trailing) | javascript | javascript | `comma-style` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [19.2 commas--dangling](reference/Javascript.md#commas--dangling) | javascript | javascript | `comma-dangle` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [20.1 semicolons--required](reference/Javascript.md#semicolons--required) | javascript | javascript | `semi` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [21.1 coercion--explicit](reference/Javascript.md#coercion--explicit) | manual | manual | | Determining where a semantic conversion belongs requires review. | +| [21.2 coercion--strings](reference/Javascript.md#coercion--strings) | javascript | javascript | `no-new-wrappers` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [21.3 coercion--numbers](reference/Javascript.md#coercion--numbers) | javascript | javascript | `radix`, `no-new-wrappers` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [21.4 coercion--comment-deviations](reference/Javascript.md#coercion--comment-deviations) | overridden | overridden | `no-bitwise` | Sample precedence: no-bitwise retains its inherited/explicit options and severity. | +| [21.5 coercion--bitwise](reference/Javascript.md#coercion--bitwise) | manual | manual | | Educational warning about bitwise numeric range. | +| [21.6 coercion--booleans](reference/Javascript.md#coercion--booleans) | javascript | javascript | `no-new-wrappers` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [22.1 naming--descriptive](reference/Javascript.md#naming--descriptive) | overridden | javascript | `id-length` | Sample precedence: id-length retains its inherited/explicit options and severity. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [22.2 naming--camelCase](reference/Javascript.md#naming--camelCase) | javascript | javascript | `camelcase` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [22.3 naming--PascalCase](reference/Javascript.md#naming--PascalCase) | javascript | javascript | `new-cap` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [22.4 naming--leading-underscore](reference/Javascript.md#naming--leading-underscore) | javascript | javascript | `no-underscore-dangle` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [22.5 naming--self-this](reference/Javascript.md#naming--self-this) | native | native | `unicorn/no-this-assignment` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [22.6 naming--PascalCase-singleton](reference/Javascript.md#naming--PascalCase-singleton) | manual | manual | | Identifying a singleton or function library requires API-level intent. | +| [22.7 naming--Acronyms-and-Initialisms](reference/Javascript.md#naming--Acronyms-and-Initialisms) | manual | manual | | Recognizing domain acronyms requires a project vocabulary. | +| [22.7 naming--constants](reference/Javascript.md#naming--constants) | manual | partial | ; guideline: `ag/constant-name` | The guide distinguishes semantic constants from ordinary const bindings without defining a boundary. Guideline: ag/constant-name requires UPPER_SNAKE_CASE for module-level const bindings initialized with primitive literals; other semantic constants remain reviewable. | +| [23.1 accessors--not-required](reference/Javascript.md#accessors--not-required) | manual | manual | | Accessor functions are explicitly optional. | +| [23.2 accessors--no-getters-setters](reference/Javascript.md#accessors--no-getters-setters) | custom | custom | `ag/no-accessors` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [23.3 accessors--boolean-prefix](reference/Javascript.md#accessors--boolean-prefix) | manual | manual | | Boolean API intent cannot be inferred reliably for arbitrary JavaScript methods. | +| [23.4 accessors--consistent](reference/Javascript.md#accessors--consistent) | manual | manual | | Consistency of get/set APIs is a design-review requirement. | +| [24.1 standard-library--isnan](reference/Javascript.md#standard-library--isnan) | javascript | javascript | `no-restricted-globals` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [24.2 standard-library--isfinite](reference/Javascript.md#standard-library--isfinite) | javascript | javascript | `no-restricted-globals` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [25.1 typescript--enum-naming-conventions](reference/Javascript.md#typescript--enum-naming-conventions) | custom | custom | `ag/enum-name` | Enum and member casing is enforced. Singular English names require review; values are unrestricted. | +| [26.1 typescript--tsconfig-strict](reference/Javascript.md#typescript--tsconfig-strict) | compiler | compiler | | Enforced by the shared tsconfig and verified with `ag-oxlint-config --check-tsconfig`. | +| [26.2 typescript--tsconfig-nouncheckedindexedaccess](reference/Javascript.md#typescript--tsconfig-nouncheckedindexedaccess) | compiler | compiler | | Enforced by the shared tsconfig and verified with `ag-oxlint-config --check-tsconfig`. | +| [25.3 typescript--caught-error-type](reference/Javascript.md#typescript--caught-error-type) | custom | custom | `ag/unknown-catch` | Explicit any is rejected on every catch binding; unannotated catches are unknown under the shared strict compiler settings. | ## Resolved rule mappings -| Source rule | Oxlint rule | Provider | Setting | Origin | Type information | -| --- | --- | --- | --- | --- | --- | -| accessor-pairs | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| array-callback-return | ag-compat/array-callback-return | javascript | `["error",{"allowImplicit":true}]` | airbnb@19.0.4/base@15.0.0 | | -| block-scoped-var | ag-compat/block-scoped-var | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| complexity | disabled | disabled | `["off",20]` | airbnb@19.0.4/base@15.0.0 | | -| class-methods-use-this | ag-compat/class-methods-use-this | javascript | `["error",{"exceptMethods":["render","getInitialState","getDefaultProps","getChildContext","componentWillMount","UNSAFE_componentWillMount","componentDidMount","componentWillReceiveProps","UNSAFE_componentWillReceiveProps","shouldComponentUpdate","componentWillUpdate","UNSAFE_componentWillUpdate","componentDidUpdate","componentWillUnmount","componentDidCatch","getSnapshotBeforeUpdate"]}]` | airbnb@19.0.4/base@15.0.0 | | -| consistent-return | ag-compat/consistent-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| curly | ag-compat/curly | javascript | `["error","all"]` | eslintrc.cjs | | -| default-case | ag-compat/default-case | javascript | `["error",{"commentPattern":"^no default$"}]` | airbnb@19.0.4/base@15.0.0 | | -| default-case-last | ag-compat/default-case-last | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| default-param-last | ag-compat/default-param-last | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| dot-notation | ag-compat/dot-notation | javascript | `["error",{"allowKeywords":true}]` | airbnb@19.0.4/base@15.0.0 | | -| dot-location | ag-compat/dot-location | javascript | `["error","property"]` | airbnb@19.0.4/base@15.0.0 | | -| eqeqeq | ag-compat/eqeqeq | javascript | `["error","always",{"null":"ignore"}]` | airbnb@19.0.4/base@15.0.0 | | -| grouped-accessor-pairs | ag-compat/grouped-accessor-pairs | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| guard-for-in | ag-compat/guard-for-in | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| max-classes-per-file | ag-compat/max-classes-per-file | javascript | `["error",1]` | airbnb@19.0.4/base@15.0.0 | | -| no-alert | ag-compat/no-alert | javascript | `"warn"` | airbnb@19.0.4/base@15.0.0 | | -| no-caller | ag-compat/no-caller | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-case-declarations | ag-compat/no-case-declarations | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-constructor-return | ag-compat/no-constructor-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-div-regex | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-else-return | ag-compat/no-else-return | javascript | `["error",{"allowElseIf":false}]` | airbnb@19.0.4/base@15.0.0 | | -| no-empty-function | ag-compat/no-empty-function | javascript | `["error",{"allow":["arrowFunctions","functions","methods"]}]` | airbnb@19.0.4/base@15.0.0 | | -| no-empty-pattern | ag-compat/no-empty-pattern | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-eq-null | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-eval | ag-compat/no-eval | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-extend-native | ag-compat/no-extend-native | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-extra-bind | ag-compat/no-extra-bind | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-extra-label | ag-compat/no-extra-label | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-fallthrough | ag-compat/no-fallthrough | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-floating-decimal | ag-compat/no-floating-decimal | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-global-assign | ag-compat/no-global-assign | javascript | `["error",{"exceptions":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| no-native-reassign | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-implicit-coercion | disabled | disabled | `["off",{"boolean":false,"number":true,"string":true,"allow":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| no-implicit-globals | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-implied-eval | ag-compat/no-implied-eval | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-invalid-this | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-iterator | ag-compat/no-iterator | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-labels | ag-compat/no-labels | javascript | `["error",{"allowLoop":false,"allowSwitch":false}]` | airbnb@19.0.4/base@15.0.0 | | -| no-lone-blocks | ag-compat/no-lone-blocks | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-loop-func | ag-compat/no-loop-func | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-magic-numbers | disabled | disabled | `["off",{"ignore":[],"ignoreArrayIndexes":true,"enforceConst":true,"detectObjects":false}]` | airbnb@19.0.4/base@15.0.0 | | -| no-multi-spaces | ag-compat/no-multi-spaces | javascript | `["error",{"ignoreEOLComments":false}]` | airbnb@19.0.4/base@15.0.0 | | -| no-multi-str | ag-compat/no-multi-str | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-new | ag-compat/no-new | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-new-func | ag-compat/no-new-func | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-new-wrappers | ag-compat/no-new-wrappers | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-nonoctal-decimal-escape | ag-compat/no-nonoctal-decimal-escape | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-octal | ag-compat/no-octal | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-octal-escape | ag-compat/no-octal-escape | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-param-reassign | ag-compat/no-param-reassign | javascript | `["error",{"props":true,"ignorePropertyModificationsFor":["acc","accumulator","e","ctx","context","req","request","res","response","$scope","staticContext"]}]` | airbnb@19.0.4/base@15.0.0 | | -| no-proto | ag-compat/no-proto | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-redeclare | ag-compat/no-redeclare | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-restricted-properties | ag-compat/no-restricted-properties | javascript | `["error",{"object":"arguments","property":"callee","message":"arguments.callee is deprecated"},{"object":"global","property":"isFinite","message":"Please use Number.isFinite instead"},{"object":"self","property":"isFinite","message":"Please use Number.isFinite instead"},{"object":"window","property":"isFinite","message":"Please use Number.isFinite instead"},{"object":"global","property":"isNaN","message":"Please use Number.isNaN instead"},{"object":"self","property":"isNaN","message":"Please use Number.isNaN instead"},{"object":"window","property":"isNaN","message":"Please use Number.isNaN instead"},{"property":"__defineGetter__","message":"Please use Object.defineProperty instead."},{"property":"__defineSetter__","message":"Please use Object.defineProperty instead."},{"object":"Math","property":"pow","message":"Use the exponentiation operator (**) instead."}]` | airbnb@19.0.4/base@15.0.0 | | -| no-return-assign | ag-compat/no-return-assign | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| no-return-await | ag-compat/no-return-await | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-script-url | ag-compat/no-script-url | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-self-assign | ag-compat/no-self-assign | javascript | `["error",{"props":true}]` | airbnb@19.0.4/base@15.0.0 | | -| no-self-compare | ag-compat/no-self-compare | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-sequences | ag-compat/no-sequences | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-throw-literal | ag-compat/no-throw-literal | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-unmodified-loop-condition | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-unused-expressions | ag-compat/no-unused-expressions | javascript | `["error",{"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false}]` | airbnb@19.0.4/base@15.0.0 | | -| no-unused-labels | ag-compat/no-unused-labels | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-useless-call | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-useless-catch | ag-compat/no-useless-catch | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-useless-concat | ag-compat/no-useless-concat | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-useless-escape | ag-compat/no-useless-escape | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-useless-return | ag-compat/no-useless-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-void | ag-compat/no-void | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-warning-comments | disabled | disabled | `["off",{"terms":["todo","fixme","xxx"],"location":"start"}]` | airbnb@19.0.4/base@15.0.0 | | -| no-with | ag-compat/no-with | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| prefer-promise-reject-errors | ag-compat/prefer-promise-reject-errors | javascript | `["error",{"allowEmptyReject":true}]` | airbnb@19.0.4/base@15.0.0 | | -| prefer-named-capture-group | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| prefer-regex-literals | ag-compat/prefer-regex-literals | javascript | `["error",{"disallowRedundantWrapping":true}]` | airbnb@19.0.4/base@15.0.0 | | -| radix | ag-compat/radix | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| require-await | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| require-unicode-regexp | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| vars-on-top | ag-compat/vars-on-top | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| wrap-iife | ag-compat/wrap-iife | javascript | `["error","outside",{"functionPrototypeMethods":false}]` | airbnb@19.0.4/base@15.0.0 | | -| yoda | ag-compat/yoda | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| for-direction | ag-compat/for-direction | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| getter-return | ag-compat/getter-return | javascript | `["error",{"allowImplicit":true}]` | airbnb@19.0.4/base@15.0.0 | | -| no-async-promise-executor | ag-compat/no-async-promise-executor | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-await-in-loop | disabled | disabled | `"off"` | eslintrc.cjs | | -| no-compare-neg-zero | ag-compat/no-compare-neg-zero | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-cond-assign | ag-compat/no-cond-assign | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| no-console | ag-compat/no-console | javascript | `"error"` | eslintrc.cjs | | -| no-constant-condition | ag-compat/no-constant-condition | javascript | `"warn"` | airbnb@19.0.4/base@15.0.0 | | -| no-control-regex | ag-compat/no-control-regex | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-debugger | ag-compat/no-debugger | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-dupe-args | ag-compat/no-dupe-args | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-dupe-else-if | ag-compat/no-dupe-else-if | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-dupe-keys | ag-compat/no-dupe-keys | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-duplicate-case | ag-compat/no-duplicate-case | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-empty | ag-compat/no-empty | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-empty-character-class | ag-compat/no-empty-character-class | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-ex-assign | ag-compat/no-ex-assign | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-extra-boolean-cast | ag-compat/no-extra-boolean-cast | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-extra-parens | disabled | disabled | `["off","all",{"conditionalAssign":true,"nestedBinaryExpressions":false,"returnAssign":false,"ignoreJSX":"all","enforceForArrowConditionals":false}]` | airbnb@19.0.4/base@15.0.0 | | -| no-extra-semi | ag-compat/no-extra-semi | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-func-assign | ag-compat/no-func-assign | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-import-assign | ag-compat/no-import-assign | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-inner-declarations | ag-compat/no-inner-declarations | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-invalid-regexp | ag-compat/no-invalid-regexp | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-irregular-whitespace | ag-compat/no-irregular-whitespace | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-loss-of-precision | ag-compat/no-loss-of-precision | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-misleading-character-class | ag-compat/no-misleading-character-class | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-obj-calls | ag-compat/no-obj-calls | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-promise-executor-return | ag-compat/no-promise-executor-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-prototype-builtins | ag-compat/no-prototype-builtins | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-regex-spaces | ag-compat/no-regex-spaces | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-setter-return | ag-compat/no-setter-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-sparse-arrays | ag-compat/no-sparse-arrays | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-template-curly-in-string | ag-compat/no-template-curly-in-string | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-unexpected-multiline | ag-compat/no-unexpected-multiline | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-unreachable | ag-compat/no-unreachable | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-unreachable-loop | ag-compat/no-unreachable-loop | javascript | `["error",{"ignore":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| no-unsafe-finally | ag-compat/no-unsafe-finally | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-unsafe-negation | ag-compat/no-unsafe-negation | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-unsafe-optional-chaining | ag-compat/no-unsafe-optional-chaining | javascript | `["error",{"disallowArithmeticOperators":true}]` | airbnb@19.0.4/base@15.0.0 | | -| no-unused-private-class-members | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-useless-backreference | ag-compat/no-useless-backreference | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-negated-in-lhs | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| require-atomic-updates | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| use-isnan | ag-compat/use-isnan | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| valid-jsdoc | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| valid-typeof | ag-compat/valid-typeof | javascript | `["error",{"requireStringLiterals":true}]` | airbnb@19.0.4/base@15.0.0 | | -| callback-return | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| global-require | ag-compat/global-require | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| handle-callback-err | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-buffer-constructor | ag-compat/no-buffer-constructor | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-mixed-requires | disabled | disabled | `["off",false]` | airbnb@19.0.4/base@15.0.0 | | -| no-new-require | ag-compat/no-new-require | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-path-concat | ag-compat/no-path-concat | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-process-env | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-process-exit | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-restricted-modules | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-sync | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| array-bracket-newline | disabled | disabled | `["off","consistent"]` | airbnb@19.0.4/base@15.0.0 | | -| array-element-newline | disabled | disabled | `["off",{"multiline":true,"minItems":3}]` | airbnb@19.0.4/base@15.0.0 | | -| array-bracket-spacing | ag-compat/array-bracket-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | | -| block-spacing | ag-compat/block-spacing | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| brace-style | ag-compat/brace-style | javascript | `["error","1tbs",{"allowSingleLine":false}]` | eslintrc.cjs | | -| camelcase | ag-compat/camelcase | javascript | `["error",{"properties":"never","ignoreDestructuring":false}]` | airbnb@19.0.4/base@15.0.0 | | -| capitalized-comments | disabled | disabled | `["off","never",{"line":{"ignorePattern":".*","ignoreInlineComments":true,"ignoreConsecutiveComments":true},"block":{"ignorePattern":".*","ignoreInlineComments":true,"ignoreConsecutiveComments":true}}]` | airbnb@19.0.4/base@15.0.0 | | -| comma-dangle | ag-compat/comma-dangle | javascript | `["error",{"arrays":"always-multiline","objects":"always-multiline","imports":"always-multiline","exports":"always-multiline","functions":"always-multiline"}]` | airbnb@19.0.4/base@15.0.0 | | -| comma-spacing | ag-compat/comma-spacing | javascript | `["error",{"before":false,"after":true}]` | airbnb@19.0.4/base@15.0.0 | | -| comma-style | ag-compat/comma-style | javascript | `["error","last",{"exceptions":{"ArrayExpression":false,"ArrayPattern":false,"ArrowFunctionExpression":false,"CallExpression":false,"FunctionDeclaration":false,"FunctionExpression":false,"ImportDeclaration":false,"ObjectExpression":false,"ObjectPattern":false,"VariableDeclaration":false,"NewExpression":false}}]` | airbnb@19.0.4/base@15.0.0 | | -| computed-property-spacing | ag-compat/computed-property-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | | -| consistent-this | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| eol-last | ag-compat/eol-last | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| function-call-argument-newline | ag-compat/function-call-argument-newline | javascript | `["error","consistent"]` | airbnb@19.0.4/base@15.0.0 | | -| func-call-spacing | ag-compat/func-call-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | | -| func-name-matching | disabled | disabled | `["off","always",{"includeCommonJSModuleExports":false,"considerPropertyDescriptor":true}]` | airbnb@19.0.4/base@15.0.0 | | -| func-names | ag-compat/func-names | javascript | `"warn"` | airbnb@19.0.4/base@15.0.0 | | -| func-style | disabled | disabled | `["off","expression"]` | airbnb@19.0.4/base@15.0.0 | | -| function-paren-newline | ag-compat/function-paren-newline | javascript | `["error","multiline-arguments"]` | airbnb@19.0.4/base@15.0.0 | | -| id-denylist | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| id-length | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| id-match | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| implicit-arrow-linebreak | ag-compat/implicit-arrow-linebreak | javascript | `["error","beside"]` | airbnb@19.0.4/base@15.0.0 | | -| indent | ag-compat/indent | javascript | `["error",4,{"SwitchCase":1}]` | eslintrc.cjs | | -| jsx-quotes | ag-compat/jsx-quotes | javascript | `["error","prefer-double"]` | airbnb@19.0.4/base@15.0.0 | | -| key-spacing | ag-compat/key-spacing | javascript | `["error",{"beforeColon":false,"afterColon":true}]` | airbnb@19.0.4/base@15.0.0 | | -| keyword-spacing | ag-compat/keyword-spacing | javascript | `["error",{"before":true,"after":true,"overrides":{"return":{"after":true},"throw":{"after":true},"case":{"after":true}}}]` | airbnb@19.0.4/base@15.0.0 | | -| line-comment-position | disabled | disabled | `["off",{"position":"above","ignorePattern":"","applyDefaultPatterns":true}]` | airbnb@19.0.4/base@15.0.0 | | -| linebreak-style | ag-compat/linebreak-style | javascript | `["error","unix"]` | airbnb@19.0.4/base@15.0.0 | | -| lines-between-class-members | ag-compat/lines-between-class-members | javascript | `["error","always",{"exceptAfterSingleLine":false}]` | airbnb@19.0.4/base@15.0.0 | | -| lines-around-comment | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| lines-around-directive | ag-compat/lines-around-directive | javascript | `["error",{"before":"always","after":"always"}]` | airbnb@19.0.4/base@15.0.0 | | -| max-depth | disabled | disabled | `["off",4]` | airbnb@19.0.4/base@15.0.0 | | -| max-len | ag-compat/max-len | javascript | `["error",{"code":120,"comments":120,"tabWidth":4,"ignoreUrls":false,"ignoreTrailingComments":false,"ignoreComments":false}]` | eslintrc.cjs | | -| max-lines | disabled | disabled | `["off",{"max":300,"skipBlankLines":true,"skipComments":true}]` | airbnb@19.0.4/base@15.0.0 | | -| max-lines-per-function | disabled | disabled | `["off",{"max":50,"skipBlankLines":true,"skipComments":true,"IIFEs":true}]` | airbnb@19.0.4/base@15.0.0 | | -| max-nested-callbacks | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| max-params | disabled | disabled | `["off",3]` | airbnb@19.0.4/base@15.0.0 | | -| max-statements | disabled | disabled | `["off",10]` | airbnb@19.0.4/base@15.0.0 | | -| max-statements-per-line | disabled | disabled | `["off",{"max":1}]` | airbnb@19.0.4/base@15.0.0 | | -| multiline-comment-style | disabled | disabled | `["off","starred-block"]` | airbnb@19.0.4/base@15.0.0 | | -| multiline-ternary | disabled | disabled | `["off","never"]` | airbnb@19.0.4/base@15.0.0 | | -| new-cap | ag-compat/new-cap | javascript | `["error",{"newIsCap":true,"newIsCapExceptions":[],"capIsNew":false,"capIsNewExceptions":["Immutable.Map","Immutable.Set","Immutable.List"]}]` | airbnb@19.0.4/base@15.0.0 | | -| new-parens | ag-compat/new-parens | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| newline-after-var | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| newline-before-return | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| newline-per-chained-call | ag-compat/newline-per-chained-call | javascript | `["error",{"ignoreChainWithDepth":4}]` | airbnb@19.0.4/base@15.0.0 | | -| no-array-constructor | ag-compat/no-array-constructor | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-bitwise | ag-compat/no-bitwise | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-continue | ag-compat/no-continue | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-inline-comments | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-lonely-if | ag-compat/no-lonely-if | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-mixed-operators | ag-compat/no-mixed-operators | javascript | `["error",{"groups":[["%","**"],["%","+"],["%","-"],["%","*"],["%","/"],["/","*"],["&","\|","<<",">>",">>>"],["==","!=","===","!=="],["&&","\|\|"]],"allowSamePrecedence":false}]` | airbnb@19.0.4/base@15.0.0 | | -| no-mixed-spaces-and-tabs | ag-compat/no-mixed-spaces-and-tabs | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-multi-assign | ag-compat/no-multi-assign | javascript | `["error"]` | airbnb@19.0.4/base@15.0.0 | | -| no-multiple-empty-lines | ag-compat/no-multiple-empty-lines | javascript | `["error",{"max":1,"maxBOF":0,"maxEOF":0}]` | airbnb@19.0.4/base@15.0.0 | | -| no-negated-condition | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-nested-ternary | ag-compat/no-nested-ternary | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-new-object | ag-compat/no-new-object | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-plusplus | ag-compat/no-plusplus | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-restricted-syntax | ag-compat/no-restricted-syntax | javascript | `["error",{"selector":"ForInStatement","message":"for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array."},{"selector":"ForOfStatement","message":"iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations."},{"selector":"LabeledStatement","message":"Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand."},{"selector":"WithStatement","message":"`with` is disallowed in strict mode because it makes code impossible to predict and optimize."}]` | airbnb@19.0.4/base@15.0.0 | | -| no-spaced-func | ag-compat/no-spaced-func | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-tabs | ag-compat/no-tabs | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-ternary | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-trailing-spaces | ag-compat/no-trailing-spaces | javascript | `["error",{"skipBlankLines":false,"ignoreComments":false}]` | airbnb@19.0.4/base@15.0.0 | | -| no-underscore-dangle | ag-compat/no-underscore-dangle | javascript | `["error",{"allow":["__REDUX_DEVTOOLS_EXTENSION_COMPOSE__"],"allowAfterThis":false,"allowAfterSuper":false,"enforceInMethodNames":true}]` | airbnb@19.0.4/base@15.0.0 | | -| no-unneeded-ternary | ag-compat/no-unneeded-ternary | javascript | `["error",{"defaultAssignment":false}]` | airbnb@19.0.4/base@15.0.0 | | -| no-whitespace-before-property | ag-compat/no-whitespace-before-property | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| nonblock-statement-body-position | ag-compat/nonblock-statement-body-position | javascript | `["error","beside",{"overrides":{}}]` | airbnb@19.0.4/base@15.0.0 | | -| object-curly-spacing | ag-compat/object-curly-spacing | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| object-curly-newline | ag-compat/object-curly-newline | javascript | `["error",{"ObjectExpression":{"minProperties":4,"multiline":true,"consistent":true},"ObjectPattern":{"minProperties":4,"multiline":true,"consistent":true},"ImportDeclaration":{"minProperties":4,"multiline":true,"consistent":true},"ExportDeclaration":{"minProperties":4,"multiline":true,"consistent":true}}]` | airbnb@19.0.4/base@15.0.0 | | -| object-property-newline | ag-compat/object-property-newline | javascript | `["error",{"allowAllPropertiesOnSameLine":true}]` | airbnb@19.0.4/base@15.0.0 | | -| one-var | ag-compat/one-var | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | | -| one-var-declaration-per-line | ag-compat/one-var-declaration-per-line | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| operator-assignment | ag-compat/operator-assignment | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| operator-linebreak | ag-compat/operator-linebreak | javascript | `["error","before",{"overrides":{"=":"none"}}]` | airbnb@19.0.4/base@15.0.0 | | -| padded-blocks | ag-compat/padded-blocks | javascript | `["error",{"blocks":"never","classes":"never","switches":"never"},{"allowSingleLineBlocks":true}]` | airbnb@19.0.4/base@15.0.0 | | -| padding-line-between-statements | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| prefer-exponentiation-operator | ag-compat/prefer-exponentiation-operator | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| prefer-object-spread | ag-compat/prefer-object-spread | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| quote-props | ag-compat/quote-props | javascript | `["error","as-needed",{"keywords":false,"unnecessary":true,"numbers":false}]` | airbnb@19.0.4/base@15.0.0 | | -| quotes | ag-compat/quotes | javascript | `["error","single",{"avoidEscape":true}]` | airbnb@19.0.4/base@15.0.0 | | -| require-jsdoc | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| semi | ag-compat/semi | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| semi-spacing | ag-compat/semi-spacing | javascript | `["error",{"before":false,"after":true}]` | airbnb@19.0.4/base@15.0.0 | | -| semi-style | ag-compat/semi-style | javascript | `["error","last"]` | airbnb@19.0.4/base@15.0.0 | | -| sort-keys | disabled | disabled | `["off","asc",{"caseSensitive":false,"natural":true}]` | airbnb@19.0.4/base@15.0.0 | | -| sort-vars | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| space-before-blocks | ag-compat/space-before-blocks | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| space-before-function-paren | ag-compat/space-before-function-paren | javascript | `["error",{"anonymous":"always","named":"never","asyncArrow":"always"}]` | airbnb@19.0.4/base@15.0.0 | | -| space-in-parens | ag-compat/space-in-parens | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | | -| space-infix-ops | ag-compat/space-infix-ops | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| space-unary-ops | ag-compat/space-unary-ops | javascript | `["error",{"words":true,"nonwords":false,"overrides":{}}]` | airbnb@19.0.4/base@15.0.0 | | -| spaced-comment | ag-compat/spaced-comment | javascript | `["error","always",{"line":{"exceptions":["-","+"],"markers":["=","!","/"]},"block":{"exceptions":["-","+"],"markers":["=","!",":","::"],"balanced":true}}]` | airbnb@19.0.4/base@15.0.0 | | -| switch-colon-spacing | ag-compat/switch-colon-spacing | javascript | `["error",{"after":true,"before":false}]` | airbnb@19.0.4/base@15.0.0 | | -| template-tag-spacing | ag-compat/template-tag-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | | -| unicode-bom | ag-compat/unicode-bom | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | | -| wrap-regex | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| init-declarations | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-catch-shadow | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-delete-var | ag-compat/no-delete-var | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-label-var | ag-compat/no-label-var | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-restricted-globals | ag-compat/no-restricted-globals | javascript | `["error",{"name":"isFinite","message":"Use Number.isFinite instead https://github.com/airbnb/javascript#standard-library--isfinite"},{"name":"isNaN","message":"Use Number.isNaN instead https://github.com/airbnb/javascript#standard-library--isnan"},"addEventListener","blur","close","closed","confirm","defaultStatus","defaultstatus","event","external","find","focus","frameElement","frames","history","innerHeight","innerWidth","length","location","locationbar","menubar","moveBy","moveTo","name","onblur","onerror","onfocus","onload","onresize","onunload","open","opener","opera","outerHeight","outerWidth","pageXOffset","pageYOffset","parent","print","removeEventListener","resizeBy","resizeTo","screen","screenLeft","screenTop","screenX","screenY","scroll","scrollbars","scrollBy","scrollTo","scrollX","scrollY","self","status","statusbar","stop","toolbar","top"]` | airbnb@19.0.4/base@15.0.0 | | -| no-shadow | ag-compat/no-shadow | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-shadow-restricted-names | ag-compat/no-shadow-restricted-names | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-undef | ag-compat/no-undef | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-undef-init | ag-compat/no-undef-init | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-undefined | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-unused-vars | ag-compat/no-unused-vars | javascript | `["error",{"vars":"all","args":"after-used","ignoreRestSiblings":true}]` | airbnb@19.0.4/base@15.0.0 | | -| no-use-before-define | ag-compat/no-use-before-define | javascript | `["error",{"functions":true,"classes":true,"variables":true}]` | airbnb@19.0.4/base@15.0.0 | | -| arrow-body-style | ag-compat/arrow-body-style | javascript | `["error","as-needed",{"requireReturnForObjectLiteral":false}]` | airbnb@19.0.4/base@15.0.0 | | -| arrow-parens | ag-compat/arrow-parens | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| arrow-spacing | ag-compat/arrow-spacing | javascript | `["error",{"before":true,"after":true}]` | airbnb@19.0.4/base@15.0.0 | | -| constructor-super | ag-compat/constructor-super | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| generator-star-spacing | ag-compat/generator-star-spacing | javascript | `["error",{"before":false,"after":true}]` | airbnb@19.0.4/base@15.0.0 | | -| no-class-assign | ag-compat/no-class-assign | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-confusing-arrow | ag-compat/no-confusing-arrow | javascript | `["error",{"allowParens":true}]` | airbnb@19.0.4/base@15.0.0 | | -| no-const-assign | ag-compat/no-const-assign | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-dupe-class-members | ag-compat/no-dupe-class-members | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-duplicate-imports | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| no-new-symbol | ag-compat/no-new-symbol | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-restricted-exports | ag-compat/no-restricted-exports | javascript | `["error",{"restrictedNamedExports":["default","then"]}]` | airbnb@19.0.4/base@15.0.0 | | -| no-this-before-super | ag-compat/no-this-before-super | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-useless-computed-key | ag-compat/no-useless-computed-key | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-useless-constructor | ag-compat/no-useless-constructor | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| no-useless-rename | ag-compat/no-useless-rename | javascript | `["error",{"ignoreDestructuring":false,"ignoreImport":false,"ignoreExport":false}]` | airbnb@19.0.4/base@15.0.0 | | -| no-var | ag-compat/no-var | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| object-shorthand | ag-compat/object-shorthand | javascript | `["error","always",{"ignoreConstructors":false,"avoidQuotes":true}]` | airbnb@19.0.4/base@15.0.0 | | -| prefer-arrow-callback | ag-compat/prefer-arrow-callback | javascript | `["error",{"allowNamedFunctions":false,"allowUnboundThis":true}]` | airbnb@19.0.4/base@15.0.0 | | -| prefer-const | ag-compat/prefer-const | javascript | `["error",{"destructuring":"any","ignoreReadBeforeAssign":true}]` | airbnb@19.0.4/base@15.0.0 | | -| prefer-destructuring | ag-compat/prefer-destructuring | javascript | `["error",{"VariableDeclarator":{"array":false,"object":true},"AssignmentExpression":{"array":true,"object":false}},{"enforceForRenamedProperties":false}]` | airbnb@19.0.4/base@15.0.0 | | -| prefer-numeric-literals | ag-compat/prefer-numeric-literals | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| prefer-reflect | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| prefer-rest-params | ag-compat/prefer-rest-params | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| prefer-spread | ag-compat/prefer-spread | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| prefer-template | ag-compat/prefer-template | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| require-yield | ag-compat/require-yield | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| rest-spread-spacing | ag-compat/rest-spread-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | | -| symbol-description | ag-compat/symbol-description | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| template-curly-spacing | ag-compat/template-curly-spacing | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| yield-star-spacing | ag-compat/yield-star-spacing | javascript | `["error","after"]` | airbnb@19.0.4/base@15.0.0 | | -| import/no-unresolved | ag-import/no-unresolved | javascript | `["error",{"commonjs":true,"caseSensitive":true}]` | airbnb@19.0.4/base@15.0.0 | | -| import/named | ag-import/named | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/default | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/namespace | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/export | ag-import/export | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-named-as-default | ag-import/no-named-as-default | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-named-as-default-member | ag-import/no-named-as-default-member | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-deprecated | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-extraneous-dependencies | ag-import/no-extraneous-dependencies | javascript | `["error",{"devDependencies":["test/**","tests/**","spec/**","**/__tests__/**","**/__mocks__/**","test.{js,jsx}","test-*.{js,jsx}","**/*{.,_}{test,spec}.{js,jsx}","**/jest.config.js","**/jest.setup.js","**/vue.config.js","**/webpack.config.js","**/webpack.config.*.js","**/rollup.config.js","**/rollup.config.*.js","**/gulpfile.js","**/gulpfile.*.js","**/Gruntfile{,.js}","**/protractor.conf.js","**/protractor.conf.*.js","**/karma.conf.js","**/.eslintrc.js"],"optionalDependencies":false}]` | airbnb@19.0.4/base@15.0.0 | | -| import/no-mutable-exports | ag-import/no-mutable-exports | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-commonjs | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-amd | ag-import/no-amd | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-nodejs-modules | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/first | ag-import/first | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/imports-first | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-duplicates | ag-import/no-duplicates | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-namespace | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/extensions | ag-import/extensions | javascript | `["error","ignorePackages",{"js":"never","mjs":"never","jsx":"never"}]` | airbnb@19.0.4/base@15.0.0 | | -| import/order | ag-import/order | javascript | `["error",{"groups":[["builtin","external","internal"]]}]` | airbnb@19.0.4/base@15.0.0 | | -| import/newline-after-import | ag-import/newline-after-import | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/prefer-default-export | ag-import/prefer-default-export | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-restricted-paths | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/max-dependencies | disabled | disabled | `["off",{"max":10}]` | airbnb@19.0.4/base@15.0.0 | | -| import/no-absolute-path | ag-import/no-absolute-path | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-dynamic-require | ag-import/no-dynamic-require | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-internal-modules | disabled | disabled | `["off",{"allow":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| import/unambiguous | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-webpack-loader-syntax | ag-import/no-webpack-loader-syntax | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-named-default | ag-import/no-named-default | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-anonymous-default-export | disabled | disabled | `["off",{"allowArray":false,"allowArrowFunction":false,"allowAnonymousClass":false,"allowAnonymousFunction":false,"allowLiteral":false,"allowObject":false}]` | airbnb@19.0.4/base@15.0.0 | | -| import/exports-last | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/group-exports | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-default-export | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-named-export | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-self-import | ag-import/no-self-import | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-cycle | ag-import/no-cycle | javascript | `["error",{}]` | airbnb@19.0.4/base@15.0.0 | | -| import/no-useless-path-segments | ag-import/no-useless-path-segments | javascript | `["error",{"commonjs":true}]` | airbnb@19.0.4/base@15.0.0 | | -| import/dynamic-import-chunkname | disabled | disabled | `["off",{"importFunctions":[],"webpackChunknameFormat":"[0-9a-zA-Z-_/.]+"}]` | airbnb@19.0.4/base@15.0.0 | | -| import/no-relative-parent-imports | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| import/no-unused-modules | disabled | disabled | `["off",{"ignoreExports":[],"missingExports":true,"unusedExports":true}]` | airbnb@19.0.4/base@15.0.0 | | -| import/no-import-module-exports | ag-import/no-import-module-exports | javascript | `["error",{"exceptions":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| import/no-relative-packages | ag-import/no-relative-packages | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| strict | ag-compat/strict | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | | -| react/display-name | disabled | disabled | `["off",{"ignoreTranspilerName":false}]` | airbnb@19.0.4/base@15.0.0 | | -| react/forbid-prop-types | ag-react/forbid-prop-types | javascript | `["error",{"forbid":["any","array","object"],"checkContextTypes":true,"checkChildContextTypes":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/forbid-dom-props | disabled | disabled | `["off",{"forbid":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-boolean-value | ag-react/jsx-boolean-value | javascript | `["error","never",{"always":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-closing-bracket-location | ag-react/jsx-closing-bracket-location | javascript | `["error","line-aligned"]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-closing-tag-location | ag-react/jsx-closing-tag-location | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-curly-spacing | ag-react/jsx-curly-spacing | javascript | `["error","never",{"allowMultiline":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-handler-names | disabled | disabled | `["off",{"eventHandlerPrefix":"handle","eventHandlerPropPrefix":"on"}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-indent-props | ag-react/jsx-indent-props | javascript | `["error",2]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-key | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-max-props-per-line | ag-react/jsx-max-props-per-line | javascript | `["error",{"maximum":1,"when":"multiline"}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-no-bind | ag-react/jsx-no-bind | javascript | `["error",{"ignoreRefs":true,"allowArrowFunctions":true,"allowFunctions":false,"allowBind":false,"ignoreDOMComponents":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-no-duplicate-props | ag-react/jsx-no-duplicate-props | javascript | `["error",{"ignoreCase":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-no-literals | disabled | disabled | `["off",{"noStrings":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-no-undef | ag-react/jsx-no-undef | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-pascal-case | ag-react/jsx-pascal-case | javascript | `["error",{"allowAllCaps":true,"ignore":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| react/sort-prop-types | disabled | disabled | `["off",{"ignoreCase":true,"callbacksLast":false,"requiredFirst":false,"sortShapeProp":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-sort-prop-types | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-sort-props | disabled | disabled | `["off",{"ignoreCase":true,"callbacksLast":false,"shorthandFirst":false,"shorthandLast":false,"noSortAlphabetically":false,"reservedFirst":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-sort-default-props | disabled | disabled | `["off",{"ignoreCase":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-uses-react | ag-react/jsx-uses-react | javascript | `["error"]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-uses-vars | ag-react/jsx-uses-vars | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-danger | ag-react/no-danger | javascript | `"warn"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-deprecated | ag-react/no-deprecated | javascript | `["error"]` | airbnb@19.0.4/base@15.0.0 | | -| react/no-did-mount-set-state | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-did-update-set-state | ag-react/no-did-update-set-state | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-will-update-set-state | ag-react/no-will-update-set-state | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-direct-mutation-state | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-is-mounted | ag-react/no-is-mounted | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-multi-comp | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-set-state | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-string-refs | ag-react/no-string-refs | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-unknown-property | ag-react/no-unknown-property | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/prefer-es6-class | ag-react/prefer-es6-class | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| react/prefer-stateless-function | ag-react/prefer-stateless-function | javascript | `["error",{"ignorePureComponents":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/prop-types | ag-react/prop-types | javascript | `["error",{"ignore":[],"customValidators":[],"skipUndeclared":false}]` | airbnb@19.0.4/base@15.0.0 | | -| react/react-in-jsx-scope | ag-react/react-in-jsx-scope | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/require-render-return | ag-react/require-render-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/self-closing-comp | ag-react/self-closing-comp | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/sort-comp | ag-react/sort-comp | javascript | `["error",{"order":["static-variables","static-methods","instance-variables","lifecycle","/^handle.+$/","/^on.+$/","getters","setters","/^(get\|set)(?!(InitialState$\|DefaultProps$\|ChildContext$)).+$/","instance-methods","everything-else","rendering"],"groups":{"lifecycle":["displayName","propTypes","contextTypes","childContextTypes","mixins","statics","defaultProps","constructor","getDefaultProps","getInitialState","state","getChildContext","getDerivedStateFromProps","componentWillMount","UNSAFE_componentWillMount","componentDidMount","componentWillReceiveProps","UNSAFE_componentWillReceiveProps","shouldComponentUpdate","componentWillUpdate","UNSAFE_componentWillUpdate","getSnapshotBeforeUpdate","componentDidUpdate","componentDidCatch","componentWillUnmount"],"rendering":["/^render.+$/","render"]}}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-wrap-multilines | ag-react/jsx-wrap-multilines | javascript | `["error",{"declaration":"parens-new-line","assignment":"parens-new-line","return":"parens-new-line","arrow":"parens-new-line","condition":"parens-new-line","logical":"parens-new-line","prop":"parens-new-line"}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-first-prop-new-line | ag-react/jsx-first-prop-new-line | javascript | `["error","multiline-multiprop"]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-equals-spacing | ag-react/jsx-equals-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-indent | ag-react/jsx-indent | javascript | `["error",2]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-no-target-blank | ag-react/jsx-no-target-blank | javascript | `["error",{"enforceDynamicLinks":"always"}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-filename-extension | ag-react/jsx-filename-extension | javascript | `["error",{"extensions":[".jsx"]}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-no-comment-textnodes | ag-react/jsx-no-comment-textnodes | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-render-return-value | ag-react/no-render-return-value | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/require-optimization | disabled | disabled | `["off",{"allowDecorators":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| react/no-find-dom-node | ag-react/no-find-dom-node | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/forbid-component-props | disabled | disabled | `["off",{"forbid":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| react/forbid-elements | disabled | disabled | `["off",{"forbid":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| react/no-danger-with-children | ag-react/no-danger-with-children | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-unused-prop-types | ag-react/no-unused-prop-types | javascript | `["error",{"customValidators":[],"skipShapeProps":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/style-prop-object | ag-react/style-prop-object | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-unescaped-entities | ag-react/no-unescaped-entities | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-children-prop | ag-react/no-children-prop | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-tag-spacing | ag-react/jsx-tag-spacing | javascript | `["error",{"closingSlash":"never","beforeSelfClosing":"always","afterOpening":"never","beforeClosing":"never"}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-space-before-closing | disabled | disabled | `["off","always"]` | airbnb@19.0.4/base@15.0.0 | | -| react/no-array-index-key | ag-react/no-array-index-key | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/require-default-props | ag-react/require-default-props | javascript | `["error",{"forbidDefaultForRequired":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/forbid-foreign-prop-types | ag-react/forbid-foreign-prop-types | javascript | `["warn",{"allowInPropTypes":true}]` | airbnb@19.0.4/base@15.0.0 | | -| react/void-dom-elements-no-children | ag-react/void-dom-elements-no-children | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/default-props-match-prop-types | ag-react/default-props-match-prop-types | javascript | `["error",{"allowRequiredDefaults":false}]` | airbnb@19.0.4/base@15.0.0 | | -| react/no-redundant-should-component-update | ag-react/no-redundant-should-component-update | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-unused-state | ag-react/no-unused-state | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/boolean-prop-naming | disabled | disabled | `["off",{"propTypeNames":["bool","mutuallyExclusiveTrueProps"],"rule":"^(is\|has)[A-Z]([A-Za-z0-9]?)+","message":""}]` | airbnb@19.0.4/base@15.0.0 | | -| react/no-typos | ag-react/no-typos | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-curly-brace-presence | ag-react/jsx-curly-brace-presence | javascript | `["error",{"props":"never","children":"never"}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-one-expression-per-line | ag-react/jsx-one-expression-per-line | javascript | `["error",{"allow":"single-child"}]` | airbnb@19.0.4/base@15.0.0 | | -| react/destructuring-assignment | ag-react/destructuring-assignment | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| react/no-access-state-in-setstate | ag-react/no-access-state-in-setstate | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/button-has-type | ag-react/button-has-type | javascript | `["error",{"button":true,"submit":true,"reset":false}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-child-element-spacing | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-this-in-sfc | ag-react/no-this-in-sfc | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-max-depth | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-props-no-multi-spaces | ag-react/jsx-props-no-multi-spaces | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-unsafe | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-fragments | ag-react/jsx-fragments | javascript | `["error","syntax"]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-curly-newline | ag-react/jsx-curly-newline | javascript | `["error",{"multiline":"consistent","singleline":"consistent"}]` | airbnb@19.0.4/base@15.0.0 | | -| react/state-in-constructor | ag-react/state-in-constructor | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | | -| react/static-property-placement | ag-react/static-property-placement | javascript | `["error","property assignment"]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-props-no-spreading | ag-react/jsx-props-no-spreading | javascript | `["error",{"html":"enforce","custom":"enforce","explicitSpread":"ignore","exceptions":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| react/prefer-read-only-props | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-no-script-url | ag-react/jsx-no-script-url | javascript | `["error",[{"name":"Link","props":["to"]}]]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-no-useless-fragment | ag-react/jsx-no-useless-fragment | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-adjacent-inline-elements | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/function-component-definition | ag-react/function-component-definition | javascript | `["error",{"namedComponents":["function-declaration","function-expression"],"unnamedComponents":"function-expression"}]` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-newline | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| react/jsx-no-constructed-context-values | ag-react/jsx-no-constructed-context-values | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-unstable-nested-components | ag-react/no-unstable-nested-components | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-namespace | ag-react/no-namespace | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/prefer-exact-props | ag-react/prefer-exact-props | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-arrow-function-lifecycle | ag-react/no-arrow-function-lifecycle | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-invalid-html-attribute | ag-react/no-invalid-html-attribute | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| react/no-unused-class-component-methods | ag-react/no-unused-class-component-methods | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/accessible-emoji | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/alt-text | ag-a11y/alt-text | javascript | `["error",{"elements":["img","object","area","input[type=\"image\"]"],"img":[],"object":[],"area":[],"input[type=\"image\"]":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/anchor-has-content | ag-a11y/anchor-has-content | javascript | `["error",{"components":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/anchor-is-valid | ag-a11y/anchor-is-valid | javascript | `["error",{"components":["Link"],"specialLink":["to"],"aspects":["noHref","invalidHref","preferButton"]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/aria-activedescendant-has-tabindex | ag-a11y/aria-activedescendant-has-tabindex | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/aria-props | ag-a11y/aria-props | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/aria-proptypes | ag-a11y/aria-proptypes | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/aria-role | ag-a11y/aria-role | javascript | `["error",{"ignoreNonDOM":false}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/aria-unsupported-elements | ag-a11y/aria-unsupported-elements | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/autocomplete-valid | disabled | disabled | `["off",{"inputComponents":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/click-events-have-key-events | ag-a11y/click-events-have-key-events | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/control-has-associated-label | ag-a11y/control-has-associated-label | javascript | `["error",{"labelAttributes":["label"],"controlComponents":[],"ignoreElements":["audio","canvas","embed","input","textarea","tr","video"],"ignoreRoles":["grid","listbox","menu","menubar","radiogroup","row","tablist","toolbar","tree","treegrid"],"depth":5}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/heading-has-content | ag-a11y/heading-has-content | javascript | `["error",{"components":[""]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/html-has-lang | ag-a11y/html-has-lang | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/iframe-has-title | ag-a11y/iframe-has-title | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/img-redundant-alt | ag-a11y/img-redundant-alt | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/interactive-supports-focus | ag-a11y/interactive-supports-focus | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/label-has-associated-control | ag-a11y/label-has-associated-control | javascript | `["error",{"labelComponents":[],"labelAttributes":[],"controlComponents":[],"assert":"both","depth":25}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/lang | ag-a11y/lang | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/media-has-caption | ag-a11y/media-has-caption | javascript | `["error",{"audio":[],"video":[],"track":[]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/mouse-events-have-key-events | ag-a11y/mouse-events-have-key-events | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/no-access-key | ag-a11y/no-access-key | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/no-autofocus | ag-a11y/no-autofocus | javascript | `["error",{"ignoreNonDOM":true}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/no-distracting-elements | ag-a11y/no-distracting-elements | javascript | `["error",{"elements":["marquee","blink"]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/no-interactive-element-to-noninteractive-role | ag-a11y/no-interactive-element-to-noninteractive-role | javascript | `["error",{"tr":["none","presentation"]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/no-noninteractive-element-interactions | ag-a11y/no-noninteractive-element-interactions | javascript | `["error",{"handlers":["onClick","onMouseDown","onMouseUp","onKeyPress","onKeyDown","onKeyUp"]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/no-noninteractive-element-to-interactive-role | ag-a11y/no-noninteractive-element-to-interactive-role | javascript | `["error",{"ul":["listbox","menu","menubar","radiogroup","tablist","tree","treegrid"],"ol":["listbox","menu","menubar","radiogroup","tablist","tree","treegrid"],"li":["menuitem","option","row","tab","treeitem"],"table":["grid"],"td":["gridcell"]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/no-noninteractive-tabindex | ag-a11y/no-noninteractive-tabindex | javascript | `["error",{"tags":[],"roles":["tabpanel"]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/no-onchange | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/no-redundant-roles | ag-a11y/no-redundant-roles | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/no-static-element-interactions | ag-a11y/no-static-element-interactions | javascript | `["error",{"handlers":["onClick","onMouseDown","onMouseUp","onKeyPress","onKeyDown","onKeyUp"]}]` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/role-has-required-aria-props | ag-a11y/role-has-required-aria-props | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/role-supports-aria-props | ag-a11y/role-supports-aria-props | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/scope | ag-a11y/scope | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/tabindex-no-positive | ag-a11y/tabindex-no-positive | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | | -| jsx-a11y/label-has-for | disabled | disabled | `["off",{"components":[],"required":{"every":["nesting","id"]},"allowChildren":false}]` | airbnb@19.0.4/base@15.0.0 | | -| jsdoc/check-access | ag-jsdoc/check-access | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-alignment | ag-jsdoc/check-alignment | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-examples | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-indentation | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-line-alignment | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-param-names | ag-jsdoc/check-param-names | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-property-names | ag-jsdoc/check-property-names | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-syntax | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-tag-names | ag-jsdoc/check-tag-names | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-template-names | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-types | ag-jsdoc/check-types | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/check-values | ag-jsdoc/check-values | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/convert-to-jsdoc-comments | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/empty-tags | ag-jsdoc/empty-tags | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/escape-inline-tags | ag-jsdoc/escape-inline-tags | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/implements-on-classes | ag-jsdoc/implements-on-classes | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/imports-as-dependencies | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/informative-docs | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/lines-before-block | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/match-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/match-name | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/multiline-blocks | ag-jsdoc/multiline-blocks | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/no-bad-blocks | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/no-blank-block-descriptions | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/no-blank-blocks | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/no-defaults | ag-jsdoc/no-defaults | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/no-missing-syntax | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/no-multi-asterisks | ag-jsdoc/no-multi-asterisks | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/no-restricted-syntax | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/no-types | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/no-undefined-types | ag-jsdoc/no-undefined-types | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/no-unnecessary-type-assertion | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/normalize-see-links | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/prefer-import-tag | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/reject-any-type | ag-jsdoc/reject-any-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/reject-function-type | ag-jsdoc/reject-function-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-asterisk-prefix | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-example | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-jsdoc | ag-jsdoc/require-jsdoc | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-next-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-next-type | ag-jsdoc/require-next-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-param | ag-jsdoc/require-param | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-param-description | ag-jsdoc/require-param-description | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-param-name | ag-jsdoc/require-param-name | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-param-type | ag-jsdoc/require-param-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-property | ag-jsdoc/require-property | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-property-description | ag-jsdoc/require-property-description | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-property-name | ag-jsdoc/require-property-name | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-property-type | ag-jsdoc/require-property-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-rejects | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-returns | ag-jsdoc/require-returns | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-returns-check | ag-jsdoc/require-returns-check | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-returns-description | ag-jsdoc/require-returns-description | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-returns-type | ag-jsdoc/require-returns-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-tags | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-template | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-template-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-throws-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-throws-type | ag-jsdoc/require-throws-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-yields | ag-jsdoc/require-yields | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-yields-check | ag-jsdoc/require-yields-check | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-yields-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-yields-type | ag-jsdoc/require-yields-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/tag-lines | ag-jsdoc/tag-lines | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/text-escaping | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/ts-method-signature-style | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/ts-no-empty-object-type | ag-jsdoc/ts-no-empty-object-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/ts-no-unnecessary-template-expression | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/ts-prefer-function-type | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/type-formatting | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | | -| jsdoc/valid-types | ag-jsdoc/valid-types | javascript | `"warn"` | jsdoc@64.3.6/recommended | | -| jsdoc/require-file-overview | ag-jsdoc/require-file-overview | javascript | `"error"` | Javascript.md | | -| jsdoc/require-description | ag-jsdoc/require-description | javascript | `"error"` | Javascript.md | | -| jsdoc/require-description-complete-sentence | ag-jsdoc/require-description-complete-sentence | javascript | `"error"` | Javascript.md | | -| jsdoc/require-hyphen-before-param-description | ag-jsdoc/require-hyphen-before-param-description | javascript | `["error","never"]` | Javascript.md | | -| jsdoc/require-throws | ag-jsdoc/require-throws | javascript | `"error"` | Javascript.md | | -| jsdoc/sort-tags | ag-jsdoc/sort-tags | javascript | `"error"` | Javascript.md | | -| sort-imports | ag-compat/sort-imports | javascript | `["error",{"ignoreCase":true,"ignoreDeclarationSort":true,"ignoreMemberSort":false,"memberSyntaxSortOrder":["none","all","multiple","single"]}]` | Javascript.md | | -| no-restricted-imports | ag-compat/no-restricted-imports | javascript | `["error",{"patterns":[{"group":["**/*-mv2","**/*-mv3"],"message":"Do not import directly from MV2/MV3 implementations. Use the appropriate alias or index file instead."}]}]` | Javascript.md | | -| import/no-unassigned-import | ag-import/no-unassigned-import | javascript | `["error",{"allow":["**/*.pcss"]}]` | Javascript.md | | -| import-newlines/enforce | ag-newlines/enforce | javascript | `["error",{"items":3,"max-len":120}]` | Javascript.md | | -| boundaries/element-types | ag-boundaries/element-types | javascript | `["error",{"default":"allow","rules":[{"from":"test-folder","disallow":["src-index"],"message":"Do not import directly from src/. Use specific submodules like src/utils instead."}]}]` | Javascript.md | | -| notice/notice | disabled | disabled | `"off"` | Javascript.md | | -| @adguard/logger-context/require-logger-context | ag-logger/require-logger-context | javascript | `["error",{"contextModuleName":"ext"}]` | Javascript.md | | -| @typescript-eslint/consistent-type-imports | typescript/consistent-type-imports | native | `["error",{"fixStyle":"inline-type-imports"}]` | Javascript.md | | -| @typescript-eslint/consistent-type-exports | typescript/consistent-type-exports | native | `"error"` | Javascript.md | opt-in required | -| @typescript-eslint/explicit-function-return-type | typescript/explicit-function-return-type | native | `"error"` | Javascript.md | | -| @typescript-eslint/explicit-member-accessibility | typescript/explicit-member-accessibility | native | `["error",{"accessibility":"explicit","overrides":{"accessors":"explicit","constructors":"no-public","methods":"explicit","properties":"off","parameterProperties":"explicit"}}]` | Javascript.md | | -| @typescript-eslint/no-explicit-any | typescript/no-explicit-any | native | `"error"` | Javascript.md | | -| @typescript-eslint/no-var-requires | typescript/no-var-requires | native | `"error"` | Javascript.md | | -| @typescript-eslint/ban-ts-comment | typescript/ban-ts-comment | native | `"error"` | Javascript.md | | -| @typescript-eslint/dot-notation | disabled | disabled | `"off"` | Javascript.md | | -| @typescript-eslint/no-non-null-assertion | disabled | disabled | `"off"` | Javascript.md | | -| unicorn/prefer-node-protocol | unicorn/prefer-node-protocol | native | `"error"` | Javascript.md | | -| unicorn/no-this-assignment | unicorn/no-this-assignment | native | `"error"` | Javascript.md | | -| ag/no-accessors | ag/no-accessors | custom | `"error"` | Javascript.md | | -| ag/no-direct-reexport | ag/no-direct-reexport | custom | `"error"` | Javascript.md | | -| ag/no-prototype-mutation | ag/no-prototype-mutation | custom | `"error"` | Javascript.md | | -| ag/no-default-side-effects | ag/no-default-side-effects | custom | `"error"` | Javascript.md | | -| ag/prefer-array-from-map | ag/prefer-array-from-map | custom | `"error"` | Javascript.md | | -| ag/require-docblock | ag/require-docblock | custom | `"error"` | Javascript.md | | +Scope `base` is always active, `guideline` is added by `policy: 'guideline'`, and `adguard-projects` by `profile: 'adguard-projects'`. The TypeScript column names the implementation used for TypeScript files when it differs from the JavaScript one. + +| Source rule | Oxlint rule | Provider | Setting | Origin | Scope | TypeScript | Type information | +| --- | --- | --- | --- | --- | --- | --- | --- | +| accessor-pairs | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| array-callback-return | ag-compat/array-callback-return | javascript | `["error",{"allowImplicit":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| block-scoped-var | ag-compat/block-scoped-var | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| complexity | disabled | disabled | `["off",20]` | airbnb@19.0.4/base@15.0.0 | base | | | +| class-methods-use-this | ag-compat/class-methods-use-this | javascript | `["error",{"exceptMethods":["render","getInitialState","getDefaultProps","getChildContext","componentWillMount","UNSAFE_componentWillMount","componentDidMount","componentWillReceiveProps","UNSAFE_componentWillReceiveProps","shouldComponentUpdate","componentWillUpdate","UNSAFE_componentWillUpdate","componentDidUpdate","componentWillUnmount","componentDidCatch","getSnapshotBeforeUpdate"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| consistent-return | ag-compat/consistent-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| curly | ag-compat/curly | javascript | `["error","all"]` | eslintrc.cjs | base | | | +| default-case | ag-compat/default-case | javascript | `["error",{"commentPattern":"^no default$"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| default-case-last | ag-compat/default-case-last | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| default-param-last | ag-compat/default-param-last | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | eslint/default-param-last: Treats optional parameters like defaults. | | +| dot-notation | ag-compat/dot-notation | javascript | `["error",{"allowKeywords":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| dot-location | ag-compat/dot-location | javascript | `["error","property"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| eqeqeq | ag-compat/eqeqeq | javascript | `["error","always",{"null":"ignore"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| grouped-accessor-pairs | ag-compat/grouped-accessor-pairs | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| guard-for-in | ag-compat/guard-for-in | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| max-classes-per-file | ag-compat/max-classes-per-file | javascript | `["error",1]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-alert | ag-compat/no-alert | javascript | `"warn"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-caller | ag-compat/no-caller | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-case-declarations | ag-compat/no-case-declarations | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-constructor-return | ag-compat/no-constructor-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-div-regex | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-else-return | ag-compat/no-else-return | javascript | `["error",{"allowElseIf":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-empty-function | ag-compat/no-empty-function | javascript | `["error",{"allow":["arrowFunctions","functions","methods"]}]` | airbnb@19.0.4/base@15.0.0 | base | eslint/no-empty-function: Understands parameter-property constructors and overload signatures. | | +| no-empty-pattern | ag-compat/no-empty-pattern | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-eq-null | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-eval | ag-compat/no-eval | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-extend-native | ag-compat/no-extend-native | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-extra-bind | ag-compat/no-extra-bind | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-extra-label | ag-compat/no-extra-label | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-fallthrough | ag-compat/no-fallthrough | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-floating-decimal | ag-compat/no-floating-decimal | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-global-assign | ag-compat/no-global-assign | javascript | `["error",{"exceptions":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-native-reassign | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-implicit-coercion | disabled | disabled | `["off",{"boolean":false,"number":true,"string":true,"allow":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-implicit-globals | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-implied-eval | ag-compat/no-implied-eval | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-invalid-this | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-iterator | ag-compat/no-iterator | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-labels | ag-compat/no-labels | javascript | `["error",{"allowLoop":false,"allowSwitch":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-lone-blocks | ag-compat/no-lone-blocks | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-loop-func | ag-compat/no-loop-func | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-magic-numbers | disabled | disabled | `["off",{"ignore":[],"ignoreArrayIndexes":true,"enforceConst":true,"detectObjects":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-multi-spaces | ag-compat/no-multi-spaces | javascript | `["error",{"ignoreEOLComments":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-multi-str | ag-compat/no-multi-str | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-new | ag-compat/no-new | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-new-func | ag-compat/no-new-func | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-new-wrappers | ag-compat/no-new-wrappers | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-nonoctal-decimal-escape | ag-compat/no-nonoctal-decimal-escape | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-octal | ag-compat/no-octal | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-octal-escape | ag-compat/no-octal-escape | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-param-reassign | ag-compat/no-param-reassign | javascript | `["error",{"props":true,"ignorePropertyModificationsFor":["acc","accumulator","e","ctx","context","req","request","res","response","$scope","staticContext"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-proto | ag-compat/no-proto | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-redeclare | ag-compat/no-redeclare | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | ag-ts/no-redeclare: Accepts overloads and declaration merges that TypeScript permits. | | +| no-restricted-properties | ag-compat/no-restricted-properties | javascript | `["error",{"object":"arguments","property":"callee","message":"arguments.callee is deprecated"},{"object":"global","property":"isFinite","message":"Please use Number.isFinite instead"},{"object":"self","property":"isFinite","message":"Please use Number.isFinite instead"},{"object":"window","property":"isFinite","message":"Please use Number.isFinite instead"},{"object":"global","property":"isNaN","message":"Please use Number.isNaN instead"},{"object":"self","property":"isNaN","message":"Please use Number.isNaN instead"},{"object":"window","property":"isNaN","message":"Please use Number.isNaN instead"},{"property":"__defineGetter__","message":"Please use Object.defineProperty instead."},{"property":"__defineSetter__","message":"Please use Object.defineProperty instead."},{"object":"Math","property":"pow","message":"Use the exponentiation operator (**) instead."}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-return-assign | ag-compat/no-return-assign | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-return-await | ag-compat/no-return-await | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-script-url | ag-compat/no-script-url | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-self-assign | ag-compat/no-self-assign | javascript | `["error",{"props":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-self-compare | ag-compat/no-self-compare | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-sequences | ag-compat/no-sequences | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-throw-literal | ag-compat/no-throw-literal | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unmodified-loop-condition | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unused-expressions | ag-compat/no-unused-expressions | javascript | `["error",{"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unused-labels | ag-compat/no-unused-labels | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-useless-call | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-useless-catch | ag-compat/no-useless-catch | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-useless-concat | ag-compat/no-useless-concat | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-useless-escape | ag-compat/no-useless-escape | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-useless-return | ag-compat/no-useless-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-void | ag-compat/no-void | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-warning-comments | disabled | disabled | `["off",{"terms":["todo","fixme","xxx"],"location":"start"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-with | ag-compat/no-with | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-promise-reject-errors | ag-compat/prefer-promise-reject-errors | javascript | `["error",{"allowEmptyReject":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-named-capture-group | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-regex-literals | ag-compat/prefer-regex-literals | javascript | `["error",{"disallowRedundantWrapping":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| radix | ag-compat/radix | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| require-await | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| require-unicode-regexp | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| vars-on-top | ag-compat/vars-on-top | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| wrap-iife | ag-compat/wrap-iife | javascript | `["error","outside",{"functionPrototypeMethods":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| yoda | ag-compat/yoda | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| for-direction | ag-compat/for-direction | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| getter-return | ag-compat/getter-return | javascript | `["error",{"allowImplicit":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-async-promise-executor | ag-compat/no-async-promise-executor | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-await-in-loop | disabled | disabled | `"off"` | eslintrc.cjs | base | | | +| no-compare-neg-zero | ag-compat/no-compare-neg-zero | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-cond-assign | ag-compat/no-cond-assign | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-console | ag-compat/no-console | javascript | `"error"` | eslintrc.cjs | base | | | +| no-constant-condition | ag-compat/no-constant-condition | javascript | `"warn"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-control-regex | ag-compat/no-control-regex | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-debugger | ag-compat/no-debugger | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-dupe-args | ag-compat/no-dupe-args | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-dupe-else-if | ag-compat/no-dupe-else-if | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-dupe-keys | ag-compat/no-dupe-keys | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-duplicate-case | ag-compat/no-duplicate-case | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-empty | ag-compat/no-empty | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-empty-character-class | ag-compat/no-empty-character-class | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-ex-assign | ag-compat/no-ex-assign | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-extra-boolean-cast | ag-compat/no-extra-boolean-cast | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-extra-parens | disabled | disabled | `["off","all",{"conditionalAssign":true,"nestedBinaryExpressions":false,"returnAssign":false,"ignoreJSX":"all","enforceForArrowConditionals":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-extra-semi | ag-compat/no-extra-semi | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-func-assign | ag-compat/no-func-assign | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-import-assign | ag-compat/no-import-assign | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-inner-declarations | ag-compat/no-inner-declarations | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-invalid-regexp | ag-compat/no-invalid-regexp | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-irregular-whitespace | ag-compat/no-irregular-whitespace | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-loss-of-precision | ag-compat/no-loss-of-precision | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-misleading-character-class | ag-compat/no-misleading-character-class | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-obj-calls | ag-compat/no-obj-calls | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-promise-executor-return | ag-compat/no-promise-executor-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-prototype-builtins | ag-compat/no-prototype-builtins | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-regex-spaces | ag-compat/no-regex-spaces | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-setter-return | ag-compat/no-setter-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-sparse-arrays | ag-compat/no-sparse-arrays | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-template-curly-in-string | ag-compat/no-template-curly-in-string | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unexpected-multiline | ag-compat/no-unexpected-multiline | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unreachable | ag-compat/no-unreachable | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unreachable-loop | ag-compat/no-unreachable-loop | javascript | `["error",{"ignore":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unsafe-finally | ag-compat/no-unsafe-finally | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unsafe-negation | ag-compat/no-unsafe-negation | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unsafe-optional-chaining | ag-compat/no-unsafe-optional-chaining | javascript | `["error",{"disallowArithmeticOperators":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unused-private-class-members | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-useless-backreference | ag-compat/no-useless-backreference | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-negated-in-lhs | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| require-atomic-updates | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| use-isnan | ag-compat/use-isnan | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| valid-jsdoc | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| valid-typeof | ag-compat/valid-typeof | javascript | `["error",{"requireStringLiterals":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| callback-return | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| global-require | ag-compat/global-require | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| handle-callback-err | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-buffer-constructor | ag-compat/no-buffer-constructor | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-mixed-requires | disabled | disabled | `["off",false]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-new-require | ag-compat/no-new-require | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-path-concat | ag-compat/no-path-concat | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-process-env | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-process-exit | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-restricted-modules | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-sync | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| array-bracket-newline | disabled | disabled | `["off","consistent"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| array-element-newline | disabled | disabled | `["off",{"multiline":true,"minItems":3}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| array-bracket-spacing | ag-compat/array-bracket-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| block-spacing | ag-compat/block-spacing | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| brace-style | ag-compat/brace-style | javascript | `["error","1tbs",{"allowSingleLine":false}]` | eslintrc.cjs | base | | | +| camelcase | ag-compat/camelcase | javascript | `["error",{"properties":"never","ignoreDestructuring":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| capitalized-comments | disabled | disabled | `["off","never",{"line":{"ignorePattern":".*","ignoreInlineComments":true,"ignoreConsecutiveComments":true},"block":{"ignorePattern":".*","ignoreInlineComments":true,"ignoreConsecutiveComments":true}}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| comma-dangle | ag-compat/comma-dangle | javascript | `["error",{"arrays":"always-multiline","objects":"always-multiline","imports":"always-multiline","exports":"always-multiline","functions":"always-multiline"}]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/comma-dangle: Also checks enums, type parameters, and tuples with the array setting, as airbnb-typescript does. | | +| comma-spacing | ag-compat/comma-spacing | javascript | `["error",{"before":false,"after":true}]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/comma-spacing: Checks type arguments and accepts the TSX `` form. | | +| comma-style | ag-compat/comma-style | javascript | `["error","last",{"exceptions":{"ArrayExpression":false,"ArrayPattern":false,"ArrowFunctionExpression":false,"CallExpression":false,"FunctionDeclaration":false,"FunctionExpression":false,"ImportDeclaration":false,"ObjectExpression":false,"ObjectPattern":false,"VariableDeclaration":false,"NewExpression":false}}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| computed-property-spacing | ag-compat/computed-property-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| consistent-this | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| eol-last | ag-compat/eol-last | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| function-call-argument-newline | ag-compat/function-call-argument-newline | javascript | `["error","consistent"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| func-call-spacing | ag-compat/func-call-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| func-name-matching | disabled | disabled | `["off","always",{"includeCommonJSModuleExports":false,"considerPropertyDescriptor":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| func-names | ag-compat/func-names | javascript | `"warn"` | airbnb@19.0.4/base@15.0.0 | base | | | +| func-style | disabled | disabled | `["off","expression"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| function-paren-newline | ag-compat/function-paren-newline | javascript | `["error","multiline-arguments"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| id-denylist | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| id-length | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| id-match | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| implicit-arrow-linebreak | ag-compat/implicit-arrow-linebreak | javascript | `["error","beside"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| indent | ag-compat/indent | javascript | `["error",4,{"SwitchCase":1}]` | eslintrc.cjs | base | ag-style/indent: Indents interfaces, enums, and type annotations. | | +| jsx-quotes | ag-compat/jsx-quotes | javascript | `["error","prefer-double"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| key-spacing | ag-compat/key-spacing | javascript | `["error",{"beforeColon":false,"afterColon":true}]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/key-spacing: Checks interface and type literal members. | | +| keyword-spacing | ag-compat/keyword-spacing | javascript | `["error",{"before":true,"after":true,"overrides":{"return":{"after":true},"throw":{"after":true},"case":{"after":true}}}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| line-comment-position | disabled | disabled | `["off",{"position":"above","ignorePattern":"","applyDefaultPatterns":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| linebreak-style | ag-compat/linebreak-style | javascript | `["error","unix"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| lines-between-class-members | ag-compat/lines-between-class-members | javascript | `["error","always",{"exceptAfterSingleLine":false}]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/lines-between-class-members: Accepts adjacent overload signatures. | | +| lines-around-comment | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | ag-style/lines-around-comment: Treats interface, type literal, enum, and namespace bodies like blocks. | | +| lines-around-directive | ag-compat/lines-around-directive | javascript | `["error",{"before":"always","after":"always"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| max-depth | disabled | disabled | `["off",4]` | airbnb@19.0.4/base@15.0.0 | base | | | +| max-len | ag-compat/max-len | javascript | `["error",{"code":120,"comments":120,"tabWidth":4,"ignoreUrls":false,"ignoreTrailingComments":false,"ignoreComments":false}]` | eslintrc.cjs | base | | | +| max-lines | disabled | disabled | `["off",{"max":300,"skipBlankLines":true,"skipComments":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| max-lines-per-function | disabled | disabled | `["off",{"max":50,"skipBlankLines":true,"skipComments":true,"IIFEs":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| max-nested-callbacks | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| max-params | disabled | disabled | `["off",3]` | airbnb@19.0.4/base@15.0.0 | base | | | +| max-statements | disabled | disabled | `["off",10]` | airbnb@19.0.4/base@15.0.0 | base | | | +| max-statements-per-line | disabled | disabled | `["off",{"max":1}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| multiline-comment-style | disabled | disabled | `["off","starred-block"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| multiline-ternary | disabled | disabled | `["off","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| new-cap | ag-compat/new-cap | javascript | `["error",{"newIsCap":true,"newIsCapExceptions":[],"capIsNew":false,"capIsNewExceptions":["Immutable.Map","Immutable.Set","Immutable.List"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| new-parens | ag-compat/new-parens | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| newline-after-var | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| newline-before-return | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| newline-per-chained-call | ag-compat/newline-per-chained-call | javascript | `["error",{"ignoreChainWithDepth":4}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-array-constructor | ag-compat/no-array-constructor | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | eslint/no-array-constructor: Accepts explicitly typed constructors such as `new Array()`. | | +| no-bitwise | ag-compat/no-bitwise | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-continue | ag-compat/no-continue | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-inline-comments | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-lonely-if | ag-compat/no-lonely-if | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-mixed-operators | ag-compat/no-mixed-operators | javascript | `["error",{"groups":[["%","**"],["%","+"],["%","-"],["%","*"],["%","/"],["/","*"],["&","\|","<<",">>",">>>"],["==","!=","===","!=="],["&&","\|\|"]],"allowSamePrecedence":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-mixed-spaces-and-tabs | ag-compat/no-mixed-spaces-and-tabs | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-multi-assign | ag-compat/no-multi-assign | javascript | `["error"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-multiple-empty-lines | ag-compat/no-multiple-empty-lines | javascript | `["error",{"max":1,"maxBOF":0,"maxEOF":0}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-negated-condition | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-nested-ternary | ag-compat/no-nested-ternary | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-new-object | ag-compat/no-new-object | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-plusplus | ag-compat/no-plusplus | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-restricted-syntax | ag-compat/no-restricted-syntax | javascript | `["error",{"selector":"ForInStatement","message":"for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array."},{"selector":"ForOfStatement","message":"iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations."},{"selector":"LabeledStatement","message":"Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand."},{"selector":"WithStatement","message":"`with` is disallowed in strict mode because it makes code impossible to predict and optimize."}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-spaced-func | ag-compat/no-spaced-func | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-tabs | ag-compat/no-tabs | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-ternary | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-trailing-spaces | ag-compat/no-trailing-spaces | javascript | `["error",{"skipBlankLines":false,"ignoreComments":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-underscore-dangle | ag-compat/no-underscore-dangle | javascript | `["error",{"allow":["__REDUX_DEVTOOLS_EXTENSION_COMPOSE__"],"allowAfterThis":false,"allowAfterSuper":false,"enforceInMethodNames":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unneeded-ternary | ag-compat/no-unneeded-ternary | javascript | `["error",{"defaultAssignment":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-whitespace-before-property | ag-compat/no-whitespace-before-property | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| nonblock-statement-body-position | ag-compat/nonblock-statement-body-position | javascript | `["error","beside",{"overrides":{}}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| object-curly-spacing | ag-compat/object-curly-spacing | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/object-curly-spacing: Checks type literals. | | +| object-curly-newline | ag-compat/object-curly-newline | javascript | `["error",{"ObjectExpression":{"minProperties":4,"multiline":true,"consistent":true},"ObjectPattern":{"minProperties":4,"multiline":true,"consistent":true},"ImportDeclaration":{"minProperties":4,"multiline":true,"consistent":true},"ExportDeclaration":{"minProperties":4,"multiline":true,"consistent":true}}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| object-property-newline | ag-compat/object-property-newline | javascript | `["error",{"allowAllPropertiesOnSameLine":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| one-var | ag-compat/one-var | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| one-var-declaration-per-line | ag-compat/one-var-declaration-per-line | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| operator-assignment | ag-compat/operator-assignment | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| operator-linebreak | ag-compat/operator-linebreak | javascript | `["error","before",{"overrides":{"=":"none"}}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| padded-blocks | ag-compat/padded-blocks | javascript | `["error",{"blocks":"never","classes":"never","switches":"never"},{"allowSingleLineBlocks":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| padding-line-between-statements | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-exponentiation-operator | ag-compat/prefer-exponentiation-operator | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-object-spread | ag-compat/prefer-object-spread | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| quote-props | ag-compat/quote-props | javascript | `["error","as-needed",{"keywords":false,"unnecessary":true,"numbers":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| quotes | ag-compat/quotes | javascript | `["error","single",{"avoidEscape":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| require-jsdoc | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| semi | ag-compat/semi | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/semi: Checks type aliases, ambient declarations, and abstract members. | | +| semi-spacing | ag-compat/semi-spacing | javascript | `["error",{"before":false,"after":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| semi-style | ag-compat/semi-style | javascript | `["error","last"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| sort-keys | disabled | disabled | `["off","asc",{"caseSensitive":false,"natural":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| sort-vars | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| space-before-blocks | ag-compat/space-before-blocks | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | ag-style/space-before-blocks: Checks interface, enum, and module bodies. | | +| space-before-function-paren | ag-compat/space-before-function-paren | javascript | `["error",{"anonymous":"always","named":"never","asyncArrow":"always"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| space-in-parens | ag-compat/space-in-parens | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| space-infix-ops | ag-compat/space-infix-ops | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | ag-style/space-infix-ops: Checks union and intersection types and enum initializers. | | +| space-unary-ops | ag-compat/space-unary-ops | javascript | `["error",{"words":true,"nonwords":false,"overrides":{}}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| spaced-comment | ag-compat/spaced-comment | javascript | `["error","always",{"line":{"exceptions":["-","+"],"markers":["=","!","/"]},"block":{"exceptions":["-","+"],"markers":["=","!",":","::"],"balanced":true}}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| switch-colon-spacing | ag-compat/switch-colon-spacing | javascript | `["error",{"after":true,"before":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| template-tag-spacing | ag-compat/template-tag-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| unicode-bom | ag-compat/unicode-bom | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| wrap-regex | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| init-declarations | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-catch-shadow | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-delete-var | ag-compat/no-delete-var | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-label-var | ag-compat/no-label-var | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-restricted-globals | ag-compat/no-restricted-globals | javascript | `["error",{"name":"isFinite","message":"Use Number.isFinite instead https://github.com/airbnb/javascript#standard-library--isfinite"},{"name":"isNaN","message":"Use Number.isNaN instead https://github.com/airbnb/javascript#standard-library--isnan"},"addEventListener","blur","close","closed","confirm","defaultStatus","defaultstatus","event","external","find","focus","frameElement","frames","history","innerHeight","innerWidth","length","location","locationbar","menubar","moveBy","moveTo","name","onblur","onerror","onfocus","onload","onresize","onunload","open","opener","opera","outerHeight","outerWidth","pageXOffset","pageYOffset","parent","print","removeEventListener","resizeBy","resizeTo","screen","screenLeft","screenTop","screenX","screenY","scroll","scrollbars","scrollBy","scrollTo","scrollX","scrollY","self","status","statusbar","stop","toolbar","top"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-shadow | ag-compat/no-shadow | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | eslint/no-shadow: Ignores type-space parameters in signatures. | | +| no-shadow-restricted-names | ag-compat/no-shadow-restricted-names | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-undef | ag-compat/no-undef | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | disabled: The TypeScript compiler reports unresolved names, including ambient types. | | +| no-undef-init | ag-compat/no-undef-init | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-undefined | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-unused-vars | ag-compat/no-unused-vars | javascript | `["error",{"vars":"all","args":"after-used","ignoreRestSiblings":true}]` | airbnb@19.0.4/base@15.0.0 | base | eslint/no-unused-vars: Understands type-only usage, signatures, and parameter properties. | | +| no-use-before-define | ag-compat/no-use-before-define | javascript | `["error",{"functions":true,"classes":true,"variables":true}]` | airbnb@19.0.4/base@15.0.0 | base | eslint/no-use-before-define: Ignores type-only references such as `typeof` queries. | | +| arrow-body-style | ag-compat/arrow-body-style | javascript | `["error","as-needed",{"requireReturnForObjectLiteral":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| arrow-parens | ag-compat/arrow-parens | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| arrow-spacing | ag-compat/arrow-spacing | javascript | `["error",{"before":true,"after":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| constructor-super | ag-compat/constructor-super | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| generator-star-spacing | ag-compat/generator-star-spacing | javascript | `["error",{"before":false,"after":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-class-assign | ag-compat/no-class-assign | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-confusing-arrow | ag-compat/no-confusing-arrow | javascript | `["error",{"allowParens":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-const-assign | ag-compat/no-const-assign | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-dupe-class-members | ag-compat/no-dupe-class-members | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | eslint/no-dupe-class-members: Accepts method overload signatures. | | +| no-duplicate-imports | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-new-symbol | ag-compat/no-new-symbol | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-restricted-exports | ag-compat/no-restricted-exports | javascript | `["error",{"restrictedNamedExports":["default","then"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-restricted-imports | disabled | disabled | `["off",{"paths":[],"patterns":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-this-before-super | ag-compat/no-this-before-super | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-useless-computed-key | ag-compat/no-useless-computed-key | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-useless-constructor | ag-compat/no-useless-constructor | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | eslint/no-useless-constructor: Keeps constructors that declare parameter properties or accessibility. | | +| no-useless-rename | ag-compat/no-useless-rename | javascript | `["error",{"ignoreDestructuring":false,"ignoreImport":false,"ignoreExport":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| no-var | ag-compat/no-var | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| object-shorthand | ag-compat/object-shorthand | javascript | `["error","always",{"ignoreConstructors":false,"avoidQuotes":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-arrow-callback | ag-compat/prefer-arrow-callback | javascript | `["error",{"allowNamedFunctions":false,"allowUnboundThis":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-const | ag-compat/prefer-const | javascript | `["error",{"destructuring":"any","ignoreReadBeforeAssign":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-destructuring | ag-compat/prefer-destructuring | javascript | `["error",{"VariableDeclarator":{"array":false,"object":true},"AssignmentExpression":{"array":true,"object":false}},{"enforceForRenamedProperties":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-numeric-literals | ag-compat/prefer-numeric-literals | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-reflect | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-rest-params | ag-compat/prefer-rest-params | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-spread | ag-compat/prefer-spread | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| prefer-template | ag-compat/prefer-template | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| require-yield | ag-compat/require-yield | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| rest-spread-spacing | ag-compat/rest-spread-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| symbol-description | ag-compat/symbol-description | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| template-curly-spacing | ag-compat/template-curly-spacing | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| yield-star-spacing | ag-compat/yield-star-spacing | javascript | `["error","after"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-unresolved | ag-import/no-unresolved | javascript | `["error",{"commonjs":true,"caseSensitive":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/named | ag-import/named | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/default | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/namespace | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/export | ag-import/export | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-named-as-default | ag-import/no-named-as-default | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-named-as-default-member | ag-import/no-named-as-default-member | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-deprecated | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-extraneous-dependencies | ag-import/no-extraneous-dependencies | javascript | `["error",{"devDependencies":["test/**","tests/**","spec/**","**/__tests__/**","**/__mocks__/**","test.{js,jsx}","test-*.{js,jsx}","**/*{.,_}{test,spec}.{js,jsx}","**/jest.config.js","**/jest.setup.js","**/vue.config.js","**/webpack.config.js","**/webpack.config.*.js","**/rollup.config.js","**/rollup.config.*.js","**/gulpfile.js","**/gulpfile.*.js","**/Gruntfile{,.js}","**/protractor.conf.js","**/protractor.conf.*.js","**/karma.conf.js","**/.eslintrc.js"],"optionalDependencies":false}]` | airbnb@19.0.4/base@15.0.0 | base | ag-import/no-extraneous-dependencies: Adds TypeScript variants of the development file globs, as airbnb-typescript does. | | +| import/no-mutable-exports | ag-import/no-mutable-exports | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-commonjs | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-amd | ag-import/no-amd | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-nodejs-modules | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/first | ag-import/first | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/imports-first | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-duplicates | ag-import/no-duplicates | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-namespace | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/extensions | ag-import/extensions | javascript | `["error","ignorePackages",{"js":"never","mjs":"never","jsx":"never"}]` | airbnb@19.0.4/base@15.0.0 | base | ag-import/extensions: TypeScript module specifiers omit TypeScript extensions as well. | | +| import/order | ag-import/order | javascript | `["error",{"groups":[["builtin","external","internal"]]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/newline-after-import | ag-import/newline-after-import | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/prefer-default-export | ag-import/prefer-default-export | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-restricted-paths | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/max-dependencies | disabled | disabled | `["off",{"max":10}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-absolute-path | ag-import/no-absolute-path | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-dynamic-require | ag-import/no-dynamic-require | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-internal-modules | disabled | disabled | `["off",{"allow":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/unambiguous | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-webpack-loader-syntax | ag-import/no-webpack-loader-syntax | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-unassigned-import | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-named-default | ag-import/no-named-default | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-anonymous-default-export | disabled | disabled | `["off",{"allowArray":false,"allowArrowFunction":false,"allowAnonymousClass":false,"allowAnonymousFunction":false,"allowLiteral":false,"allowObject":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/exports-last | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/group-exports | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-default-export | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-named-export | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-self-import | ag-import/no-self-import | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-cycle | ag-import/no-cycle | javascript | `["error",{}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-useless-path-segments | ag-import/no-useless-path-segments | javascript | `["error",{"commonjs":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/dynamic-import-chunkname | disabled | disabled | `["off",{"importFunctions":[],"webpackChunknameFormat":"[0-9a-zA-Z-_/.]+"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-relative-parent-imports | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-unused-modules | disabled | disabled | `["off",{"ignoreExports":[],"missingExports":true,"unusedExports":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-import-module-exports | ag-import/no-import-module-exports | javascript | `["error",{"exceptions":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| import/no-relative-packages | ag-import/no-relative-packages | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| strict | ag-compat/strict | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/display-name | disabled | disabled | `["off",{"ignoreTranspilerName":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/forbid-prop-types | ag-react/forbid-prop-types | javascript | `["error",{"forbid":["any","array","object"],"checkContextTypes":true,"checkChildContextTypes":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/forbid-dom-props | disabled | disabled | `["off",{"forbid":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-boolean-value | ag-react/jsx-boolean-value | javascript | `["error","never",{"always":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-closing-bracket-location | ag-react/jsx-closing-bracket-location | javascript | `["error","line-aligned"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-closing-tag-location | ag-react/jsx-closing-tag-location | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-curly-spacing | ag-react/jsx-curly-spacing | javascript | `["error","never",{"allowMultiline":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-handler-names | disabled | disabled | `["off",{"eventHandlerPrefix":"handle","eventHandlerPropPrefix":"on"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-indent-props | ag-react/jsx-indent-props | javascript | `["error",2]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-key | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-max-props-per-line | ag-react/jsx-max-props-per-line | javascript | `["error",{"maximum":1,"when":"multiline"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-no-bind | ag-react/jsx-no-bind | javascript | `["error",{"ignoreRefs":true,"allowArrowFunctions":true,"allowFunctions":false,"allowBind":false,"ignoreDOMComponents":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-no-duplicate-props | ag-react/jsx-no-duplicate-props | javascript | `["error",{"ignoreCase":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-no-literals | disabled | disabled | `["off",{"noStrings":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-no-undef | ag-react/jsx-no-undef | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-pascal-case | ag-react/jsx-pascal-case | javascript | `["error",{"allowAllCaps":true,"ignore":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/sort-prop-types | disabled | disabled | `["off",{"ignoreCase":true,"callbacksLast":false,"requiredFirst":false,"sortShapeProp":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-sort-prop-types | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-sort-props | disabled | disabled | `["off",{"ignoreCase":true,"callbacksLast":false,"shorthandFirst":false,"shorthandLast":false,"noSortAlphabetically":false,"reservedFirst":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-sort-default-props | disabled | disabled | `["off",{"ignoreCase":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-uses-react | ag-react/jsx-uses-react | javascript | `["error"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-uses-vars | ag-react/jsx-uses-vars | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-danger | ag-react/no-danger | javascript | `"warn"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-deprecated | ag-react/no-deprecated | javascript | `["error"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-did-mount-set-state | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-did-update-set-state | ag-react/no-did-update-set-state | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-will-update-set-state | ag-react/no-will-update-set-state | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-direct-mutation-state | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-is-mounted | ag-react/no-is-mounted | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-multi-comp | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-set-state | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-string-refs | ag-react/no-string-refs | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-unknown-property | ag-react/no-unknown-property | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/prefer-es6-class | ag-react/prefer-es6-class | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/prefer-stateless-function | ag-react/prefer-stateless-function | javascript | `["error",{"ignorePureComponents":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/prop-types | ag-react/prop-types | javascript | `["error",{"ignore":[],"customValidators":[],"skipUndeclared":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/react-in-jsx-scope | ag-react/react-in-jsx-scope | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/require-render-return | ag-react/require-render-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/self-closing-comp | ag-react/self-closing-comp | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/sort-comp | ag-react/sort-comp | javascript | `["error",{"order":["static-variables","static-methods","instance-variables","lifecycle","/^handle.+$/","/^on.+$/","getters","setters","/^(get\|set)(?!(InitialState$\|DefaultProps$\|ChildContext$)).+$/","instance-methods","everything-else","rendering"],"groups":{"lifecycle":["displayName","propTypes","contextTypes","childContextTypes","mixins","statics","defaultProps","constructor","getDefaultProps","getInitialState","state","getChildContext","getDerivedStateFromProps","componentWillMount","UNSAFE_componentWillMount","componentDidMount","componentWillReceiveProps","UNSAFE_componentWillReceiveProps","shouldComponentUpdate","componentWillUpdate","UNSAFE_componentWillUpdate","getSnapshotBeforeUpdate","componentDidUpdate","componentDidCatch","componentWillUnmount"],"rendering":["/^render.+$/","render"]}}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-wrap-multilines | ag-react/jsx-wrap-multilines | javascript | `["error",{"declaration":"parens-new-line","assignment":"parens-new-line","return":"parens-new-line","arrow":"parens-new-line","condition":"parens-new-line","logical":"parens-new-line","prop":"parens-new-line"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-first-prop-new-line | ag-react/jsx-first-prop-new-line | javascript | `["error","multiline-multiprop"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-equals-spacing | ag-react/jsx-equals-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-indent | ag-react/jsx-indent | javascript | `["error",2]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-no-target-blank | ag-react/jsx-no-target-blank | javascript | `["error",{"enforceDynamicLinks":"always"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-filename-extension | ag-react/jsx-filename-extension | javascript | `["error",{"extensions":[".jsx"]}]` | airbnb@19.0.4/base@15.0.0 | base | ag-react/jsx-filename-extension: Allows JSX in .tsx files. | | +| react/jsx-no-comment-textnodes | ag-react/jsx-no-comment-textnodes | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-render-return-value | ag-react/no-render-return-value | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/require-optimization | disabled | disabled | `["off",{"allowDecorators":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-find-dom-node | ag-react/no-find-dom-node | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/forbid-component-props | disabled | disabled | `["off",{"forbid":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/forbid-elements | disabled | disabled | `["off",{"forbid":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-danger-with-children | ag-react/no-danger-with-children | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-unused-prop-types | ag-react/no-unused-prop-types | javascript | `["error",{"customValidators":[],"skipShapeProps":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/style-prop-object | ag-react/style-prop-object | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-unescaped-entities | ag-react/no-unescaped-entities | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-children-prop | ag-react/no-children-prop | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-tag-spacing | ag-react/jsx-tag-spacing | javascript | `["error",{"closingSlash":"never","beforeSelfClosing":"always","afterOpening":"never","beforeClosing":"never"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-space-before-closing | disabled | disabled | `["off","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-array-index-key | ag-react/no-array-index-key | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/require-default-props | ag-react/require-default-props | javascript | `["error",{"forbidDefaultForRequired":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/forbid-foreign-prop-types | ag-react/forbid-foreign-prop-types | javascript | `["warn",{"allowInPropTypes":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/void-dom-elements-no-children | ag-react/void-dom-elements-no-children | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/default-props-match-prop-types | ag-react/default-props-match-prop-types | javascript | `["error",{"allowRequiredDefaults":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-redundant-should-component-update | ag-react/no-redundant-should-component-update | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-unused-state | ag-react/no-unused-state | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/boolean-prop-naming | disabled | disabled | `["off",{"propTypeNames":["bool","mutuallyExclusiveTrueProps"],"rule":"^(is\|has)[A-Z]([A-Za-z0-9]?)+","message":""}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-typos | ag-react/no-typos | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-curly-brace-presence | ag-react/jsx-curly-brace-presence | javascript | `["error",{"props":"never","children":"never"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-one-expression-per-line | ag-react/jsx-one-expression-per-line | javascript | `["error",{"allow":"single-child"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/destructuring-assignment | ag-react/destructuring-assignment | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-access-state-in-setstate | ag-react/no-access-state-in-setstate | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/button-has-type | ag-react/button-has-type | javascript | `["error",{"button":true,"submit":true,"reset":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-child-element-spacing | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-this-in-sfc | ag-react/no-this-in-sfc | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-max-depth | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-props-no-multi-spaces | ag-react/jsx-props-no-multi-spaces | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-unsafe | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-fragments | ag-react/jsx-fragments | javascript | `["error","syntax"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-curly-newline | ag-react/jsx-curly-newline | javascript | `["error",{"multiline":"consistent","singleline":"consistent"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/state-in-constructor | ag-react/state-in-constructor | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/static-property-placement | ag-react/static-property-placement | javascript | `["error","property assignment"]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-props-no-spreading | ag-react/jsx-props-no-spreading | javascript | `["error",{"html":"enforce","custom":"enforce","explicitSpread":"ignore","exceptions":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/prefer-read-only-props | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-no-script-url | ag-react/jsx-no-script-url | javascript | `["error",[{"name":"Link","props":["to"]}]]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-no-useless-fragment | ag-react/jsx-no-useless-fragment | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-adjacent-inline-elements | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/function-component-definition | ag-react/function-component-definition | javascript | `["error",{"namedComponents":["function-declaration","function-expression"],"unnamedComponents":"function-expression"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-newline | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/jsx-no-constructed-context-values | ag-react/jsx-no-constructed-context-values | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-unstable-nested-components | ag-react/no-unstable-nested-components | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-namespace | ag-react/no-namespace | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/prefer-exact-props | ag-react/prefer-exact-props | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-arrow-function-lifecycle | ag-react/no-arrow-function-lifecycle | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-invalid-html-attribute | ag-react/no-invalid-html-attribute | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| react/no-unused-class-component-methods | ag-react/no-unused-class-component-methods | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/accessible-emoji | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/alt-text | ag-a11y/alt-text | javascript | `["error",{"elements":["img","object","area","input[type=\"image\"]"],"img":[],"object":[],"area":[],"input[type=\"image\"]":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/anchor-has-content | ag-a11y/anchor-has-content | javascript | `["error",{"components":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/anchor-is-valid | ag-a11y/anchor-is-valid | javascript | `["error",{"components":["Link"],"specialLink":["to"],"aspects":["noHref","invalidHref","preferButton"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/aria-activedescendant-has-tabindex | ag-a11y/aria-activedescendant-has-tabindex | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/aria-props | ag-a11y/aria-props | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/aria-proptypes | ag-a11y/aria-proptypes | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/aria-role | ag-a11y/aria-role | javascript | `["error",{"ignoreNonDOM":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/aria-unsupported-elements | ag-a11y/aria-unsupported-elements | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/autocomplete-valid | disabled | disabled | `["off",{"inputComponents":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/click-events-have-key-events | ag-a11y/click-events-have-key-events | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/control-has-associated-label | ag-a11y/control-has-associated-label | javascript | `["error",{"labelAttributes":["label"],"controlComponents":[],"ignoreElements":["audio","canvas","embed","input","textarea","tr","video"],"ignoreRoles":["grid","listbox","menu","menubar","radiogroup","row","tablist","toolbar","tree","treegrid"],"depth":5}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/heading-has-content | ag-a11y/heading-has-content | javascript | `["error",{"components":[""]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/html-has-lang | ag-a11y/html-has-lang | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/iframe-has-title | ag-a11y/iframe-has-title | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/img-redundant-alt | ag-a11y/img-redundant-alt | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/interactive-supports-focus | ag-a11y/interactive-supports-focus | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/label-has-associated-control | ag-a11y/label-has-associated-control | javascript | `["error",{"labelComponents":[],"labelAttributes":[],"controlComponents":[],"assert":"both","depth":25}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/lang | ag-a11y/lang | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/media-has-caption | ag-a11y/media-has-caption | javascript | `["error",{"audio":[],"video":[],"track":[]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/mouse-events-have-key-events | ag-a11y/mouse-events-have-key-events | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/no-access-key | ag-a11y/no-access-key | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/no-autofocus | ag-a11y/no-autofocus | javascript | `["error",{"ignoreNonDOM":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/no-distracting-elements | ag-a11y/no-distracting-elements | javascript | `["error",{"elements":["marquee","blink"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/no-interactive-element-to-noninteractive-role | ag-a11y/no-interactive-element-to-noninteractive-role | javascript | `["error",{"tr":["none","presentation"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/no-noninteractive-element-interactions | ag-a11y/no-noninteractive-element-interactions | javascript | `["error",{"handlers":["onClick","onMouseDown","onMouseUp","onKeyPress","onKeyDown","onKeyUp"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/no-noninteractive-element-to-interactive-role | ag-a11y/no-noninteractive-element-to-interactive-role | javascript | `["error",{"ul":["listbox","menu","menubar","radiogroup","tablist","tree","treegrid"],"ol":["listbox","menu","menubar","radiogroup","tablist","tree","treegrid"],"li":["menuitem","option","row","tab","treeitem"],"table":["grid"],"td":["gridcell"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/no-noninteractive-tabindex | ag-a11y/no-noninteractive-tabindex | javascript | `["error",{"tags":[],"roles":["tabpanel"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/no-onchange | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/no-redundant-roles | ag-a11y/no-redundant-roles | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/no-static-element-interactions | ag-a11y/no-static-element-interactions | javascript | `["error",{"handlers":["onClick","onMouseDown","onMouseUp","onKeyPress","onKeyDown","onKeyUp"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/role-has-required-aria-props | ag-a11y/role-has-required-aria-props | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/role-supports-aria-props | ag-a11y/role-supports-aria-props | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/scope | ag-a11y/scope | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/tabindex-no-positive | ag-a11y/tabindex-no-positive | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsx-a11y/label-has-for | disabled | disabled | `["off",{"components":[],"required":{"every":["nesting","id"]},"allowChildren":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| jsdoc/check-access | ag-jsdoc/check-access | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/check-alignment | ag-jsdoc/check-alignment | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/check-examples | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/check-indentation | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/check-line-alignment | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/check-param-names | ag-jsdoc/check-param-names | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/check-property-names | ag-jsdoc/check-property-names | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/check-syntax | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/check-tag-names | ag-jsdoc/check-tag-names | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | ag-jsdoc/check-tag-names: Uses TypeScript tag semantics. | | +| jsdoc/check-template-names | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/check-types | ag-jsdoc/check-types | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/check-values | ag-jsdoc/check-values | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/convert-to-jsdoc-comments | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/empty-tags | ag-jsdoc/empty-tags | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/escape-inline-tags | ag-jsdoc/escape-inline-tags | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/implements-on-classes | ag-jsdoc/implements-on-classes | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/imports-as-dependencies | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/informative-docs | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/lines-before-block | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/match-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/match-name | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/multiline-blocks | ag-jsdoc/multiline-blocks | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/no-bad-blocks | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/no-blank-block-descriptions | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/no-blank-blocks | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/no-defaults | ag-jsdoc/no-defaults | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/no-missing-syntax | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/no-multi-asterisks | ag-jsdoc/no-multi-asterisks | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/no-restricted-syntax | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/no-types | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | ag-jsdoc/no-types: Types come from TypeScript annotations rather than duplicated JSDoc types. | | +| jsdoc/no-undefined-types | ag-jsdoc/no-undefined-types | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | disabled: Types come from TypeScript annotations. | | +| jsdoc/no-unnecessary-type-assertion | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/normalize-see-links | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/prefer-import-tag | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/reject-any-type | ag-jsdoc/reject-any-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/reject-function-type | ag-jsdoc/reject-function-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-asterisk-prefix | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-description-complete-sentence | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-example | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-hyphen-before-param-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-jsdoc | ag-jsdoc/require-jsdoc | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-next-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-next-type | ag-jsdoc/require-next-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | disabled: Types come from TypeScript annotations. | | +| jsdoc/require-param | ag-jsdoc/require-param | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-param-description | ag-jsdoc/require-param-description | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-param-name | ag-jsdoc/require-param-name | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-param-type | ag-jsdoc/require-param-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | disabled: Types come from TypeScript annotations. | | +| jsdoc/require-property | ag-jsdoc/require-property | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-property-description | ag-jsdoc/require-property-description | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-property-name | ag-jsdoc/require-property-name | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-property-type | ag-jsdoc/require-property-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | disabled: Types come from TypeScript annotations. | | +| jsdoc/require-rejects | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-returns | ag-jsdoc/require-returns | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-returns-check | ag-jsdoc/require-returns-check | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-returns-description | ag-jsdoc/require-returns-description | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-returns-type | ag-jsdoc/require-returns-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | disabled: Types come from TypeScript annotations. | | +| jsdoc/require-tags | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-template | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-template-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-throws | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-throws-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-throws-type | ag-jsdoc/require-throws-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | disabled: Types come from TypeScript annotations. | | +| jsdoc/require-yields | ag-jsdoc/require-yields | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-yields-check | ag-jsdoc/require-yields-check | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-yields-description | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-yields-type | ag-jsdoc/require-yields-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | disabled: Types come from TypeScript annotations. | | +| jsdoc/sort-tags | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/tag-lines | ag-jsdoc/tag-lines | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/text-escaping | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/ts-method-signature-style | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/ts-no-empty-object-type | ag-jsdoc/ts-no-empty-object-type | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/ts-no-unnecessary-template-expression | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/ts-prefer-function-type | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/type-formatting | disabled | disabled | `"off"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/valid-types | ag-jsdoc/valid-types | javascript | `"warn"` | jsdoc@64.3.6/recommended | base | | | +| jsdoc/require-file-overview | ag-jsdoc/require-file-overview | javascript | `"error"` | Javascript.md | base | | | +| sort-imports | ag-compat/sort-imports | javascript | `["error",{"ignoreCase":true,"ignoreDeclarationSort":true,"ignoreMemberSort":false,"memberSyntaxSortOrder":["none","all","multiple","single"]}]` | Javascript.md | base | | | +| unicorn/prefer-node-protocol | unicorn/prefer-node-protocol | native | `"error"` | Javascript.md | base | | | +| unicorn/no-this-assignment | unicorn/no-this-assignment | native | `"error"` | Javascript.md | base | | | +| ag/no-accessors | ag/no-accessors | custom | `"error"` | Javascript.md | base | | | +| ag/no-direct-reexport | ag/no-direct-reexport | custom | `"error"` | Javascript.md | base | | | +| ag/no-prototype-mutation | ag/no-prototype-mutation | custom | `"error"` | Javascript.md | base | | | +| ag/no-default-side-effects | ag/no-default-side-effects | custom | `"error"` | Javascript.md | base | | | +| ag/prefer-array-from-map | ag/prefer-array-from-map | custom | `"error"` | Javascript.md | base | | | +| ag/require-docblock | ag/require-docblock | custom | `"error"` | Javascript.md | base | | | +| ag/enum-name | ag/enum-name | custom | `"error"` | Javascript.md | base (TypeScript) | | | +| ag/unknown-catch | ag/unknown-catch | custom | `"error"` | Javascript.md | base (TypeScript) | | | +| import/no-commonjs | import/no-commonjs | native | `"error"` | Javascript.md | guideline | | | +| import/no-namespace | import/no-namespace | native | `"error"` | Javascript.md | guideline | | | +| import/prefer-default-export | ag-import/prefer-default-export | disabled | `"off"` | Javascript.md | guideline | | | +| import/no-default-export | import/no-default-export | native | `"error"` | Javascript.md | guideline | | | +| import/order | ag-import/order | javascript | `["error",{"groups":[["builtin","external","internal"],"parent",["sibling","index"]],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true}}]` | Javascript.md | guideline | | | +| line-comment-position | ag-compat/line-comment-position | javascript | `["error",{"position":"above","ignorePattern":"^\\s*(?:oxlint-\|@ts-)","applyDefaultPatterns":true}]` | Javascript.md | guideline | | | +| lines-around-comment | ag-compat/lines-around-comment | javascript | `["error",{"beforeBlockComment":false,"beforeLineComment":true,"allowBlockStart":true,"allowObjectStart":true,"allowArrayStart":true,"allowClassStart":true}]` | Javascript.md | guideline | ag-style/lines-around-comment: Treats interface, type literal, enum, and namespace bodies like blocks. | | +| prefer-destructuring | ag-compat/prefer-destructuring | javascript | `["error",{"VariableDeclarator":{"array":true,"object":true},"AssignmentExpression":{"array":true,"object":false}},{"enforceForRenamedProperties":false}]` | Javascript.md | guideline | | | +| react/jsx-indent | ag-react/jsx-indent | javascript | `["error",4]` | Javascript.md | guideline | | | +| react/jsx-indent-props | ag-react/jsx-indent-props | javascript | `["error",4]` | Javascript.md | guideline | | | +| newline-per-chained-call | ag-compat/newline-per-chained-call | javascript | `["error",{"ignoreChainWithDepth":2}]` | Javascript.md | guideline | | | +| padding-line-between-statements | ag-compat/padding-line-between-statements | javascript | `["error",{"blankLine":"always","prev":"block-like","next":"*"}]` | Javascript.md | guideline | | | +| id-length | ag-compat/id-length | javascript | `["error",{"min":2,"properties":"never"}]` | Javascript.md | guideline | | | +| max-len | ag-compat/max-len | javascript | `["error",{"code":120,"comments":120,"tabWidth":4,"ignoreUrls":false,"ignoreTrailingComments":false,"ignoreComments":false,"ignoreStrings":true,"ignoreTemplateLiterals":true}]` | Javascript.md | guideline | | | +| ag/no-multiline-string-concat | ag/no-multiline-string-concat | custom | `"error"` | Javascript.md | guideline | | | +| ag/multiline-condition-layout | ag/multiline-condition-layout | custom | `"error"` | Javascript.md | guideline | | | +| ag/require-docblock | ag/require-docblock | custom | `["error",{"lineCommentRuns":true}]` | Javascript.md | guideline | | | +| ag/constant-name | ag/constant-name | custom | `"error"` | Javascript.md | guideline | | | +| jsdoc/require-description | ag-jsdoc/require-description | javascript | `"error"` | adguard-projects | adguard-projects | | | +| jsdoc/require-description-complete-sentence | ag-jsdoc/require-description-complete-sentence | javascript | `"error"` | adguard-projects | adguard-projects | | | +| jsdoc/require-hyphen-before-param-description | ag-jsdoc/require-hyphen-before-param-description | javascript | `["error","never"]` | adguard-projects | adguard-projects | | | +| jsdoc/require-throws | ag-jsdoc/require-throws | javascript | `"error"` | adguard-projects | adguard-projects | | | +| jsdoc/sort-tags | ag-jsdoc/sort-tags | javascript | `"error"` | adguard-projects | adguard-projects | | | +| no-restricted-imports | ag-compat/no-restricted-imports | javascript | `["error",{"patterns":[{"group":["**/*-mv2","**/*-mv3"],"message":"Do not import directly from MV2/MV3 implementations. Use the appropriate alias or index file instead."}]}]` | adguard-projects | adguard-projects | | | +| import/no-unassigned-import | ag-import/no-unassigned-import | javascript | `["error",{"allow":["**/*.pcss"]}]` | adguard-projects | adguard-projects | | | +| import-newlines/enforce | ag-newlines/enforce | javascript | `["error",{"items":3,"max-len":120}]` | adguard-projects | adguard-projects | | | +| boundaries/element-types | ag-boundaries/element-types | javascript | `["error",{"default":"allow","rules":[{"from":"test-folder","disallow":["src-index"],"message":"Do not import directly from src/. Use specific submodules like src/utils instead."}]}]` | adguard-projects | adguard-projects | | | +| notice/notice | ag-notice/notice | disabled | `"off"` | adguard-projects | adguard-projects | | | +| @adguard/logger-context/require-logger-context | ag-logger/require-logger-context | javascript | `["error",{"contextModuleName":"ext"}]` | adguard-projects | adguard-projects | | | +| @typescript-eslint/consistent-type-imports | typescript/consistent-type-imports | native | `["error",{"fixStyle":"inline-type-imports"}]` | adguard-projects | adguard-projects | | | +| @typescript-eslint/consistent-type-exports | typescript/consistent-type-exports | native | `"error"` | adguard-projects | adguard-projects | | opt-in required | +| @typescript-eslint/explicit-function-return-type | typescript/explicit-function-return-type | native | `"error"` | adguard-projects | adguard-projects | | | +| @typescript-eslint/explicit-member-accessibility | typescript/explicit-member-accessibility | native | `["error",{"accessibility":"explicit","overrides":{"accessors":"explicit","constructors":"no-public","methods":"explicit","properties":"off","parameterProperties":"explicit"}}]` | adguard-projects | adguard-projects | | | +| @typescript-eslint/no-explicit-any | typescript/no-explicit-any | native | `"error"` | adguard-projects | adguard-projects | | | +| @typescript-eslint/no-var-requires | typescript/no-var-requires | native | `"error"` | adguard-projects | adguard-projects | | | +| @typescript-eslint/ban-ts-comment | typescript/ban-ts-comment | native | `"error"` | adguard-projects | adguard-projects | | | +| @typescript-eslint/dot-notation | typescript/dot-notation | disabled | `"off"` | adguard-projects | adguard-projects | | opt-in required | +| @typescript-eslint/no-non-null-assertion | typescript/no-non-null-assertion | disabled | `"off"` | adguard-projects | adguard-projects | | | diff --git a/packages/oxlint-config/README.md b/packages/oxlint-config/README.md index a868d56..dafc4f9 100644 --- a/packages/oxlint-config/README.md +++ b/packages/oxlint-config/README.md @@ -9,11 +9,48 @@ pnpm exec oxlint --config .oxlintrc.json . The CLI writes all required root settings and preserves an existing file unless `--force` is supplied. `--help` lists its options. -`createConfig({ language, environment, sourceType, typeAware, policy })` returns independent, mutable configurations. Languages are `javascript` and `typescript`; environments are `browser`, `node`, and `both`; source types are `module`, `script`, and `commonjs`; policies are `compatibility` and `guideline`. Defaults are JavaScript, browser, module, syntax-only linting, and the compatibility policy. CommonJS filename extensions remain CommonJS. The string language shorthand is also supported. +`createConfig({ language, environment, sourceType, typeAware, policy, profile, importGroups })` returns independent, mutable configurations. Languages are `javascript` and `typescript`; environments are `browser`, `node`, and `both`; source types are `module`, `script`, and `commonjs`; policies are `compatibility` and `guideline`; profiles are `guide` and `adguard-projects`. Defaults are JavaScript, browser, module, syntax-only linting, the compatibility policy, and the guide profile. CommonJS filename extensions remain CommonJS. The string language shorthand is also supported. The CLI accepts the same choices as `--policy`, `--profile`, and `--import-groups`. -Exports include `javascript`, `typescript`, and the `node` environment overlay, JSON preset subpaths `/javascript`, `/typescript`, `/node`, and compiler settings at `/tsconfig`. Keep root `settings` when composing presets. Use TypeScript 7 for checking and tsx for executing TypeScript scripts. +Exports include `javascript`, `typescript`, and the `node` environment overlay, JSON preset subpaths `/javascript`, `/typescript`, `/javascript-adguard-projects`, `/typescript-adguard-projects`, `/node`, and compiler settings at `/tsconfig`. Keep root `settings` when composing presets. Use TypeScript 7 for checking and tsx for executing TypeScript scripts. -Sample settings override conflicting guide prose. The default `compatibility` policy preserves that behavior. The opt-in `guideline` policy reverses only the documented sample/prose conflict (`import/prefer-default-export`) and records the choice in `settings.agPolicy`; other project-specific overlays remain consumer responsibilities. Compiler checks and human review complement syntactic lint rules. The companion rule catalog records every numbered clause and its disposition. +## Policies + +The default `compatibility` policy reproduces the sample `.eslintrc.js`, including inherited Airbnb settings that conflict with the guide's prose. The opt-in `guideline` policy enforces the prose instead and records the choice in `settings.agPolicy`: + +| Clause | Guideline policy | +| --- | --- | +| 5.2 array destructuring | `prefer-destructuring` also checks declarations | +| 6.2, 18.13 long strings | `max-len` ignores strings and template literals; `ag/no-multiline-string-concat` | +| 10.1 modules | `import/no-commonjs` for `module` sources (off for `.cjs`/`.cts`) | +| 10.2 wildcard imports | `import/no-namespace` | +| 10.6 named exports | `import/prefer-default-export` off; `import/no-default-export` | +| 10.10, 10.11 import groups and order | `import/order` with blank lines between groups and path alphabetization | +| 16.1 multiline conditions | `ag/multiline-condition-layout` | +| 17.1 multiline comments | `ag/require-docblock` also rejects runs of `//` comments | +| 17.2 single-line comments | `line-comment-position` and `lines-around-comment` | +| 18.1 indentation | four-space `react/jsx-indent` and `react/jsx-indent-props` | +| 18.6 method chains | `newline-per-chained-call` breaks chains longer than two calls | +| 18.7 blank line after blocks | `padding-line-between-statements` | +| 22.1 descriptive names | `id-length` with a minimum of two characters | +| 22.7 constants | `ag/constant-name` for module-level primitive constants | + +Clause 10.10's prose separates built-in, package, parent, and sibling imports, while its example keeps built-in and package imports together. `importGroups: 'example'` (the default) follows the example; `importGroups: 'prose'` separates every category. Configuration files that must use a default export, such as bundler configurations, can turn off `import/no-default-export` in an override. + +## AdGuard projects profile + +`profile: 'adguard-projects'` adds conventions shared by AdGuard extension projects that the guide does not require: stricter JSDoc rules, MV2/MV3 import restrictions, `.pcss` side-effect imports, `import-newlines`, `boundaries` (with default elements in `settings`), logger context checks, and TypeScript rules such as `explicit-function-return-type`, `explicit-member-accessibility`, `consistent-type-imports`, `no-explicit-any`, and `ban-ts-comment`. The catalog labels these mappings with the `adguard-projects` origin. + +## TypeScript + +The guide applies the JavaScript rules to TypeScript. The TypeScript override replaces pinned JavaScript implementations that report valid TypeScript or ignore TypeScript syntax with TypeScript-aware implementations of the same rules and options, such as overload-aware `no-dupe-class-members`, merge-aware `no-redeclare`, and Stylistic `comma-dangle` for enums, tuples, and type parameters. `docs/coverage.md` lists every replacement and its reason. + +Clauses 26.1 and 26.2 are compiler settings. Extend `/tsconfig`, or verify an existing project with: + +```sh +pnpm exec ag-oxlint-config --check-tsconfig tsconfig.json +``` + +The check follows the `extends` chain and reports `strict`, `noUncheckedIndexedAccess`, and a disabled `useUnknownInCatchVariables`. `checkTsconfig(path)` provides the same check programmatically. Compiler checks and human review complement syntactic lint rules. The companion rule catalog records every numbered clause and its disposition under both policies. ## Type-Aware Linting @@ -22,9 +59,9 @@ The default presets remain syntax-only. Install `oxlint-tsgolint@7.0.2002` along ```ts import { createConfig } from '@agcodeguidelines/oxlint-config'; -export default createConfig({ language: 'typescript', typeAware: true }); +export default createConfig({ language: 'typescript', typeAware: true, profile: 'adguard-projects' }); ``` -Use this configuration with Oxlint's JavaScript/TypeScript config support, or serialize it to JSON. The root `options.typeAware` enables the service; `consistent-type-exports` is enabled only for TypeScript files in this mode. The catalog marks these mappings with `requiresTypeInfo`. Other typed policies must be selected explicitly by the consumer; this is not complete Airbnb TypeScript parity. +Use this configuration with Oxlint's JavaScript/TypeScript config support, or serialize it to JSON. The root `options.typeAware` enables the service; with `profile: 'adguard-projects'`, `consistent-type-exports` is enabled only for TypeScript files in this mode. The catalog marks these mappings with `requiresTypeInfo`. Other typed policies must be selected explicitly by the consumer; this is not complete Airbnb TypeScript parity. Provide a valid TypeScript 7 project and build dependent packages' declarations before linting. Keep native compiler checking and declaration emission as separate build gates. This option does not enable Oxlint's experimental compiler diagnostics or replace bundling, browser transforms, or human guideline review. diff --git a/packages/oxlint-config/package.json b/packages/oxlint-config/package.json index b5f3160..16a88bb 100644 --- a/packages/oxlint-config/package.json +++ b/packages/oxlint-config/package.json @@ -10,6 +10,8 @@ }, "./javascript": "./dist/javascript.json", "./typescript": "./dist/typescript.json", + "./javascript-adguard-projects": "./dist/javascript-adguard-projects.json", + "./typescript-adguard-projects": "./dist/typescript-adguard-projects.json", "./node": "./dist/node.json", "./tsconfig": "./tsconfig.base.json" }, diff --git a/packages/oxlint-config/src/cli.ts b/packages/oxlint-config/src/cli.ts index f9b9368..0e99930 100644 --- a/packages/oxlint-config/src/cli.ts +++ b/packages/oxlint-config/src/cli.ts @@ -2,6 +2,7 @@ /** @file Materialize a complete Oxlint configuration for a consumer project. */ import { writeFile } from 'node:fs/promises'; +import { checkTsconfig } from './compiler'; import { createConfig } from './index'; import type { ConfigOptions } from './index'; @@ -10,10 +11,13 @@ export const usage = `Usage: ag-oxlint-config [options] --language javascript|typescript Source language (default: javascript) --environment browser|node|both Globals (default: browser) - --policy compatibility|guideline Policy profile (default: compatibility) + --policy compatibility|guideline Sample-first or prose-first rules (default: compatibility) + --profile guide|adguard-projects Add rules shared by AdGuard projects (default: guide) + --import-groups example|prose Clause 10.10 grouping for the guideline policy (default: example) --source-type module|script|commonjs Module semantics (default: module) --output PATH Output file (default: .oxlintrc.json) --force Replace an existing output file + --check-tsconfig [PATH] Verify compiler options required by the guide (default: tsconfig.json) --help Print this help `; @@ -27,9 +31,10 @@ export function parseArguments(args: string[]): { output: string; force: boolean; help: boolean; + tsconfig?: string; } { - const result = { - options: {} as ConfigOptions, + const result: ReturnType = { + options: {}, output: '.oxlintrc.json', force: false, help: false, @@ -45,7 +50,14 @@ export function parseArguments(args: string[]): { result.force = true; } else if (flag === '--help') { result.help = true; - } else if (['--language', '--environment', '--policy', '--source-type', '--output'].includes(flag)) { + } else if (flag === '--check-tsconfig') { + const value = args[index + 1]; + result.tsconfig = value && !value.startsWith('--') ? value : 'tsconfig.json'; + index += value && !value.startsWith('--') ? 1 : 0; + } else if ( + ['--language', '--environment', '--policy', '--profile', '--import-groups', '--source-type', '--output'] + .includes(flag) + ) { const value = args[index + 1]; if (!value || value.startsWith('--')) { throw new TypeError(`Missing value for ${flag}`); @@ -54,7 +66,8 @@ export function parseArguments(args: string[]): { if (flag === '--output') { result.output = value; } else { - const key = flag === '--source-type' ? 'sourceType' : flag.slice(2); + const keys: Record = { '--source-type': 'sourceType', '--import-groups': 'importGroups' }; + const key = keys[flag] ?? flag.slice(2); Object.assign(result.options, { [key]: value }); } } else { @@ -75,12 +88,20 @@ export async function runCli( print: (text: string) => void = (text) => process.stdout.write(text), ): Promise { const { - options, output, force, help, + options, output, force, help, tsconfig, } = parseArguments(args); if (help) { print(usage); return; } + if (tsconfig !== undefined) { + const { files, problems } = await checkTsconfig(tsconfig); + if (problems.length > 0) { + throw new Error(`${files[0]} does not meet the guide:\n${problems.map((problem) => ` ${problem}`).join('\n')}`); + } + print(`${files[0]} meets the guide's compiler requirements.\n`); + return; + } const text = `${JSON.stringify(createConfig(options), null, 2)}\n`; if (output === '-') { print(text); diff --git a/packages/oxlint-config/src/compiler.ts b/packages/oxlint-config/src/compiler.ts new file mode 100644 index 0000000..f454ea8 --- /dev/null +++ b/packages/oxlint-config/src/compiler.ts @@ -0,0 +1,128 @@ +/** @file Verify that a consumer tsconfig enables the compiler options required by the guide. */ +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { dirname, isAbsolute, resolve } from 'node:path'; + +export interface TsconfigCheck { + /** Resolved configuration files, starting with the checked file. */ + files: string[]; + /** Compiler options after applying the `extends` chain. */ + compilerOptions: Record; + /** Unmet guide requirements. */ + problems: string[]; +} + +/** + * Find the next significant character after whitespace and comments. + * @param text - Configuration text. + * @param start - Offset to scan from. + * @returns Offset of the next significant character. + */ +function skipTrivia(text: string, start: number): number { + let index = start; + while (index < text.length) { + if (/\s/u.test(text[index]!)) { + index += 1; + } else if (text.startsWith('//', index)) { + const end = text.indexOf('\n', index); + index = end === -1 ? text.length : end; + } else if (text.startsWith('/*', index)) { + const end = text.indexOf('*/', index + 2); + index = end === -1 ? text.length : end + 2; + } else { + break; + } + } + return index; +} + +/** + * Parse JSON with comments and trailing commas, as TypeScript accepts in tsconfig files. + * @param text - Configuration text. + * @returns Parsed value. + */ +export function parseJsonc(text: string): unknown { + let output = ''; + let index = 0; + while (index < text.length) { + index = skipTrivia(text, index); + const character = text[index]; + if (character === '"') { + const literal = /"(?:[^"\\]|\\.)*"/uy; + literal.lastIndex = index; + const value = literal.exec(text)?.[0] ?? text.slice(index); + output += value; + index += value.length; + } else if (character === ',' && ['}', ']'].includes(text[skipTrivia(text, index + 1)] ?? '')) { + index += 1; + } else if (character !== undefined) { + output += character; + index += 1; + } + } + return JSON.parse(output); +} + +/** + * Resolve an `extends` entry the way TypeScript does for relative paths and packages. + * @param specifier - Value from the `extends` field. + * @param from - Configuration file that declares it. + * @returns Absolute path of the extended configuration. + */ +function resolveExtends(specifier: string, from: string): string { + if (specifier.startsWith('.') || isAbsolute(specifier)) { + const path = resolve(dirname(from), specifier); + return path.endsWith('.json') ? path : `${path}.json`; + } + const require = createRequire(from); + try { + return require.resolve(specifier); + } catch { + return require.resolve(`${specifier}${specifier.endsWith('.json') ? '' : '.json'}`); + } +} + +/** + * Load a configuration and its `extends` chain; later files override earlier ones. + * @param path - Configuration path. + * @param seen - Files on the current chain, used to reject cycles. + * @returns Resolved files and merged compiler options. + */ +async function load(path: string, seen: string[]): Promise> { + if (seen.includes(path)) { + throw new Error(`Circular tsconfig extends: ${[...seen, path].join(' -> ')}`); + } + const config = parseJsonc(await readFile(path, 'utf8')) as { + extends?: string | string[]; + compilerOptions?: Record; + }; + const parents = config.extends === undefined ? [] : [config.extends].flat(); + const files = [path]; + let compilerOptions: Record = {}; + for (const parent of parents) { + const loaded = await load(resolveExtends(parent, path), [...seen, path]); + files.push(...loaded.files); + compilerOptions = { ...compilerOptions, ...loaded.compilerOptions }; + } + return { files, compilerOptions: { ...compilerOptions, ...config.compilerOptions } }; +} + +/** + * Check clauses 26.1 and 26.2 and the unknown catch variables that clause 25.3 relies on. + * @param path - tsconfig file to check. + * @returns Resolved options and any unmet requirements. + */ +export async function checkTsconfig(path = 'tsconfig.json'): Promise { + const { files, compilerOptions } = await load(resolve(path), []); + const problems: string[] = []; + if (compilerOptions.strict !== true) { + problems.push('26.1: set "strict": true'); + } + if (compilerOptions.noUncheckedIndexedAccess !== true) { + problems.push('26.2: set "noUncheckedIndexedAccess": true'); + } + if (compilerOptions.useUnknownInCatchVariables === false) { + problems.push('25.3: do not disable "useUnknownInCatchVariables"'); + } + return { files, compilerOptions, problems }; +} diff --git a/packages/oxlint-config/src/index.ts b/packages/oxlint-config/src/index.ts index 71f9fc0..8a914af 100644 --- a/packages/oxlint-config/src/index.ts +++ b/packages/oxlint-config/src/index.ts @@ -1,22 +1,43 @@ /** @file Complete, portable Oxlint configurations derived from the pinned guide. */ import { catalog } from '@agcodeguidelines/rule-catalog'; +import type { + ImportGroups, + Overlay, + Policy, + Profile, +} from '@agcodeguidelines/rule-catalog'; import type { OxlintConfig } from 'oxlint'; +import { checkTsconfig, parseJsonc } from './compiler'; +import type { TsconfigCheck } from './compiler'; + +export type { + ImportGroups, + Policy, + Profile, + TsconfigCheck, +}; export type Language = 'javascript' | 'typescript'; export type Environment = 'browser' | 'node' | 'both'; -export type Policy = 'compatibility' | 'guideline'; export interface ConfigOptions { language?: Language; environment?: Environment; sourceType?: 'module' | 'script' | 'commonjs'; typeAware?: boolean; + /** `guideline` enforces guide prose where it conflicts with the sample configuration. */ policy?: Policy; + /** `adguard-projects` adds rules shared by AdGuard projects that the guide does not require. */ + profile?: Profile; + /** Import grouping for clause 10.10 under the guideline policy. */ + importGroups?: ImportGroups; } +type Rules = NonNullable; + /** * Create an independent configuration with all plugin settings at its root. - * @param options - Language, globals, and module semantics for the consumer. + * @param options - Language, globals, module semantics, policy, and profile for the consumer. * @returns A complete mutable Oxlint configuration. */ export function createConfig(options: ConfigOptions | Language = {}): OxlintConfig { @@ -26,22 +47,55 @@ export function createConfig(options: ConfigOptions | Language = {}): OxlintConf sourceType = 'module', typeAware = false, policy = 'compatibility', + profile = 'guide', + importGroups, } = typeof options === 'string' ? { language: options } : options; if ( !['javascript', 'typescript'].includes(language) || !['browser', 'node', 'both'].includes(environment) || !['module', 'script', 'commonjs'].includes(sourceType) || !['compatibility', 'guideline'].includes(policy) + || !['guide', 'adguard-projects'].includes(profile) ) { - throw new TypeError('Unsupported language, environment, or policy.'); + throw new TypeError('Unsupported language, environment, source type, policy, or profile.'); } if (typeof typeAware !== 'boolean' || (typeAware && language !== 'typescript')) { throw new TypeError('Type-aware linting requires the TypeScript preset and a boolean typeAware option.'); } - const rules = structuredClone(catalog.rules) as NonNullable; + if (importGroups !== undefined && (policy !== 'guideline' || !['example', 'prose'].includes(importGroups))) { + throw new TypeError('importGroups requires the guideline policy and one of: example, prose.'); + } + const rules = structuredClone(catalog.rules) as Rules; + const typescriptRules = structuredClone(catalog.typescriptRules) as Rules; const settings = structuredClone(catalog.settings); + const overlays: Overlay[] = []; + if (policy === 'guideline') { + overlays.push(catalog.policies.guideline); + } + if (profile === 'adguard-projects') { + overlays.push(catalog.profiles['adguard-projects']); + } + overlays.forEach((overlay) => { + Object.assign(rules, structuredClone(overlay.rules)); + Object.assign(typescriptRules, structuredClone(overlay.typescriptRules)); + Object.assign(settings, structuredClone(overlay.settings)); + }); + if (policy === 'guideline') { + rules['ag-import/order'] = structuredClone( + catalog.policies.guideline.importGroups[importGroups ?? 'example'], + ) as Rules[string]; + if (sourceType !== 'module') { + rules['import/no-commonjs'] = 'off'; + } + } + if (!typeAware) { + catalog.mappings + .filter((mapping) => mapping.requiresTypeInfo && mapping.target) + .forEach((mapping) => delete typescriptRules[mapping.target!]); + } const providers = { ag: '@agcodeguidelines/oxlint-plugin', + 'ag-ts': '@agcodeguidelines/oxlint-plugin/typescript', 'ag-compat': '@agcodeguidelines/oxlint-plugin/compat', 'ag-style': '@agcodeguidelines/oxlint-plugin/stylistic', 'ag-jsdoc': '@agcodeguidelines/oxlint-plugin/jsdoc', @@ -59,6 +113,7 @@ export function createConfig(options: ConfigOptions | Language = {}): OxlintConf settings.agSourceType = sourceType; settings.agTypeScript = language === 'typescript'; settings.agPolicy = policy; + settings.agProfile = profile; const config: OxlintConfig = { categories: { correctness: 'off', @@ -69,7 +124,7 @@ export function createConfig(options: ConfigOptions | Language = {}): OxlintConf restriction: 'off', nursery: 'off', }, - plugins: ['eslint', 'unicorn', 'typescript'], + plugins: ['eslint', 'unicorn', 'typescript', 'import'], jsPlugins: Object.entries(providers).map(([name, specifier]) => ({ name, specifier })), env: { ...catalog.env, @@ -79,71 +134,20 @@ export function createConfig(options: ConfigOptions | Language = {}): OxlintConf node: environment !== 'browser', }, settings, - rules: rules as OxlintConfig['rules'], + rules, overrides: [], }; - if (policy === 'guideline') { - // Preserve compatibility defaults; only reverse documented sample/prose conflicts. - rules['ag-import/prefer-default-export'] = 'off'; - } if (typeAware) { config.options = { typeAware: true }; } + if (policy === 'guideline' && sourceType === 'module') { + // CommonJS filename extensions keep CommonJS semantics. + config.overrides!.push({ files: ['**/*.{cjs,cts}'], rules: { 'import/no-commonjs': 'off' } }); + } if (language === 'typescript') { - const unused = rules['ag-compat/no-unused-vars']; settings['import/extensions'] = ['.js', '.mjs', '.jsx', '.ts', '.tsx', '.mts', '.cts']; settings['import/resolver'] = { typescript: true, node: { extensions: settings['import/extensions'] } }; - config.overrides = [ - { - files: ['**/*.{ts,tsx,mts,cts}'], - rules: { - 'ag-compat/no-undef': 'off', - // Native syntax-aware rules understand signatures and parameter properties. - 'ag-compat/no-unused-vars': 'off', - 'eslint/no-unused-vars': Array.isArray(unused) - ? [unused[0], { caughtErrors: 'none', ...(unused[1] as object) }] - : [unused ?? 'off', { caughtErrors: 'none' }], - 'ag-compat/no-shadow': 'off', - 'eslint/no-shadow': rules['ag-compat/no-shadow'], - 'ag-compat/no-useless-constructor': 'off', - 'eslint/no-useless-constructor': rules['ag-compat/no-useless-constructor'], - 'ag-compat/no-empty-function': 'off', - 'eslint/no-empty-function': rules['ag-compat/no-empty-function'], - 'ag-compat/indent': 'off', - 'ag-style/indent': rules['ag-compat/indent'], - 'ag/enum-name': 'error', - 'ag/unknown-catch': 'error', - 'ag-react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }], - 'ag-import/extensions': [ - 'error', - 'ignorePackages', - { - js: 'never', - jsx: 'never', - ts: 'never', - tsx: 'never', - mts: 'never', - cts: 'never', - }, - ], - 'ag-jsdoc/check-tag-names': ['warn', { typed: true }], - 'ag-jsdoc/require-param-type': 'off', - 'ag-jsdoc/require-property-type': 'off', - 'ag-jsdoc/require-returns-type': 'off', - 'ag-jsdoc/require-next-type': 'off', - 'ag-jsdoc/require-yields-type': 'off', - 'ag-jsdoc/require-throws-type': 'off', - 'ag-jsdoc/no-undefined-types': 'off', - 'ag-jsdoc/no-types': 'error', - ...Object.fromEntries( - catalog.mappings - .filter((mapping) => mapping.target?.startsWith('typescript/') ?? false) - .filter((mapping) => !mapping.requiresTypeInfo || typeAware) - .map((mapping) => [mapping.target as string, mapping.setting]), - ), - }, - }, - ]; + config.overrides!.unshift({ files: ['**/*.{ts,tsx,mts,cts}'], rules: typescriptRules }); } return config; } @@ -151,3 +155,4 @@ export function createConfig(options: ConfigOptions | Language = {}): OxlintConf export const javascript = createConfig(); export const typescript = createConfig({ language: 'typescript' }); export const node = { env: { browser: false, node: true } } satisfies OxlintConfig; +export { checkTsconfig, parseJsonc }; diff --git a/packages/oxlint-plugin/README.md b/packages/oxlint-plugin/README.md index 7b6a070..ba62eb0 100644 --- a/packages/oxlint-plugin/README.md +++ b/packages/oxlint-plugin/README.md @@ -11,7 +11,12 @@ Use `@agcodeguidelines/oxlint-config` to materialize the full preset. The defaul - `no-prototype-mutation` - `no-default-side-effects` - `prefer-array-from-map` -- `require-docblock` +- `require-docblock` (option `lineCommentRuns` also rejects runs of `//` comments) +- `no-multiline-string-concat` +- `multiline-condition-layout` +- `constant-name` + +The `/typescript` export provides TypeScript-aware variants of pinned core rules: `no-redeclare` accepts overloads and the declaration merges that TypeScript permits. The `/compat`, `/jsdoc`, `/react`, `/jsx-a11y`, `/import`, and `/stylistic` exports provide isolated upstream rules through Oxlint's JavaScript plugin API. They are not an ESLint plugin distribution and do not depend on the ESLint engine. diff --git a/packages/oxlint-plugin/package.json b/packages/oxlint-plugin/package.json index dd97f01..ca9850b 100644 --- a/packages/oxlint-plugin/package.json +++ b/packages/oxlint-plugin/package.json @@ -8,6 +8,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./typescript": { + "types": "./dist/typescript.d.ts", + "default": "./dist/typescript.js" + }, "./compat": { "types": "./dist/compat.d.ts", "default": "./dist/compat.js" diff --git a/packages/oxlint-plugin/src/helpers.ts b/packages/oxlint-plugin/src/helpers.ts index 16778fe..14c2a20 100644 --- a/packages/oxlint-plugin/src/helpers.ts +++ b/packages/oxlint-plugin/src/helpers.ts @@ -59,3 +59,80 @@ export function isDefaultMutation(node: ESTree.Node): boolean { } return false; } + +/** + * Remove TypeScript-only wrappers that do not change a runtime value. + * @param node - Expression to inspect. + * @returns The wrapped runtime expression. + */ +export function unwrapTypeExpression(node: ESTree.Node): ESTree.Node { + let current = node; + while ( + current.type === 'TSAsExpression' + || current.type === 'TSSatisfiesExpression' + || current.type === 'TSTypeAssertion' + || current.type === 'TSNonNullExpression' + || current.type === 'ParenthesizedExpression' + ) { + current = current.expression; + } + return current; +} + +/** + * Determine whether an expression is a string without runtime substitutions. + * @param node - Operand to inspect. + * @returns Whether the operand is a string literal or a template without expressions. + */ +export function isStaticString(node: ESTree.Node): boolean { + return (node.type === 'Literal' && typeof node.value === 'string') + || (node.type === 'TemplateLiteral' && node.expressions.length === 0); +} + +/** + * Flatten a chain of `+` operations into its operands in source order. + * @param node - Expression at the top of the chain. + * @returns Operands that are not themselves additions. + */ +export function concatenationOperands(node: ESTree.Node): ESTree.Node[] { + if (node.type === 'BinaryExpression' && node.operator === '+') { + return [...concatenationOperands(node.left), ...concatenationOperands(node.right)]; + } + return [node]; +} + +/** + * Determine whether a module-level initializer is a primitive constant value. + * @param node - Declarator initializer. + * @returns Whether the value is a string, number, bigint, or boolean literal. + */ +export function isPrimitiveConstant(node: ESTree.Node): boolean { + const value = unwrapTypeExpression(node); + if (value.type === 'Literal') { + return 'bigint' in value || ['string', 'number', 'boolean'].includes(typeof value.value); + } + if (value.type === 'UnaryExpression' && ['-', '+'].includes(value.operator)) { + return value.argument.type === 'Literal' && typeof value.argument.value === 'number'; + } + return value.type === 'TemplateLiteral' && value.expressions.length === 0; +} + +/** + * Check the documented constant naming form. + * @param name - Binding name. + * @returns Whether the name is UPPER_SNAKE_CASE. + */ +export function isUpperSnakeCase(name: string): boolean { + return /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/u.test(name); +} + +/** + * Recognize comments that configure tools rather than describe code. + * @param value - Comment text without delimiters. + * @returns Whether the comment is a directive. + */ +export function isDirectiveComment(value: string): boolean { + return value.startsWith('/') + || /^\s*(?:eslint|oxlint|@ts-|global\s|globals\s|exported\s|istanbul\s|c8\s|v8\s|prettier-ignore|#(?:end)?region\b)/u + .test(value); +} diff --git a/packages/oxlint-plugin/src/index.ts b/packages/oxlint-plugin/src/index.ts index e9b204c..6f037d0 100644 --- a/packages/oxlint-plugin/src/index.ts +++ b/packages/oxlint-plugin/src/index.ts @@ -5,45 +5,66 @@ import { definePlugin, defineRule } from '@oxlint/plugins'; import type { ESTree, RuleMeta } from '@oxlint/plugins'; import { + concatenationOperands, isDefaultMutation, + isDirectiveComment, isPascalCase, + isPrimitiveConstant, + isStaticString, + isUpperSnakeCase, memberName, + unwrapTypeExpression, writesPrototype, } from './helpers'; /** - * Describe a source-backed rule with no configurable detection heuristics. + * Describe a source-backed rule. * @param anchor - Source guideline anchor. * @param message - Diagnostic text. + * @param schema - Option schema; most rules have no configurable heuristics. * @returns Rule metadata. */ -function metadata(anchor: string, message: string): RuleMeta { +function metadata(anchor: string, message: string, schema: RuleMeta['schema'] = []): RuleMeta { return { type: 'suggestion', docs: { description: message, url: `https://github.com/AdguardTeam/CodeGuidelines/blob/master/JavaScript/Javascript.md#${anchor}`, }, - schema: [], + schema, messages: { guideline: message }, }; } +type Report = (node: ESTree.Node) => void; + +/** + * Report accessor-kind members. + * @param report - Reporter bound to the rule context. + * @returns Handler for members that expose a `kind`. + */ +function reportAccessorKind(report: Report): (node: ESTree.Node & { kind: string }) => void { + return (node) => { + if (node.kind === 'get' || node.kind === 'set') { + report(node); + } + }; +} + export const rules = { 'no-accessors': defineRule({ meta: metadata('accessors--no-getters-setters', 'Use explicit accessor methods instead of getters or setters.'), create(context) { + const report: Report = (node) => context.report({ node, messageId: 'guideline' }); + const accessor = reportAccessorKind(report); return { - Property(node) { - if (node.kind === 'get' || node.kind === 'set') { - context.report({ node, messageId: 'guideline' }); - } - }, - MethodDefinition(node) { - if (node.kind === 'get' || node.kind === 'set') { - context.report({ node, messageId: 'guideline' }); - } - }, + Property: accessor, + MethodDefinition: accessor, + TSAbstractMethodDefinition: accessor, + TSMethodSignature: accessor, + // Auto-accessors define a getter and setter pair. + AccessorProperty: report, + TSAbstractAccessorProperty: report, }; }, }), @@ -77,11 +98,11 @@ export const rules = { create(context) { return { CatchClause(node) { - if ( - node.param?.type === 'Identifier' - && node.param.typeAnnotation?.typeAnnotation.type === 'TSAnyKeyword' - ) { - context.report({ node: node.param.typeAnnotation, messageId: 'guideline' }); + const annotation = node.param && 'typeAnnotation' in node.param + ? node.param.typeAnnotation + : null; + if (annotation?.typeAnnotation.type === 'TSAnyKeyword') { + context.report({ node: annotation, messageId: 'guideline' }); } }, }; @@ -161,18 +182,143 @@ export const rules = { }, }), 'require-docblock': defineRule({ - meta: metadata('comments--multiline', 'Use /** ... */ for multiline block comments.'), + meta: metadata('comments--multiline', 'Use /** ... */ for multiline block comments.', [{ + type: 'object', + properties: { lineCommentRuns: { type: 'boolean' } }, + additionalProperties: false, + }]), create(context) { + const { lineCommentRuns = false } = (context.options[0] ?? {}) as { lineCommentRuns?: boolean }; + const { lines } = context.sourceCode; + const standalone = (comment: ESTree.Comment) => (lines[comment.loc.start.line - 1] ?? '') + .slice(0, comment.loc.start.column) + .trim() === ''; return { Program() { + let run: ESTree.Comment[] = []; + const flush = () => { + if (run.length > 1) { + context.report({ + loc: { start: run[0]!.loc.start, end: run.at(-1)!.loc.end }, + messageId: 'guideline', + }); + } + run = []; + }; context.sourceCode.getAllComments().forEach((comment) => { - if ( - comment.type === 'Block' - && /\r|\n/u.test(comment.value) - && !comment.value.startsWith('*') - ) { - context.report({ loc: comment.loc, messageId: 'guideline' }); + if (comment.type === 'Block') { + flush(); + if (/\r|\n/u.test(comment.value) && !comment.value.startsWith('*')) { + context.report({ loc: comment.loc, messageId: 'guideline' }); + } + return; + } + if (!lineCommentRuns) { + return; + } + if (!standalone(comment) || isDirectiveComment(comment.value)) { + flush(); + return; + } + if (run.length > 0 && run.at(-1)!.loc.end.line + 1 !== comment.loc.start.line) { + flush(); + } + run.push(comment); + }); + flush(); + }, + }; + }, + }), + 'no-multiline-string-concat': defineRule({ + meta: metadata( + 'strings--line-length', + 'Do not break a long string across lines with concatenation.', + ), + create(context) { + return { + BinaryExpression(node) { + if (node.operator !== '+' || (node.parent.type === 'BinaryExpression' && node.parent.operator === '+')) { + return; + } + const operands = concatenationOperands(node); + const broken = operands.some((operand, index) => { + const next = operands[index + 1]; + return next !== undefined + && isStaticString(operand) + && isStaticString(next) + && operand.loc.end.line !== next.loc.start.line; + }); + if (broken) { + context.report({ node, messageId: 'guideline' }); + } + }, + }; + }, + }), + 'multiline-condition-layout': defineRule({ + meta: metadata( + 'control-statements', + 'Start a multiline condition on a new line and put the closing parenthesis on its own line.', + ), + create(context) { + const { sourceCode } = context; + const check = (test: ESTree.Node, open: ESTree.Token | null, close: ESTree.Token | null) => { + if (!open || !close || unwrapTypeExpression(test).type !== 'LogicalExpression') { + return; + } + const first = sourceCode.getTokenAfter(open)!; + const last = sourceCode.getTokenBefore(close)!; + if ( + first.loc.start.line !== last.loc.end.line + && (first.loc.start.line === open.loc.end.line || last.loc.end.line === close.loc.start.line) + ) { + context.report({ node: test, messageId: 'guideline' }); + } + }; + const statement = (node: ESTree.IfStatement | ESTree.WhileStatement) => check( + node.test, + sourceCode.getTokenAfter(sourceCode.getFirstToken(node)!), + sourceCode.getTokenBefore('consequent' in node ? node.consequent : node.body), + ); + return { + IfStatement: statement, + WhileStatement: statement, + DoWhileStatement(node) { + const keyword = sourceCode.getTokenAfter(node.body)!; + const last = sourceCode.getLastToken(node)!; + check( + node.test, + sourceCode.getTokenAfter(keyword), + last.value === ')' ? last : sourceCode.getTokenBefore(last), + ); + }, + }; + }, + }), + 'constant-name': defineRule({ + meta: metadata( + 'naming--constants', + 'Use UPPER_SNAKE_CASE for module-level constants initialized with primitive literals.', + ), + create(context) { + return { + Program(program) { + program.body.forEach((statement) => { + const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement; + if (declaration?.type !== 'VariableDeclaration' || declaration.kind !== 'const') { + return; } + declaration.declarations.forEach((declarator) => { + if ( + declarator.id.type === 'Identifier' + && declarator.init + && isPrimitiveConstant(declarator.init) + && !isUpperSnakeCase(declarator.id.name) + ) { + context.report({ node: declarator.id, messageId: 'guideline' }); + } + }); }); }, }; diff --git a/packages/oxlint-plugin/src/typescript.ts b/packages/oxlint-plugin/src/typescript.ts new file mode 100644 index 0000000..0254558 --- /dev/null +++ b/packages/oxlint-plugin/src/typescript.ts @@ -0,0 +1,140 @@ +/** + * @file TypeScript-aware variants of pinned core rules whose upstream versions predate TypeScript syntax. + */ +import type { + ESTree, + Plugin, + Rule, + RuleMeta, +} from '@oxlint/plugins'; +import builtinRules from '../../../vendor/core/lib/rules/index'; + +import { legacyContext } from './legacy-context'; + +type DeclarationKind = 'interface' | 'type' | 'namespace' | 'class' | 'function' | 'enum' | 'variable' | 'other'; + +interface Declaration { + kind: DeclarationKind; + implementation: boolean; +} + +type Container = ESTree.Node & { body: ESTree.Node[] }; + +/** + * Remove an export wrapper from a statement. + * @param statement - Statement in a declaration container. + * @returns The declaration or statement itself. + */ +function unwrapExport(statement: ESTree.Node): ESTree.Node | null { + if (statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration') { + return statement.declaration as ESTree.Node | null; + } + return statement; +} + +/** + * Collect the names a statement declares with their TypeScript declaration kinds. + * @param statement - Statement in a declaration container. + * @returns Declared names and kinds. + */ +function declarationsOf(statement: ESTree.Node): [string, Declaration][] { + const node = unwrapExport(statement); + const single = (id: ESTree.Node | null | undefined, kind: DeclarationKind, implementation = false) => ( + id?.type === 'Identifier' ? [[id.name, { kind, implementation }] as [string, Declaration]] : [] + ); + switch (node?.type) { + case 'VariableDeclaration': + return node.declarations.flatMap((declarator) => single(declarator.id, 'variable')); + case 'FunctionDeclaration': + case 'TSDeclareFunction': + return single(node.id, 'function', node.body !== null && node.body !== undefined); + case 'ClassDeclaration': + return single(node.id, 'class'); + case 'TSInterfaceDeclaration': + return single(node.id, 'interface'); + case 'TSTypeAliasDeclaration': + return single(node.id, 'type'); + case 'TSEnumDeclaration': + return single(node.id, 'enum'); + case 'TSModuleDeclaration': + return single(node.id, 'namespace'); + case 'ImportDeclaration': + return node.specifiers.map((specifier) => [specifier.local.name, { kind: 'other', implementation: false }]); + default: + return []; + } +} + +/** + * Find the statement list that directly contains a declared identifier. + * @param identifier - Identifier reported as a redeclaration. + * @returns The block, module body, or program containing the declaration. + */ +function containerOf(identifier: ESTree.Node): Container | null { + let current: ESTree.Node | null = identifier.parent; + while (current && current.type !== 'VariableDeclaration' && !/Declaration$|^TSDeclareFunction$/u.test(current.type)) { + current = current.type === 'VariableDeclarator' ? current.parent : null; + } + let parent = current?.parent ?? null; + if (parent?.type === 'ExportNamedDeclaration' || parent?.type === 'ExportDefaultDeclaration') { + parent = parent.parent; + } + return parent && 'body' in parent && Array.isArray(parent.body) ? (parent as Container) : null; +} + +/** + * Decide whether declarations form a merge that TypeScript permits. + * @param declarations - Every same-name declaration in one container. + * @returns Whether the declarations are an allowed TypeScript declaration merge. + */ +export function isAllowedMerge(declarations: Declaration[]): boolean { + const count = (kind: DeclarationKind) => declarations.filter((declaration) => declaration.kind === kind).length; + if (declarations.length < 2 || count('other') > 0) { + return false; + } + const values = new Set( + declarations + .map((declaration) => declaration.kind) + .filter((kind) => ['class', 'function', 'enum', 'variable'].includes(kind)), + ); + const typeAliases = count('type'); + return values.size <= 1 + && typeAliases <= 1 + && (typeAliases === 0 || count('interface') + count('class') + count('enum') === 0) + && count('class') <= 1 + && count('variable') <= 1 + && (count('variable') === 0 || count('namespace') === 0) + && declarations.filter((declaration) => declaration.kind === 'function' && declaration.implementation).length <= 1; +} + +const noRedeclare = builtinRules.get('no-redeclare')!; + +export const rules = { + 'no-redeclare': { + meta: noRedeclare.meta as RuleMeta, + create(context) { + const filtered = Object.create(context, { + report: { + value: (descriptor: Parameters[0] & { messageId?: string }) => { + const { node } = descriptor as { node?: ESTree.Node }; + const container = descriptor.messageId === 'redeclared' && node?.type === 'Identifier' + ? containerOf(node) + : null; + const declarations = container?.body + .flatMap(declarationsOf) + .filter(([name]) => name === (node as ESTree.IdentifierReference).name) + .map(([, declaration]) => declaration) ?? []; + // Overloads and permitted interface, namespace, class, function, or enum merges + // declare one TypeScript symbol. + if (!isAllowedMerge(declarations)) { + context.report(descriptor); + } + }, + }, + }) as typeof context; + return legacyContext(noRedeclare as unknown as Rule, filtered); + }, + } satisfies Rule, +}; + +export default { meta: { name: 'ag-ts' }, rules } as Plugin; diff --git a/packages/rule-catalog/README.md b/packages/rule-catalog/README.md index 1c64054..4d430fa 100644 --- a/packages/rule-catalog/README.md +++ b/packages/rule-catalog/README.md @@ -2,4 +2,4 @@ Pinned source hashes, effective inherited sample settings, rule mappings, and dispositions for all 134 numbered AdGuard JavaScript guideline clauses. -Import `catalog` and `severity` from the package. The catalog distinguishes native, JavaScript, custom, compiler, manual, overridden, and disabled behavior. Sample settings take precedence over conflicting prose. Compiler and manual dispositions are part of the policy's documented contract. +Import `catalog` and `severity` from the package. Each clause has a disposition for the default `compatibility` policy (its top-level fields) and for the opt-in `guideline` policy (`clause.guideline`). Dispositions distinguish native, JavaScript, custom, compiler, partial, manual, and overridden enforcement; `partial` means the configured rules enforce only part of the clause. Mappings are scoped to the base preset, the `guideline` policy, or the `adguard-projects` profile, and record the TypeScript implementation used in TypeScript files. `catalog.policies` and `catalog.profiles` hold the corresponding rule overlays. Compiler and manual dispositions are part of the policy's documented contract. diff --git a/packages/rule-catalog/src/catalog.json b/packages/rule-catalog/src/catalog.json index d2f745f..c060504 100644 --- a/packages/rule-catalog/src/catalog.json +++ b/packages/rule-catalog/src/catalog.json @@ -1100,6 +1100,13 @@ ] } ], + "no-restricted-imports": [ + "off", + { + "paths": [], + "patterns": [] + } + ], "no-this-before-super": "error", "no-useless-computed-key": "error", "no-useless-constructor": "error", @@ -1258,6 +1265,7 @@ ], "import/unambiguous": "off", "import/no-webpack-loader-syntax": "error", + "import/no-unassigned-import": "off", "import/no-named-default": "error", "import/no-anonymous-default-export": [ "off", @@ -2001,7 +2009,10 @@ "jsdoc/reject-any-type": "warn", "jsdoc/reject-function-type": "warn", "jsdoc/require-asterisk-prefix": "off", + "jsdoc/require-description": "off", + "jsdoc/require-description-complete-sentence": "off", "jsdoc/require-example": "off", + "jsdoc/require-hyphen-before-param-description": "off", "jsdoc/require-jsdoc": "warn", "jsdoc/require-next-description": "off", "jsdoc/require-next-type": "warn", @@ -2021,12 +2032,14 @@ "jsdoc/require-tags": "off", "jsdoc/require-template": "off", "jsdoc/require-template-description": "off", + "jsdoc/require-throws": "off", "jsdoc/require-throws-description": "off", "jsdoc/require-throws-type": "warn", "jsdoc/require-yields": "warn", "jsdoc/require-yields-check": "warn", "jsdoc/require-yields-description": "off", "jsdoc/require-yields-type": "warn", + "jsdoc/sort-tags": "off", "jsdoc/tag-lines": "warn", "jsdoc/text-escaping": "off", "jsdoc/ts-method-signature-style": "off", @@ -2064,18 +2077,6 @@ "forbidExtraProps", "exact", "Object.freeze" - ], - "boundaries/elements": [ - { - "type": "src-index", - "pattern": "src/index.ts", - "mode": "file" - }, - { - "type": "test-folder", - "pattern": "test", - "mode": "folder" - } ] }, "env": { @@ -2190,7 +2191,13 @@ "target": "ag-compat/default-param-last", "setting": "error", "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "eslint/default-param-last", + "setting": "error", + "implementation": "native", + "reason": "Treats optional parameters like defaults." + } }, { "source": "dot-notation", @@ -2312,7 +2319,22 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "eslint/no-empty-function", + "setting": [ + "error", + { + "allow": [ + "arrowFunctions", + "functions", + "methods" + ] + } + ], + "implementation": "native", + "reason": "Understands parameter-property constructors and overload signatures." + } }, { "source": "no-empty-pattern", @@ -2572,7 +2594,13 @@ "target": "ag-compat/no-redeclare", "setting": "error", "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-ts/no-redeclare", + "setting": "error", + "implementation": "javascript", + "reason": "Accepts overloads and declaration merges that TypeScript permits." + } }, { "source": "no-restricted-properties", @@ -3408,7 +3436,25 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/comma-dangle", + "setting": [ + "error", + { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "always-multiline", + "enums": "always-multiline", + "generics": "always-multiline", + "tuples": "always-multiline" + } + ], + "implementation": "javascript", + "reason": "Also checks enums, type parameters, and tuples with the array setting, as airbnb-typescript does." + } }, { "source": "comma-spacing", @@ -3421,7 +3467,19 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/comma-spacing", + "setting": [ + "error", + { + "before": false, + "after": true + } + ], + "implementation": "javascript", + "reason": "Checks type arguments and accepts the TSX `` form." + } }, { "source": "comma-style", @@ -3578,7 +3636,19 @@ } ], "implementation": "javascript", - "origin": "eslintrc.cjs" + "origin": "eslintrc.cjs", + "typescript": { + "target": "ag-style/indent", + "setting": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "implementation": "javascript", + "reason": "Indents interfaces, enums, and type annotations." + } }, { "source": "jsx-quotes", @@ -3601,7 +3671,19 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/key-spacing", + "setting": [ + "error", + { + "beforeColon": false, + "afterColon": true + } + ], + "implementation": "javascript", + "reason": "Checks interface and type literal members." + } }, { "source": "keyword-spacing", @@ -3662,14 +3744,35 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/lines-between-class-members", + "setting": [ + "error", + "always", + { + "exceptAfterSingleLine": false + } + ], + "implementation": "javascript", + "reason": "Accepts adjacent overload signatures." + } }, { "source": "lines-around-comment", "target": null, "setting": "off", "implementation": "disabled", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/lines-around-comment", + "setting": [ + "off", + {} + ], + "implementation": "disabled", + "reason": "Treats interface, type literal, enum, and namespace bodies like blocks." + } }, { "source": "lines-around-directive", @@ -3856,7 +3959,13 @@ "target": "ag-compat/no-array-constructor", "setting": "error", "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "eslint/no-array-constructor", + "setting": "error", + "implementation": "native", + "reason": "Accepts explicitly typed constructors such as `new Array()`." + } }, { "source": "no-bitwise", @@ -4115,7 +4224,16 @@ "always" ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/object-curly-spacing", + "setting": [ + "error", + "always" + ], + "implementation": "javascript", + "reason": "Checks type literals." + } }, { "source": "object-curly-newline", @@ -4286,7 +4404,16 @@ "always" ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/semi", + "setting": [ + "error", + "always" + ], + "implementation": "javascript", + "reason": "Checks type aliases, ambient declarations, and abstract members." + } }, { "source": "semi-spacing", @@ -4337,7 +4464,13 @@ "target": "ag-compat/space-before-blocks", "setting": "error", "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/space-before-blocks", + "setting": "error", + "implementation": "javascript", + "reason": "Checks interface, enum, and module bodies." + } }, { "source": "space-before-function-paren", @@ -4368,7 +4501,13 @@ "target": "ag-compat/space-infix-ops", "setting": "error", "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/space-infix-ops", + "setting": "error", + "implementation": "javascript", + "reason": "Checks union and intersection types and enum initializers." + } }, { "source": "space-unary-ops", @@ -4568,7 +4707,13 @@ "target": "ag-compat/no-shadow", "setting": "error", "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "eslint/no-shadow", + "setting": "error", + "implementation": "native", + "reason": "Ignores type-space parameters in signatures." + } }, { "source": "no-shadow-restricted-names", @@ -4582,7 +4727,13 @@ "target": "ag-compat/no-undef", "setting": "error", "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": null, + "setting": "off", + "implementation": "disabled", + "reason": "The TypeScript compiler reports unresolved names, including ambient types." + } }, { "source": "no-undef-init", @@ -4610,7 +4761,21 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "eslint/no-unused-vars", + "setting": [ + "error", + { + "caughtErrors": "none", + "vars": "all", + "args": "after-used", + "ignoreRestSiblings": true + } + ], + "implementation": "native", + "reason": "Understands type-only usage, signatures, and parameter properties." + } }, { "source": "no-use-before-define", @@ -4624,7 +4789,20 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "eslint/no-use-before-define", + "setting": [ + "error", + { + "functions": true, + "classes": true, + "variables": true + } + ], + "implementation": "native", + "reason": "Ignores type-only references such as `typeof` queries." + } }, { "source": "arrow-body-style", @@ -4713,7 +4891,13 @@ "target": "ag-compat/no-dupe-class-members", "setting": "error", "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "eslint/no-dupe-class-members", + "setting": "error", + "implementation": "native", + "reason": "Accepts method overload signatures." + } }, { "source": "no-duplicate-imports", @@ -4744,6 +4928,19 @@ "implementation": "javascript", "origin": "airbnb@19.0.4/base@15.0.0" }, + { + "source": "no-restricted-imports", + "target": null, + "setting": [ + "off", + { + "paths": [], + "patterns": [] + } + ], + "implementation": "disabled", + "origin": "airbnb@19.0.4/base@15.0.0" + }, { "source": "no-this-before-super", "target": "ag-compat/no-this-before-super", @@ -4763,7 +4960,13 @@ "target": "ag-compat/no-useless-constructor", "setting": "error", "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "eslint/no-useless-constructor", + "setting": "error", + "implementation": "native", + "reason": "Keeps constructors that declare parameter properties or accessibility." + } }, { "source": "no-useless-rename", @@ -5020,7 +5223,59 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-import/no-extraneous-dependencies", + "setting": [ + "error", + { + "devDependencies": [ + "test/**", + "tests/**", + "spec/**", + "**/__tests__/**", + "**/__mocks__/**", + "test.{js,jsx}", + "test.{ts,tsx}", + "test-*.{js,jsx}", + "test-*.{ts,tsx}", + "**/*{.,_}{test,spec}.{js,jsx}", + "**/*{.,_}{test,spec}.{ts,tsx}", + "**/jest.config.js", + "**/jest.config.ts", + "**/jest.setup.js", + "**/jest.setup.ts", + "**/vue.config.js", + "**/vue.config.ts", + "**/webpack.config.js", + "**/webpack.config.ts", + "**/webpack.config.*.js", + "**/webpack.config.*.ts", + "**/rollup.config.js", + "**/rollup.config.ts", + "**/rollup.config.*.js", + "**/rollup.config.*.ts", + "**/gulpfile.js", + "**/gulpfile.ts", + "**/gulpfile.*.js", + "**/gulpfile.*.ts", + "**/Gruntfile{,.js}", + "**/Gruntfile{,.ts}", + "**/protractor.conf.js", + "**/protractor.conf.ts", + "**/protractor.conf.*.js", + "**/protractor.conf.*.ts", + "**/karma.conf.js", + "**/karma.conf.ts", + "**/.eslintrc.js", + "**/.eslintrc.ts" + ], + "optionalDependencies": false + } + ], + "implementation": "javascript", + "reason": "Adds TypeScript variants of the development file globs, as airbnb-typescript does." + } }, { "source": "import/no-mutable-exports", @@ -5091,7 +5346,25 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-import/extensions", + "setting": [ + "error", + "ignorePackages", + { + "js": "never", + "mjs": "never", + "jsx": "never", + "ts": "never", + "tsx": "never", + "mts": "never", + "cts": "never" + } + ], + "implementation": "javascript", + "reason": "TypeScript module specifiers omit TypeScript extensions as well." + } }, { "source": "import/order", @@ -5184,6 +5457,13 @@ "implementation": "javascript", "origin": "airbnb@19.0.4/base@15.0.0" }, + { + "source": "import/no-unassigned-import", + "target": null, + "setting": "off", + "implementation": "disabled", + "origin": "airbnb@19.0.4/base@15.0.0" + }, { "source": "import/no-named-default", "target": "ag-import/no-named-default", @@ -5849,7 +6129,21 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-react/jsx-filename-extension", + "setting": [ + "error", + { + "extensions": [ + ".jsx", + ".tsx" + ] + } + ], + "implementation": "javascript", + "reason": "Allows JSX in .tsx files." + } }, { "source": "react/jsx-no-comment-textnodes", @@ -6831,7 +7125,18 @@ "target": "ag-jsdoc/check-tag-names", "setting": "warn", "implementation": "javascript", - "origin": "jsdoc@64.3.6/recommended" + "origin": "jsdoc@64.3.6/recommended", + "typescript": { + "target": "ag-jsdoc/check-tag-names", + "setting": [ + "warn", + { + "typed": true + } + ], + "implementation": "javascript", + "reason": "Uses TypeScript tag semantics." + } }, { "source": "jsdoc/check-template-names", @@ -6978,14 +7283,26 @@ "target": null, "setting": "off", "implementation": "disabled", - "origin": "jsdoc@64.3.6/recommended" + "origin": "jsdoc@64.3.6/recommended", + "typescript": { + "target": "ag-jsdoc/no-types", + "setting": "error", + "implementation": "javascript", + "reason": "Types come from TypeScript annotations rather than duplicated JSDoc types." + } }, { "source": "jsdoc/no-undefined-types", "target": "ag-jsdoc/no-undefined-types", "setting": "warn", "implementation": "javascript", - "origin": "jsdoc@64.3.6/recommended" + "origin": "jsdoc@64.3.6/recommended", + "typescript": { + "target": null, + "setting": "off", + "implementation": "disabled", + "reason": "Types come from TypeScript annotations." + } }, { "source": "jsdoc/no-unnecessary-type-assertion", @@ -7029,6 +7346,20 @@ "implementation": "disabled", "origin": "jsdoc@64.3.6/recommended" }, + { + "source": "jsdoc/require-description", + "target": null, + "setting": "off", + "implementation": "disabled", + "origin": "jsdoc@64.3.6/recommended" + }, + { + "source": "jsdoc/require-description-complete-sentence", + "target": null, + "setting": "off", + "implementation": "disabled", + "origin": "jsdoc@64.3.6/recommended" + }, { "source": "jsdoc/require-example", "target": null, @@ -7036,6 +7367,13 @@ "implementation": "disabled", "origin": "jsdoc@64.3.6/recommended" }, + { + "source": "jsdoc/require-hyphen-before-param-description", + "target": null, + "setting": "off", + "implementation": "disabled", + "origin": "jsdoc@64.3.6/recommended" + }, { "source": "jsdoc/require-jsdoc", "target": "ag-jsdoc/require-jsdoc", @@ -7055,7 +7393,13 @@ "target": "ag-jsdoc/require-next-type", "setting": "warn", "implementation": "javascript", - "origin": "jsdoc@64.3.6/recommended" + "origin": "jsdoc@64.3.6/recommended", + "typescript": { + "target": null, + "setting": "off", + "implementation": "disabled", + "reason": "Types come from TypeScript annotations." + } }, { "source": "jsdoc/require-param", @@ -7083,7 +7427,13 @@ "target": "ag-jsdoc/require-param-type", "setting": "warn", "implementation": "javascript", - "origin": "jsdoc@64.3.6/recommended" + "origin": "jsdoc@64.3.6/recommended", + "typescript": { + "target": null, + "setting": "off", + "implementation": "disabled", + "reason": "Types come from TypeScript annotations." + } }, { "source": "jsdoc/require-property", @@ -7111,7 +7461,13 @@ "target": "ag-jsdoc/require-property-type", "setting": "warn", "implementation": "javascript", - "origin": "jsdoc@64.3.6/recommended" + "origin": "jsdoc@64.3.6/recommended", + "typescript": { + "target": null, + "setting": "off", + "implementation": "disabled", + "reason": "Types come from TypeScript annotations." + } }, { "source": "jsdoc/require-rejects", @@ -7146,7 +7502,13 @@ "target": "ag-jsdoc/require-returns-type", "setting": "warn", "implementation": "javascript", - "origin": "jsdoc@64.3.6/recommended" + "origin": "jsdoc@64.3.6/recommended", + "typescript": { + "target": null, + "setting": "off", + "implementation": "disabled", + "reason": "Types come from TypeScript annotations." + } }, { "source": "jsdoc/require-tags", @@ -7169,6 +7531,13 @@ "implementation": "disabled", "origin": "jsdoc@64.3.6/recommended" }, + { + "source": "jsdoc/require-throws", + "target": null, + "setting": "off", + "implementation": "disabled", + "origin": "jsdoc@64.3.6/recommended" + }, { "source": "jsdoc/require-throws-description", "target": null, @@ -7181,7 +7550,13 @@ "target": "ag-jsdoc/require-throws-type", "setting": "warn", "implementation": "javascript", - "origin": "jsdoc@64.3.6/recommended" + "origin": "jsdoc@64.3.6/recommended", + "typescript": { + "target": null, + "setting": "off", + "implementation": "disabled", + "reason": "Types come from TypeScript annotations." + } }, { "source": "jsdoc/require-yields", @@ -7209,6 +7584,19 @@ "target": "ag-jsdoc/require-yields-type", "setting": "warn", "implementation": "javascript", + "origin": "jsdoc@64.3.6/recommended", + "typescript": { + "target": null, + "setting": "off", + "implementation": "disabled", + "reason": "Types come from TypeScript annotations." + } + }, + { + "source": "jsdoc/sort-tags", + "target": null, + "setting": "off", + "implementation": "disabled", "origin": "jsdoc@64.3.6/recommended" }, { @@ -7274,44 +7662,6 @@ "implementation": "javascript", "origin": "Javascript.md" }, - { - "source": "jsdoc/require-description", - "target": "ag-jsdoc/require-description", - "setting": "error", - "implementation": "javascript", - "origin": "Javascript.md" - }, - { - "source": "jsdoc/require-description-complete-sentence", - "target": "ag-jsdoc/require-description-complete-sentence", - "setting": "error", - "implementation": "javascript", - "origin": "Javascript.md" - }, - { - "source": "jsdoc/require-hyphen-before-param-description", - "target": "ag-jsdoc/require-hyphen-before-param-description", - "setting": [ - "error", - "never" - ], - "implementation": "javascript", - "origin": "Javascript.md" - }, - { - "source": "jsdoc/require-throws", - "target": "ag-jsdoc/require-throws", - "setting": "error", - "implementation": "javascript", - "origin": "Javascript.md" - }, - { - "source": "jsdoc/sort-tags", - "target": "ag-jsdoc/sort-tags", - "setting": "error", - "implementation": "javascript", - "origin": "Javascript.md" - }, { "source": "sort-imports", "target": "ag-compat/sort-imports", @@ -7333,54 +7683,430 @@ "origin": "Javascript.md" }, { - "source": "no-restricted-imports", - "target": "ag-compat/no-restricted-imports", - "setting": [ - "error", - { - "patterns": [ - { - "group": [ - "**/*-mv2", - "**/*-mv3" - ], - "message": "Do not import directly from MV2/MV3 implementations. Use the appropriate alias or index file instead." - } - ] - } - ], - "implementation": "javascript", + "source": "unicorn/prefer-node-protocol", + "target": "unicorn/prefer-node-protocol", + "setting": "error", + "implementation": "native", "origin": "Javascript.md" }, { - "source": "import/no-unassigned-import", - "target": "ag-import/no-unassigned-import", - "setting": [ - "error", - { - "allow": [ - "**/*.pcss" - ] - } - ], - "implementation": "javascript", + "source": "unicorn/no-this-assignment", + "target": "unicorn/no-this-assignment", + "setting": "error", + "implementation": "native", "origin": "Javascript.md" }, { - "source": "import-newlines/enforce", - "target": "ag-newlines/enforce", - "setting": [ - "error", - { - "items": 3, - "max-len": 120 - } - ], - "implementation": "javascript", + "source": "ag/no-accessors", + "target": "ag/no-accessors", + "setting": "error", + "implementation": "custom", "origin": "Javascript.md" }, { - "source": "boundaries/element-types", + "source": "ag/no-direct-reexport", + "target": "ag/no-direct-reexport", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md" + }, + { + "source": "ag/no-prototype-mutation", + "target": "ag/no-prototype-mutation", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md" + }, + { + "source": "ag/no-default-side-effects", + "target": "ag/no-default-side-effects", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md" + }, + { + "source": "ag/prefer-array-from-map", + "target": "ag/prefer-array-from-map", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md" + }, + { + "source": "ag/require-docblock", + "target": "ag/require-docblock", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md" + }, + { + "source": "ag/enum-name", + "target": "ag/enum-name", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md", + "language": "typescript" + }, + { + "source": "ag/unknown-catch", + "target": "ag/unknown-catch", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md", + "language": "typescript" + }, + { + "source": "import/no-commonjs", + "target": "import/no-commonjs", + "setting": "error", + "implementation": "native", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "import/no-namespace", + "target": "import/no-namespace", + "setting": "error", + "implementation": "native", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "import/prefer-default-export", + "target": "ag-import/prefer-default-export", + "setting": "off", + "implementation": "disabled", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "import/no-default-export", + "target": "import/no-default-export", + "setting": "error", + "implementation": "native", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "import/order", + "target": "ag-import/order", + "setting": [ + "error", + { + "groups": [ + [ + "builtin", + "external", + "internal" + ], + "parent", + [ + "sibling", + "index" + ] + ], + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "line-comment-position", + "target": "ag-compat/line-comment-position", + "setting": [ + "error", + { + "position": "above", + "ignorePattern": "^\\s*(?:oxlint-|@ts-)", + "applyDefaultPatterns": true + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "lines-around-comment", + "target": "ag-compat/lines-around-comment", + "setting": [ + "error", + { + "beforeBlockComment": false, + "beforeLineComment": true, + "allowBlockStart": true, + "allowObjectStart": true, + "allowArrayStart": true, + "allowClassStart": true + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline", + "typescript": { + "target": "ag-style/lines-around-comment", + "setting": [ + "error", + { + "beforeBlockComment": false, + "beforeLineComment": true, + "allowBlockStart": true, + "allowObjectStart": true, + "allowArrayStart": true, + "allowClassStart": true, + "allowInterfaceStart": true, + "allowTypeStart": true, + "allowEnumStart": true, + "allowModuleStart": true + } + ], + "implementation": "javascript", + "reason": "Treats interface, type literal, enum, and namespace bodies like blocks." + } + }, + { + "source": "prefer-destructuring", + "target": "ag-compat/prefer-destructuring", + "setting": [ + "error", + { + "VariableDeclarator": { + "array": true, + "object": true + }, + "AssignmentExpression": { + "array": true, + "object": false + } + }, + { + "enforceForRenamedProperties": false + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "react/jsx-indent", + "target": "ag-react/jsx-indent", + "setting": [ + "error", + 4 + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "react/jsx-indent-props", + "target": "ag-react/jsx-indent-props", + "setting": [ + "error", + 4 + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "newline-per-chained-call", + "target": "ag-compat/newline-per-chained-call", + "setting": [ + "error", + { + "ignoreChainWithDepth": 2 + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "padding-line-between-statements", + "target": "ag-compat/padding-line-between-statements", + "setting": [ + "error", + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "id-length", + "target": "ag-compat/id-length", + "setting": [ + "error", + { + "min": 2, + "properties": "never" + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "max-len", + "target": "ag-compat/max-len", + "setting": [ + "error", + { + "code": 120, + "comments": 120, + "tabWidth": 4, + "ignoreUrls": false, + "ignoreTrailingComments": false, + "ignoreComments": false, + "ignoreStrings": true, + "ignoreTemplateLiterals": true + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "ag/no-multiline-string-concat", + "target": "ag/no-multiline-string-concat", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "ag/multiline-condition-layout", + "target": "ag/multiline-condition-layout", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "ag/require-docblock", + "target": "ag/require-docblock", + "setting": [ + "error", + { + "lineCommentRuns": true + } + ], + "implementation": "custom", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "ag/constant-name", + "target": "ag/constant-name", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "jsdoc/require-description", + "target": "ag-jsdoc/require-description", + "setting": "error", + "implementation": "javascript", + "origin": "adguard-projects", + "scope": "adguard-projects" + }, + { + "source": "jsdoc/require-description-complete-sentence", + "target": "ag-jsdoc/require-description-complete-sentence", + "setting": "error", + "implementation": "javascript", + "origin": "adguard-projects", + "scope": "adguard-projects" + }, + { + "source": "jsdoc/require-hyphen-before-param-description", + "target": "ag-jsdoc/require-hyphen-before-param-description", + "setting": [ + "error", + "never" + ], + "implementation": "javascript", + "origin": "adguard-projects", + "scope": "adguard-projects" + }, + { + "source": "jsdoc/require-throws", + "target": "ag-jsdoc/require-throws", + "setting": "error", + "implementation": "javascript", + "origin": "adguard-projects", + "scope": "adguard-projects" + }, + { + "source": "jsdoc/sort-tags", + "target": "ag-jsdoc/sort-tags", + "setting": "error", + "implementation": "javascript", + "origin": "adguard-projects", + "scope": "adguard-projects" + }, + { + "source": "no-restricted-imports", + "target": "ag-compat/no-restricted-imports", + "setting": [ + "error", + { + "patterns": [ + { + "group": [ + "**/*-mv2", + "**/*-mv3" + ], + "message": "Do not import directly from MV2/MV3 implementations. Use the appropriate alias or index file instead." + } + ] + } + ], + "implementation": "javascript", + "origin": "adguard-projects", + "scope": "adguard-projects" + }, + { + "source": "import/no-unassigned-import", + "target": "ag-import/no-unassigned-import", + "setting": [ + "error", + { + "allow": [ + "**/*.pcss" + ] + } + ], + "implementation": "javascript", + "origin": "adguard-projects", + "scope": "adguard-projects" + }, + { + "source": "import-newlines/enforce", + "target": "ag-newlines/enforce", + "setting": [ + "error", + { + "items": 3, + "max-len": 120 + } + ], + "implementation": "javascript", + "origin": "adguard-projects", + "scope": "adguard-projects" + }, + { + "source": "boundaries/element-types", "target": "ag-boundaries/element-types", "setting": [ "error", @@ -7398,14 +8124,16 @@ } ], "implementation": "javascript", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects" }, { "source": "notice/notice", - "target": null, + "target": "ag-notice/notice", "setting": "off", "implementation": "disabled", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects" }, { "source": "@adguard/logger-context/require-logger-context", @@ -7417,7 +8145,8 @@ } ], "implementation": "javascript", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects" }, { "source": "@typescript-eslint/consistent-type-imports", @@ -7429,14 +8158,18 @@ } ], "implementation": "native", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects", + "language": "typescript" }, { "source": "@typescript-eslint/consistent-type-exports", "target": "typescript/consistent-type-exports", "setting": "error", "implementation": "native", - "origin": "Javascript.md", + "origin": "adguard-projects", + "scope": "adguard-projects", + "language": "typescript", "requiresTypeInfo": true }, { @@ -7444,7 +8177,9 @@ "target": "typescript/explicit-function-return-type", "setting": "error", "implementation": "native", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects", + "language": "typescript" }, { "source": "@typescript-eslint/explicit-member-accessibility", @@ -7463,98 +8198,55 @@ } ], "implementation": "native", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects", + "language": "typescript" }, { "source": "@typescript-eslint/no-explicit-any", "target": "typescript/no-explicit-any", "setting": "error", "implementation": "native", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects", + "language": "typescript" }, { "source": "@typescript-eslint/no-var-requires", "target": "typescript/no-var-requires", "setting": "error", "implementation": "native", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects", + "language": "typescript" }, { "source": "@typescript-eslint/ban-ts-comment", "target": "typescript/ban-ts-comment", "setting": "error", "implementation": "native", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects", + "language": "typescript" }, { "source": "@typescript-eslint/dot-notation", - "target": null, + "target": "typescript/dot-notation", "setting": "off", "implementation": "disabled", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects", + "language": "typescript", + "requiresTypeInfo": true }, { "source": "@typescript-eslint/no-non-null-assertion", - "target": null, + "target": "typescript/no-non-null-assertion", "setting": "off", "implementation": "disabled", - "origin": "Javascript.md" - }, - { - "source": "unicorn/prefer-node-protocol", - "target": "unicorn/prefer-node-protocol", - "setting": "error", - "implementation": "native", - "origin": "Javascript.md" - }, - { - "source": "unicorn/no-this-assignment", - "target": "unicorn/no-this-assignment", - "setting": "error", - "implementation": "native", - "origin": "Javascript.md" - }, - { - "source": "ag/no-accessors", - "target": "ag/no-accessors", - "setting": "error", - "implementation": "custom", - "origin": "Javascript.md" - }, - { - "source": "ag/no-direct-reexport", - "target": "ag/no-direct-reexport", - "setting": "error", - "implementation": "custom", - "origin": "Javascript.md" - }, - { - "source": "ag/no-prototype-mutation", - "target": "ag/no-prototype-mutation", - "setting": "error", - "implementation": "custom", - "origin": "Javascript.md" - }, - { - "source": "ag/no-default-side-effects", - "target": "ag/no-default-side-effects", - "setting": "error", - "implementation": "custom", - "origin": "Javascript.md" - }, - { - "source": "ag/prefer-array-from-map", - "target": "ag/prefer-array-from-map", - "setting": "error", - "implementation": "custom", - "origin": "Javascript.md" - }, - { - "source": "ag/require-docblock", - "target": "ag/require-docblock", - "setting": "error", - "implementation": "custom", - "origin": "Javascript.md" + "origin": "adguard-projects", + "scope": "adguard-projects", + "language": "typescript" } ], "clauses": [ @@ -7565,7 +8257,12 @@ "line": 43, "rules": [], "enforcement": "manual", - "reason": "Educational semantics; supported browser targets must be selected by the consumer." + "reason": "Educational semantics; supported browser targets must be selected by the consumer.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Educational semantics; supported browser targets must be selected by the consumer." + } }, { "id": "types--complex", @@ -7574,7 +8271,12 @@ "line": 65, "rules": [], "enforcement": "manual", - "reason": "Educational explanation of reference values." + "reason": "Educational explanation of reference values.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Educational explanation of reference values." + } }, { "id": "references--prefer-const", @@ -7586,7 +8288,15 @@ "no-const-assign" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "prefer-const", + "no-const-assign" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "references--disallow-var", @@ -7597,7 +8307,14 @@ "no-var" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-var" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "references--block-scope", @@ -7608,7 +8325,14 @@ "no-undef" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-undef" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "objects--quoted-props", @@ -7619,7 +8343,14 @@ "quote-props" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "quote-props" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "objects--prototype-builtins", @@ -7630,7 +8361,14 @@ "no-prototype-builtins" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-prototype-builtins" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "objects--rest-spread", @@ -7641,7 +8379,14 @@ "prefer-object-spread" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "prefer-object-spread" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "es6-array-spreads", @@ -7650,7 +8395,12 @@ "line": 205, "rules": [], "enforcement": "manual", - "reason": "Recognizing arbitrary copying algorithms requires semantic intent; reviewed manually." + "reason": "Recognizing arbitrary copying algorithms requires semantic intent; reviewed manually.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Recognizing arbitrary copying algorithms requires semantic intent; reviewed manually." + } }, { "id": "arrays--from-iterable", @@ -7659,7 +8409,12 @@ "line": 222, "rules": [], "enforcement": "manual", - "reason": "An unknown JavaScript value may be array-like rather than iterable; no speculative rewrite." + "reason": "An unknown JavaScript value may be array-like rather than iterable; no speculative rewrite.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "An unknown JavaScript value may be array-like rather than iterable; no speculative rewrite." + } }, { "id": "arrays--from-array-like", @@ -7668,7 +8423,12 @@ "line": 235, "rules": [], "enforcement": "manual", - "reason": "Whether an arbitrary runtime value is array-like requires runtime information." + "reason": "Whether an arbitrary runtime value is array-like requires runtime information.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Whether an arbitrary runtime value is array-like requires runtime information." + } }, { "id": "arrays--mapping", @@ -7679,7 +8439,14 @@ "ag/prefer-array-from-map" ], "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "ag/prefer-array-from-map" + ], + "enforcement": "custom", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "arrays--callback-return", @@ -7690,7 +8457,14 @@ "array-callback-return" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "array-callback-return" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "destructuring--object", @@ -7701,7 +8475,14 @@ "prefer-destructuring" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "prefer-destructuring" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "destructuring--array", @@ -7711,8 +8492,15 @@ "rules": [ "prefer-destructuring" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "Inherited prefer-destructuring checks assignments but not declarations such as `const first = arr[0]`; the guideline policy checks both.", + "guideline": { + "rules": [ + "prefer-destructuring" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "destructuring--object-over-array", @@ -7721,7 +8509,12 @@ "line": 328, "rules": [], "enforcement": "manual", - "reason": "Distinguishing multiple return values from an intentional array API requires review." + "reason": "Distinguishing multiple return values from an intentional array API requires review.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Distinguishing multiple return values from an intentional array API requires review." + } }, { "id": "strings--quotes", @@ -7732,7 +8525,14 @@ "quotes" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "quotes" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "strings--line-length", @@ -7744,7 +8544,15 @@ "no-useless-concat" ], "enforcement": "overridden", - "reason": "Sample precedence: max-len, no-useless-concat retains its inherited/explicit options and severity." + "reason": "Sample precedence: max-len, no-useless-concat retains its inherited/explicit options and severity.", + "guideline": { + "rules": [ + "max-len", + "ag/no-multiline-string-concat" + ], + "enforcement": "custom", + "reason": "max-len ignores strings and template literals; ag/no-multiline-string-concat rejects strings broken across lines with concatenation." + } }, { "id": "es6-template-literals", @@ -7756,7 +8564,15 @@ "template-curly-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "prefer-template", + "template-curly-spacing" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "strings--eval", @@ -7767,7 +8583,14 @@ "no-eval" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-eval" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "strings--escaping", @@ -7778,7 +8601,14 @@ "no-useless-escape" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-useless-escape" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--iife", @@ -7789,7 +8619,14 @@ "wrap-iife" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "wrap-iife" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--in-blocks", @@ -7797,10 +8634,19 @@ "text": "Never declare a function in a non-function block (`if`, `while`, etc). Assign the function to a variable instead. Browsers will allow you to do it, but they all interpret it differently, which is bad news bears. eslint: [`no-loop-func`](https://eslint.org/docs/rules/no-loop-func)", "line": 452, "rules": [ - "no-loop-func" + "no-loop-func", + "no-inner-declarations" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-loop-func", + "no-inner-declarations" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--arguments-shadow", @@ -7811,7 +8657,14 @@ "no-shadow-restricted-names" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-shadow-restricted-names" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "es6-rest", @@ -7822,7 +8675,14 @@ "prefer-rest-params" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "prefer-rest-params" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "es6-default-parameters", @@ -7833,7 +8693,14 @@ "no-param-reassign" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-param-reassign" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--default-side-effects", @@ -7844,7 +8711,14 @@ "ag/no-default-side-effects" ], "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "ag/no-default-side-effects" + ], + "enforcement": "custom", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--defaults-last", @@ -7855,7 +8729,14 @@ "default-param-last" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "default-param-last" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--constructor", @@ -7866,7 +8747,14 @@ "no-new-func" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-new-func" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--signature-spacing", @@ -7878,7 +8766,15 @@ "space-before-blocks" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "space-before-function-paren", + "space-before-blocks" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--mutate-params", @@ -7889,7 +8785,14 @@ "no-param-reassign" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-param-reassign" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--reassign-params", @@ -7900,7 +8803,14 @@ "no-param-reassign" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-param-reassign" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--spread-vs-apply", @@ -7911,7 +8821,14 @@ "prefer-spread" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "prefer-spread" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "functions--signature-invocation-indentation", @@ -7922,7 +8839,14 @@ "function-paren-newline" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "function-paren-newline" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "arrows--use-them", @@ -7934,7 +8858,15 @@ "arrow-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "prefer-arrow-callback", + "arrow-spacing" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "arrows--one-arg-parens", @@ -7945,7 +8877,14 @@ "arrow-parens" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "arrow-parens" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--implicit-arrow-linebreak", @@ -7956,7 +8895,14 @@ "implicit-arrow-linebreak" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "implicit-arrow-linebreak" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "constructors--use-class", @@ -7967,7 +8913,14 @@ "ag/no-prototype-mutation" ], "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "ag/no-prototype-mutation" + ], + "enforcement": "custom", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "constructors--extends", @@ -7978,7 +8931,14 @@ "ag/no-prototype-mutation" ], "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "ag/no-prototype-mutation" + ], + "enforcement": "custom", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "constructors--chaining", @@ -7987,7 +8947,12 @@ "line": 811, "rules": [], "enforcement": "manual", - "reason": "Method chaining is optional API design guidance." + "reason": "Method chaining is optional API design guidance.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Method chaining is optional API design guidance." + } }, { "id": "constructors--no-useless", @@ -7998,7 +8963,14 @@ "no-useless-constructor" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-useless-constructor" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "classes--methods-use-this", @@ -8009,7 +8981,14 @@ "class-methods-use-this" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "class-methods-use-this" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "modules--use-them", @@ -8021,7 +9000,15 @@ "import/no-amd" ], "enforcement": "overridden", - "reason": "Sample precedence: import/no-commonjs, import/no-amd retains its inherited/explicit options and severity." + "reason": "Sample precedence: import/no-commonjs, import/no-amd retains its inherited/explicit options and severity.", + "guideline": { + "rules": [ + "import/no-commonjs", + "import/no-amd" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "modules--no-wildcard", @@ -8032,7 +9019,14 @@ "import/no-namespace" ], "enforcement": "overridden", - "reason": "Sample precedence: import/no-namespace retains its inherited/explicit options and severity." + "reason": "Sample precedence: import/no-namespace retains its inherited/explicit options and severity.", + "guideline": { + "rules": [ + "import/no-namespace" + ], + "enforcement": "native", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "modules--no-export-from-import", @@ -8043,7 +9037,14 @@ "ag/no-direct-reexport" ], "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "ag/no-direct-reexport" + ], + "enforcement": "custom", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "modules--no-duplicate-imports", @@ -8051,10 +9052,19 @@ "text": "Only import from a path in one place.", "line": 962, "rules": [ - "no-duplicate-imports" + "no-duplicate-imports", + "import/no-duplicates" ], - "enforcement": "overridden", - "reason": "Sample precedence: no-duplicate-imports retains its inherited/explicit options and severity." + "enforcement": "javascript", + "reason": "Airbnb disables no-duplicate-imports in favor of import/no-duplicates, which enforces the clause.", + "guideline": { + "rules": [ + "no-duplicate-imports", + "import/no-duplicates" + ], + "enforcement": "javascript", + "reason": "Airbnb disables no-duplicate-imports in favor of import/no-duplicates, which enforces the clause." + } }, { "id": "modules--no-mutable-exports", @@ -8065,7 +9075,14 @@ "import/no-mutable-exports" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "import/no-mutable-exports" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "modules--prefer-named-export", @@ -8076,7 +9093,14 @@ "import/prefer-default-export" ], "enforcement": "overridden", - "reason": "Sample precedence: import/prefer-default-export retains its inherited/explicit options and severity." + "reason": "Sample precedence: import/prefer-default-export retains its inherited/explicit options and severity.", + "guideline": { + "rules": [ + "import/no-default-export" + ], + "enforcement": "native", + "reason": "import/prefer-default-export is disabled and import/no-default-export rejects default exports." + } }, { "id": "modules--imports-first", @@ -8087,7 +9111,14 @@ "import/first" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "import/first" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "modules--multiline-imports-over-newlines", @@ -8098,7 +9129,14 @@ "object-curly-newline" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "object-curly-newline" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "modules--import-extensions", @@ -8109,7 +9147,14 @@ "import/extensions" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "import/extensions" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "modules--import-grouping", @@ -8119,8 +9164,15 @@ "rules": [ "import/order" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "Inherited import/order places built-in, package, and internal imports first, but does not require blank lines between groups or separate parent from sibling imports; use the guideline policy.", + "guideline": { + "rules": [ + "import/order" + ], + "enforcement": "javascript", + "reason": "import/order requires blank lines between groups. `importGroups: example` follows the clause example (built-in and package imports share a block); `prose` separates every listed category." + } }, { "id": "modules--import-order", @@ -8130,8 +9182,15 @@ "rules": [ "import/order" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "overridden", + "reason": "Inherited import/order options do not alphabetize; the guideline policy orders imports by path.", + "guideline": { + "rules": [ + "import/order" + ], + "enforcement": "javascript", + "reason": "import/order alphabetizes imports by path within each group." + } }, { "id": "modules--import-member-order", @@ -8143,7 +9202,15 @@ "sort-imports" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "import/order", + "sort-imports" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "modules--import-node-protocol", @@ -8154,7 +9221,14 @@ "unicorn/prefer-node-protocol" ], "enforcement": "native", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "unicorn/prefer-node-protocol" + ], + "enforcement": "native", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "properties--dot", @@ -8165,7 +9239,14 @@ "dot-notation" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "dot-notation" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "properties--bracket", @@ -8176,7 +9257,14 @@ "dot-notation" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "dot-notation" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "es2016-properties--exponentiation-operator", @@ -8187,7 +9275,14 @@ "no-restricted-properties" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-restricted-properties" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "variables--const", @@ -8199,7 +9294,15 @@ "prefer-const" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-undef", + "prefer-const" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "variables--one-const", @@ -8210,7 +9313,14 @@ "one-var" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "one-var" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "variables--define-where-used", @@ -8219,7 +9329,12 @@ "line": 1231, "rules": [], "enforcement": "manual", - "reason": "A reasonable declaration location is a human judgment." + "reason": "A reasonable declaration location is a human judgment.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "A reasonable declaration location is a human judgment." + } }, { "id": "variables--no-chain-assignment", @@ -8230,7 +9345,14 @@ "no-multi-assign" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-multi-assign" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "variables--unary-increment-decrement", @@ -8241,7 +9363,14 @@ "no-plusplus" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-plusplus" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "variables--linebreak", @@ -8253,7 +9382,15 @@ "operator-linebreak" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "max-len", + "operator-linebreak" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "variables--no-unused-vars", @@ -8264,7 +9401,14 @@ "no-unused-vars" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-unused-vars" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "hoisting--about", @@ -8273,7 +9417,12 @@ "line": 1404, "rules": [], "enforcement": "manual", - "reason": "Educational explanation of hoisting and temporal dead zones." + "reason": "Educational explanation of hoisting and temporal dead zones.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Educational explanation of hoisting and temporal dead zones." + } }, { "id": "hoisting--anon-expressions", @@ -8282,7 +9431,12 @@ "line": 1440, "rules": [], "enforcement": "manual", - "reason": "Educational explanation of anonymous function hoisting." + "reason": "Educational explanation of anonymous function hoisting.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Educational explanation of anonymous function hoisting." + } }, { "id": "hoisting--named-expresions", @@ -8291,7 +9445,12 @@ "line": 1455, "rules": [], "enforcement": "manual", - "reason": "Educational explanation of named function expression hoisting." + "reason": "Educational explanation of named function expression hoisting.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Educational explanation of named function expression hoisting." + } }, { "id": "hoisting--declarations", @@ -8300,7 +9459,12 @@ "line": 1484, "rules": [], "enforcement": "manual", - "reason": "Educational explanation of function declaration hoisting." + "reason": "Educational explanation of function declaration hoisting.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Educational explanation of function declaration hoisting." + } }, { "id": "comparison--eqeqeq", @@ -8311,7 +9475,14 @@ "eqeqeq" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "eqeqeq" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "comparison--if", @@ -8320,7 +9491,12 @@ "line": 1506, "rules": [], "enforcement": "manual", - "reason": "Educational explanation of boolean coercion." + "reason": "Educational explanation of boolean coercion.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Educational explanation of boolean coercion." + } }, { "id": "comparison--shortcuts", @@ -8329,7 +9505,12 @@ "line": 1523, "rules": [], "enforcement": "manual", - "reason": "Requires semantic types for arbitrary JavaScript expressions; review manually." + "reason": "Requires semantic types for arbitrary JavaScript expressions; review manually.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Requires semantic types for arbitrary JavaScript expressions; review manually." + } }, { "id": "comparison--moreinfo", @@ -8338,7 +9519,12 @@ "line": 1558, "rules": [], "enforcement": "manual", - "reason": "Further-reading reference, not a code requirement." + "reason": "Further-reading reference, not a code requirement.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Further-reading reference, not a code requirement." + } }, { "id": "comparison--switch-blocks", @@ -8349,7 +9535,14 @@ "no-case-declarations" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-case-declarations" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "comparison--nested-ternaries", @@ -8360,7 +9553,14 @@ "no-nested-ternary" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-nested-ternary" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "comparison--unneeded-ternary", @@ -8371,7 +9571,14 @@ "no-unneeded-ternary" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-unneeded-ternary" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "comparison--no-mixed-operators", @@ -8382,7 +9589,14 @@ "no-mixed-operators" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-mixed-operators" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "blocks--braces", @@ -8394,7 +9608,15 @@ "brace-style" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "curly", + "brace-style" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "blocks--cuddled-elses", @@ -8405,7 +9627,14 @@ "brace-style" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "brace-style" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "blocks--no-else-return", @@ -8416,7 +9645,14 @@ "no-else-return" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-else-return" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "control-statements", @@ -8426,8 +9662,16 @@ "rules": [ "operator-linebreak" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "operator-linebreak puts logical operators first; starting the condition on its own line is enforced by the guideline policy.", + "guideline": { + "rules": [ + "operator-linebreak", + "ag/multiline-condition-layout" + ], + "enforcement": "custom", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "control-statement--value-selection", @@ -8438,7 +9682,14 @@ "no-unused-expressions" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-unused-expressions" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "comments--multiline", @@ -8448,8 +9699,15 @@ "rules": [ "ag/require-docblock" ], - "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "Multiline /* */ blocks are rejected; runs of // line comments (the clause example) are rejected only by the guideline policy.", + "guideline": { + "rules": [ + "ag/require-docblock" + ], + "enforcement": "custom", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "comments--singleline", @@ -8461,7 +9719,15 @@ "lines-around-comment" ], "enforcement": "overridden", - "reason": "Sample precedence: line-comment-position, lines-around-comment retains its inherited/explicit options and severity." + "reason": "Sample precedence: line-comment-position, lines-around-comment retains its inherited/explicit options and severity.", + "guideline": { + "rules": [ + "line-comment-position", + "lines-around-comment" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "comments--spaces", @@ -8472,7 +9738,14 @@ "spaced-comment" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "spaced-comment" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "comments--actionitems", @@ -8481,7 +9754,12 @@ "line": 1979, "rules": [], "enforcement": "manual", - "reason": "TODO/FIXME classify work urgency; the author annotation is explicitly optional." + "reason": "TODO/FIXME classify work urgency; the author annotation is explicitly optional.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "TODO/FIXME classify work urgency; the author annotation is explicitly optional." + } }, { "id": "comments-jsdoc", @@ -8489,15 +9767,17 @@ "text": "To force right style of comments every eslint config should connect [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc). With all recommended rules + `jsdoc/require-file-overview`", "line": 1988, "rules": [ - "jsdoc/require-file-overview", - "jsdoc/require-description", - "jsdoc/require-description-complete-sentence", - "jsdoc/require-hyphen-before-param-description", - "jsdoc/require-throws", - "jsdoc/sort-tags" + "jsdoc/require-file-overview" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "The recommended eslint-plugin-jsdoc configuration plus jsdoc/require-file-overview.", + "guideline": { + "rules": [ + "jsdoc/require-file-overview" + ], + "enforcement": "javascript", + "reason": "The recommended eslint-plugin-jsdoc configuration plus jsdoc/require-file-overview." + } }, { "id": "whitespace--spaces", @@ -8505,10 +9785,21 @@ "text": "Use soft tabs (space character) set to 4 spaces. eslint: [`indent`](https://eslint.org/docs/rules/indent)", "line": 1995, "rules": [ - "indent" + "indent", + "react/jsx-indent", + "react/jsx-indent-props" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "indent requires four spaces; inherited react/jsx-indent and react/jsx-indent-props require two-space JSX, which the guideline policy changes to four.", + "guideline": { + "rules": [ + "indent", + "react/jsx-indent", + "react/jsx-indent-props" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--before-blocks", @@ -8519,7 +9810,14 @@ "space-before-blocks" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "space-before-blocks" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--around-keywords", @@ -8530,7 +9828,14 @@ "keyword-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "keyword-spacing" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--infix-ops", @@ -8541,7 +9846,14 @@ "space-infix-ops" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "space-infix-ops" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--newline-at-end", @@ -8552,7 +9864,14 @@ "eol-last" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "eol-last" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--chains", @@ -8563,8 +9882,16 @@ "newline-per-chained-call", "no-whitespace-before-property" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "Inherited newline-per-chained-call allows four calls on one line; the guideline policy allows two.", + "guideline": { + "rules": [ + "newline-per-chained-call", + "no-whitespace-before-property" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--after-blocks", @@ -8575,7 +9902,14 @@ "padding-line-between-statements" ], "enforcement": "overridden", - "reason": "Sample precedence: padding-line-between-statements retains its inherited/explicit options and severity." + "reason": "Sample precedence: padding-line-between-statements retains its inherited/explicit options and severity.", + "guideline": { + "rules": [ + "padding-line-between-statements" + ], + "enforcement": "partial", + "reason": "padding-line-between-statements requires a blank line after block-like statements; blank lines between multiline members of object and array literals remain reviewable." + } }, { "id": "whitespace--padded-blocks", @@ -8586,7 +9920,14 @@ "padded-blocks" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "padded-blocks" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--no-multiple-blanks", @@ -8597,7 +9938,14 @@ "no-multiple-empty-lines" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-multiple-empty-lines" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--in-parens", @@ -8608,7 +9956,14 @@ "space-in-parens" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "space-in-parens" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--in-brackets", @@ -8619,7 +9974,14 @@ "array-bracket-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "array-bracket-spacing" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--in-braces", @@ -8630,7 +9992,14 @@ "object-curly-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "object-curly-spacing" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--max-len", @@ -8641,7 +10010,14 @@ "max-len" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "max-len" + ], + "enforcement": "javascript", + "reason": "max-len exempts strings and template literals, as the clause notes." + } }, { "id": "whitespace--block-spacing", @@ -8652,7 +10028,14 @@ "block-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "block-spacing" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--comma-spacing", @@ -8663,7 +10046,14 @@ "comma-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "comma-spacing" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--computed-property-spacing", @@ -8674,7 +10064,14 @@ "computed-property-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "computed-property-spacing" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--func-call-spacing", @@ -8685,7 +10082,14 @@ "func-call-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "func-call-spacing" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--key-spacing", @@ -8696,7 +10100,14 @@ "key-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "key-spacing" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--no-trailing-spaces", @@ -8707,7 +10118,14 @@ "no-trailing-spaces" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-trailing-spaces" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "whitespace--no-multiple-empty-lines", @@ -8718,7 +10136,14 @@ "no-multiple-empty-lines" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-multiple-empty-lines" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "commas--leading-trailing", @@ -8729,7 +10154,14 @@ "comma-style" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "comma-style" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "commas--dangling", @@ -8740,7 +10172,14 @@ "comma-dangle" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "comma-dangle" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "semicolons--required", @@ -8751,7 +10190,14 @@ "semi" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "semi" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "coercion--explicit", @@ -8760,7 +10206,12 @@ "line": 2671, "rules": [], "enforcement": "manual", - "reason": "Determining where a semantic conversion belongs requires review." + "reason": "Determining where a semantic conversion belongs requires review.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Determining where a semantic conversion belongs requires review." + } }, { "id": "coercion--strings", @@ -8771,7 +10222,14 @@ "no-new-wrappers" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-new-wrappers" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "coercion--numbers", @@ -8783,7 +10241,15 @@ "no-new-wrappers" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "radix", + "no-new-wrappers" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "coercion--comment-deviations", @@ -8794,7 +10260,14 @@ "no-bitwise" ], "enforcement": "overridden", - "reason": "Sample precedence: no-bitwise retains its inherited/explicit options and severity." + "reason": "Sample precedence: no-bitwise retains its inherited/explicit options and severity.", + "guideline": { + "rules": [ + "no-bitwise" + ], + "enforcement": "overridden", + "reason": "Sample precedence: no-bitwise retains its inherited/explicit options and severity." + } }, { "id": "coercion--bitwise", @@ -8803,7 +10276,12 @@ "line": 2733, "rules": [], "enforcement": "manual", - "reason": "Educational warning about bitwise numeric range." + "reason": "Educational warning about bitwise numeric range.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Educational warning about bitwise numeric range." + } }, { "id": "coercion--booleans", @@ -8814,7 +10292,14 @@ "no-new-wrappers" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-new-wrappers" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "naming--descriptive", @@ -8825,7 +10310,14 @@ "id-length" ], "enforcement": "overridden", - "reason": "Sample precedence: id-length retains its inherited/explicit options and severity." + "reason": "Sample precedence: id-length retains its inherited/explicit options and severity.", + "guideline": { + "rules": [ + "id-length" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "naming--camelCase", @@ -8836,7 +10328,14 @@ "camelcase" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "camelcase" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "naming--PascalCase", @@ -8847,7 +10346,14 @@ "new-cap" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "new-cap" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "naming--leading-underscore", @@ -8858,7 +10364,14 @@ "no-underscore-dangle" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-underscore-dangle" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "naming--self-this", @@ -8869,7 +10382,14 @@ "unicorn/no-this-assignment" ], "enforcement": "native", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "unicorn/no-this-assignment" + ], + "enforcement": "native", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "naming--PascalCase-singleton", @@ -8878,7 +10398,12 @@ "line": 2864, "rules": [], "enforcement": "manual", - "reason": "Identifying a singleton or function library requires API-level intent." + "reason": "Identifying a singleton or function library requires API-level intent.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Identifying a singleton or function library requires API-level intent." + } }, { "id": "naming--Acronyms-and-Initialisms", @@ -8887,7 +10412,12 @@ "line": 2876, "rules": [], "enforcement": "manual", - "reason": "Recognizing domain acronyms requires a project vocabulary." + "reason": "Recognizing domain acronyms requires a project vocabulary.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Recognizing domain acronyms requires a project vocabulary." + } }, { "id": "naming--constants", @@ -8896,7 +10426,14 @@ "line": 2912, "rules": [], "enforcement": "manual", - "reason": "The guide distinguishes semantic constants from ordinary const bindings without defining a boundary." + "reason": "The guide distinguishes semantic constants from ordinary const bindings without defining a boundary.", + "guideline": { + "rules": [ + "ag/constant-name" + ], + "enforcement": "partial", + "reason": "ag/constant-name requires UPPER_SNAKE_CASE for module-level const bindings initialized with primitive literals; other semantic constants remain reviewable." + } }, { "id": "accessors--not-required", @@ -8905,7 +10442,12 @@ "line": 2927, "rules": [], "enforcement": "manual", - "reason": "Accessor functions are explicitly optional." + "reason": "Accessor functions are explicitly optional.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Accessor functions are explicitly optional." + } }, { "id": "accessors--no-getters-setters", @@ -8916,7 +10458,14 @@ "ag/no-accessors" ], "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "ag/no-accessors" + ], + "enforcement": "custom", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "accessors--boolean-prefix", @@ -8925,7 +10474,12 @@ "line": 2957, "rules": [], "enforcement": "manual", - "reason": "Boolean API intent cannot be inferred reliably for arbitrary JavaScript methods." + "reason": "Boolean API intent cannot be inferred reliably for arbitrary JavaScript methods.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Boolean API intent cannot be inferred reliably for arbitrary JavaScript methods." + } }, { "id": "accessors--consistent", @@ -8934,7 +10488,12 @@ "line": 2972, "rules": [], "enforcement": "manual", - "reason": "Consistency of get/set APIs is a design-review requirement." + "reason": "Consistency of get/set APIs is a design-review requirement.", + "guideline": { + "rules": [], + "enforcement": "manual", + "reason": "Consistency of get/set APIs is a design-review requirement." + } }, { "id": "standard-library--isnan", @@ -8945,7 +10504,14 @@ "no-restricted-globals" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-restricted-globals" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "standard-library--isfinite", @@ -8956,7 +10522,14 @@ "no-restricted-globals" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "guideline": { + "rules": [ + "no-restricted-globals" + ], + "enforcement": "javascript", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + } }, { "id": "typescript--enum-naming-conventions", @@ -8967,7 +10540,14 @@ "ag/enum-name" ], "enforcement": "custom", - "reason": "Enum and member casing is enforced. Singular English names require review; values are unrestricted." + "reason": "Enum and member casing is enforced. Singular English names require review; values are unrestricted.", + "guideline": { + "rules": [ + "ag/enum-name" + ], + "enforcement": "custom", + "reason": "Enum and member casing is enforced. Singular English names require review; values are unrestricted." + } }, { "id": "typescript--tsconfig-strict", @@ -8976,7 +10556,12 @@ "line": 3084, "rules": [], "enforcement": "compiler", - "reason": "Enforced by shared tsconfig settings and compiler integration tests." + "reason": "Enforced by the shared tsconfig and verified with `ag-oxlint-config --check-tsconfig`.", + "guideline": { + "rules": [], + "enforcement": "compiler", + "reason": "Enforced by the shared tsconfig and verified with `ag-oxlint-config --check-tsconfig`." + } }, { "id": "typescript--tsconfig-nouncheckedindexedaccess", @@ -8985,7 +10570,12 @@ "line": 3095, "rules": [], "enforcement": "compiler", - "reason": "Enforced by shared tsconfig settings and compiler integration tests." + "reason": "Enforced by the shared tsconfig and verified with `ag-oxlint-config --check-tsconfig`.", + "guideline": { + "rules": [], + "enforcement": "compiler", + "reason": "Enforced by the shared tsconfig and verified with `ag-oxlint-config --check-tsconfig`." + } }, { "id": "typescript--caught-error-type", @@ -8996,7 +10586,14 @@ "ag/unknown-catch" ], "enforcement": "custom", - "reason": "Explicit any is rejected; unannotated catches use unknown through strict compiler settings." + "reason": "Explicit any is rejected on every catch binding; unannotated catches are unknown under the shared strict compiler settings.", + "guideline": { + "rules": [ + "ag/unknown-catch" + ], + "enforcement": "custom", + "reason": "Explicit any is rejected on every catch binding; unannotated catches are unknown under the shared strict compiler settings." + } } ], "rules": { @@ -10637,14 +12234,6 @@ "ag-jsdoc/ts-no-empty-object-type": "warn", "ag-jsdoc/valid-types": "warn", "ag-jsdoc/require-file-overview": "error", - "ag-jsdoc/require-description": "error", - "ag-jsdoc/require-description-complete-sentence": "error", - "ag-jsdoc/require-hyphen-before-param-description": [ - "error", - "never" - ], - "ag-jsdoc/require-throws": "error", - "ag-jsdoc/sort-tags": "error", "ag-compat/sort-imports": [ "error", { @@ -10659,63 +12248,491 @@ ] } ], - "ag-compat/no-restricted-imports": [ + "unicorn/prefer-node-protocol": "error", + "unicorn/no-this-assignment": "error", + "ag/no-accessors": "error", + "ag/no-direct-reexport": "error", + "ag/no-prototype-mutation": "error", + "ag/no-default-side-effects": "error", + "ag/prefer-array-from-map": "error", + "ag/require-docblock": "error" + }, + "typescriptRules": { + "ag-compat/default-param-last": "off", + "eslint/default-param-last": "error", + "ag-compat/no-empty-function": "off", + "eslint/no-empty-function": [ "error", { - "patterns": [ - { - "group": [ - "**/*-mv2", - "**/*-mv3" - ], - "message": "Do not import directly from MV2/MV3 implementations. Use the appropriate alias or index file instead." - } + "allow": [ + "arrowFunctions", + "functions", + "methods" ] } ], - "ag-import/no-unassigned-import": [ + "ag-compat/no-redeclare": "off", + "ag-ts/no-redeclare": "error", + "ag-compat/comma-dangle": "off", + "ag-style/comma-dangle": [ "error", { - "allow": [ - "**/*.pcss" - ] + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "always-multiline", + "enums": "always-multiline", + "generics": "always-multiline", + "tuples": "always-multiline" } ], - "ag-newlines/enforce": [ + "ag-compat/comma-spacing": "off", + "ag-style/comma-spacing": [ "error", { - "items": 3, - "max-len": 120 + "before": false, + "after": true } ], - "ag-boundaries/element-types": [ + "ag-compat/indent": "off", + "ag-style/indent": [ "error", + 4, { - "default": "allow", - "rules": [ - { - "from": "test-folder", - "disallow": [ - "src-index" - ], - "message": "Do not import directly from src/. Use specific submodules like src/utils instead." - } - ] + "SwitchCase": 1 + } + ], + "ag-compat/key-spacing": "off", + "ag-style/key-spacing": [ + "error", + { + "beforeColon": false, + "afterColon": true } ], - "ag-logger/require-logger-context": [ + "ag-compat/lines-between-class-members": "off", + "ag-style/lines-between-class-members": [ "error", + "always", { - "contextModuleName": "ext" + "exceptAfterSingleLine": false } ], - "unicorn/prefer-node-protocol": "error", - "unicorn/no-this-assignment": "error", - "ag/no-accessors": "error", - "ag/no-direct-reexport": "error", - "ag/no-prototype-mutation": "error", - "ag/no-default-side-effects": "error", - "ag/prefer-array-from-map": "error", - "ag/require-docblock": "error" + "ag-compat/no-array-constructor": "off", + "eslint/no-array-constructor": "error", + "ag-compat/object-curly-spacing": "off", + "ag-style/object-curly-spacing": [ + "error", + "always" + ], + "ag-compat/semi": "off", + "ag-style/semi": [ + "error", + "always" + ], + "ag-compat/space-before-blocks": "off", + "ag-style/space-before-blocks": "error", + "ag-compat/space-infix-ops": "off", + "ag-style/space-infix-ops": "error", + "ag-compat/no-shadow": "off", + "eslint/no-shadow": "error", + "ag-compat/no-undef": "off", + "ag-compat/no-unused-vars": "off", + "eslint/no-unused-vars": [ + "error", + { + "caughtErrors": "none", + "vars": "all", + "args": "after-used", + "ignoreRestSiblings": true + } + ], + "ag-compat/no-use-before-define": "off", + "eslint/no-use-before-define": [ + "error", + { + "functions": true, + "classes": true, + "variables": true + } + ], + "ag-compat/no-dupe-class-members": "off", + "eslint/no-dupe-class-members": "error", + "ag-compat/no-useless-constructor": "off", + "eslint/no-useless-constructor": "error", + "ag-import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": [ + "test/**", + "tests/**", + "spec/**", + "**/__tests__/**", + "**/__mocks__/**", + "test.{js,jsx}", + "test.{ts,tsx}", + "test-*.{js,jsx}", + "test-*.{ts,tsx}", + "**/*{.,_}{test,spec}.{js,jsx}", + "**/*{.,_}{test,spec}.{ts,tsx}", + "**/jest.config.js", + "**/jest.config.ts", + "**/jest.setup.js", + "**/jest.setup.ts", + "**/vue.config.js", + "**/vue.config.ts", + "**/webpack.config.js", + "**/webpack.config.ts", + "**/webpack.config.*.js", + "**/webpack.config.*.ts", + "**/rollup.config.js", + "**/rollup.config.ts", + "**/rollup.config.*.js", + "**/rollup.config.*.ts", + "**/gulpfile.js", + "**/gulpfile.ts", + "**/gulpfile.*.js", + "**/gulpfile.*.ts", + "**/Gruntfile{,.js}", + "**/Gruntfile{,.ts}", + "**/protractor.conf.js", + "**/protractor.conf.ts", + "**/protractor.conf.*.js", + "**/protractor.conf.*.ts", + "**/karma.conf.js", + "**/karma.conf.ts", + "**/.eslintrc.js", + "**/.eslintrc.ts" + ], + "optionalDependencies": false + } + ], + "ag-import/extensions": [ + "error", + "ignorePackages", + { + "js": "never", + "mjs": "never", + "jsx": "never", + "ts": "never", + "tsx": "never", + "mts": "never", + "cts": "never" + } + ], + "ag-react/jsx-filename-extension": [ + "error", + { + "extensions": [ + ".jsx", + ".tsx" + ] + } + ], + "ag-jsdoc/check-tag-names": [ + "warn", + { + "typed": true + } + ], + "ag-jsdoc/no-types": "error", + "ag-jsdoc/no-undefined-types": "off", + "ag-jsdoc/require-next-type": "off", + "ag-jsdoc/require-param-type": "off", + "ag-jsdoc/require-property-type": "off", + "ag-jsdoc/require-returns-type": "off", + "ag-jsdoc/require-throws-type": "off", + "ag-jsdoc/require-yields-type": "off", + "ag/enum-name": "error", + "ag/unknown-catch": "error" + }, + "policies": { + "guideline": { + "rules": { + "import/no-commonjs": "error", + "import/no-namespace": "error", + "ag-import/prefer-default-export": "off", + "import/no-default-export": "error", + "ag-import/order": [ + "error", + { + "groups": [ + [ + "builtin", + "external", + "internal" + ], + "parent", + [ + "sibling", + "index" + ] + ], + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ], + "ag-compat/line-comment-position": [ + "error", + { + "position": "above", + "ignorePattern": "^\\s*(?:oxlint-|@ts-)", + "applyDefaultPatterns": true + } + ], + "ag-compat/lines-around-comment": [ + "error", + { + "beforeBlockComment": false, + "beforeLineComment": true, + "allowBlockStart": true, + "allowObjectStart": true, + "allowArrayStart": true, + "allowClassStart": true + } + ], + "ag-compat/prefer-destructuring": [ + "error", + { + "VariableDeclarator": { + "array": true, + "object": true + }, + "AssignmentExpression": { + "array": true, + "object": false + } + }, + { + "enforceForRenamedProperties": false + } + ], + "ag-react/jsx-indent": [ + "error", + 4 + ], + "ag-react/jsx-indent-props": [ + "error", + 4 + ], + "ag-compat/newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 2 + } + ], + "ag-compat/padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + } + ], + "ag-compat/id-length": [ + "error", + { + "min": 2, + "properties": "never" + } + ], + "ag-compat/max-len": [ + "error", + { + "code": 120, + "comments": 120, + "tabWidth": 4, + "ignoreUrls": false, + "ignoreTrailingComments": false, + "ignoreComments": false, + "ignoreStrings": true, + "ignoreTemplateLiterals": true + } + ], + "ag/no-multiline-string-concat": "error", + "ag/multiline-condition-layout": "error", + "ag/require-docblock": [ + "error", + { + "lineCommentRuns": true + } + ], + "ag/constant-name": "error" + }, + "typescriptRules": { + "ag-compat/lines-around-comment": "off", + "ag-style/lines-around-comment": [ + "error", + { + "beforeBlockComment": false, + "beforeLineComment": true, + "allowBlockStart": true, + "allowObjectStart": true, + "allowArrayStart": true, + "allowClassStart": true, + "allowInterfaceStart": true, + "allowTypeStart": true, + "allowEnumStart": true, + "allowModuleStart": true + } + ] + }, + "settings": {}, + "importGroups": { + "example": [ + "error", + { + "groups": [ + [ + "builtin", + "external", + "internal" + ], + "parent", + [ + "sibling", + "index" + ] + ], + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ], + "prose": [ + "error", + { + "groups": [ + "builtin", + "external", + "internal", + "parent", + [ + "sibling", + "index" + ] + ], + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ] + } + } + }, + "profiles": { + "adguard-projects": { + "rules": { + "ag-jsdoc/require-description": "error", + "ag-jsdoc/require-description-complete-sentence": "error", + "ag-jsdoc/require-hyphen-before-param-description": [ + "error", + "never" + ], + "ag-jsdoc/require-throws": "error", + "ag-jsdoc/sort-tags": "error", + "ag-compat/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "**/*-mv2", + "**/*-mv3" + ], + "message": "Do not import directly from MV2/MV3 implementations. Use the appropriate alias or index file instead." + } + ] + } + ], + "ag-import/no-unassigned-import": [ + "error", + { + "allow": [ + "**/*.pcss" + ] + } + ], + "ag-newlines/enforce": [ + "error", + { + "items": 3, + "max-len": 120 + } + ], + "ag-boundaries/element-types": [ + "error", + { + "default": "allow", + "rules": [ + { + "from": "test-folder", + "disallow": [ + "src-index" + ], + "message": "Do not import directly from src/. Use specific submodules like src/utils instead." + } + ] + } + ], + "ag-notice/notice": "off", + "ag-logger/require-logger-context": [ + "error", + { + "contextModuleName": "ext" + } + ] + }, + "typescriptRules": { + "typescript/consistent-type-imports": [ + "error", + { + "fixStyle": "inline-type-imports" + } + ], + "typescript/consistent-type-exports": "error", + "typescript/explicit-function-return-type": "error", + "typescript/explicit-member-accessibility": [ + "error", + { + "accessibility": "explicit", + "overrides": { + "accessors": "explicit", + "constructors": "no-public", + "methods": "explicit", + "properties": "off", + "parameterProperties": "explicit" + } + } + ], + "typescript/no-explicit-any": "error", + "typescript/no-var-requires": "error", + "typescript/ban-ts-comment": "error", + "typescript/dot-notation": "off", + "typescript/no-non-null-assertion": "off" + }, + "settings": { + "boundaries/elements": [ + { + "type": "src-index", + "pattern": "src/index.ts", + "mode": "file" + }, + { + "type": "test-folder", + "pattern": "test", + "mode": "folder" + } + ] + } + } } } diff --git a/packages/rule-catalog/src/index.ts b/packages/rule-catalog/src/index.ts index e55be92..5a1c883 100644 --- a/packages/rule-catalog/src/index.ts +++ b/packages/rule-catalog/src/index.ts @@ -5,15 +5,33 @@ import snapshot from './catalog.json' with { type: 'json' }; export type RuleSetting = string | number | readonly unknown[]; export type RuleMap = Record; -export type Enforcement = 'native' | 'javascript' | 'custom' | 'compiler' | 'manual' | 'overridden'; +export type Enforcement = 'native' | 'javascript' | 'custom' | 'compiler' | 'partial' | 'manual' | 'overridden'; +export type Policy = 'compatibility' | 'guideline'; +export type Profile = 'guide' | 'adguard-projects'; +export type ImportGroups = 'example' | 'prose'; +export type Implementation = 'native' | 'javascript' | 'custom' | 'disabled'; -export interface Clause { +/** How one policy enforces a guideline clause. */ +export interface Disposition { + rules: string[]; + enforcement: Enforcement; + reason: string; +} + +/** A numbered guideline clause; top-level fields describe the compatibility policy. */ +export interface Clause extends Disposition { id: string; number: string; text: string; line: number; - rules: string[]; - enforcement: Enforcement; + guideline: Disposition; +} + +/** The TypeScript-aware implementation used for a rule in TypeScript files. */ +export interface TypeScriptEquivalent { + target: string | null; + setting: RuleSetting; + implementation: Implementation; reason: string; } @@ -21,9 +39,21 @@ export interface Mapping { source: string; target: string | null; setting: RuleSetting; - implementation: 'native' | 'javascript' | 'custom' | 'disabled'; + implementation: Implementation; origin: string; + /** Absent for the base preset. */ + scope?: 'guideline' | 'adguard-projects'; + /** Rules that only apply to TypeScript files. */ + language?: 'typescript'; requiresTypeInfo?: boolean; + typescript?: TypeScriptEquivalent; +} + +/** Rules and settings layered over the base preset. */ +export interface Overlay { + rules: RuleMap; + typescriptRules: RuleMap; + settings: Record; } export interface Catalog { @@ -34,6 +64,9 @@ export interface Catalog { mappings: Mapping[]; clauses: Clause[]; rules: RuleMap; + typescriptRules: RuleMap; + policies: { guideline: Overlay & { importGroups: Record } }; + profiles: { 'adguard-projects': Overlay }; } export const catalog = snapshot as Catalog; diff --git a/scripts/generate.ts b/scripts/generate.ts index 6c4dd4d..f8cf384 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -15,7 +15,10 @@ import { severity } from '../packages/rule-catalog/src/index'; import type { Catalog, Clause, + Disposition, + ImportGroups, Mapping, + Policy, RuleMap, RuleSetting, } from '../packages/rule-catalog/src/index'; @@ -104,11 +107,6 @@ apply(airbnb, 'airbnb@19.0.4/base@15.0.0'); hash('airbnb.json', airbnbText); apply(jsdoc.configs.recommended ?? {}, 'jsdoc@64.3.6/recommended'); hash('jsdoc@64.3.6/recommended', JSON.stringify(jsdoc.configs.recommended)); -// Default boundary elements for the catalog (consumers override via settings). -settings['boundaries/elements'] = [ - { type: 'src-index', pattern: 'src/index.ts', mode: 'file' }, - { type: 'test-folder', pattern: 'test', mode: 'folder' }, -]; const sampleText = await readFile('docs/reference/eslintrc.upstream.txt', 'utf8'); const guideText = await readFile('docs/reference/Javascript.md', 'utf8'); hash('Javascript.md', guideText); @@ -116,19 +114,99 @@ hash('eslintrc.cjs', sampleText); verifyReference(sampleText, sample); apply(sample, 'eslintrc.cjs'); -const additions: RuleMap = { +type Scope = 'base' | NonNullable; + +/** + * Read inherited rule options without the severity. + * @param name - Source rule identifier. + * @returns Options from the resolved sample configuration. + */ +function inheritedOptions(name: string): unknown[] { + const setting = baseline[name]; + return Array.isArray(setting) ? setting.slice(1) : []; +} + +// Requirements stated by the guide that the sample configuration does not configure. +const guideAdditions: RuleMap = { 'jsdoc/require-file-overview': 'error', - 'jsdoc/require-description': 'error', - 'jsdoc/require-description-complete-sentence': 'error', - 'jsdoc/require-hyphen-before-param-description': ['error', 'never'], - 'jsdoc/require-throws': 'error', - 'jsdoc/sort-tags': 'error', 'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: true, ignoreMemberSort: false, memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], }], + 'unicorn/prefer-node-protocol': 'error', + 'unicorn/no-this-assignment': 'error', + 'ag/no-accessors': 'error', + 'ag/no-direct-reexport': 'error', + 'ag/no-prototype-mutation': 'error', + 'ag/no-default-side-effects': 'error', + 'ag/prefer-array-from-map': 'error', + 'ag/require-docblock': 'error', +}; +const guideTypeScriptAdditions: RuleMap = { + 'ag/enum-name': 'error', + 'ag/unknown-catch': 'error', +}; + +// The opt-in guideline policy resolves documented conflicts between guide prose and the sample in favor of prose. +const importOrder = (groups: unknown[]): RuleSetting => ['error', { + groups, + 'newlines-between': 'always', + alphabetize: { order: 'asc', caseInsensitive: true }, +}]; +const importGroups: Record = { + // The guide's example keeps built-in and package imports in one block. + example: importOrder([['builtin', 'external', 'internal'], 'parent', ['sibling', 'index']]), + // The guide's prose separates every listed category. + prose: importOrder(['builtin', 'external', 'internal', 'parent', ['sibling', 'index']]), +}; +const guidelineAdditions: RuleMap = { + 'import/no-commonjs': 'error', + 'import/no-namespace': 'error', + 'import/prefer-default-export': 'off', + 'import/no-default-export': 'error', + 'import/order': importGroups.example, + 'line-comment-position': ['error', { + ...(inheritedOptions('line-comment-position')[0] as object), + ignorePattern: '^\\s*(?:oxlint-|@ts-)', + }], + // Clause 17.2 governs single-line comments; block comments keep their inherited freedom. + 'lines-around-comment': ['error', { + beforeBlockComment: false, + beforeLineComment: true, + allowBlockStart: true, + allowObjectStart: true, + allowArrayStart: true, + allowClassStart: true, + }], + 'prefer-destructuring': ['error', { + ...(inheritedOptions('prefer-destructuring')[0] as object), + VariableDeclarator: { array: true, object: true }, + }, ...inheritedOptions('prefer-destructuring').slice(1)], + 'react/jsx-indent': ['error', 4], + 'react/jsx-indent-props': ['error', 4], + 'newline-per-chained-call': ['error', { ignoreChainWithDepth: 2 }], + 'padding-line-between-statements': ['error', { blankLine: 'always', prev: 'block-like', next: '*' }], + 'id-length': ['error', { min: 2, properties: 'never' }], + 'max-len': ['error', { + ...(inheritedOptions('max-len')[0] as object), + ignoreStrings: true, + ignoreTemplateLiterals: true, + }], + 'ag/no-multiline-string-concat': 'error', + 'ag/multiline-condition-layout': 'error', + 'ag/require-docblock': ['error', { lineCommentRuns: true }], + 'ag/constant-name': 'error', +}; + +// Rules used by AdGuard projects that the guide does not require; opt-in through the adguard-projects profile. +const projectAdditions: RuleMap = { + 'jsdoc/require-description': 'error', + 'jsdoc/require-description-complete-sentence': 'error', + 'jsdoc/require-hyphen-before-param-description': ['error', 'never'], + 'jsdoc/require-throws': 'error', + 'jsdoc/sort-tags': 'error', 'no-restricted-imports': ['error', { patterns: [{ group: ['**/*-mv2', '**/*-mv3'], @@ -147,6 +225,8 @@ const additions: RuleMap = { }], 'notice/notice': 'off', '@adguard/logger-context/require-logger-context': ['error', { contextModuleName: 'ext' }], +}; +const projectTypeScriptAdditions: RuleMap = { '@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports' }], '@typescript-eslint/consistent-type-exports': 'error', '@typescript-eslint/explicit-function-return-type': 'error', @@ -165,21 +245,181 @@ const additions: RuleMap = { '@typescript-eslint/ban-ts-comment': 'error', '@typescript-eslint/dot-notation': 'off', '@typescript-eslint/no-non-null-assertion': 'off', - 'unicorn/prefer-node-protocol': 'error', - 'unicorn/no-this-assignment': 'error', - 'ag/no-accessors': 'error', - 'ag/no-direct-reexport': 'error', - 'ag/no-prototype-mutation': 'error', - 'ag/no-default-side-effects': 'error', - 'ag/prefer-array-from-map': 'error', - 'ag/require-docblock': 'error', +}; +const projectSettings: Record = { + // Default boundary elements (consumers override via settings). + 'boundaries/elements': [ + { type: 'src-index', pattern: 'src/index.ts', mode: 'file' }, + { type: 'test-folder', pattern: 'test', mode: 'folder' }, + ], +}; + +/** + * Replace the options of a rule setting while keeping its severity. + * @param setting - Original setting. + * @param update - Derives new options from the original options. + * @returns Setting with derived options. + */ +function withOptions(setting: RuleSetting, update: (options: unknown[]) => unknown[]): RuleSetting { + return Array.isArray(setting) ? [setting[0], ...update(setting.slice(1))] : [setting, ...update([])]; +} + +interface Equivalent { + target: string | null; + reason: string; + adapt?: (setting: RuleSetting) => RuleSetting; +} + +const TYPESCRIPT_EXTENSIONS = { + ts: 'never', + tsx: 'never', + mts: 'never', + cts: 'never', +}; + +// "In TypeScript we use the same rules as in JavaScript": each entry selects a TypeScript-aware +// implementation of the same rule and options. Entries are limited to rules whose pinned JavaScript +// implementation reports valid TypeScript or ignores TypeScript syntax (see tests/typescript-preset.test.ts). +const TYPESCRIPT_EQUIVALENTS: Record = { + 'no-undef': { target: null, reason: 'The TypeScript compiler reports unresolved names, including ambient types.' }, + 'no-unused-vars': { + target: 'eslint/no-unused-vars', + reason: 'Understands type-only usage, signatures, and parameter properties.', + adapt: (setting) => withOptions(setting, ([options]) => [{ caughtErrors: 'none', ...(options as object) }]), + }, + 'no-shadow': { target: 'eslint/no-shadow', reason: 'Ignores type-space parameters in signatures.' }, + 'no-useless-constructor': { + target: 'eslint/no-useless-constructor', + reason: 'Keeps constructors that declare parameter properties or accessibility.', + }, + 'no-empty-function': { + target: 'eslint/no-empty-function', + reason: 'Understands parameter-property constructors and overload signatures.', + }, + 'no-dupe-class-members': { target: 'eslint/no-dupe-class-members', reason: 'Accepts method overload signatures.' }, + 'no-redeclare': { + target: 'ag-ts/no-redeclare', + reason: 'Accepts overloads and declaration merges that TypeScript permits.', + }, + 'no-array-constructor': { + target: 'eslint/no-array-constructor', + reason: 'Accepts explicitly typed constructors such as `new Array()`.', + }, + 'default-param-last': { target: 'eslint/default-param-last', reason: 'Treats optional parameters like defaults.' }, + 'no-use-before-define': { + target: 'eslint/no-use-before-define', + reason: 'Ignores type-only references such as `typeof` queries.', + }, + indent: { target: 'ag-style/indent', reason: 'Indents interfaces, enums, and type annotations.' }, + 'comma-dangle': { + target: 'ag-style/comma-dangle', + reason: 'Also checks enums, type parameters, and tuples with the array setting, as airbnb-typescript does.', + adapt: (setting) => withOptions(setting, ([options]) => { + const base = typeof options === 'string' + ? Object.fromEntries(['arrays', 'objects', 'imports', 'exports', 'functions'].map((key) => [key, options])) + : (options as Record); + return [{ + ...base, + enums: base.arrays, + generics: base.arrays, + tuples: base.arrays, + }]; + }), + }, + 'comma-spacing': { + target: 'ag-style/comma-spacing', + reason: 'Checks type arguments and accepts the TSX `` form.', + }, + 'key-spacing': { target: 'ag-style/key-spacing', reason: 'Checks interface and type literal members.' }, + 'lines-between-class-members': { + target: 'ag-style/lines-between-class-members', + reason: 'Accepts adjacent overload signatures.', + }, + 'object-curly-spacing': { target: 'ag-style/object-curly-spacing', reason: 'Checks type literals.' }, + semi: { target: 'ag-style/semi', reason: 'Checks type aliases, ambient declarations, and abstract members.' }, + 'space-before-blocks': { + target: 'ag-style/space-before-blocks', + reason: 'Checks interface, enum, and module bodies.', + }, + 'space-infix-ops': { + target: 'ag-style/space-infix-ops', + reason: 'Checks union and intersection types and enum initializers.', + }, + 'lines-around-comment': { + target: 'ag-style/lines-around-comment', + reason: 'Treats interface, type literal, enum, and namespace bodies like blocks.', + adapt: (setting) => withOptions(setting, ([options]) => { + const allow = (options ?? {}) as Record; + return [{ + ...allow, + allowInterfaceStart: allow.allowBlockStart, + allowTypeStart: allow.allowBlockStart, + allowEnumStart: allow.allowBlockStart, + allowModuleStart: allow.allowBlockStart, + }]; + }), + }, + 'import/extensions': { + target: 'ag-import/extensions', + reason: 'TypeScript module specifiers omit TypeScript extensions as well.', + adapt: (setting) => withOptions(setting, ([mode, options]) => [ + mode, + { ...(options as object), ...TYPESCRIPT_EXTENSIONS }, + ]), + }, + 'import/no-extraneous-dependencies': { + target: 'ag-import/no-extraneous-dependencies', + reason: 'Adds TypeScript variants of the development file globs, as airbnb-typescript does.', + adapt: (setting) => withOptions(setting, ([options]) => { + const { devDependencies } = options as { devDependencies: string[] }; + return [{ + ...(options as object), + devDependencies: devDependencies.flatMap((glob) => { + const typed = glob.replace(/\bjs(x?)\b/gu, 'ts$1'); + return typed === glob ? [glob] : [glob, typed]; + }), + }]; + }), + }, + 'react/jsx-filename-extension': { + target: 'ag-react/jsx-filename-extension', + reason: 'Allows JSX in .tsx files.', + adapt: (setting) => withOptions(setting, () => [{ extensions: ['.jsx', '.tsx'] }]), + }, + 'jsdoc/check-tag-names': { + target: 'ag-jsdoc/check-tag-names', + reason: 'Uses TypeScript tag semantics.', + adapt: (setting) => withOptions(setting, () => [{ typed: true }]), + }, + 'jsdoc/no-types': { + target: 'ag-jsdoc/no-types', + reason: 'Types come from TypeScript annotations rather than duplicated JSDoc types.', + adapt: () => 'error', + }, + ...Object.fromEntries([ + 'jsdoc/require-param-type', + 'jsdoc/require-property-type', + 'jsdoc/require-returns-type', + 'jsdoc/require-next-type', + 'jsdoc/require-yields-type', + 'jsdoc/require-throws-type', + 'jsdoc/no-undefined-types', + ].map((name) => [name, { target: null, reason: 'Types come from TypeScript annotations.' }])), }; +const typescriptPlugin = await import('../packages/oxlint-plugin/src/typescript'); +const typescriptTargets = new Set([ + ...native, + ...Object.keys(style.rules).map((name) => `ag-style/${name}`), + ...Object.keys(typescriptPlugin.rules).map((name) => `ag-ts/${name}`), +]); + const extraClauses: Record = { 'types--primitives': [], 'references--block-scope': ['no-undef'], 'arrays--mapping': ['ag/prefer-array-from-map'], 'strings--line-length': ['max-len', 'no-useless-concat'], + 'functions--in-blocks': ['no-loop-func', 'no-inner-declarations'], 'functions--arguments-shadow': ['no-shadow-restricted-names'], 'es6-default-parameters': ['no-param-reassign'], 'functions--default-side-effects': ['ag/no-default-side-effects'], @@ -188,6 +428,7 @@ const extraClauses: Record = { 'modules--use-them': ['import/no-commonjs', 'import/no-amd'], 'modules--no-wildcard': ['import/no-namespace'], 'modules--no-export-from-import': ['ag/no-direct-reexport'], + 'modules--no-duplicate-imports': ['no-duplicate-imports', 'import/no-duplicates'], 'modules--prefer-named-export': ['import/prefer-default-export'], 'modules--import-node-protocol': ['unicorn/prefer-node-protocol'], 'properties--bracket': ['dot-notation'], @@ -195,14 +436,8 @@ const extraClauses: Record = { 'control-statement--value-selection': ['no-unused-expressions'], 'comments--multiline': ['ag/require-docblock'], 'comments--singleline': ['line-comment-position', 'lines-around-comment'], - 'comments-jsdoc': [ - 'jsdoc/require-file-overview', - 'jsdoc/require-description', - 'jsdoc/require-description-complete-sentence', - 'jsdoc/require-hyphen-before-param-description', - 'jsdoc/require-throws', - 'jsdoc/sort-tags', - ], + 'comments-jsdoc': ['jsdoc/require-file-overview'], + 'whitespace--spaces': ['indent', 'react/jsx-indent', 'react/jsx-indent-props'], 'whitespace--after-blocks': ['padding-line-between-statements'], 'coercion--comment-deviations': ['no-bitwise'], 'naming--self-this': ['unicorn/no-this-assignment'], @@ -211,6 +446,14 @@ const extraClauses: Record = { 'typescript--caught-error-type': ['ag/unknown-catch'], }; +// Rules that the guideline policy adds to a clause's compatibility rules. +const guidelineClauses: Record = { + 'strings--line-length': ['max-len', 'ag/no-multiline-string-concat'], + 'modules--prefer-named-export': ['import/no-default-export'], + 'control-statements': ['operator-linebreak', 'ag/multiline-condition-layout'], + 'naming--constants': ['ag/constant-name'], +}; + const manual: Record = { 'types--primitives': 'Educational semantics; supported browser targets must be selected by the consumer.', 'types--complex': 'Educational explanation of reference values.', @@ -241,6 +484,65 @@ const manual: Record = { 'accessors--consistent': 'Consistency of get/set APIs is a design-review requirement.', }; +// Clauses whose linked rules are active but do not enforce every stated requirement. +const partial: Record> = { + compatibility: { + 'destructuring--array': + 'Inherited prefer-destructuring checks assignments but not declarations such as `const first = arr[0]`; ' + + 'the guideline policy checks both.', + 'modules--import-grouping': + 'Inherited import/order places built-in, package, and internal imports first, but does not require blank ' + + 'lines between groups or separate parent from sibling imports; use the guideline policy.', + 'comments--multiline': + 'Multiline /* */ blocks are rejected; runs of // line comments (the clause example) are rejected only by ' + + 'the guideline policy.', + 'control-statements': + 'operator-linebreak puts logical operators first; starting the condition on its own line is enforced by ' + + 'the guideline policy.', + 'whitespace--spaces': + 'indent requires four spaces; inherited react/jsx-indent and react/jsx-indent-props require two-space ' + + 'JSX, which the guideline policy changes to four.', + 'whitespace--chains': + 'Inherited newline-per-chained-call allows four calls on one line; the guideline policy allows two.', + }, + guideline: { + 'whitespace--after-blocks': + 'padding-line-between-statements requires a blank line after block-like statements; blank lines between ' + + 'multiline members of object and array literals remain reviewable.', + 'naming--constants': + 'ag/constant-name requires UPPER_SNAKE_CASE for module-level const bindings initialized with primitive ' + + 'literals; other semantic constants remain reviewable.', + }, +}; + +// Clauses whose explanation is more specific than the enforcement category. +const reasons: Record> = { + compatibility: { + 'modules--no-duplicate-imports': + 'Airbnb disables no-duplicate-imports in favor of import/no-duplicates, which enforces the clause.', + 'modules--import-order': + 'Inherited import/order options do not alphabetize; the guideline policy orders imports by path.', + 'comments-jsdoc': 'The recommended eslint-plugin-jsdoc configuration plus jsdoc/require-file-overview.', + 'typescript--enum-naming-conventions': + 'Enum and member casing is enforced. Singular English names require review; values are unrestricted.', + 'typescript--caught-error-type': + 'Explicit any is rejected on every catch binding; unannotated catches are unknown under the shared ' + + 'strict compiler settings.', + }, + guideline: { + 'modules--import-grouping': + 'import/order requires blank lines between groups. `importGroups: example` follows the clause example ' + + '(built-in and package imports share a block); `prose` separates every listed category.', + 'modules--import-order': 'import/order alphabetizes imports by path within each group.', + 'modules--prefer-named-export': + 'import/prefer-default-export is disabled and import/no-default-export rejects default exports.', + 'strings--line-length': + 'max-len ignores strings and template literals; ag/no-multiline-string-concat rejects strings broken ' + + 'across lines with concatenation.', + 'whitespace--max-len': 'max-len exempts strings and template literals, as the clause notes.', + }, +}; + const clauseMatches = [...guideText.matchAll(/^- \[(\d+\.\d+)\]\(#([^)]+)\)(.*)$/gmu)]; const clauses: Clause[] = clauseMatches.map((match, index) => { const start = match.index; @@ -262,31 +564,84 @@ const clauses: Clause[] = clauseMatches.map((match, index) => { rules: linked, enforcement: 'manual', reason: manual[id] ?? '', + guideline: { rules: [...new Set(guidelineClauses[id] ?? linked)], enforcement: 'manual', reason: '' }, }; }); -const guideOptions: RuleMap = { - 'no-const-assign': 'error', - 'no-var': 'error', - 'no-restricted-properties': ['error', { object: 'Math', property: 'pow', message: 'Use the ** operator.' }], - 'padding-line-between-statements': ['error', { blankLine: 'always', prev: 'block-like', next: '*' }], -}; -clauses.forEach((clause) => clause.rules.forEach((rule) => { - if (baseline[rule] === undefined && additions[rule] === undefined && !rule.startsWith('ag/')) { - additions[rule] = guideOptions[rule] ?? 'error'; +const known = new Set([ + ...Object.keys(baseline), + ...Object.keys(guideAdditions), + ...Object.keys(guideTypeScriptAdditions), + ...Object.keys(guidelineAdditions), +]); +clauses.forEach((clause) => [...clause.rules, ...clause.guideline.rules].forEach((rule) => { + if (!known.has(rule)) { + throw new Error(`Clause ${clause.id} links unmapped rule ${rule}`); } })); const mappings: Mapping[] = []; -const rules: RuleMap = {}; +const layers: Record = { + base: { rules: {}, typescriptRules: {} }, + guideline: { rules: {}, typescriptRules: {} }, + 'adguard-projects': { rules: {}, typescriptRules: {} }, +}; /** - * Choose a concrete provider, refusing to silently drop an enabled setting. + * Choose the concrete provider for a source rule. + * @param source - Original rule identifier. + * @returns Target rule and implementation kind. + */ +function resolveTarget(source: string): { target: string; implementation: Mapping['implementation'] } { + if (source.startsWith('ag/')) { + return { target: source, implementation: 'custom' }; + } + const prefixes: [string, Provider | null, string][] = [ + ['jsdoc/', jsdoc, 'ag-jsdoc/'], + ['react/', react, 'ag-react/'], + ['jsx-a11y/', accessibility, 'ag-a11y/'], + ['import/', imports, 'ag-import/'], + ['import-newlines/', newlines, 'ag-newlines/'], + ['boundaries/', boundaries, 'ag-boundaries/'], + ['notice/', notice, 'ag-notice/'], + ['@adguard/logger-context/', logger, 'ag-logger/'], + ]; + if (builtinRules.has(source)) { + return { target: `ag-compat/${source}`, implementation: 'javascript' }; + } + for (const [prefix, provider, namespace] of prefixes) { + const name = source.slice(prefix.length); + if (source.startsWith(prefix) && provider?.rules[name]) { + return { target: `${namespace}${name}`, implementation: 'javascript' }; + } + } + if (source.startsWith('@typescript-eslint/') && native.has(`typescript/${source.slice('@typescript-eslint/'.length)}`)) { + return { target: `typescript/${source.slice('@typescript-eslint/'.length)}`, implementation: 'native' }; + } + if (style.rules[source]) { + return { target: `ag-style/${source}`, implementation: 'javascript' }; + } + if (native.has(source)) { + return { target: source, implementation: 'native' }; + } + throw new Error(`No implementation for rule ${source}`); +} + +/** + * Record a source rule in one configuration layer, refusing to silently drop an enabled setting. * @param source - Original rule identifier. * @param originalSetting - Resolved setting. * @param origin - Source of the setting. + * @param scope - Configuration layer. + * @param language - Restrict the rule to TypeScript files. */ -function mapRule(source: string, originalSetting: RuleSetting, origin: string): void { +function mapRule( + source: string, + originalSetting: RuleSetting, + origin: string, + scope: Scope, + language?: 'typescript', +): void { let setting = originalSetting; if (source === 'import/no-cycle' && Array.isArray(setting)) { const options = { ...setting[1] }; @@ -295,129 +650,150 @@ function mapRule(source: string, originalSetting: RuleSetting, origin: string): } setting = [setting[0], options]; } - const level = Array.isArray(setting) ? setting[0] : setting; - if (level === 'off' || level === 0) { + const disabled = severity(setting) === 0; + const scoped = scope === 'base' ? {} : { scope }; + if (disabled && scope === 'base') { + mappings.push({ + source, target: null, setting, implementation: 'disabled', origin, + }); + } else { + const { target, implementation } = resolveTarget(source); + const layer = layers[scope]; + const rules = language === 'typescript' ? layer.typescriptRules : layer.rules; + if (rules[target] !== undefined && JSON.stringify(rules[target]) !== JSON.stringify(setting)) { + throw new Error(`Conflicting provider mapping for ${source}: ${target}`); + } + rules[target] = setting; mappings.push({ source, - target: null, + target, setting, - implementation: 'disabled', + implementation: disabled ? 'disabled' : implementation, origin, + ...scoped, + ...(language ? { language } : {}), + ...(typedNative.has(target) ? { requiresTypeInfo: true } : {}), }); - return; - } - let target: string | null = null; - let implementation: Mapping['implementation'] = 'javascript'; - if (source.startsWith('ag/')) { - target = source; - implementation = 'custom'; - } else if (builtinRules.has(source)) { - target = `ag-compat/${source}`; - } else if (source.startsWith('jsdoc/') && jsdoc.rules[source.slice(6)]) { - target = `ag-jsdoc/${source.slice(6)}`; - } else if (source.startsWith('react/') && react.rules[source.slice(6)]) { - target = `ag-react/${source.slice(6)}`; - } else if (source.startsWith('jsx-a11y/') && accessibility.rules[source.slice(9)]) { - target = `ag-a11y/${source.slice(9)}`; - } else if (source.startsWith('import/') && imports.rules[source.slice(7)]) { - target = `ag-import/${source.slice(7)}`; - } else if (source.startsWith('import-newlines/') && newlines.rules[source.slice('import-newlines/'.length)]) { - target = `ag-newlines/${source.slice('import-newlines/'.length)}`; - } else if (source.startsWith('boundaries/') && boundaries.rules[source.slice('boundaries/'.length)]) { - target = `ag-boundaries/${source.slice('boundaries/'.length)}`; - } else if (source.startsWith('notice/') && notice.rules[source.slice(7)]) { - target = `ag-notice/${source.slice(7)}`; - } else if (source.startsWith('@adguard/logger-context/') && logger.rules[source.slice('@adguard/logger-context/'.length)]) { - target = `ag-logger/${source.slice('@adguard/logger-context/'.length)}`; - } else if (source.startsWith('@typescript-eslint/') && native.has(`typescript/${source.slice('@typescript-eslint/'.length)}`)) { - target = `typescript/${source.slice('@typescript-eslint/'.length)}`; - implementation = 'native'; - } else if (style.rules[source]) { - target = `ag-style/${source}`; - } else if (native.has(source)) { - target = source; - implementation = 'native'; - } - if (!target) { - throw new Error(`No implementation for enabled rule ${source}`); } - if (rules[target] !== undefined && JSON.stringify(rules[target]) !== JSON.stringify(setting)) { - // Directive spacing and block spacing are independent constraints of one rule. - if (target === 'ag-style/padding-line-between-statements') { - setting = [level, ...(rules[target] as unknown[]).slice(1), ...(setting as unknown[]).slice(1)]; - } else { - throw new Error(`Conflicting provider mapping for ${source}: ${target}`); + const equivalent = TYPESCRIPT_EQUIVALENTS[source]; + if (equivalent && !language) { + const mapping = mappings.at(-1)!; + if (equivalent.target !== null && !typescriptTargets.has(equivalent.target) + && equivalent.target !== resolveTarget(source).target) { + throw new Error(`Missing TypeScript implementation ${equivalent.target} for ${source}`); + } + const typed = equivalent.adapt ? equivalent.adapt(setting) : setting; + const typedSetting = equivalent.target === null || (disabled && !equivalent.adapt) ? 'off' : typed; + mapping.typescript = { + target: equivalent.target, + setting: typedSetting, + implementation: equivalent.target === null || severity(typedSetting) === 0 + ? 'disabled' + : equivalent.target.startsWith('eslint/') ? 'native' : 'javascript', + reason: equivalent.reason, + }; + const layer = layers[scope].typescriptRules; + if (mapping.target && mapping.target !== equivalent.target) { + layer[mapping.target] = 'off'; + } + // A rule disabled in JavaScript needs no TypeScript entry unless the equivalent enables it. + if (equivalent.target && (mapping.target !== null || severity(typedSetting) > 0)) { + layer[equivalent.target] = typedSetting; } } - rules[target] = setting; - if (target.startsWith('typescript/')) { - // TypeScript-only rules live in the preset override; keep them out of - // the shared root so plain JavaScript consumers stay unaffected. - delete rules[target]; - } - mappings.push({ - source, - target, - setting, - implementation, - origin, - ...(typedNative.has(target) ? { requiresTypeInfo: true } : {}), - }); } -Object.entries(baseline).forEach(([name, setting]) => mapRule(name, setting, origins[name] ?? 'baseline')); -Object.entries(additions).forEach(([name, setting]) => { - if (baseline[name] === undefined || severity(baseline[name]) === 0) { - delete baseline[name]; - const existing = mappings.findIndex((mapping) => mapping.source === name); - if (existing !== -1) { - mappings.splice(existing, 1); - } - mapRule(name, setting, 'Javascript.md'); +Object.entries(guideAdditions).forEach(([name]) => { + if (baseline[name] !== undefined && severity(baseline[name]) > 0) { + throw new Error(`Guide addition ${name} would replace an enabled sample setting`); } + delete baseline[name]; +}); +Object.entries(baseline).forEach(([name, setting]) => mapRule(name, setting, origins[name] ?? 'baseline', 'base')); +Object.entries(guideAdditions).forEach(([name, setting]) => mapRule(name, setting, 'Javascript.md', 'base')); +Object.entries(guideTypeScriptAdditions).forEach(([name, setting]) => { + mapRule(name, setting, 'Javascript.md', 'base', 'typescript'); }); -for (let index = mappings.length - 1; index >= 0; index -= 1) { - const mapping = mappings[index]!; - if (mapping.implementation === 'disabled' && rules[mapping.source] !== undefined) { - mappings.splice(index, 1); +Object.entries(guidelineAdditions).forEach(([name, setting]) => { + mapRule(name, setting, 'Javascript.md', 'guideline'); +}); +Object.entries(projectAdditions).forEach(([name, setting]) => { + mapRule(name, setting, 'adguard-projects', 'adguard-projects'); +}); +Object.entries(projectTypeScriptAdditions).forEach(([name, setting]) => { + mapRule(name, setting, 'adguard-projects', 'adguard-projects', 'typescript'); +}); +Object.keys(TYPESCRIPT_EQUIVALENTS).forEach((source) => { + if (!mappings.some((mapping) => mapping.source === source)) { + throw new Error(`TypeScript equivalent for unmapped rule ${source}`); } -} +}); -clauses.forEach((clause) => { +/** + * Classify a clause for one policy from the rules active in that policy. + * @param clause - Guideline clause. + * @param disposition - Rules linked for this policy. + * @param policy - Policy being described. + * @returns Enforcement and explanation. + */ +function classify(clause: Clause, disposition: Disposition, policy: Policy): Disposition { + const { rules } = disposition; if (clause.id.startsWith('typescript--tsconfig')) { - clause.enforcement = 'compiler'; - clause.reason = 'Enforced by shared tsconfig settings and compiler integration tests.'; - } else if (clause.id === 'typescript--enum-naming-conventions') { - clause.enforcement = 'custom'; - clause.reason = 'Enum and member casing is enforced. Singular English names require review; values are unrestricted.'; - } else if (clause.id === 'typescript--caught-error-type') { - clause.enforcement = 'custom'; - clause.reason = 'Explicit any is rejected; unannotated catches use unknown through strict compiler settings.'; - } else if (clause.rules.length > 0) { - const matched = mappings.filter((mapping) => clause.rules.includes(mapping.source)); - const disabled = matched.filter((mapping) => mapping.implementation === 'disabled'); - const active = matched.filter((mapping) => mapping.implementation !== 'disabled'); - if ( - disabled.length > 0 - || clause.id === 'modules--prefer-named-export' - || clause.id === 'strings--line-length' - || clause.id === 'coercion--comment-deviations' - ) { - clause.enforcement = 'overridden'; - clause.reason = `Sample precedence: ${clause.rules.join(', ')} retains its inherited/explicit options and severity.`; - } else if (active.length > 0) { - clause.enforcement = active.some((mapping) => mapping.implementation === 'custom') - ? 'custom' - : active.some((mapping) => mapping.implementation === 'javascript') - ? 'javascript' - : 'native'; - clause.reason - ||= 'Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.'; + return { + rules, + enforcement: 'compiler', + reason: 'Enforced by the shared tsconfig and verified with `ag-oxlint-config --check-tsconfig`.', + }; + } + const active = rules.map((rule) => { + const candidates = mappings.filter((mapping) => mapping.source === rule + && (mapping.scope === undefined || (policy === 'guideline' && mapping.scope === 'guideline'))); + return candidates.at(-1); + }); + const enabled = active.filter((mapping) => mapping && mapping.implementation !== 'disabled'); + const override = reasons[policy][clause.id] + ?? (policy === 'guideline' ? reasons.compatibility[clause.id] : undefined); + if (rules.length === 0 || enabled.length === 0) { + if (rules.length > 0) { + return { + rules, + enforcement: 'overridden', + reason: override ?? `Sample precedence: ${rules.join(', ')} retains its inherited/explicit options and severity.`, + }; } + return { rules, enforcement: 'manual', reason: manual[clause.id] ?? '' }; + } + const partialReason = partial[policy][clause.id]; + if (partialReason) { + return { rules, enforcement: 'partial', reason: partialReason }; } - if (!clause.reason) { - throw new Error(`Unclassified source clause: ${clause.id}`); + // The comment-deviation clause is satisfied by a justified directive; the others lose to sample options. + const forced = clause.id === 'coercion--comment-deviations' || (policy === 'compatibility' + && ['modules--prefer-named-export', 'strings--line-length', 'modules--import-order'].includes(clause.id)); + if (forced || (enabled.length < rules.length && !override)) { + return { + rules, + enforcement: 'overridden', + reason: override ?? `Sample precedence: ${rules.join(', ')} retains its inherited/explicit options and severity.`, + }; } + const kinds = enabled.map((mapping) => mapping!.implementation); + return { + rules, + enforcement: kinds.includes('custom') ? 'custom' : kinds.includes('javascript') ? 'javascript' : 'native', + reason: override ?? 'Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.', + }; +} + +clauses.forEach((clause) => { + const compatibility = classify(clause, clause, 'compatibility'); + Object.assign(clause, compatibility); + clause.guideline = classify(clause, clause.guideline, 'guideline'); + [compatibility, clause.guideline].forEach((disposition) => { + if (!disposition.reason) { + throw new Error(`Unclassified source clause: ${clause.id}`); + } + }); }); const catalog: Catalog = { @@ -427,28 +803,52 @@ const catalog: Catalog = { env, mappings, clauses, - rules, + rules: layers.base.rules, + typescriptRules: layers.base.typescriptRules, + policies: { + guideline: { ...layers.guideline, settings: {}, importGroups }, + }, + profiles: { + 'adguard-projects': { ...layers['adguard-projects'], settings: projectSettings }, + }, }; + +const cell = (text: string) => text.replaceAll('|', '\\|'); +const ruleList = (rules: string[]) => rules.map((rule) => `\`${rule}\``).join(', '); +const scopeLabel = (mapping: Mapping) => mapping.scope ?? (mapping.language === 'typescript' ? 'base (TypeScript)' : 'base'); const report = [ '# Guideline coverage', '', - 'Generated by `pnpm catalog:generate`. Sample settings and inherited disabled rules take precedence over prose.', + 'Generated by `pnpm catalog:generate`. The default `compatibility` policy gives the sample configuration and its ' + + 'inherited disabled rules precedence over prose. The opt-in `guideline` policy enforces the prose where the two ' + + 'conflict. `partial` marks clauses whose rules enforce only part of the stated requirement.', '', - '| Clause | Enforcement | Rules | Interpretation |', - '| --- | --- | --- | --- |', - ...clauses.map( - (clause) => `| [${clause.number} ${clause.id}](reference/Javascript.md#${clause.id})` - + ` | ${clause.enforcement} | ${clause.rules.map((rule) => `\`${rule}\``).join(', ')}` - + ` | ${clause.reason.replaceAll('|', '\\|')} |`, - ), + '| Clause | Compatibility | Guideline | Rules | Interpretation |', + '| --- | --- | --- | --- | --- |', + ...clauses.map((clause) => { + const sameRules = JSON.stringify(clause.rules) === JSON.stringify(clause.guideline.rules); + const rules = sameRules + ? ruleList(clause.rules) + : `${ruleList(clause.rules)}; guideline: ${ruleList(clause.guideline.rules)}`; + const interpretation = clause.reason === clause.guideline.reason + ? clause.reason + : `${clause.reason} Guideline: ${clause.guideline.reason}`; + return `| [${clause.number} ${clause.id}](reference/Javascript.md#${clause.id})` + + ` | ${clause.enforcement} | ${clause.guideline.enforcement} | ${rules} | ${cell(interpretation)} |`; + }), '', '## Resolved rule mappings', '', - '| Source rule | Oxlint rule | Provider | Setting | Origin | Type information |', - '| --- | --- | --- | --- | --- | --- |', + 'Scope `base` is always active, `guideline` is added by `policy: \'guideline\'`, and `adguard-projects` by ' + + '`profile: \'adguard-projects\'`. The TypeScript column names the implementation used for TypeScript files when it ' + + 'differs from the JavaScript one.', + '', + '| Source rule | Oxlint rule | Provider | Setting | Origin | Scope | TypeScript | Type information |', + '| --- | --- | --- | --- | --- | --- | --- | --- |', ...mappings.map( (mapping) => `| ${mapping.source} | ${mapping.target ?? 'disabled'} | ${mapping.implementation}` - + ` | \`${JSON.stringify(mapping.setting).replaceAll('|', '\\|')}\` | ${mapping.origin}` + + ` | \`${cell(JSON.stringify(mapping.setting))}\` | ${mapping.origin} | ${scopeLabel(mapping)}` + + ` | ${mapping.typescript ? `${mapping.typescript.target ?? 'disabled'}: ${cell(mapping.typescript.reason)}` : ''}` + ` | ${mapping.requiresTypeInfo ? 'opt-in required' : ''} |`, ), '', @@ -467,5 +867,5 @@ for (const [file, content] of [ } } process.stdout.write( - `Catalog: ${clauses.length} clauses; ${Object.keys(rules).length} enabled rules; generate.ts\n`, + `Catalog: ${clauses.length} clauses; ${Object.keys(catalog.rules).length} enabled rules; generate.ts\n`, ); diff --git a/scripts/source-policy.ts b/scripts/source-policy.ts index 66d22cc..b319bac 100644 --- a/scripts/source-policy.ts +++ b/scripts/source-policy.ts @@ -23,10 +23,13 @@ const externalLoaders: Record = { 'scripts/airbnb.ts': 'Pinned Airbnb configuration data evaluated in a VM.', 'scripts/generate.ts': 'Installed upstream metadata used to build the rule catalog.', 'scripts/import-upstream.ts': 'Pinned upstream fixtures evaluated in a VM.', + 'packages/oxlint-config/src/compiler.ts': 'Consumer package specifiers in tsconfig extends chains.', 'tests/behavior.test.ts': 'Resolve the installed TypeScript compiler executable.', + 'tests/clauses.test.ts': 'Resolve the installed plugin entrypoints for CLI fixtures.', 'tests/consumer.test.ts': 'Resolve tools inside the temporary packed consumer.', 'tests/gaps.test.ts': 'Resolve the installed plugin entrypoints for CLI fixtures.', 'tests/integration.test.ts': 'Resolve the installed plugin entrypoints for CLI fixtures.', + 'tests/typescript-preset.test.ts': 'Resolve the installed plugin entrypoints for CLI fixtures.', 'tests/typescript-syntax.test.ts': 'Resolve the installed plugin entrypoints for CLI fixtures.', }; const scopeKinds = new Set([ diff --git a/scripts/write-configs.ts b/scripts/write-configs.ts index b0327df..fa29aac 100644 --- a/scripts/write-configs.ts +++ b/scripts/write-configs.ts @@ -10,5 +10,9 @@ for (const language of ['javascript', 'typescript'] as const) { `packages/oxlint-config/dist/${language}.json`, `${JSON.stringify(createConfig(language), null, 2)}\n`, ); + await writeFile( + `packages/oxlint-config/dist/${language}-adguard-projects.json`, + `${JSON.stringify(createConfig({ language, profile: 'adguard-projects' }), null, 2)}\n`, + ); } await writeFile('packages/oxlint-config/dist/node.json', `${JSON.stringify(node, null, 2)}\n`); diff --git a/tests/behavior.test.ts b/tests/behavior.test.ts index da93583..eda3263 100644 --- a/tests/behavior.test.ts +++ b/tests/behavior.test.ts @@ -132,7 +132,7 @@ it('uses the compiler for type safety without requiring a typed lint service', ( }); it('opts into the typed lint service without enabling compiler diagnostics', () => { - const config = createConfig({ language: 'typescript', typeAware: true }); + const config = createConfig({ language: 'typescript', typeAware: true, profile: 'adguard-projects' }); expect(config.options).toEqual({ typeAware: true }); expect(config.overrides![0]!.rules!['typescript/consistent-type-exports']).toBe('error'); expect(config.rules!['typescript/consistent-type-exports']).toBeUndefined(); @@ -150,7 +150,7 @@ it('rejects type-aware linting outside the TypeScript preset', () => { it('runs type-dependent export diagnostics in an isolated consumer project', async () => { const directory = await mkdtemp(join(tmpdir(), 'ag-typed-lint-')); try { - const preset = createConfig({ language: 'typescript', typeAware: true }); + const preset = createConfig({ language: 'typescript', typeAware: true, profile: 'adguard-projects' }); await writeFile(join(directory, '.oxlintrc.json'), JSON.stringify({ categories: preset.categories, plugins: ['typescript'], diff --git a/tests/catalog.test.ts b/tests/catalog.test.ts index 14d2eb2..76434eb 100644 --- a/tests/catalog.test.ts +++ b/tests/catalog.test.ts @@ -90,15 +90,17 @@ describe('source and scenario inventory', () => { it('keeps disabled inherited settings disabled and prevents stale fixture targets', () => { catalog.mappings - .filter((mapping) => severity(mapping.setting) === 0) + .filter((mapping) => mapping.scope === undefined && severity(mapping.setting) === 0) .forEach((mapping) => { expect(mapping.implementation).toBe('disabled'); expect(mapping.target).toBeNull(); }); upstream.forEach((fixture) => { - expect(catalog.mappings.find((mapping) => mapping.source === fixture.sourceRule)?.target).toBe( - fixture.target, + // Fixtures target the enabled implementation, which may belong to an opt-in layer. + const enabled = catalog.mappings.find( + (mapping) => mapping.source === fixture.sourceRule && mapping.implementation !== 'disabled', ); + expect(enabled?.target).toBe(fixture.target); expect(fixture.origin).toMatch(/^(?:eslint|eslint-stylistic|eslint-plugin-)/u); }); }); diff --git a/tests/clause-cases.ts b/tests/clause-cases.ts new file mode 100644 index 0000000..f1d2503 --- /dev/null +++ b/tests/clause-cases.ts @@ -0,0 +1,1081 @@ +/** + * @file Good and bad examples for guideline clauses, derived from the pinned guide and run with the full preset. + */ +import type { ImportGroups, Policy } from '../packages/rule-catalog/src/index'; + +export interface ClauseCase { + /** Clause anchor from the guide. */ + clause: string; + policy: Policy | 'both'; + importGroups?: ImportGroups; + extension?: 'js' | 'jsx' | 'cjs' | 'ts' | 'tsx'; + /** Directory for the linted file, used by relative import examples. */ + directory?: string; + /** Example that violates the clause. */ + bad?: string; + /** Example that satisfies the clause. */ + good?: string; + /** Oxlint rule codes (`plugin/rule`) that enforce the clause. */ + rules: string[]; + /** The bad example is not reported under this policy (a partial or overridden clause). */ + expectMiss?: boolean; + /** The good example is reported under this policy (a sample setting conflicts with the clause). */ + expectConflict?: boolean; + /** Omit the file overview comment that every fixture otherwise starts with. */ + bare?: boolean; + /** Omit the final newline. */ + noFinalNewline?: boolean; +} + +export const clauseGlobals = [ + '$', 'check', 'compute', 'convert', 'first', 'flag', 'items', 'report', 'second', 'source', 'third', 'userName', +]; + +const LONG = 'a'.repeat(130); +// Builds template substitutions in fixture source text without writing `${` inside a string literal. +const DOLLAR = '$'; + +export const clauseCases: ClauseCase[] = [ + { + clause: 'references--prefer-const', + policy: 'both', + bad: 'let value = 1;\nreport(value);', + good: 'const value = 1;\nreport(value);', + rules: ['ag-compat/prefer-const'], + }, + { + clause: 'references--disallow-var', + policy: 'both', + bad: 'var count = 1;\ncount += 1;\nreport(count);', + good: 'let count = 1;\ncount += 1;\nreport(count);', + rules: ['ag-compat/no-var'], + }, + { + clause: 'references--block-scope', + policy: 'both', + bad: 'if (flag) {\n const inner = 1;\n report(inner);\n}\nreport(inner);', + good: 'const outer = 1;\nreport(outer);', + rules: ['ag-compat/no-undef'], + }, + { + clause: 'objects--quoted-props', + policy: 'both', + bad: "report({ 'first': 1, 'data-key': 2 });", + good: "report({ first: 1, 'data-key': 2 });", + rules: ['ag-compat/quote-props'], + }, + { + clause: 'objects--prototype-builtins', + policy: 'both', + bad: "report(source.hasOwnProperty('key'));", + good: "report(Object.prototype.hasOwnProperty.call(source, 'key'));", + rules: ['ag-compat/no-prototype-builtins'], + }, + { + clause: 'objects--rest-spread', + policy: 'both', + bad: 'report(Object.assign({}, source, { extra: 3 }));', + good: 'report({ ...source, extra: 3 });', + rules: ['ag-compat/prefer-object-spread'], + }, + { + clause: 'arrays--mapping', + policy: 'both', + bad: 'report([...items].map(String));', + good: 'report(Array.from(items, String));', + rules: ['ag/prefer-array-from-map'], + }, + { + clause: 'arrays--callback-return', + policy: 'both', + bad: 'report(items.map((item) => {\n item.toString();\n}));', + good: 'report(items.map((item) => item.toString()));', + rules: ['ag-compat/array-callback-return'], + }, + { + clause: 'destructuring--object', + policy: 'both', + bad: 'const firstName = source.firstName;\nreport(firstName);', + good: 'const { firstName } = source;\nreport(firstName);', + rules: ['ag-compat/prefer-destructuring'], + }, + { + clause: 'destructuring--array', + policy: 'both', + bad: 'let head;\nhead = items[0];\nreport(head);', + good: 'let head;\n[head] = items;\nreport(head);', + rules: ['ag-compat/prefer-destructuring'], + }, + { + clause: 'destructuring--array', + policy: 'compatibility', + bad: 'const head = items[0];\nreport(head);', + rules: ['ag-compat/prefer-destructuring'], + expectMiss: true, + }, + { + clause: 'destructuring--array', + policy: 'guideline', + bad: 'const head = items[0];\nreport(head);', + good: 'const [head] = items;\nreport(head);', + rules: ['ag-compat/prefer-destructuring'], + }, + { + clause: 'strings--quotes', + policy: 'both', + bad: 'report("double");', + good: "report('single');", + rules: ['ag-compat/quotes'], + }, + { + clause: 'strings--quotes', + policy: 'both', + extension: 'ts', + bad: 'type Mode = "light";\nreport(null as Mode | null);', + good: "type Mode = 'light';\nreport(null as Mode | null);", + rules: ['ag-compat/quotes'], + }, + { + clause: 'strings--line-length', + policy: 'compatibility', + bad: "report('first part of a message '\n + 'second part of a message');", + rules: ['ag/no-multiline-string-concat'], + expectMiss: true, + }, + { + clause: 'strings--line-length', + policy: 'guideline', + bad: "report('first part of a message '\n + 'second part of a message');", + good: `report('${LONG}');`, + rules: ['ag/no-multiline-string-concat', 'ag-compat/max-len'], + }, + { + clause: 'es6-template-literals', + policy: 'both', + bad: "report('Hello ' + userName + '!');", + good: `report(\`Hello ${DOLLAR}{userName}!\`);`, + rules: ['ag-compat/prefer-template', 'ag-compat/template-curly-spacing'], + }, + { + clause: 'strings--eval', + policy: 'both', + bad: "report(eval('first'));", + good: 'report(first);', + rules: ['ag-compat/no-eval'], + }, + { + clause: 'strings--escaping', + policy: 'both', + bad: "report('a \\i b');", + good: "report('a i b');", + rules: ['ag-compat/no-useless-escape'], + }, + { + clause: 'functions--iife', + policy: 'both', + bad: '(function () {\n report(1);\n})();', + good: '(function () {\n report(1);\n}());', + rules: ['ag-compat/wrap-iife'], + }, + { + clause: 'functions--in-blocks', + policy: 'both', + bad: 'if (flag) {\n function test() {\n report(1);\n }\n test();\n}', + good: 'if (flag) {\n const test = () => {\n report(1);\n };\n test();\n}', + rules: ['ag-compat/no-loop-func', 'ag-compat/no-inner-declarations'], + }, + { + clause: 'functions--arguments-shadow', + policy: 'both', + extension: 'cjs', + bad: 'module.exports = function run(name, options, arguments) {\n return arguments;\n};', + good: 'module.exports = function run(name, options, args) {\n return args;\n};', + rules: ['ag-compat/no-shadow-restricted-names'], + }, + { + clause: 'es6-rest', + policy: 'both', + bad: "report(function concat() {\n return Array.prototype.slice.call(arguments).join('');\n});", + good: "report((...args) => args.join(''));", + rules: ['ag-compat/prefer-rest-params'], + }, + { + clause: 'es6-default-parameters', + policy: 'both', + bad: 'report((options) => {\n options = options || {};\n return options;\n});', + good: 'report((options = {}) => options);', + rules: ['ag-compat/no-param-reassign'], + }, + { + clause: 'functions--default-side-effects', + policy: 'both', + bad: 'let counter = 1;\nreport((next = counter += 1) => next);', + good: 'report((next = 1) => next);', + rules: ['ag/no-default-side-effects'], + }, + { + clause: 'functions--defaults-last', + policy: 'both', + bad: 'report((options = {}, name) => [options, name]);', + good: 'report((name, options = {}) => [options, name]);', + rules: ['ag-compat/default-param-last'], + }, + { + clause: 'functions--defaults-last', + policy: 'both', + extension: 'ts', + bad: 'report((options = {}, name: string) => [options, name]);', + good: 'report((options = {}, name?: string) => [options, name]);', + rules: ['eslint/default-param-last'], + }, + { + clause: 'functions--constructor', + policy: 'both', + bad: "report(new Function('a', 'b', 'return a + b'));", + good: 'report((left, right) => left + right);', + rules: ['ag-compat/no-new-func'], + }, + { + clause: 'functions--signature-spacing', + policy: 'both', + bad: 'report(function(){\n return 1;\n});', + good: 'report(function () {\n return 1;\n});', + rules: ['ag-compat/space-before-function-paren', 'ag-compat/space-before-blocks'], + }, + { + clause: 'functions--mutate-params', + policy: 'both', + bad: 'report((target) => {\n target.key = 1;\n});', + good: 'report((target) => ({ ...target, key: 1 }));', + rules: ['ag-compat/no-param-reassign'], + }, + { + clause: 'functions--reassign-params', + policy: 'both', + bad: 'report((value) => {\n value = 1;\n return value;\n});', + good: 'report((value) => {\n const next = value + 1;\n return next;\n});', + rules: ['ag-compat/no-param-reassign'], + }, + { + clause: 'functions--spread-vs-apply', + policy: 'both', + bad: 'report(Math.max.apply(Math, items));', + good: 'report(Math.max(...items));', + rules: ['ag-compat/prefer-spread'], + }, + { + clause: 'functions--signature-invocation-indentation', + policy: 'both', + bad: 'report(first,\n second);', + good: 'report(\n first,\n second,\n);', + rules: ['ag-compat/function-paren-newline'], + }, + { + clause: 'arrows--use-them', + policy: 'both', + bad: 'report(items.map(function (item) {\n return item * 2;\n}));', + good: 'report(items.map((item) => item * 2));', + rules: ['ag-compat/prefer-arrow-callback', 'ag-compat/arrow-spacing'], + }, + { + clause: 'arrows--one-arg-parens', + policy: 'both', + bad: 'report(items.map(item => item * 2));', + good: 'report(items.map((item) => item * 2));', + rules: ['ag-compat/arrow-parens'], + }, + { + clause: 'whitespace--implicit-arrow-linebreak', + policy: 'both', + bad: 'report((item) =>\n item.value);', + good: 'report((item) => item.value);', + rules: ['ag-compat/implicit-arrow-linebreak'], + }, + { + clause: 'constructors--use-class', + policy: 'both', + bad: 'function Queue() {}\nQueue.prototype.pop = () => null;\nreport(Queue);', + good: 'class Queue {\n pop() {\n return this.last;\n }\n}\nreport(Queue);', + rules: ['ag/no-prototype-mutation'], + }, + { + clause: 'constructors--extends', + policy: 'both', + bad: 'function Queue() {}\nfunction PeekableQueue() {}\nPeekableQueue.prototype = Object.create(Queue.prototype);\n' + + 'report(PeekableQueue);', + good: 'class Queue {}\nreport(class PeekableQueue extends Queue {});', + rules: ['ag/no-prototype-mutation'], + }, + { + clause: 'constructors--no-useless', + policy: 'both', + bad: 'report(class Jedi {\n constructor() {}\n});', + good: 'report(class Jedi {\n speak() {\n return this.name;\n }\n});', + rules: ['ag-compat/no-useless-constructor'], + }, + { + clause: 'classes--methods-use-this', + policy: 'both', + bad: 'report(class Foo {\n bar() {\n return 1;\n }\n});', + good: 'report(class Foo {\n static bar() {\n return 1;\n }\n});', + rules: ['ag-compat/class-methods-use-this'], + }, + { + clause: 'modules--use-them', + policy: 'compatibility', + bad: "const helper = require('./helper');\n\nmodule.exports = helper;", + rules: ['import/no-commonjs'], + expectMiss: true, + }, + { + clause: 'modules--use-them', + policy: 'guideline', + bad: "const helper = require('./helper');\n\nmodule.exports = helper;", + good: "import { first } from './helpers';\n\nreport(first);", + rules: ['import/no-commonjs', 'ag-import/no-amd'], + }, + { + clause: 'modules--no-wildcard', + policy: 'compatibility', + bad: "import * as helpers from './helpers';\n\nreport(helpers);", + rules: ['import/no-namespace'], + expectMiss: true, + }, + { + clause: 'modules--no-wildcard', + policy: 'guideline', + bad: "import * as helpers from './helpers';\n\nreport(helpers);", + good: "import { first } from './helpers';\n\nreport(first);", + rules: ['import/no-namespace'], + }, + { + clause: 'modules--no-export-from-import', + policy: 'both', + bad: "export { first, second } from './helpers';", + good: "import { first, second } from './helpers';\n\nexport { first, second };", + rules: ['ag/no-direct-reexport'], + }, + { + clause: 'modules--no-duplicate-imports', + policy: 'both', + bad: "import { first } from './helpers';\nimport { second } from './helpers';\n\nreport(first, second);", + good: "import { first, second } from './helpers';\n\nreport(first, second);", + rules: ['ag-import/no-duplicates'], + }, + { + clause: 'modules--no-mutable-exports', + policy: 'both', + bad: 'let count = 1;\nlet total = 2;\n\nexport { count, total };', + good: 'const COUNT = 1;\nconst TOTAL = 2;\n\nexport { COUNT, TOTAL };', + rules: ['ag-import/no-mutable-exports'], + }, + { + clause: 'modules--prefer-named-export', + policy: 'compatibility', + bad: 'export default function foo() {}', + rules: ['import/no-default-export'], + expectMiss: true, + }, + { + clause: 'modules--prefer-named-export', + policy: 'guideline', + bad: 'export default function foo() {}', + good: 'export function foo() {}', + rules: ['import/no-default-export', 'ag-import/prefer-default-export'], + }, + { + clause: 'modules--imports-first', + policy: 'both', + bad: "import { first } from './helpers';\n\nreport(first);\nimport { second } from './helpers';\n\nreport(second);", + good: "import { first, second } from './helpers';\n\nreport(first);\nreport(second);", + rules: ['ag-import/first'], + }, + { + clause: 'modules--multiline-imports-over-newlines', + policy: 'both', + bad: "import { first, fourth, second, third } from './helpers';\n\nreport(first, second, third, fourth);", + good: "import {\n first,\n fourth,\n second,\n third,\n} from './helpers';\n\n" + + 'report(first, second, third, fourth);', + rules: ['ag-compat/object-curly-newline'], + }, + { + clause: 'modules--import-extensions', + policy: 'both', + bad: "import { first } from './helpers.js';\n\nreport(first);", + good: "import { first } from './helpers';\n\nreport(first);", + rules: ['ag-import/extensions'], + }, + { + clause: 'modules--import-grouping', + policy: 'both', + directory: 'src', + bad: "import foo from './foo';\nimport fs from 'node:fs';\n\nreport(foo, fs);", + good: "import fs from 'node:fs';\n\nimport foo from './foo';\n\nreport(foo, fs);", + rules: ['ag-import/order'], + }, + { + clause: 'modules--import-grouping', + policy: 'compatibility', + directory: 'src', + bad: "import fs from 'node:fs';\nimport bar from '../bar';\nimport foo from './foo';\n\nreport(fs, bar, foo);", + rules: ['ag-import/order'], + expectMiss: true, + }, + { + clause: 'modules--import-grouping', + policy: 'guideline', + importGroups: 'example', + directory: 'src', + bad: "import fs from 'node:fs';\nimport bar from '../bar';\nimport foo from './foo';\n\nreport(fs, bar, foo);", + good: "import fs from 'node:fs';\nimport npmPackage from 'npm-package';\n\nimport bar from '../bar';\n\n" + + "import foo from './foo';\n\nreport(fs, npmPackage, bar, foo);", + rules: ['ag-import/order'], + }, + { + clause: 'modules--import-grouping', + policy: 'guideline', + importGroups: 'prose', + directory: 'src', + bad: "import fs from 'node:fs';\nimport npmPackage from 'npm-package';\n\nimport bar from '../bar';\n\n" + + "import foo from './foo';\n\nreport(fs, npmPackage, bar, foo);", + good: "import fs from 'node:fs';\n\nimport npmPackage from 'npm-package';\n\nimport bar from '../bar';\n\n" + + "import foo from './foo';\n\nreport(fs, npmPackage, bar, foo);", + rules: ['ag-import/order'], + }, + { + clause: 'modules--import-order', + policy: 'compatibility', + bad: "import second from 'b';\nimport first from 'a';\n\nreport(first, second);", + rules: ['ag-import/order'], + expectMiss: true, + }, + { + clause: 'modules--import-order', + policy: 'guideline', + bad: "import second from 'b';\nimport first from 'a';\n\nreport(first, second);", + good: "import second from 'a';\nimport first from 'b';\n\nreport(first, second);", + rules: ['ag-import/order'], + }, + { + clause: 'modules--import-member-order', + policy: 'both', + bad: "import { second, first } from './helpers';\n\nreport(first, second);", + good: "import { first, second } from './helpers';\n\nreport(first, second);", + rules: ['ag-import/order', 'ag-compat/sort-imports'], + }, + { + clause: 'modules--import-node-protocol', + policy: 'both', + bad: "import fs from 'fs';\n\nreport(fs);", + good: "import fs from 'node:fs';\n\nreport(fs);", + rules: ['unicorn/prefer-node-protocol'], + }, + { + clause: 'properties--dot', + policy: 'both', + bad: "report(source['jedi']);", + good: 'report(source.jedi);', + rules: ['ag-compat/dot-notation'], + }, + { + clause: 'properties--bracket', + policy: 'both', + bad: "report(source['jedi']);", + good: 'report((property) => source[property]);', + rules: ['ag-compat/dot-notation'], + }, + { + clause: 'es2016-properties--exponentiation-operator', + policy: 'both', + bad: 'report(Math.pow(2, 10));', + good: 'report(2 ** 10);', + rules: ['ag-compat/no-restricted-properties'], + }, + { + clause: 'variables--const', + policy: 'both', + bad: 'superPower = compute();', + good: 'const superPower = compute();\nreport(superPower);', + rules: ['ag-compat/no-undef', 'ag-compat/prefer-const'], + }, + { + clause: 'variables--one-const', + policy: 'both', + bad: 'const head = 1, tail = 2;\nreport(head, tail);', + good: 'const head = 1;\nconst tail = 2;\nreport(head, tail);', + rules: ['ag-compat/one-var'], + }, + { + clause: 'variables--no-chain-assignment', + policy: 'both', + bad: 'let head;\nlet tail;\nhead = tail = compute();\nreport(head, tail);', + good: 'const head = compute();\nconst tail = head;\nreport(head, tail);', + rules: ['ag-compat/no-multi-assign'], + }, + { + clause: 'variables--unary-increment-decrement', + policy: 'both', + bad: 'let count = 1;\ncount++;\nreport(count);', + good: 'let count = 1;\ncount += 1;\nreport(count);', + rules: ['ag-compat/no-plusplus'], + }, + { + clause: 'variables--linebreak', + policy: 'both', + bad: 'const result =\n compute();\nreport(result);', + good: 'const result = (\n compute()\n);\nreport(result);', + rules: ['ag-compat/max-len', 'ag-compat/operator-linebreak'], + }, + { + clause: 'variables--no-unused-vars', + policy: 'both', + bad: 'const unused = compute();', + good: 'const used = compute();\nreport(used);', + rules: ['ag-compat/no-unused-vars'], + }, + { + clause: 'variables--no-unused-vars', + policy: 'both', + extension: 'ts', + bad: 'const unused: number = compute();', + good: 'interface Shape {\n area(size: number): number;\n}\nconst used: Shape | null = compute();\nreport(used);', + rules: ['eslint/no-unused-vars'], + }, + { + clause: 'comparison--eqeqeq', + policy: 'both', + bad: 'report(source == 1);', + good: 'report(source === 1);', + rules: ['ag-compat/eqeqeq'], + }, + { + clause: 'comparison--switch-blocks', + policy: 'both', + bad: 'switch (source) {\n case 1:\n const value = 1;\n report(value);\n break;\n' + + ' default:\n break;\n}', + good: 'switch (source) {\n case 1: {\n const value = 1;\n report(value);\n break;\n }\n' + + ' default:\n break;\n}', + rules: ['ag-compat/no-case-declarations'], + }, + { + clause: 'comparison--nested-ternaries', + policy: 'both', + bad: 'report(first ? 1 : second ? 2 : 3);', + good: 'const fallback = second ? 2 : 3;\nreport(first ? 1 : fallback);', + rules: ['ag-compat/no-nested-ternary'], + }, + { + clause: 'comparison--unneeded-ternary', + policy: 'both', + bad: 'report(flag ? true : false);', + good: 'report(Boolean(flag));', + rules: ['ag-compat/no-unneeded-ternary'], + }, + { + clause: 'comparison--no-mixed-operators', + policy: 'both', + bad: 'report(first && second || third);', + good: 'report((first && second) || third);', + rules: ['ag-compat/no-mixed-operators'], + }, + { + clause: 'blocks--braces', + policy: 'both', + bad: 'if (flag) report(1);', + good: 'if (flag) {\n report(1);\n}', + rules: ['ag-compat/curly', 'ag-compat/brace-style'], + }, + { + clause: 'blocks--cuddled-elses', + policy: 'both', + bad: 'if (flag) {\n report(1);\n}\nelse {\n report(2);\n}', + good: 'if (flag) {\n report(1);\n} else {\n report(2);\n}', + rules: ['ag-compat/brace-style'], + }, + { + clause: 'blocks--no-else-return', + policy: 'both', + bad: 'report(() => {\n if (flag) {\n return 1;\n } else {\n return 2;\n }\n});', + good: 'report(() => {\n if (flag) {\n return 1;\n }\n\n return 2;\n});', + rules: ['ag-compat/no-else-return'], + }, + { + clause: 'control-statements', + policy: 'both', + bad: 'if (\n first === 123 &&\n second === 1\n) {\n report(1);\n}', + good: 'if (\n first === 123\n && second === 1\n) {\n report(1);\n}', + rules: ['ag-compat/operator-linebreak', 'ag/multiline-condition-layout'], + }, + { + clause: 'control-statements', + policy: 'compatibility', + bad: 'if (first === 123\n && second === 1) {\n report(1);\n}', + rules: ['ag-compat/operator-linebreak', 'ag/multiline-condition-layout'], + expectMiss: true, + }, + { + clause: 'control-statements', + policy: 'guideline', + bad: 'if (first === 123\n && second === 1) {\n report(1);\n}', + rules: ['ag/multiline-condition-layout'], + }, + { + clause: 'control-statement--value-selection', + policy: 'both', + bad: '!flag && report(1);', + good: 'if (!flag) {\n report(1);\n}', + rules: ['ag-compat/no-unused-expressions'], + }, + { + clause: 'comments--multiline', + policy: 'both', + bad: '/* make() returns a new element\n based on the passed in tag name */\nreport(1);', + good: '/**\n * make() returns a new element\n * based on the passed-in tag name\n */\nreport(1);', + rules: ['ag/require-docblock'], + }, + { + clause: 'comments--multiline', + policy: 'compatibility', + bad: '// make() returns a new element\n// based on the passed in tag name\nreport(1);', + rules: ['ag/require-docblock'], + expectMiss: true, + }, + { + clause: 'comments--multiline', + policy: 'guideline', + bad: '// make() returns a new element\n// based on the passed in tag name\nreport(1);', + rules: ['ag/require-docblock'], + }, + { + clause: 'comments--singleline', + policy: 'compatibility', + bad: 'report(1); // is current tab', + rules: ['ag-compat/line-comment-position', 'ag-compat/lines-around-comment'], + expectMiss: true, + }, + { + clause: 'comments--singleline', + policy: 'guideline', + bad: "report('fetching type...');\n// set the default type\nreport(1); // is current tab", + good: "report('fetching type...');\n\n// set the default type\nreport(1);\n\nif (flag) {\n" + + ' // set the default type\n report(2);\n}', + rules: ['ag-compat/line-comment-position', 'ag-compat/lines-around-comment'], + }, + { + // The clause covers single-line comments; block comments need no preceding blank line. + clause: 'comments--singleline', + policy: 'guideline', + good: 'report({\n first: 1,\n /**\n * Documents the property.\n */\n second: 2,\n});', + rules: ['ag-compat/line-comment-position', 'ag-compat/lines-around-comment'], + }, + { + clause: 'comments--singleline', + policy: 'guideline', + extension: 'ts', + good: 'interface Tab {\n // is current tab\n active: boolean;\n}\nreport(null as Tab | null);', + rules: ['ag-style/lines-around-comment'], + }, + { + clause: 'comments--spaces', + policy: 'both', + bad: '//is current tab\nreport(1);', + good: '// is current tab\nreport(1);', + rules: ['ag-compat/spaced-comment'], + }, + { + clause: 'comments-jsdoc', + policy: 'both', + bare: true, + bad: 'report(1);', + good: '/**\n * @file Reports a value.\n */\n\nreport(1);', + rules: ['ag-jsdoc/require-file-overview'], + }, + { + clause: 'whitespace--spaces', + policy: 'both', + bad: 'if (flag) {\n report(1);\n}', + good: 'if (flag) {\n report(1);\n}', + rules: ['ag-compat/indent', 'ag-react/jsx-indent', 'ag-react/jsx-indent-props'], + }, + { + clause: 'whitespace--spaces', + policy: 'compatibility', + extension: 'jsx', + good: 'report(\n
\n \n
,\n);', + rules: ['ag-react/jsx-indent'], + expectConflict: true, + }, + { + clause: 'whitespace--spaces', + policy: 'guideline', + extension: 'jsx', + bad: 'report(\n
\n \n
,\n);', + good: 'report(\n
\n \n
,\n);', + rules: ['ag-react/jsx-indent', 'ag-react/jsx-indent-props'], + }, + { + clause: 'whitespace--before-blocks', + policy: 'both', + bad: 'if (flag){\n report(1);\n}', + good: 'if (flag) {\n report(1);\n}', + rules: ['ag-compat/space-before-blocks'], + }, + { + clause: 'whitespace--before-blocks', + policy: 'both', + extension: 'ts', + bad: 'interface Shape{\n area: number;\n}\nreport(null as Shape | null);', + good: 'interface Shape {\n area: number;\n}\nreport(null as Shape | null);', + rules: ['ag-style/space-before-blocks'], + }, + { + clause: 'whitespace--around-keywords', + policy: 'both', + bad: 'if(flag) {\n report(1);\n}', + good: 'if (flag) {\n report(1);\n}', + rules: ['ag-compat/keyword-spacing'], + }, + { + clause: 'whitespace--infix-ops', + policy: 'both', + bad: 'report(first+second);', + good: 'report(first + second);', + rules: ['ag-compat/space-infix-ops'], + }, + { + clause: 'whitespace--infix-ops', + policy: 'both', + extension: 'ts', + bad: 'type Value = string|number;\nreport(null as Value | null);', + good: 'type Value = string | number;\nreport(null as Value | null);', + rules: ['ag-style/space-infix-ops'], + }, + { + clause: 'whitespace--newline-at-end', + policy: 'both', + noFinalNewline: true, + bad: 'report(1);', + rules: ['ag-compat/eol-last'], + }, + { + clause: 'whitespace--newline-at-end', + policy: 'both', + good: 'report(1);', + rules: ['ag-compat/eol-last'], + }, + { + clause: 'whitespace--chains', + policy: 'both', + bad: "$('#items').find('.selected').highlight().end().find('.open').updateCount();", + good: "$('#items')\n .find('.selected')\n .highlight()\n .end()\n .find('.open')\n .updateCount();", + rules: ['ag-compat/newline-per-chained-call', 'ag-compat/no-whitespace-before-property'], + }, + { + clause: 'whitespace--chains', + policy: 'compatibility', + bad: "report(items.filter(check).map(convert).join(','));", + rules: ['ag-compat/newline-per-chained-call'], + expectMiss: true, + }, + { + clause: 'whitespace--chains', + policy: 'guideline', + bad: "report(items.filter(check).map(convert).join(','));", + good: "report(items\n .filter(check)\n .map(convert)\n .join(','));", + rules: ['ag-compat/newline-per-chained-call'], + }, + { + clause: 'whitespace--after-blocks', + policy: 'compatibility', + bad: 'if (flag) {\n report(1);\n}\nreport(2);', + rules: ['ag-compat/padding-line-between-statements'], + expectMiss: true, + }, + { + clause: 'whitespace--after-blocks', + policy: 'guideline', + bad: 'if (flag) {\n report(1);\n}\nreport(2);', + good: 'if (flag) {\n report(1);\n}\n\nreport(2);', + rules: ['ag-compat/padding-line-between-statements'], + }, + { + clause: 'whitespace--after-blocks', + policy: 'guideline', + bad: 'report({\n foo() {\n return 1;\n },\n bar() {\n return 2;\n },\n});', + rules: ['ag-compat/padding-line-between-statements'], + expectMiss: true, + }, + { + clause: 'whitespace--padded-blocks', + policy: 'both', + bad: 'if (flag) {\n\n report(1);\n\n}', + good: 'if (flag) {\n report(1);\n}', + rules: ['ag-compat/padded-blocks'], + }, + { + clause: 'whitespace--no-multiple-blanks', + policy: 'both', + bad: 'report(1);\n\n\nreport(2);', + good: 'report(1);\n\nreport(2);', + rules: ['ag-compat/no-multiple-empty-lines'], + }, + { + clause: 'whitespace--in-parens', + policy: 'both', + bad: 'report( 1 );', + good: 'report(1);', + rules: ['ag-compat/space-in-parens'], + }, + { + clause: 'whitespace--in-brackets', + policy: 'both', + bad: 'report([ 1, 2 ]);', + good: 'report([1, 2]);', + rules: ['ag-compat/array-bracket-spacing'], + }, + { + clause: 'whitespace--in-braces', + policy: 'both', + bad: 'report({first: 1});', + good: 'report({ first: 1 });', + rules: ['ag-compat/object-curly-spacing'], + }, + { + clause: 'whitespace--in-braces', + policy: 'both', + extension: 'ts', + bad: 'type Value = {first: string};\nreport(null as Value | null);', + good: 'type Value = { first: string };\nreport(null as Value | null);', + rules: ['ag-style/object-curly-spacing'], + }, + { + clause: 'whitespace--max-len', + policy: 'both', + bad: `report(first, second, third, ${Array.from({ length: 12 }, () => 'first + second').join(', ')});`, + good: 'report(\n first,\n second,\n third,\n);', + rules: ['ag-compat/max-len'], + }, + { + clause: 'whitespace--block-spacing', + policy: 'both', + bad: 'report(() => {return 1;});', + good: 'report(() => { return 1; });', + rules: ['ag-compat/block-spacing'], + }, + { + clause: 'whitespace--comma-spacing', + policy: 'both', + bad: 'report(first ,second);', + good: 'report(first, second);', + rules: ['ag-compat/comma-spacing'], + }, + { + clause: 'whitespace--computed-property-spacing', + policy: 'both', + bad: "const key = 'name';\nreport(source[ key ]);", + good: "const key = 'name';\nreport(source[key]);", + rules: ['ag-compat/computed-property-spacing'], + }, + { + clause: 'whitespace--func-call-spacing', + policy: 'both', + bad: 'report (1);', + good: 'report(1);', + rules: ['ag-compat/func-call-spacing'], + }, + { + clause: 'whitespace--key-spacing', + policy: 'both', + bad: 'report({ first:1 });', + good: 'report({ first: 1 });', + rules: ['ag-compat/key-spacing'], + }, + { + clause: 'whitespace--key-spacing', + policy: 'both', + extension: 'ts', + bad: 'interface Shape {\n area : number;\n}\nreport(null as Shape | null);', + good: 'interface Shape {\n area: number;\n}\nreport(null as Shape | null);', + rules: ['ag-style/key-spacing'], + }, + { + clause: 'whitespace--no-trailing-spaces', + policy: 'both', + bad: 'report(1); \nreport(2);', + good: 'report(1);\nreport(2);', + rules: ['ag-compat/no-trailing-spaces'], + }, + { + clause: 'whitespace--no-multiple-empty-lines', + policy: 'both', + bad: 'report(1);\n', + good: 'report(1);', + rules: ['ag-compat/no-multiple-empty-lines'], + }, + { + clause: 'commas--leading-trailing', + policy: 'both', + bad: 'report([\n first\n , second,\n]);', + good: 'report([\n first,\n second,\n]);', + rules: ['ag-compat/comma-style'], + }, + { + clause: 'commas--dangling', + policy: 'both', + bad: 'report({\n first: 1,\n second: 2\n});', + good: 'report({\n first: 1,\n second: 2,\n});', + rules: ['ag-compat/comma-dangle'], + }, + { + clause: 'commas--dangling', + policy: 'both', + extension: 'ts', + bad: "enum Color {\n Red = 'Red',\n Green = 'Green'\n}\nreport(Color);", + good: "enum Color {\n Red = 'Red',\n Green = 'Green',\n}\nreport(Color);", + rules: ['ag-style/comma-dangle'], + }, + { + clause: 'semicolons--required', + policy: 'both', + bad: 'report(1)', + good: 'report(1);', + rules: ['ag-compat/semi'], + }, + { + clause: 'semicolons--required', + policy: 'both', + extension: 'ts', + bad: 'type Value = string\nreport(null as Value | null);', + good: 'type Value = string;\nreport(null as Value | null);', + rules: ['ag-style/semi'], + }, + { + clause: 'coercion--strings', + policy: 'both', + bad: 'report(new String(source));', + good: 'report(String(source));', + rules: ['ag-compat/no-new-wrappers'], + }, + { + clause: 'coercion--numbers', + policy: 'both', + bad: 'report(parseInt(source));', + good: 'report(parseInt(source, 10));', + rules: ['ag-compat/radix', 'ag-compat/no-new-wrappers'], + }, + { + clause: 'coercion--comment-deviations', + policy: 'both', + good: '/**\n * Bitshifting the String to coerce it to a Number made it a lot faster.\n */\nreport(source >> 0);', + rules: ['ag-compat/no-bitwise'], + expectConflict: true, + }, + { + clause: 'coercion--booleans', + policy: 'both', + bad: 'report(new Boolean(flag));', + good: 'report(Boolean(flag));', + rules: ['ag-compat/no-new-wrappers'], + }, + { + clause: 'naming--descriptive', + policy: 'compatibility', + bad: 'const q = compute();\nreport(q);', + rules: ['ag-compat/id-length'], + expectMiss: true, + }, + { + clause: 'naming--descriptive', + policy: 'guideline', + bad: 'const q = compute();\nreport(q);', + good: 'const query = compute();\nreport(query);', + rules: ['ag-compat/id-length'], + }, + { + clause: 'naming--camelCase', + policy: 'both', + bad: 'const snake_case = compute();\nreport(snake_case);', + good: 'const camelCase = compute();\nreport(camelCase);', + rules: ['ag-compat/camelcase'], + }, + { + clause: 'naming--PascalCase', + policy: 'both', + bad: 'const user = class {};\nreport(new user());', + good: 'class User {}\nreport(new User());', + rules: ['ag-compat/new-cap'], + }, + { + clause: 'naming--leading-underscore', + policy: 'both', + bad: 'const _hidden = compute();\nreport(_hidden);', + good: 'const hidden = compute();\nreport(hidden);', + rules: ['ag-compat/no-underscore-dangle'], + }, + { + clause: 'naming--self-this', + policy: 'both', + bad: 'report(function make() {\n const self = this;\n return () => self;\n});', + good: 'report(function make() {\n return () => this;\n});', + rules: ['unicorn/no-this-assignment'], + }, + { + clause: 'naming--constants', + policy: 'guideline', + bad: "const childCombinator = '>';\nreport(childCombinator);", + good: "const CHILD_COMBINATOR = '>';\nreport(CHILD_COMBINATOR);", + rules: ['ag/constant-name'], + }, + { + clause: 'naming--constants', + policy: 'guideline', + bad: "report(() => {\n const childCombinator = '>';\n return childCombinator;\n});", + rules: ['ag/constant-name'], + expectMiss: true, + }, + { + clause: 'accessors--no-getters-setters', + policy: 'both', + bad: 'report(class Dragon {\n get age() {\n return this.value;\n }\n});', + good: 'report(class Dragon {\n getAge() {\n return this.value;\n }\n});', + rules: ['ag/no-accessors'], + }, + { + clause: 'accessors--no-getters-setters', + policy: 'both', + extension: 'ts', + bad: 'interface Dragon {\n get age(): number;\n}\nreport(null as Dragon | null);', + good: 'interface Dragon {\n getAge(): number;\n}\nreport(null as Dragon | null);', + rules: ['ag/no-accessors'], + }, + { + clause: 'standard-library--isnan', + policy: 'both', + bad: 'report(isNaN(source));', + good: 'report(Number.isNaN(source));', + rules: ['ag-compat/no-restricted-globals'], + }, + { + clause: 'standard-library--isfinite', + policy: 'both', + bad: 'report(isFinite(source));', + good: 'report(Number.isFinite(source));', + rules: ['ag-compat/no-restricted-globals'], + }, + { + clause: 'typescript--enum-naming-conventions', + policy: 'both', + extension: 'ts', + bad: "enum COLOR {\n RED = 'RED',\n}\nreport(COLOR);", + good: "enum Color {\n Red = 'Red',\n}\nreport(Color);", + rules: ['ag/enum-name'], + }, + { + clause: 'typescript--caught-error-type', + policy: 'both', + extension: 'ts', + bad: 'try {\n report(1);\n} catch (error: any) {\n report(error);\n}', + good: 'try {\n report(1);\n} catch (error: unknown) {\n report(error);\n}', + rules: ['ag/unknown-catch'], + }, +]; diff --git a/tests/clauses.test.ts b/tests/clauses.test.ts new file mode 100644 index 0000000..fa6061b --- /dev/null +++ b/tests/clauses.test.ts @@ -0,0 +1,151 @@ +/** + * @file Run each clause's good and bad examples through the complete materialized preset for each policy. + */ +import { createRequire } from 'node:module'; + +import { + beforeAll, + describe, + expect, + it, +} from 'vitest'; + +import { createConfig } from '../packages/oxlint-config/src/index'; +import { catalog } from '../packages/rule-catalog/src/index'; +import { clauseCases, clauseGlobals } from './clause-cases'; +import { lintBatch } from './cli'; + +import type { Enforcement, Policy } from '../packages/rule-catalog/src/index'; +import type { ClauseCase } from './clause-cases'; +import type { Diagnostic } from './cli'; + +const REQUIRE = createRequire(import.meta.url); +const HEADER = '/**\n * @file Clause fixture.\n */\n\n'; +const POLICIES: Policy[] = ['compatibility', 'guideline']; +const SHARED_FILES = { + 'package.json': '{"name":"clauses","type":"module","dependencies":{"a":"1.0.0","b":"1.0.0","npm-package":"1.0.0"}}', + 'helpers.js': 'export const first = 1;\nexport const second = 2;\nexport const third = 3;\nexport const fourth = 4;\n', + 'bar.js': 'export default 1;\n', + 'src/foo.js': 'export default 1;\n', +}; + +interface Run { + example: ClauseCase; + policy: Policy; + index: number; +} + +const runs: Run[] = clauseCases.flatMap((example, index) => (example.policy === 'both' ? POLICIES : [example.policy]) + .map((policy) => ({ example, policy, index }))); +const outcomes = new Map(); + +const key = (run: Run, kind: 'bad' | 'good') => `${run.index}/${run.policy}/${kind}`; +const code = (diagnostic: Diagnostic) => diagnostic.code?.replace(/^([^()]+)\(([^)]+)\)$/u, '$1/$2'); +const source = (example: ClauseCase, text: string) => `${example.bare ? '' : HEADER}${text}${example.noFinalNewline ? '' : '\n'}`; + +beforeAll(async () => { + const groups = Object.groupBy(runs, (run) => [ + run.policy, + run.example.extension ?? 'js', + run.example.importGroups ?? '', + ].join('|')); + for (const [group, members] of Object.entries(groups)) { + const [policy, extension, importGroups] = group.split('|') as [Policy, string, string]; + const base = createConfig({ + language: ['ts', 'tsx'].includes(extension) ? 'typescript' : 'javascript', + environment: 'both', + policy, + ...(importGroups ? { importGroups: importGroups as 'example' | 'prose' } : {}), + }); + const config = { + ...base, + globals: Object.fromEntries(clauseGlobals.map((name) => [name, 'readonly'])), + jsPlugins: (base.jsPlugins as { name: string; specifier: string }[]) + .map((provider) => ({ ...provider, specifier: REQUIRE.resolve(provider.specifier) })), + }; + const files: Record = { ...SHARED_FILES }; + const names = new Map(); + for (const run of members ?? []) { + for (const kind of ['bad', 'good'] as const) { + const text = run.example[kind]; + if (text !== undefined) { + const directory = run.example.directory ? `${run.example.directory}/` : ''; + const name = `${directory}case-${run.index}-${kind}.${extension}`; + files[name] = source(run.example, text); + names.set(key(run, kind), name); + } + } + } + const results = await lintBatch(config, files); + names.forEach((name, id) => outcomes.set(id, results.get(name) ?? [])); + } +}, 300000); + +describe('guideline clause examples', () => { + runs.forEach((run) => { + const { example, policy } = run; + const label = `${policy}: ${example.clause} [${example.extension ?? 'js'}${example.importGroups ? `, ${example.importGroups}` : ''}]`; + if (example.bad !== undefined) { + it(`${label} ${example.expectMiss ? 'does not report' : 'reports'} the bad example`, () => { + const reported = outcomes.get(key(run, 'bad'))!.filter((diagnostic) => example.rules.includes(code(diagnostic)!)); + if (example.expectMiss) { + expect(reported).toEqual([]); + } else { + expect(reported.length, JSON.stringify(outcomes.get(key(run, 'bad')))).toBeGreaterThan(0); + } + }); + } + if (example.good !== undefined) { + it(`${label} ${example.expectConflict ? 'reports' : 'accepts'} the good example`, () => { + const reported = outcomes.get(key(run, 'good'))!.filter((diagnostic) => example.rules.includes(code(diagnostic)!)); + if (example.expectConflict) { + expect(reported.length).toBeGreaterThan(0); + } else { + expect(reported, JSON.stringify(reported)).toEqual([]); + } + }); + } + }); +}); + +describe('clause dispositions match executable examples', () => { + const disposition = (clause: (typeof catalog.clauses)[number], policy: Policy) => ( + policy === 'compatibility' ? clause : clause.guideline + ); + const covering = (id: string, policy: Policy) => runs.filter( + (run) => run.example.clause === id && run.policy === policy, + ); + const enforced: Enforcement[] = ['native', 'javascript', 'custom', 'partial']; + + POLICIES.forEach((policy) => { + it.each(catalog.clauses.filter((clause) => enforced.includes(disposition(clause, policy).enforcement)))( + `${policy}: $number $id has a reported bad example`, + (clause) => { + const cases = covering(clause.id, policy); + expect(cases.some(({ example }) => example.bad !== undefined && !example.expectMiss)).toBe(true); + }, + ); + + it.each(catalog.clauses.filter((clause) => ['partial', 'overridden'].includes(disposition(clause, policy).enforcement)))( + `${policy}: $number $id demonstrates what is not enforced`, + (clause) => { + const cases = covering(clause.id, policy); + expect(cases.some(({ example }) => example.expectMiss || example.expectConflict)).toBe(true); + }, + ); + }); + + it('marks a clause partial or overridden only when an example shows the gap', () => { + runs.filter(({ example }) => example.expectMiss || example.expectConflict).forEach(({ example, policy }) => { + const clause = catalog.clauses.find((entry) => entry.id === example.clause)!; + expect(['partial', 'overridden'], `${policy}: ${example.clause}`) + .toContain(disposition(clause, policy).enforcement); + }); + }); + + it('covers both import grouping interpretations', () => { + const variants = runs.filter(({ example }) => example.clause === 'modules--import-grouping') + .map(({ example }) => example.importGroups); + expect(variants).toEqual(expect.arrayContaining(['example', 'prose'])); + }); +}); diff --git a/tests/compiler.test.ts b/tests/compiler.test.ts new file mode 100644 index 0000000..a3c92fa --- /dev/null +++ b/tests/compiler.test.ts @@ -0,0 +1,129 @@ +/** + * @file Verify consumer tsconfig checks for the guide's compiler clauses. + */ +import { + mkdir, + mkdtemp, + rm, + writeFile, +} from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { + afterEach, + beforeEach, + describe, + expect, + it, +} from 'vitest'; + +import { parseArguments, runCli } from '../packages/oxlint-config/src/cli'; +import { checkTsconfig, parseJsonc } from '../packages/oxlint-config/src/compiler'; + +let directory: string; + +beforeEach(async () => { + directory = await mkdtemp(join(tmpdir(), 'ag-tsconfig-')); +}); + +afterEach(async () => { + await rm(directory, { recursive: true, force: true }); +}); + +/** + * Write a file below the temporary project. + * @param name - Relative file name. + * @param content - File text or JSON value. + */ +async function write(name: string, content: unknown): Promise { + const path = join(directory, name); + await mkdir(join(path, '..'), { recursive: true }); + await writeFile(path, typeof content === 'string' ? content : JSON.stringify(content)); +} + +describe('JSON with comments', () => { + it.each([ + ['{"a": 1}', { a: 1 }], + ['{\n // comment\n "a": 1, /* block */\n}', { a: 1 }], + ['{"list": [1, 2,],}', { list: [1, 2] }], + ['{"url": "http://example.com/*x*/", "comma": ",}"}', { url: 'http://example.com/*x*/', comma: ',}' }], + ['{"escaped": "quote \\" // not a comment"}', { escaped: 'quote " // not a comment' }], + ['// leading\n{"a": true} // trailing', { a: true }], + ['{"a": 1 /* unterminated', null], + ])('parses %j', (text, expected) => { + if (expected === null) { + expect(() => parseJsonc(text)).toThrow(); + } else { + expect(parseJsonc(text)).toEqual(expected); + } + }); +}); + +describe('tsconfig requirements', () => { + it('accepts the shared compiler settings', async () => { + const result = await checkTsconfig('packages/oxlint-config/tsconfig.base.json'); + expect(result.problems).toEqual([]); + expect(result.compilerOptions).toMatchObject({ strict: true, noUncheckedIndexedAccess: true }); + }); + + it('reports every missing clause requirement', async () => { + await write('tsconfig.json', { compilerOptions: { useUnknownInCatchVariables: false } }); + const { problems } = await checkTsconfig(join(directory, 'tsconfig.json')); + expect(problems).toEqual([ + '26.1: set "strict": true', + '26.2: set "noUncheckedIndexedAccess": true', + '25.3: do not disable "useUnknownInCatchVariables"', + ]); + }); + + it('follows relative, package, and array extends chains with later files taking precedence', async () => { + await write('node_modules/shared-config/package.json', { name: 'shared-config', version: '1.0.0' }); + await write('node_modules/shared-config/tsconfig.json', { compilerOptions: { strict: true } }); + await write('configs/strict.json', '{\n // Relative base\n "compilerOptions": { "noUncheckedIndexedAccess": true, },\n}'); + await write('configs/loose.json', { compilerOptions: { strict: false } }); + await write('tsconfig.json', { extends: ['./configs/loose', 'shared-config/tsconfig.json', './configs/strict.json'] }); + const result = await checkTsconfig(join(directory, 'tsconfig.json')); + expect(result.problems).toEqual([]); + expect(result.files).toHaveLength(4); + }); + + it('resolves package names without an explicit json extension', async () => { + await write('node_modules/shared-config/package.json', { name: 'shared-config', version: '1.0.0' }); + await write('node_modules/shared-config/base.json', { compilerOptions: { strict: true, noUncheckedIndexedAccess: true } }); + await write('tsconfig.json', { extends: 'shared-config/base' }); + expect((await checkTsconfig(join(directory, 'tsconfig.json'))).problems).toEqual([]); + }); + + it('lets a project disable an inherited requirement and reports it', async () => { + await write('base.json', { compilerOptions: { strict: true, noUncheckedIndexedAccess: true } }); + await write('tsconfig.json', { extends: './base.json', compilerOptions: { strict: false } }); + expect((await checkTsconfig(join(directory, 'tsconfig.json'))).problems).toEqual(['26.1: set "strict": true']); + }); + + it('rejects circular extends chains', async () => { + await write('first.json', { extends: './second.json' }); + await write('second.json', { extends: './first.json' }); + await expect(checkTsconfig(join(directory, 'first.json'))).rejects.toThrow(/Circular tsconfig extends/u); + }); +}); + +describe('tsconfig CLI', () => { + it('parses the optional path', () => { + expect(parseArguments(['--check-tsconfig']).tsconfig).toBe('tsconfig.json'); + expect(parseArguments(['--check-tsconfig', 'app/tsconfig.json']).tsconfig).toBe('app/tsconfig.json'); + expect(parseArguments(['--check-tsconfig', '--force']).tsconfig).toBe('tsconfig.json'); + }); + + it('prints success and fails with every unmet requirement', async () => { + await write('good.json', { compilerOptions: { strict: true, noUncheckedIndexedAccess: true } }); + await write('bad.json', { compilerOptions: { strict: true } }); + let output = ''; + await runCli(['--check-tsconfig', join(directory, 'good.json')], (text) => { + output += text; + }); + expect(output).toContain("meets the guide's compiler requirements"); + await expect(runCli(['--check-tsconfig', join(directory, 'bad.json')], () => undefined)) + .rejects.toThrow(/26\.2: set "noUncheckedIndexedAccess": true/u); + }); +}); diff --git a/tests/config-cli.test.ts b/tests/config-cli.test.ts index 9d4b6f5..f70ff26 100644 --- a/tests/config-cli.test.ts +++ b/tests/config-cli.test.ts @@ -36,6 +36,12 @@ it('accepts an explicit policy profile', () => { expect(parseArguments(['--policy', 'compatibility']).options).toEqual({ policy: 'compatibility' }); }); +it('accepts the project profile and import grouping options', () => { + expect(parseArguments(['--profile', 'adguard-projects']).options).toEqual({ profile: 'adguard-projects' }); + expect(parseArguments(['--policy', 'guideline', '--import-groups', 'prose']).options) + .toEqual({ policy: 'guideline', importGroups: 'prose' }); +}); + it.each( [ ['--unknown'], @@ -48,6 +54,9 @@ it.each( ['--policy', 'strict'], ['--environment', 'worker'], ['--source-type', 'automatic'], + ['--profile', 'custom'], + ['--import-groups', 'prose'], + ['--policy', 'guideline', '--import-groups', 'sorted'], ].map((args) => ({ args })), )('rejects invalid arguments $args', ({ args }) => { expect(() => parseArguments(args)).toThrow(TypeError); diff --git a/tests/consumer.test.ts b/tests/consumer.test.ts index 41ea154..076930d 100644 --- a/tests/consumer.test.ts +++ b/tests/consumer.test.ts @@ -206,6 +206,15 @@ describe('installed package contract', () => { ), ); expect(json.rules['ag-compat/no-console']).toBe('error'); + const project = JSON.parse( + await readFile( + join(directory, 'node_modules/@agcodeguidelines/oxlint-config/dist/typescript-adguard-projects.json'), + 'utf8', + ), + ); + expect(project.settings.agProfile).toBe('adguard-projects'); + expect(project.overrides[0].rules['typescript/explicit-function-return-type']).toBe('error'); + expect(json.rules['ag-jsdoc/require-description']).toBeUndefined(); }); it('runs opt-in typed rules with the separately installed service', async () => { @@ -217,8 +226,9 @@ describe('installed package contract', () => { await writeFile(join(directory, 'typed-config.ts'), [ "import { writeFileSync } from 'node:fs';", "import { createConfig } from '@agcodeguidelines/oxlint-config';", - "const preset = createConfig({ language: 'typescript', typeAware: true });", + "const preset = createConfig({ language: 'typescript', typeAware: true, profile: 'adguard-projects' });", "if (preset.settings.agPolicy !== 'compatibility') throw new Error('Default policy changed');", + "if (preset.settings.agProfile !== 'adguard-projects') throw new Error('Profile not recorded');", "writeFileSync('typed.json', JSON.stringify({", ' categories: preset.categories,', " plugins: ['typescript'],", diff --git a/tests/custom-cases.ts b/tests/custom-cases.ts index db9c927..0889871 100644 --- a/tests/custom-cases.ts +++ b/tests/custom-cases.ts @@ -1,16 +1,22 @@ /** * @file Structural, operator, and syntax boundary cases for the custom rules. */ +import type { RuleTester } from 'oxlint/plugins-dev'; + export interface CustomCase { rule: string; name: string; code: string; errors: number; lang: 'jsx' | 'tsx'; + options?: RuleTester.ValidTestCase['options']; } export const customCases: CustomCase[] = []; +// Builds template substitutions in fixture source text without writing `${` inside a string literal. +const DOLLAR = '$'; + /** * Register a distinct custom-rule input and its expected diagnostic count. * @param rule - Custom rule name. @@ -18,14 +24,23 @@ export const customCases: CustomCase[] = []; * @param code - Source text. * @param errors - Expected diagnostics. * @param lang - Parser language. + * @param options - Rule options. */ -function add(rule: string, name: string, code: string, errors: number, lang: 'jsx' | 'tsx' = 'jsx'): void { +function add( + rule: string, + name: string, + code: string, + errors: number, + lang: 'jsx' | 'tsx' = 'jsx', + options?: CustomCase['options'], +): void { customCases.push({ rule, name, code, errors, lang, + ...(options ? { options } : {}), }); } @@ -197,6 +212,9 @@ const catchForms = [ ['catch (error:\nany) {}', 1], ['catch ({ message }) {}', 0], ['catch ([first]) {}', 0], + ['catch ({ message }: any) {}', 1], + ['catch ([first]: any) {}', 1], + ['catch ({ message }: unknown) {}', 0], ] as const; for (const [form, errors] of catchForms) { for (const [context, wrap] of [ @@ -299,3 +317,114 @@ for (const newline of ['\n', '\r\n', '\r']) { 'const value = 1;', '/* before */ const value = 1; /* after */', ].forEach((code) => add('require-docblock', code, code, 0)); + +[ + ['abstract getter', 'abstract class Shape { abstract get area(): number; }', 1], + ['abstract setter', 'abstract class Shape { abstract set area(next: number); }', 1], + ['interface accessors', 'interface Shape { get area(): number; set area(next: number); }', 2], + ['type literal getter', 'type Shape = { get area(): number };', 1], + ['auto-accessor', 'class Counter { accessor count = 0; }', 1], + ['static auto-accessor', 'class Counter { static accessor count = 0; }', 1], + ['abstract auto-accessor', 'abstract class Counter { abstract accessor count: number; }', 1], + ['abstract method', 'abstract class Shape { abstract getArea(): number; }', 0], + ['interface method', 'interface Shape { getArea(): number; }', 0], + ['interface property', 'interface Shape { area: number; }', 0], + ['type literal method', 'type Shape = { getArea(): number };', 0], +].forEach(([name, code, errors]) => { + add('no-accessors', `TypeScript ${name as string}`, code as string, errors as number, 'tsx'); +}); + +for (const [name, code, errors] of [ + ['two literals', "const message = 'first part '\n + 'second part';", 1], + ['operator at line end', "const message = 'first part ' +\n 'second part';", 1], + ['three literals', "const message = 'first '\n + 'second '\n + 'third';", 1], + ['templates', 'const message = `first `\n + `second`;', 1], + ['argument', "report('first part '\n + 'second part');", 1], + ['literal after value', "const message = prefix + 'first '\n + 'second';", 1], + ['same line', "const message = 'first ' + 'second';", 0], + ['value then literal', "const message = prefix\n + 'second';", 0], + ['literal then value', "const message = 'first '\n + suffix;", 0], + ['substitution', `const message = \`first ${DOLLAR}{value}\`\n + 'second';`, 0], + ['numbers', 'const total = 1\n + 2;', 0], + ['subtraction', "const value = 'first'\n - 'second';", 0], + ['separated by value', "const message = 'first '\n + value\n + 'second';", 0], +] as const) { + add('no-multiline-string-concat', name, code, errors); +} + +const layouts = [ + ['operator continues first line', 'if (first === 1\n && second === 2) {\n work();\n}', 1], + ['closing parenthesis after condition', 'if (\n first\n && second) {\n work();\n}', 1], + ['condition starts after parenthesis', 'if (first\n && second\n) {\n work();\n}', 1], + ['while', 'while (first\n || second) {\n work();\n}', 1], + ['do while', 'do {\n work();\n} while (first\n && second);', 1], + ['do while without semicolon', 'do {\n work();\n} while (first\n && second)', 1], + ['grouped operand', 'if ((first || second)\n && third) {\n work();\n}', 1], + ['good if', 'if (\n first === 1\n && second === 2\n) {\n work();\n}', 0], + ['good while', 'while (\n first\n || second\n) {\n work();\n}', 0], + ['good do while', 'do {\n work();\n} while (\n first\n && second\n);', 0], + ['single line', 'if (first && second) {\n work();\n}', 0], + ['multiline call', 'if (check({\n first,\n})) {\n work();\n}', 0], + ['negated group', 'if (!(first\n && second)) {\n work();\n}', 0], + ['single line while', 'while (first || second) {\n work();\n}', 0], +] as const; +for (const [name, code, errors] of layouts) { + add('multiline-condition-layout', name, code, errors); +} +add('multiline-condition-layout', 'type assertion', 'if ((first\n && second) as boolean) {\n work();\n}', 1, 'tsx'); + +for (const [code, errors] of [ + ["const childCombinator = '>';", 1], + ['export const maxSize = 10;', 1], + ['const offset = -1;', 1], + ['const limit = 10n;', 1], + ['const enabled = true;', 1], + ['const label = `text`;', 1], + ["const first = 'a', SECOND = 'b';", 1], + ["const _PRIVATE = 'a';", 1], + ["const CHILD_COMBINATOR = '>';", 0], + ['export const MAX_SIZE_2 = 10;', 0], + ['const X = 1;', 0], + ['const value = compute();', 0], + ['let counter = 0;', 0], + ['function run() { const local = 1; return local; }', 0], + ['const { first } = source;', 0], + ['const pattern = /x/u;', 0], + ['const empty = null;', 0], + [`const label = \`${DOLLAR}{value}\`;`, 0], + ['const items = [];', 0], + ['const negated = !flag;', 0], + ['const declared = undefined;', 0], + ['export default 1;', 0], + ['const value = compute();\nexport { value };', 0], +] as const) { + add('constant-name', code, code, errors); +} +for (const [code, errors] of [ + ["const mode = 'light' as const;", 1], + ['const size = 1 satisfies number;', 1], + ['const size = (1 as number)!;', 1], + ["const MODE = 'light' as const;", 0], +] as const) { + add('constant-name', `TypeScript ${code}`, code, errors, 'tsx'); +} + +const runs = { lineCommentRuns: true }; +for (const [name, code, errors] of [ + ['two lines', '// first\n// second\nwork();', 1], + ['three lines', '// first\n// second\n// third\nwork();', 1], + ['two runs', '// first\n// second\n\n// third\n// fourth\nwork();', 2], + ['indented run', 'function run() {\n // first\n // second\n work();\n}', 1], + ['single line', '// only\nwork();', 0], + ['separated', '// first\nwork();\n// second\nwork();', 0], + ['blank line', '// first\n\n// second\nwork();', 0], + ['trailing comment', 'work(); // trailing\n// next\nwork();', 0], + ['directive', '// eslint-disable-next-line no-console\n// explains\nwork();', 0], + ['oxlint directive', '// oxlint-disable-next-line no-console\n// explains\nwork();', 0], + ['triple slash', '/// \n/// ', 0], + ['block between', '// first\n/* second */\n// third\nwork();', 0], + ['multiline block', '/* first\n second */\nwork();', 1], +] as const) { + add('require-docblock', `line runs: ${name}`, code, errors, 'jsx', [runs]); +} +add('require-docblock', 'line runs disabled explicitly', '// alpha\n// beta\nwork();', 0, 'jsx', [{ lineCommentRuns: false }]); diff --git a/tests/custom.test.ts b/tests/custom.test.ts index 306a061..7f33961 100644 --- a/tests/custom.test.ts +++ b/tests/custom.test.ts @@ -17,6 +17,7 @@ Object.entries(rules).forEach(([name, rule]) => { name: item.name, code: item.code, languageOptions: { parserOptions: { lang: item.lang } }, + ...(item.options ? { options: item.options } : {}), }); tester.run(name, rule, { valid: cases.filter((item) => !item.errors).map(makeCase), @@ -34,8 +35,11 @@ describe('custom plugin contract', () => { it('exports stable names and source-linked metadata', () => { expect(plugin.meta?.name).toBe('ag'); expect(Object.keys(rules)).toEqual(Object.keys(rules)); - Object.values(rules).forEach((rule) => { - expect(rule.meta?.schema).toEqual([]); + Object.entries(rules).forEach(([name, rule]) => { + // Only the multiline comment rule has an option: the guideline policy's line-comment runs. + expect(rule.meta?.schema).toEqual(name === 'require-docblock' + ? [{ type: 'object', properties: { lineCommentRuns: { type: 'boolean' } }, additionalProperties: false }] + : []); expect(rule.meta?.docs?.url).toContain('CodeGuidelines/blob/master/JavaScript/Javascript.md#'); expect(rule.meta?.fixable).toBeUndefined(); }); diff --git a/tests/fixtures/typescript/classes.ts b/tests/fixtures/typescript/classes.ts new file mode 100644 index 0000000..dc0c9b1 --- /dev/null +++ b/tests/fixtures/typescript/classes.ts @@ -0,0 +1,15 @@ +/** + * @file Abstract classes, parameter properties, and typed members. + */ + +export abstract class Shape { + protected constructor(protected readonly name: string) {} + + public abstract area(): number; + + public describe(): string { + return `${this.name}: ${this.area()}`; + } +} + +export const SHAPE_KINDS = ['square'] as const; diff --git a/tests/fixtures/typescript/component.tsx b/tests/fixtures/typescript/component.tsx new file mode 100644 index 0000000..3857ba5 --- /dev/null +++ b/tests/fixtures/typescript/component.tsx @@ -0,0 +1,20 @@ +/** + * @file A typed React component and generic arrow function. + */ +import React from 'react'; + +interface Props { + title: string; +} + +export const identity = (value: Value): Value => value; + +/** + * Render a title. + * @param props Component properties. + * @param props.title Title text. + * @returns The rendered title. + */ +export function Title({ title }: Props): React.JSX.Element { + return

{identity(title)}

; +} diff --git a/tests/fixtures/typescript/declarations.d.ts b/tests/fixtures/typescript/declarations.d.ts new file mode 100644 index 0000000..d7836e9 --- /dev/null +++ b/tests/fixtures/typescript/declarations.d.ts @@ -0,0 +1,17 @@ +/** + * @file Ambient declarations. + */ + +declare global { + interface Window { + adguard: unknown; + } +} + +declare module 'untyped-package' { + export function run(): void; + + export const VERSION: string; +} + +export {}; diff --git a/tests/fixtures/typescript/imports.ts b/tests/fixtures/typescript/imports.ts new file mode 100644 index 0000000..ea6a7db --- /dev/null +++ b/tests/fixtures/typescript/imports.ts @@ -0,0 +1,19 @@ +/** + * @file Type-only and inline type imports and exports. + */ +import { type Shape } from './classes'; +import { Square } from './square'; +import type { Pair } from './types'; + +export type { Pair }; + +/** + * Measure a shape. + * @param shape Shape to measure. + * @returns The shape area. + */ +export function measure(shape: Shape): number { + return shape.area(); +} + +export const UNIT = new Square(1); diff --git a/tests/fixtures/typescript/merging.ts b/tests/fixtures/typescript/merging.ts new file mode 100644 index 0000000..876e054 --- /dev/null +++ b/tests/fixtures/typescript/merging.ts @@ -0,0 +1,47 @@ +/** + * @file Declaration merges that TypeScript permits. + */ + +interface Size { + width: number; +} + +interface Size { + height: number; +} + +export interface Widget { + size: Size; +} + +export class Widget { + public readonly name: string = 'widget'; +} + +/** + * Build a size. + * @returns The default size. + */ +export function build(): number { + return build.SIZE; +} + +export namespace build { + export const SIZE = 2; +} + +export enum Direction { + Up = 'Up', + Down = 'Down', +} + +export namespace Direction { + export const DEFAULT = Direction.Up; +} + +export type Mode = 'light' | 'dark'; + +export const Mode = { + Light: 'light', + Dark: 'dark', +} as const; diff --git a/tests/fixtures/typescript/namespace-merge.ts b/tests/fixtures/typescript/namespace-merge.ts new file mode 100644 index 0000000..e176309 --- /dev/null +++ b/tests/fixtures/typescript/namespace-merge.ts @@ -0,0 +1,11 @@ +/** + * @file A class merged with a namespace. + */ + +export class Box { + public readonly width: number = Box.DEFAULT_WIDTH; +} + +export namespace Box { + export const DEFAULT_WIDTH = 1; +} diff --git a/tests/fixtures/typescript/overloads.ts b/tests/fixtures/typescript/overloads.ts new file mode 100644 index 0000000..f245400 --- /dev/null +++ b/tests/fixtures/typescript/overloads.ts @@ -0,0 +1,44 @@ +/** + * @file Function, method, and constructor overloads. + */ + +/** + * Convert a value to its own type. + * @param input Value to convert. + * @returns The converted value. + */ +export function convert(input: string): string; + +/** + * Convert a value to its own type. + * @param input Value to convert. + * @returns The converted value. + */ +export function convert(input: number): number; + +/** + * Convert a value to its own type. + * @param input Value to convert. + * @returns The converted value. + */ +export function convert(input: string | number): string | number { + return input; +} + +export class Parser { + private readonly prefix: string; + + public constructor(prefix: string); + + public constructor(prefix: number); + + public constructor(prefix: string | number) { + this.prefix = String(prefix); + } + + public parse(input: string): string; + public parse(input: number): number; + public parse(input: string | number): string | number { + return typeof input === 'string' ? `${this.prefix}${input}` : input; + } +} diff --git a/tests/fixtures/typescript/square.ts b/tests/fixtures/typescript/square.ts new file mode 100644 index 0000000..391b965 --- /dev/null +++ b/tests/fixtures/typescript/square.ts @@ -0,0 +1,16 @@ +/** + * @file A concrete shape. + */ +import { Shape } from './classes'; + +export class Square extends Shape { + public constructor(private readonly side: number) { + super('square'); + } + + public area(): number { + return this.side ** 2; + } +} + +export const SQUARE_SIDES = 4; diff --git a/tests/fixtures/typescript/types.ts b/tests/fixtures/typescript/types.ts new file mode 100644 index 0000000..99bce65 --- /dev/null +++ b/tests/fixtures/typescript/types.ts @@ -0,0 +1,78 @@ +/** + * @file Generic, tuple, mapped, and conditional types with runtime helpers. + */ + +export type Pair = [ + string, + number, +]; + +export type Mapped = { [Key in keyof Value]?: Value[Key] }; + +export type Conditional = Value extends string ? 'text' : 'other'; + +export interface Dictionary { + readonly name: string; + [key: string]: unknown; +} + +export const THEME = { mode: 'light' } as const satisfies Record; + +/** + * Read the first value of a list. + * @param values Values to read. + * @returns The first value, if any. + */ +export function first(values: Value[]): Value | undefined { + return values[0]; +} + +/** + * Check whether a value is text. + * @param value Value to check. + * @returns Whether the value is text. + */ +export function isText(value: unknown): value is string { + return typeof value === 'string'; +} + +/** + * Assert that a value is text. + * @param value Value to check. + * @throws {TypeError} When the value is not text. + */ +export function assertText(value: unknown): asserts value is string { + if (!isText(value)) { + throw new TypeError('Not text'); + } +} + +/** + * Greet a user. + * @param greeting Greeting to use. + * @param name Optional user name. + * @returns The greeting. + */ +export function greet(greeting = 'hi', name?: string): string { + return `${greeting} ${name ?? ''}`; +} + +/** + * Describe the event target. + * @param this Window that receives the event. + * @param event Handled event. + * @returns The event type. + */ +export function describe(this: Window, event: Event): string { + return `${this.name}:${event.type}`; +} + +/** + * Pick an element that must exist. + * @returns The element. + */ +export function root(): Element { + return document.querySelector('main')!; +} + +export const EMPTY = new Array(); diff --git a/tests/gap-cases.ts b/tests/gap-cases.ts index 8ca3d17..1e96ee8 100644 --- a/tests/gap-cases.ts +++ b/tests/gap-cases.ts @@ -10,11 +10,14 @@ export interface GapCase { files?: Record; validFiles?: Record; invalidFiles?: Record; + /** Configuration layer that enables the rule; absent for the base preset. */ + scope?: 'guideline' | 'adguard-projects'; } export const gapCases: GapCase[] = [ { rule: '@typescript-eslint/consistent-type-exports', + scope: 'adguard-projects', valid: 'export type { Value } from "./dependency";', invalid: 'export { Value } from "./dependency";', extension: 'ts', @@ -260,6 +263,7 @@ export const gapCases: GapCase[] = [ }, { rule: 'import/no-unassigned-import', + scope: 'adguard-projects', valid: 'import value from "./dependency";', invalid: 'import "./dependency";', }, @@ -270,16 +274,19 @@ export const gapCases: GapCase[] = [ }, { rule: 'no-restricted-imports', + scope: 'adguard-projects', valid: 'import value from "./allowed";', invalid: 'import value from "./feature-mv2";', }, { rule: 'import-newlines/enforce', + scope: 'adguard-projects', valid: 'import { a } from "./dependency";', invalid: 'import { a, b, c, d } from "./dependency";', }, { rule: 'boundaries/element-types', + scope: 'adguard-projects', // Resolves extensionless '../src/index' to src/index.ts via the // TypeScript-aware import resolver. language: 'typescript', @@ -294,44 +301,99 @@ export const gapCases: GapCase[] = [ }, { rule: '@adguard/logger-context/require-logger-context', + scope: 'adguard-projects', valid: 'logger.error("[ext.main]: message");', invalid: 'logger.error("message");', }, { rule: '@typescript-eslint/no-explicit-any', + scope: 'adguard-projects', valid: 'export const value: unknown = 1;', invalid: 'export const value: any = 1;', extension: 'ts', }, { rule: '@typescript-eslint/explicit-function-return-type', + scope: 'adguard-projects', valid: 'export function read(): number { return 1; }', invalid: 'export function read() { return 1; }', extension: 'ts', }, { rule: '@typescript-eslint/no-var-requires', + scope: 'adguard-projects', valid: 'import value from "./dependency";\nconsole.log(value);', invalid: 'export const value = require("./dependency");', extension: 'ts', }, { rule: '@typescript-eslint/ban-ts-comment', + scope: 'adguard-projects', valid: 'export const value = 1;', invalid: 'export const value = 1; // @ts-ignore', extension: 'ts', }, { rule: '@typescript-eslint/explicit-member-accessibility', + scope: 'adguard-projects', valid: 'export class Value { public read(): number { return 1; } }', invalid: 'export class Value { read(): number { return 1; } }', extension: 'ts', }, { rule: '@typescript-eslint/consistent-type-imports', + scope: 'adguard-projects', valid: 'import { type Value } from "./dependency";\nconst x: Value = 1 as Value;\nconsole.log(x);', invalid: 'import { Value } from "./dependency";\nconst x: Value = 1 as Value;\nconsole.log(x);', extension: 'ts', files: { 'dependency.ts': 'export type Value = number;' }, }, + { + rule: 'import/no-commonjs', + scope: 'guideline', + valid: 'import value from "./dependency";\n\nexport { value };', + invalid: 'const value = require("./dependency");\n\nmodule.exports = value;', + }, + { + rule: 'import/no-namespace', + scope: 'guideline', + valid: 'import { named } from "./dependency";\n\nexport { named };', + invalid: 'import * as dependency from "./dependency";\n\nexport { dependency };', + }, + { + rule: 'import/no-default-export', + scope: 'guideline', + valid: 'export const value = 1;', + invalid: 'const value = 1;\n\nexport default value;', + }, + { + rule: 'import/order', + scope: 'guideline', + valid: 'import fs from "node:fs";\n\nimport value from "./dependency";\n\nexport { fs, value };', + invalid: 'import fs from "node:fs";\nimport value from "./dependency";\n\nexport { fs, value };', + }, + { + rule: 'line-comment-position', + scope: 'guideline', + valid: '// Explains the value.\nconst value = 1;', + invalid: 'const value = 1; // Explains the value.', + }, + { + rule: 'lines-around-comment', + scope: 'guideline', + valid: 'work();\n\n// Explains the value.\nconst value = 1;', + invalid: 'work();\n// Explains the value.\nconst value = 1;', + }, + { + rule: 'padding-line-between-statements', + scope: 'guideline', + valid: 'if (ready) {\n work();\n}\n\nwork();', + invalid: 'if (ready) {\n work();\n}\nwork();', + }, + { + rule: 'id-length', + scope: 'guideline', + valid: 'const query = 1;', + invalid: 'const q = 1;', + }, ]; diff --git a/tests/gaps.test.ts b/tests/gaps.test.ts index 117b5cf..5683d3f 100644 --- a/tests/gaps.test.ts +++ b/tests/gaps.test.ts @@ -22,12 +22,16 @@ const outcomes = new Map(); beforeAll(async () => { for (const example of gapCases) { - const mapping = catalog.mappings.find((entry) => entry.source === example.rule)!; + const mapping = catalog.mappings.find( + (entry) => entry.source === example.rule && entry.scope === example.scope, + )!; const target = mapping.target!; for (const kind of ['valid', 'invalid'] as const) { const base = createConfig({ language: example.language, typeAware: mapping.requiresTypeInfo ?? false, + policy: example.scope === 'guideline' ? 'guideline' : 'compatibility', + profile: example.scope === 'adguard-projects' ? 'adguard-projects' : 'guide', }); const providers = base.jsPlugins as { name: string; specifier: string }[]; const config = { @@ -61,11 +65,14 @@ beforeAll(async () => { const extra = kind === 'valid' ? example.validFiles : example.invalidFiles; const names = extra ? Object.keys(extra) : [filename]; outcomes.set( - `${example.rule}/${kind}`, + `${example.scope ?? 'base'}:${example.rule}/${kind}`, names.flatMap((name) => result.get(name) ?? []), ); } catch (error: unknown) { - outcomes.set(`${example.rule}/${kind}`, [{ severity: 'error', message: String(error) }]); + outcomes.set( + `${example.scope ?? 'base'}:${example.rule}/${kind}`, + [{ severity: 'error', message: String(error) }], + ); } } } @@ -74,14 +81,16 @@ beforeAll(async () => { describe('supplementary rule and multi-file coverage', () => { gapCases.forEach((example) => { ['valid', 'invalid'].forEach((kind) => { - it(`${example.rule}/${kind}`, () => { - const diagnostics = outcomes.get(`${example.rule}/${kind}`); + it(`${example.scope ?? 'base'}: ${example.rule}/${kind}`, () => { + const diagnostics = outcomes.get(`${example.scope ?? 'base'}:${example.rule}/${kind}`); expect(diagnostics).toBeDefined(); if (kind === 'valid') { expect(diagnostics).toEqual([]); } else { expect(diagnostics?.length).toBeGreaterThan(0); - const { target } = catalog.mappings.find((entry) => entry.source === example.rule)!; + const { target } = catalog.mappings.find( + (entry) => entry.source === example.rule && entry.scope === example.scope, + )!; if (example.rule === 'import/export') { expect(diagnostics?.[0]?.message).toContain('Duplicated export'); return; diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 19f20fa..92ed249 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -122,6 +122,56 @@ describe('public configuration API', () => { expect(createConfig().rules!['ag-import/prefer-default-export']).toBe('error'); }); + it('layers the guideline policy over the sample configuration', () => { + const compatibility = createConfig(); + const guideline = createConfig({ policy: 'guideline' }); + expect(guideline.rules!['ag-import/order']).toEqual(catalog.policies.guideline.importGroups.example); + expect(createConfig({ policy: 'guideline', importGroups: 'prose' }).rules!['ag-import/order']) + .toEqual(catalog.policies.guideline.importGroups.prose); + expect(compatibility.rules!['ag-import/order']).toEqual(catalog.rules['ag-import/order']); + ['import/no-commonjs', 'import/no-namespace', 'import/no-default-export', 'ag/constant-name'].forEach((rule) => { + expect(compatibility.rules![rule as keyof typeof compatibility.rules]).toBeUndefined(); + expect(guideline.rules![rule as keyof typeof guideline.rules]).toBe('error'); + }); + expect(guideline.plugins).toContain('import'); + expect(guideline.overrides).toEqual([{ files: ['**/*.{cjs,cts}'], rules: { 'import/no-commonjs': 'off' } }]); + const script = createConfig({ policy: 'guideline', sourceType: 'commonjs' }); + expect(script.rules!['import/no-commonjs']).toBe('off'); + expect(script.overrides).toEqual([]); + expect(() => createConfig({ importGroups: 'prose' })).toThrow(/guideline policy/u); + expect(() => createConfig({ policy: 'guideline', importGroups: 'sorted' as 'prose' })).toThrow(/importGroups/u); + }); + + it('applies TypeScript equivalents after the guideline overlay', () => { + const guideline = createConfig({ language: 'typescript', policy: 'guideline' }); + const typescript = guideline.overrides![0]!; + expect(typescript.files).toEqual(['**/*.{ts,tsx,mts,cts}']); + expect(typescript.rules!['ag-compat/lines-around-comment']).toBe('off'); + expect(typescript.rules!['ag-style/lines-around-comment']).toMatchObject(['error', { allowInterfaceStart: true }]); + expect(typescript.rules!['ag-ts/no-redeclare']).toBe('error'); + expect(guideline.overrides![1]!.files).toEqual(['**/*.{cjs,cts}']); + }); + + it('keeps AdGuard project conventions in an opt-in profile', () => { + const guide = createConfig({ language: 'typescript' }); + const project = createConfig({ language: 'typescript', profile: 'adguard-projects' }); + expect(guide.settings!.agProfile).toBe('guide'); + expect(project.settings!.agProfile).toBe('adguard-projects'); + expect(guide.settings!['boundaries/elements']).toBeUndefined(); + expect(project.settings!['boundaries/elements']).toEqual( + catalog.profiles['adguard-projects'].settings['boundaries/elements'], + ); + expect(guide.rules!['ag-compat/no-restricted-imports']).toBeUndefined(); + expect(project.rules!['ag-compat/no-restricted-imports']).toBeDefined(); + expect(guide.overrides![0]!.rules!['typescript/no-explicit-any']).toBeUndefined(); + expect(project.overrides![0]!.rules!['typescript/no-explicit-any']).toBe('error'); + expect(project.overrides![0]!.rules!['typescript/consistent-type-exports']).toBeUndefined(); + expect(() => createConfig({ profile: 'custom' as 'guide' })).toThrow(/profile/u); + catalog.mappings.filter((mapping) => mapping.scope === 'adguard-projects').forEach((mapping) => { + expect(mapping.origin).toBe('adguard-projects'); + }); + }); + it('selects syntax-only linting for both consumer languages', () => { expect(createConfig('javascript').options?.typeAware).not.toBe(true); expect(createConfig('typescript').options?.typeAware).not.toBe(true); @@ -132,11 +182,14 @@ describe('public configuration API', () => { expect(catalog.baseline['no-await-in-loop']).toBe('off'); expect(catalog.baseline['jsdoc/require-file-overview']).toBeUndefined(); expect(catalog.rules['ag-jsdoc/require-file-overview']).toBe('error'); - expect(catalog.rules['ag-jsdoc/require-description']).toBe('error'); - expect(catalog.rules['ag-jsdoc/require-description-complete-sentence']).toBe('error'); - expect(catalog.rules['ag-jsdoc/require-hyphen-before-param-description']).toEqual(['error', 'never']); - expect(catalog.rules['ag-jsdoc/require-throws']).toBe('error'); - expect(catalog.rules['ag-jsdoc/sort-tags']).toBe('error'); + // Stricter JSDoc rules are AdGuard project conventions, not guide requirements. + const project = catalog.profiles['adguard-projects'].rules; + expect(catalog.rules['ag-jsdoc/require-description']).toBeUndefined(); + expect(project['ag-jsdoc/require-description']).toBe('error'); + expect(project['ag-jsdoc/require-description-complete-sentence']).toBe('error'); + expect(project['ag-jsdoc/require-hyphen-before-param-description']).toEqual(['error', 'never']); + expect(project['ag-jsdoc/require-throws']).toBe('error'); + expect(project['ag-jsdoc/sort-tags']).toBe('error'); expect(catalog.baseline['brace-style']).toEqual(['error', '1tbs', { allowSingleLine: false }]); expect(catalog.baseline.indent).toEqual(['error', 4, { SwitchCase: 1 }]); expect(catalog.baseline['react/jsx-indent']).toEqual(['error', 2]); diff --git a/tests/typescript-preset.test.ts b/tests/typescript-preset.test.ts new file mode 100644 index 0000000..f70c094 --- /dev/null +++ b/tests/typescript-preset.test.ts @@ -0,0 +1,107 @@ +/** + * @file Run the complete TypeScript preset on idiomatic TypeScript and on TypeScript-only violations. + */ +import { readdirSync, readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; + +import { + beforeAll, + describe, + expect, + it, +} from 'vitest'; + +import { createConfig } from '../packages/oxlint-config/src/index'; +import { catalog } from '../packages/rule-catalog/src/index'; +import { lintBatch } from './cli'; + +import type { Policy } from '../packages/rule-catalog/src/index'; +import type { Diagnostic } from './cli'; + +const REQUIRE = createRequire(import.meta.url); +const CORPUS = new URL('./fixtures/typescript/', import.meta.url); +const HEADER = '/**\n * @file TypeScript fixture.\n */\n\n'; +const POLICIES: Policy[] = ['compatibility', 'guideline']; +// Builds template substitutions in fixture source text without writing `${` inside a string literal. +const DOLLAR = '$'; +const corpus = Object.fromEntries(readdirSync(CORPUS).map((name) => [name, readFileSync(new URL(name, CORPUS), 'utf8')])); + +// Each violation is missed by the pinned JavaScript implementation but reported by the TypeScript-aware one, +// or guards that a replacement keeps reporting genuine errors. Duplicate class members are omitted: Oxc +// rejects them in TypeScript before rules run, so the replacement only has to accept overloads. +const violations: [string, string, string][] = [ + ['enum trailing comma', "enum Color {\n Red = 'Red',\n Green = 'Green'\n}\n\nexport { Color };", 'ag-style/comma-dangle'], + ['tuple trailing comma', 'type Pair = [\n string,\n number\n];\n\nexport type { Pair };', 'ag-style/comma-dangle'], + ['type parameter trailing comma', 'type Pair<\n First,\n Second\n> = [First, Second];\n\nexport type { Pair };', 'ag-style/comma-dangle'], + ['union spacing', 'type Value = string|number;\n\nexport type { Value };', 'ag-style/space-infix-ops'], + ['type literal spacing', 'type Value = {first: string};\n\nexport type { Value };', 'ag-style/object-curly-spacing'], + ['interface key spacing', 'interface Shape {\n area : number;\n}\n\nexport type { Shape };', 'ag-style/key-spacing'], + ['interface block spacing', 'interface Shape{\n area: number;\n}\n\nexport type { Shape };', 'ag-style/space-before-blocks'], + ['type alias semicolon', 'type Value = string\n\nexport type { Value };', 'ag-style/semi'], + [ + 'abstract member semicolon', + 'abstract class Shape {\n public abstract area(): number\n}\n\nexport { Shape };', + 'ag-style/semi', + ], + ['type argument spacing', 'type Table = Map;\n\nexport type { Table };', 'ag-style/comma-spacing'], + ['var redeclaration', 'var value = 1;\nvar value = 2;\n\nexport { value };', 'ag-ts/no-redeclare'], + ['array constructor', 'const VALUES = new Array(1, 2);\n\nexport { VALUES };', 'eslint/no-array-constructor'], + ['use before define', 'const FIRST = SECOND;\nconst SECOND = 1;\n\nexport { FIRST, SECOND };', 'eslint/no-use-before-define'], + [ + 'default before required', + `const greet = (greeting = 'hi', name: string): string => \`${DOLLAR}{greeting} ${DOLLAR}{name}\`;\n\n` + + 'export { greet };', + 'eslint/default-param-last', + ], +]; + +const outcomes = new Map(); + +beforeAll(async () => { + for (const policy of POLICIES) { + const base = createConfig({ language: 'typescript', policy }); + const config = { + ...base, + jsPlugins: (base.jsPlugins as { name: string; specifier: string }[]) + .map((provider) => ({ ...provider, specifier: REQUIRE.resolve(provider.specifier) })), + }; + const files = { + 'package.json': '{"name":"typescript-corpus","type":"module","dependencies":{"react":"19.0.0"}}', + ...corpus, + ...Object.fromEntries(violations.map(([, code], index) => [`violations/case-${index}.ts`, `${HEADER}${code}\n`])), + }; + const results = await lintBatch(config, files); + results.forEach((diagnostics, name) => outcomes.set(`${policy}/${name}`, diagnostics)); + } +}, 300000); + +describe.each(POLICIES)('%s TypeScript preset', (policy) => { + it.each(Object.keys(corpus))('accepts idiomatic TypeScript in %s', (name) => { + const diagnostics = outcomes.get(`${policy}/${name}`)! + // The React package is not installed in the temporary project. + .filter((diagnostic) => !(diagnostic.code === 'ag-import(no-unresolved)' && diagnostic.message.includes("'react'"))); + expect(diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`)).toEqual([]); + }); + + it.each(violations.map(([label, , rule], index) => [label, rule, index] as const))( + 'reports %s with %s', + (_label, rule, index) => { + const codes = outcomes.get(`${policy}/violations/case-${index}.ts`)! + .map((diagnostic) => diagnostic.code?.replace(/^([^()]+)\(([^)]+)\)$/u, '$1/$2')); + expect(codes).toContain(rule); + }, + ); +}); + +it('records a TypeScript implementation for every rule it replaces', () => { + const replaced = Object.entries(catalog.typescriptRules) + .filter(([, setting]) => setting === 'off') + .map(([target]) => target); + replaced.forEach((target) => { + const mapping = catalog.mappings.find((entry) => entry.target === target && !entry.scope); + expect(mapping?.typescript, target).toBeDefined(); + }); + violations.forEach(([, , rule]) => { + expect(Object.keys(catalog.typescriptRules)).toContain(rule); + }); +}); diff --git a/tests/typescript-rules.test.ts b/tests/typescript-rules.test.ts new file mode 100644 index 0000000..9797488 --- /dev/null +++ b/tests/typescript-rules.test.ts @@ -0,0 +1,92 @@ +/** + * @file Test TypeScript-aware variants of pinned core rules through Oxlint's runtime. + */ +import { RuleTester } from 'oxlint/plugins-dev'; +import { describe, expect, it } from 'vitest'; + +import plugin, { isAllowedMerge, rules } from '../packages/oxlint-plugin/src/typescript'; + +RuleTester.describe = describe; +RuleTester.it = it; + +const typescript = { languageOptions: { parserOptions: { lang: 'ts' } } } as const; + +const valid = [ + ['function overloads', 'function convert(input: string): string;\nfunction convert(input: number): number;\nfunction convert(input: unknown): unknown { return input; }'], + ['exported overloads', 'export function convert(input: string): string;\nexport function convert(input: unknown): unknown { return input; }'], + ['ambient overloads', 'declare function convert(input: string): string;\ndeclare function convert(input: number): number;'], + ['interface merge', 'interface Box { width: number }\ninterface Box { height: number }'], + ['class and interface', 'interface Widget { size: number }\nclass Widget { name = "widget"; }'], + ['class and namespace', 'class Box {}\nnamespace Box { export const SIZE = 1; }'], + ['function and namespace', 'function build(): number { return 1; }\nnamespace build { export const SIZE = 1; }'], + ['enum and namespace', 'enum Color { Red }\nnamespace Color { export const DEFAULT = 1; }'], + ['enum merge', 'enum Color { Red }\nenum Color { Green = 1 }'], + ['namespace merge', 'namespace Space { export const A = 1; }\nnamespace Space { export const B = 2; }'], + ['type and value', 'type Mode = "light" | "dark";\nconst Mode = { Light: "light" };'], + ['interface and value', 'interface Mode { name: string }\nconst Mode = { name: "light" };'], + ['nested block merge', 'function run() {\n interface Box { width: number }\n interface Box { height: number }\n}'], + ['namespace body merge', 'namespace Outer {\n export interface Box { width: number }\n export interface Box { height: number }\n}'], + ['distinct names', 'let first = 1;\nlet second = 2;'], +] as const; + +const invalid = [ + ['var redeclaration', 'var value = 1;\nvar value = 2;', 1], + ['two implementations', 'function run() {}\nfunction run() {}', 1], + ['class and function', 'class Box {}\nfunction Box() {}', 1], + ['import and interface', 'import { Box } from "./box";\ninterface Box { width: number }', 1], + ['hoisted var in block', 'var value = 1;\nif (value) {\n var value = 2;\n}', 1], + ['switch case', 'switch (mode) {\n case 1:\n var value = 1;\n var value = 2;\n}', 1], +] as const; + +new RuleTester().run('no-redeclare', rules['no-redeclare'], { + valid: valid.map(([name, code]) => ({ name, code, ...typescript })), + invalid: [ + ...invalid.map(([name, code, errors]) => ({ + name, + code, + ...typescript, + errors: Array.from({ length: errors }, () => ({ messageId: 'redeclared' })), + })), + { + // Other message kinds pass through unchanged. Only script-scope bindings are globals that a + // comment can declare; the pinned scope adapter models those for JavaScript sources. + name: 'declared by comment', + code: '/* global value */\nvar value = 1;', + options: [{ builtinGlobals: true }], + languageOptions: { sourceType: 'script', env: { builtin: true }, parserOptions: { lang: 'jsx' } }, + errors: [{ messageId: 'redeclaredBySyntax' }], + }, + ], +}); + +describe('TypeScript rule plugin contract', () => { + it('exposes TypeScript-aware rules under their core names', () => { + expect(plugin.meta?.name).toBe('ag-ts'); + expect(Object.keys(rules)).toEqual(['no-redeclare']); + }); + + it('rejects merges that TypeScript does not permit', () => { + // The parser already rejects these as duplicate declarations; the predicate agrees. + const declaration = (kind: Parameters[0][number]['kind'], implementation = false) => ({ + kind, + implementation, + }); + expect(isAllowedMerge([declaration('class'), declaration('class')])).toBe(false); + expect(isAllowedMerge([declaration('type'), declaration('interface')])).toBe(false); + expect(isAllowedMerge([declaration('type'), declaration('type')])).toBe(false); + expect(isAllowedMerge([declaration('variable'), declaration('namespace')])).toBe(false); + expect(isAllowedMerge([declaration('variable'), declaration('variable')])).toBe(false); + expect(isAllowedMerge([declaration('function', true), declaration('function', true)])).toBe(false); + expect(isAllowedMerge([declaration('class'), declaration('enum')])).toBe(false); + expect(isAllowedMerge([declaration('function'), declaration('function', true)])).toBe(true); + }); + + it('requires at least two declarations to describe a merge', () => { + expect(isAllowedMerge([])).toBe(false); + expect(isAllowedMerge([{ kind: 'interface', implementation: false }])).toBe(false); + expect(isAllowedMerge([ + { kind: 'interface', implementation: false }, + { kind: 'interface', implementation: false }, + ])).toBe(true); + }); +}); diff --git a/tests/typescript-syntax.test.ts b/tests/typescript-syntax.test.ts index 48066c1..154b2db 100644 --- a/tests/typescript-syntax.test.ts +++ b/tests/typescript-syntax.test.ts @@ -44,7 +44,8 @@ const extensions = ['ts', 'tsx', 'mts', 'cts']; let counts: Map; beforeAll(async () => { - const preset = createConfig('typescript'); + // The explicit-* and ban-ts-comment rules belong to the AdGuard projects profile. + const preset = createConfig({ language: 'typescript', profile: 'adguard-projects' }); const overlay = preset.overrides![0]!.rules!; const names = [ 'eslint/no-unused-vars', From ae047c3eec4d20afff943ce5da0eea194c45c847 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 06:41:10 +0000 Subject: [PATCH 2/4] fix(newlines): keep type modifiers valid when splitting imports The TypeScript port of eslint-plugin-import-newlines 1.4.0 mistranslated the fixer's type check. Upstream lib/enforce.js reads `currentNode.imported.parent.importKind`; the parent of the imported name is the ImportSpecifier, so only an inline `type` modifier is reprinted. The port read `currentNode.parent.importKind`, the ImportDeclaration's kind, instead. Every specifier of `import type { ... }` was printed as `type X` (TS2206: the 'type' modifier cannot be used on a named import when 'import type' is used), and inline modifiers in `import { type A, B }` were dropped. This is not an Oxc AST difference. Oxc and @typescript-eslint/parser both set importKind 'type' on the declaration for `import type { A }` and 'value' on its specifiers, and 'type' only on specifiers that carry an inline modifier. With the upstream rule and @typescript-eslint/parser the fix output is valid. An adapter in newlines.ts would have had to fake specifier parents to hide a port bug, so the reviewed patch in scripts/vendor-patches/import-newlines/src/enforce.ts.json now reads the imported name's parent, as upstream does. The vendored file is reproduced from the pinned upstream source and that patch, and vendor/manifest.json was updated with `pnpm run snapshots:record`. A new CLI test runs `oxlint --fix` with only ag-newlines/enforce on type-only, aliased, inline, default-plus-inline, and joined imports and on `export type` forms. It checks the exact fixed text and requires a second run to report no diagnostics, which include Oxc's TS1363 and TS2206 parser errors. The members stay unindented, as upstream emits them; the indent rule owns their indentation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MCeDV2VhbUtLyAP9XRG1zL --- .../import-newlines/src/enforce.ts.json | 2 +- tests/behavior.test.ts | 69 +++++++++++++++++++ vendor/import-newlines/src/enforce.ts | 3 +- vendor/manifest.json | 4 +- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/scripts/vendor-patches/import-newlines/src/enforce.ts.json b/scripts/vendor-patches/import-newlines/src/enforce.ts.json index 80ab720..b50ee16 100644 --- a/scripts/vendor-patches/import-newlines/src/enforce.ts.json +++ b/scripts/vendor-patches/import-newlines/src/enforce.ts.json @@ -1,6 +1,6 @@ [ { "before": "const SPEC_IMPORT = 'ImportSpecifier';\nconst SPEC_DEFAULT_IMPORT = 'ImportDefaultSpecifier';\nconst SPEC_NAMESPACE_IMPORT = 'ImportNamespaceSpecifier';\nconst nonSplittableImportTypes = new Set([SPEC_DEFAULT_IMPORT, SPEC_NAMESPACE_IMPORT]);\n\nconst applyAliasAndType = (currentNode) => {\n const localName = currentNode.local.name;\n if (!currentNode.imported) {\n return localName;\n }\n const importedName = currentNode.imported.name;\n const importedNameWithPrefix = currentNode.imported.parent.importKind === 'type'\n ? `type ${importedName}`\n : importedName;\n return importedName !== localName\n ? `${importedNameWithPrefix} as ${localName}`\n : importedNameWithPrefix;\n};\n\nconst outputComment = (commentNode) => {\n const commentNodeType = commentNode && commentNode.type;\n switch (commentNodeType) {\n case 'Block':\n return `\\n\\n/*${commentNode.value}*/\\n`;\n case 'Line':\n return `\\n\\n//${commentNode.value}\\n`;\n default:\n return '';\n }\n};\n\nconst getCommentLineCount = (commentNode) => {\n let newLineCount = 0;\n if (commentNode) {\n const newLinesInComment = commentNode.value.match(/\\n/g);\n if (Array.isArray(newLinesInComment)) {\n newLineCount = newLinesInComment.length;\n }\n }\n return 1 + newLineCount;\n};\n\nconst getCommentsInsideImport = (node) => {\n const comments = [];\n if (node.parent.comments) {\n const [nodeStartPos, nodeEndPos] = node.range;\n node.parent.comments.some((comment) => {\n const [commentStartPos] = comment.range;\n if (commentStartPos > nodeEndPos) {\n // Comments after the import are ignored\n return true;\n }\n\n if (commentStartPos > nodeStartPos) {\n // The comment starts inside the import\n comments.push(comment);\n }\n return false;\n });\n }\n return comments;\n};\n\n/**\n * Special case for when semicolons are omitted in code style but\n * there is an IIFE with a comment and a semicolon before it on\n * the next token, resulting in the comment being treated as part\n * of the import statement\n */\nconst findTrailingCommentInImport = (node, comments) => {\n let trailingCommentBeforeEnd = null;\n const [, nodeSourceEndPos] = node.source.range;\n comments.some((comment) => {\n const [commentStartPos] = comment.range;\n if (commentStartPos > nodeSourceEndPos) {\n // The comment starts after the import source token\n trailingCommentBeforeEnd = comment;\n return true;\n }\n return false;\n });\n return trailingCommentBeforeEnd;\n};\n\n/**\n * Special case for when there is a comment inside the import\n * between the last specifier and the source line\n */\nconst findCommentBeforeLastLine = (node, comments) => {\n let commentBeforeLastLine = null;\n if (node.specifiers.length > 0) {\n const lastSpecifierEndLine = node.specifiers[node.specifiers.length - 1].loc.end.line;\n const sourceLine = node.source.loc.start.line;\n comments.some((comment) => {\n const commentStartLine = comment.loc.start.line;\n if (commentStartLine > lastSpecifierEndLine) {\n const commentEndLine = comment.loc.end.line;\n if (commentEndLine < sourceLine) {\n // The comment is between the last specifier and the source line\n commentBeforeLastLine = comment;\n return true;\n }\n }\n return false;\n });\n }\n return commentBeforeLastLine;\n};\n\nconst fixer = (node, semi, spacer = '\\n') => (eslintFixer) => {\n const comments = getCommentsInsideImport(node);\n const trailingComment = findTrailingCommentInImport(node, comments);\n const lastLineComment = findCommentBeforeLastLine(node, comments);\n let defaultImport = '';\n let namespaceImport = '';\n const objectImports = [];\n const {\n specifiers,\n importKind,\n } = node;\n specifiers.forEach((currentNode) => {\n switch (currentNode.type) {\n case SPEC_DEFAULT_IMPORT:\n defaultImport = applyAliasAndType(currentNode);\n break;\n case SPEC_NAMESPACE_IMPORT:\n namespaceImport = `* as ${currentNode.local.name}`;\n break;\n case SPEC_IMPORT:\n objectImports.push(applyAliasAndType(currentNode));\n break;\n default:\n break;\n }\n });\n const hasObjectImports = objectImports.length > 0;\n const namespaceImportValue = namespaceImport.length > 0\n ? hasObjectImports ? `${namespaceImport}, ` : namespaceImport\n : namespaceImport;\n const defaultImportValue = defaultImport.length > 0\n ? hasObjectImports || namespaceImportValue.length > 0 ? `${defaultImport}, ` : defaultImport\n : defaultImport;\n const objectImportsValue = hasObjectImports\n ? `{${spacer}${objectImports.join(`,${spacer}`)}${spacer}}`\n : '';\n\n let lastLineCommentForSingleLineImport = '';\n if (lastLineComment) {\n if (hasObjectImports && spacer === '\\n') {\n // If there is a last line comment, add it to the object imports\n objectImports.push(outputComment(lastLineComment)\n .trim());\n } else {\n // or if there are no object imports, as a prefix for the whole import\n lastLineCommentForSingleLineImport = outputComment(lastLineComment)\n .trimStart();\n }\n }\n\n const importKeyword = importKind === 'type' ? 'import type' : 'import';\n const newValue = [\n lastLineCommentForSingleLineImport,\n importKeyword,\n ' ',\n defaultImportValue,\n namespaceImportValue,\n objectImportsValue,\n ' from ',\n node.source.raw,\n outputComment(trailingComment),\n semi || trailingComment ? ';' : '',\n ].join('');\n return eslintFixer.replaceText(node, newValue);\n};\n\nconst DEFAULT_ITEMS = 4;\nconst MIN_ITEMS = 0;\nconst DEFAULT_MAX_LENGTH = Infinity;\nconst MIN_MAX_LENGTH = 17;\nconst DEFAULT_SEMICOLON = true;\nconst DEFAULT_FORCE_SINGLE_LINE = true;\nconst DEFAULT_ALLOW_BLANK_LINES = false;\n\nmodule.exports = {\n meta: {\n type: 'layout',\n docs: {\n description: 'enforce multiple lines for import statements past a certain number of items',\n category: 'Stylistic Issues',\n url: 'https://github.com/SeinopSys/eslint-plugin-import-newlines',\n },\n fixable: 'whitespace',\n schema: {\n oneOf: [\n {\n type: 'array',\n minItems: 1,\n maxItems: 1,\n items: {\n type: 'object',\n properties: {\n items: {\n type: 'number',\n minimum: 0,\n },\n 'max-len': {\n type: 'number',\n minimum: 17,\n },\n semi: {\n type: 'boolean',\n },\n forceSingleLine: {\n type: 'boolean',\n },\n allowBlankLines: {\n type: 'boolean',\n },\n },\n },\n },\n {\n type: 'array',\n minItems: 0,\n maxItems: 2,\n items: {\n type: 'number',\n },\n },\n ],\n },\n messages: {\n mustSplitMany: 'Imports must be broken into multiple lines if there are more than {{maxItems}} elements.',\n mustSplitLong: 'Imports must be broken into multiple lines if the line length exceeds {{maxLineLength}} characters, saw {{lineLength}}.',\n mustNotSplit: 'Imports must not be broken into multiple lines if there are {{maxItems}} or less elements.',\n noBlankBetween: 'Import lines cannot have blank lines between them.',\n limitLineCount: 'Import lines must have one element per line. (Expected import to span {{expectedLineCount}} lines, saw {{importLineCount}})',\n },\n },\n create(context) {\n let maxItems = DEFAULT_ITEMS;\n let maxLineLength = DEFAULT_MAX_LENGTH;\n // Legacy default, prior to a refactoring this value was not set for array type configurations\n let includeSemi = false;\n let forceSingleLine = DEFAULT_FORCE_SINGLE_LINE;\n let allowBlankLines = DEFAULT_ALLOW_BLANK_LINES;\n const firstOption = context.options[0];\n if (typeof firstOption === 'object') {\n // Supported object-based configuration\n ({\n items: maxItems = DEFAULT_ITEMS,\n 'max-len': maxLineLength = DEFAULT_MAX_LENGTH,\n semi: includeSemi = DEFAULT_SEMICOLON,\n forceSingleLine = DEFAULT_FORCE_SINGLE_LINE,\n allowBlankLines = DEFAULT_ALLOW_BLANK_LINES,\n } = firstOption);\n } else {\n // Legacy array-based configuration\n [\n maxItems = DEFAULT_ITEMS,\n maxLineLength = DEFAULT_MAX_LENGTH,\n ] = context.options;\n }\n if (maxItems < MIN_ITEMS) {\n throw new Error(`Minimum items must not be less than ${MIN_MAX_LENGTH}`);\n }\n if (maxLineLength < MIN_MAX_LENGTH) {\n throw new Error(`Maximum line length must not be less than ${MIN_MAX_LENGTH}`);\n }\n return {\n ImportDeclaration(node) {\n const { specifiers } = node;\n\n // If blank lines are allowed, skip checking them by setting this to true\n let blankLinesChecked = allowBlankLines;\n const startColumn = node.loc.start.column;\n const commentsInsideImport = getCommentsInsideImport(node);\n const commentBeforeLastLine = findCommentBeforeLastLine(node, commentsInsideImport);\n // # of lines between the start of the import statement and the end of the import source\n const linesOfActualImport = node.source.loc.end.line - node.loc.start.line;\n // We include the line count of the last line comment in the calculation\n const commentsCompensation = commentBeforeLastLine\n ? getCommentLineCount(commentBeforeLastLine)\n : 0;\n const importLineCount = 1 + linesOfActualImport - commentsCompensation;\n const importedItems = specifiers.reduce((a, c) => a + (c.type === SPEC_IMPORT ? 1 : 0), 0);\n\n specifiers.slice(1).forEach((currentItem, index) => {\n const previousItem = specifiers[index];\n const previousEndLine = previousItem.loc.end.line;\n const currentStartLine = currentItem.loc.start.line;\n const lineDifference = currentStartLine - previousEndLine;\n if (!blankLinesChecked && lineDifference > 1) {\n context.report({\n node,\n messageId: 'noBlankBetween',\n fix: fixer(node, includeSemi),\n });\n blankLinesChecked = true;\n }\n });\n\n if (!blankLinesChecked) {\n const singleLine = importLineCount === 1;\n if (singleLine) {\n const line = context.getSourceCode().getText(node);\n if (line.length > maxLineLength) {\n const canBeSplit = specifiers.length > 2\n || specifiers.some((specifier) => !nonSplittableImportTypes.has(specifier.type));\n // There's nothing we can really do about a very long line\n // that has only default and namespace imports (barring a\n // refactor of the import statement itself) so we'll just\n // ignore it.\n if (canBeSplit) {\n context.report({\n node,\n messageId: 'mustSplitLong',\n data: { maxLineLength, lineLength: line.length },\n fix: fixer(node, includeSemi),\n });\n }\n return;\n }\n if (importedItems > maxItems) {\n context.report({\n node,\n messageId: 'mustSplitMany',\n data: { maxItems },\n fix: fixer(node, includeSemi),\n });\n }\n return;\n }\n\n // One item per line + line with import + line with from\n const expectedLineCount = importedItems + 2;\n if (importLineCount !== expectedLineCount) {\n context.report({\n node,\n messageId: 'limitLineCount',\n data: { expectedLineCount, importLineCount },\n fix: fixer(node, includeSemi),\n });\n return;\n }\n\n if (forceSingleLine && importedItems <= maxItems) {\n let fixedValue;\n const fix = fixer(node, includeSemi, ' ');\n fix({\n replaceText: (_node, value) => {\n fixedValue = value;\n },\n });\n // Only enforce this rule if fixing it would not cause going over the line length limit\n if (fixedValue.length + startColumn <= maxLineLength) {\n context.report({\n node,\n messageId: 'mustNotSplit',\n data: { maxItems },\n fix,\n });\n }\n }\n }\n },\n };\n },\n};\n", - "after": "import type {\n Comment, Fixer, LegacyRule, Node, RuleContext,\n} from '../../types';\n\nconst SPEC_IMPORT = 'ImportSpecifier';\nconst SPEC_DEFAULT_IMPORT = 'ImportDefaultSpecifier';\nconst SPEC_NAMESPACE_IMPORT = 'ImportNamespaceSpecifier';\nconst nonSplittableImportTypes = new Set([SPEC_DEFAULT_IMPORT, SPEC_NAMESPACE_IMPORT]);\n\n/**\n * Render the local binding with its alias and type prefix.\n * @param currentNode The import specifier node.\n * @returns The rendered specifier text.\n */\nfunction applyAliasAndType(currentNode: Node<'ImportSpecifier' | 'ImportDefaultSpecifier'>): string {\n const { local } = currentNode;\n const localName = (local as Node<'Identifier'>).name;\n const { imported } = (currentNode as Node<'ImportSpecifier'>);\n if (!imported) {\n return localName;\n }\n const importedName = (imported as Node<'Identifier'>).name;\n const { parent } = (currentNode as unknown as { parent: { importKind?: string } });\n const importedNameWithPrefix = parent.importKind === 'type'\n ? `type ${importedName}`\n : importedName;\n return importedName !== localName\n ? `${importedNameWithPrefix} as ${localName}`\n : importedNameWithPrefix;\n}\n\n/**\n * Render comments attached to the import statement.\n * @param commentNode The comment node.\n * @returns The rendered comment text.\n */\nfunction outputComment(commentNode: Comment | null | undefined): string {\n const commentNodeType = commentNode && commentNode.type;\n switch (commentNodeType) {\n case 'Block':\n return `\\n\\n/*${(commentNode as Comment).value}*/\\n`;\n case 'Line':\n return `\\n\\n//${(commentNode as Comment).value}\\n`;\n default:\n return '';\n }\n}\n\n/**\n * Count the lines occupied by a comment node.\n * @param commentNode The comment node.\n * @returns The line count including the comment itself.\n */\nfunction getCommentLineCount(commentNode: Comment | null | undefined): number {\n let newLineCount = 0;\n if (commentNode) {\n const newLinesInComment = (commentNode as Comment).value.match(/\\n/g);\n if (Array.isArray(newLinesInComment)) {\n newLineCount = newLinesInComment.length;\n }\n }\n return 1 + newLineCount;\n}\n\n/**\n * Parent with optional attached comments.\n */\ntype ImportParent = { parent: { comments?: ((Comment & { value: string }) & { range: [number, number] })[] } };\n\n/**\n * Collect comments starting inside the import statement.\n * @param node The import declaration node.\n * @returns The comments inside the import.\n */\nfunction getCommentsInsideImport(node: Node<'ImportDeclaration'>): (Comment & { value: string })[] {\n const comments: (Comment & { value: string })[] = [];\n const { parent } = (node as unknown as ImportParent);\n if (parent.comments) {\n const [nodeStartPos, nodeEndPos] = (node as unknown as { range: [number, number] }).range;\n parent.comments.some((comment) => {\n const [commentStartPos] = (comment as unknown as { range: [number, number] }).range;\n if (commentStartPos > nodeEndPos) {\n // Comments after the import are ignored\n return true;\n }\n\n if (commentStartPos > nodeStartPos) {\n // The comment starts inside the import\n comments.push(comment as Comment | Comment);\n }\n return false;\n });\n }\n return comments;\n}\n\n/**\n * Special case for when semicolons are omitted in code style but\n * there is an IIFE with a comment and a semicolon before it on\n * the next token, resulting in the comment being treated as part\n * of the import statement.\n * @param node The import declaration node.\n * @param comments The comments inside the import.\n * @returns The trailing comment when present.\n */\nfunction findTrailingCommentInImport(\n node: Node<'ImportDeclaration'>,\n comments: (Comment & { value: string })[],\n): (Comment & { value: string }) | null {\n let trailingCommentBeforeEnd: (Comment & { value: string }) | null = null;\n const [, nodeSourceEndPos] = ((node.source as unknown as { range: [number, number] }).range);\n comments.some((comment) => {\n const [commentStartPos] = ((comment as unknown as { range: [number, number] }).range);\n if (commentStartPos > nodeSourceEndPos) {\n // The comment starts after the import source token\n trailingCommentBeforeEnd = comment;\n return true;\n }\n return false;\n });\n return trailingCommentBeforeEnd;\n}\n\n/**\n * Special case for when there is a comment inside the import\n * between the last specifier and the source line.\n * @param node The import declaration node.\n * @param comments The comments inside the import.\n * @returns The comment before the last line when present.\n */\nfunction findCommentBeforeLastLine(\n node: Node<'ImportDeclaration'>,\n comments: (Comment & { value: string })[],\n): (Comment & { value: string }) | null {\n let commentBeforeLastLine: (Comment & { value: string }) | null = null;\n if (node.specifiers.length > 0) {\n const lastSpecifierEndLine = node.specifiers[node.specifiers.length - 1]!.loc!.end.line;\n const sourceLine = node.source.loc!.start.line;\n comments.some((comment) => {\n const commentStartLine = comment.loc!.start.line;\n if (commentStartLine > lastSpecifierEndLine) {\n const commentEndLine = comment.loc!.end.line;\n if (commentEndLine < sourceLine) {\n // The comment is between the last specifier and the source line\n commentBeforeLastLine = comment;\n return true;\n }\n }\n return false;\n });\n }\n return commentBeforeLastLine;\n}\n\ntype ImportNewlinesOptions =\n | [{ items?: number; 'max-len'?: number; semi?: boolean; forceSingleLine?: boolean; allowBlankLines?: boolean }?]\n | [number?, number?];\n\n/**\n * Build the fixer that rewrites the import statement.\n * @param node The import declaration node.\n * @param semi Whether to include a semicolon.\n * @param spacer The separator between specifiers.\n * @returns The fixer function.\n */\nfunction fixer(node: Node<'ImportDeclaration'>, semi: boolean, spacer = '\\n') {\n return (eslintFixer: Fixer) => {\n const comments = getCommentsInsideImport(node);\n const trailingComment = findTrailingCommentInImport(node, comments);\n const lastLineComment = findCommentBeforeLastLine(node, comments);\n let defaultImport = '';\n let namespaceImport = '';\n const objectImports: string[] = [];\n const {\n specifiers,\n importKind,\n } = node as Node<'ImportDeclaration'> & { importKind?: string };\n for (let specifierIndex = 0; specifierIndex < specifiers.length; specifierIndex += 1) {\n const currentNode = specifiers[specifierIndex]!;\n switch (currentNode.type) {\n case SPEC_DEFAULT_IMPORT:\n defaultImport = applyAliasAndType(currentNode as Node<'ImportDefaultSpecifier'>);\n break;\n case SPEC_NAMESPACE_IMPORT:\n namespaceImport = `* as ${(currentNode.local as Node<'Identifier'>).name}`;\n break;\n case SPEC_IMPORT:\n objectImports.push(applyAliasAndType(currentNode as Node<'ImportSpecifier'>));\n break;\n default:\n break;\n }\n }\n const hasObjectImports = objectImports.length > 0;\n let namespaceImportValue = namespaceImport;\n if (namespaceImport.length > 0 && hasObjectImports) {\n namespaceImportValue = `${namespaceImport}, `;\n }\n let defaultImportValue = defaultImport;\n if (defaultImport.length > 0 && (hasObjectImports || namespaceImportValue.length > 0)) {\n defaultImportValue = `${defaultImport}, `;\n }\n const objectImportsValue = hasObjectImports\n ? `{${spacer}${objectImports.join(`,${spacer}`)}${spacer}}`\n : '';\n\n let lastLineCommentForSingleLineImport = '';\n if (lastLineComment) {\n if (hasObjectImports && spacer === '\\n') {\n // If there is a last line comment, add it to the object imports\n objectImports.push(outputComment(lastLineComment).trim());\n } else {\n // or if there are no object imports, as a prefix for the whole import\n lastLineCommentForSingleLineImport = outputComment(lastLineComment).trimStart();\n }\n }\n\n const importKeyword = importKind === 'type' ? 'import type' : 'import';\n const newValue = [\n lastLineCommentForSingleLineImport,\n importKeyword,\n ' ',\n defaultImportValue,\n namespaceImportValue,\n objectImportsValue,\n ' from ',\n (node.source as unknown as { raw: string }).raw,\n outputComment(trailingComment),\n semi || trailingComment ? ';' : '',\n ].join('');\n return eslintFixer.replaceText(node, newValue);\n };\n}\n\nconst DEFAULT_ITEMS = 4;\nconst MIN_ITEMS = 0;\nconst DEFAULT_MAX_LENGTH = Infinity;\nconst MIN_MAX_LENGTH = 17;\nconst DEFAULT_SEMICOLON = true;\nconst DEFAULT_FORCE_SINGLE_LINE = true;\nconst DEFAULT_ALLOW_BLANK_LINES = false;\n\nconst rule: LegacyRule = {\n meta: {\n type: 'layout',\n docs: {\n description: 'enforce multiple lines for import statements past a certain number of items',\n category: 'Stylistic Issues',\n url: 'https://github.com/SeinopSys/eslint-plugin-import-newlines',\n },\n fixable: 'whitespace',\n schema: {\n oneOf: [\n {\n type: 'array',\n minItems: 1,\n maxItems: 1,\n items: {\n type: 'object',\n properties: {\n items: {\n type: 'number',\n minimum: 0,\n },\n 'max-len': {\n type: 'number',\n minimum: 17,\n },\n semi: {\n type: 'boolean',\n },\n forceSingleLine: {\n type: 'boolean',\n },\n allowBlankLines: {\n type: 'boolean',\n },\n },\n },\n },\n {\n type: 'array',\n minItems: 0,\n maxItems: 2,\n items: {\n type: 'number',\n },\n },\n ],\n },\n messages: {\n mustSplitMany: 'Imports must be broken into multiple lines if there are more than {{maxItems}} elements.',\n mustSplitLong: 'Imports must be broken into multiple lines if the line length exceeds {{maxLineLength}} characters, saw {{lineLength}}.',\n mustNotSplit: 'Imports must not be broken into multiple lines if there are {{maxItems}} or less elements.',\n noBlankBetween: 'Import lines cannot have blank lines between them.',\n limitLineCount: 'Import lines must have one element per line. (Expected import to span {{expectedLineCount}} lines, saw {{importLineCount}})',\n },\n },\n create(context: RuleContext) {\n let maxItems = DEFAULT_ITEMS;\n let maxLineLength = DEFAULT_MAX_LENGTH;\n // Legacy default, prior to a refactoring this value was not set for array type configurations\n let includeSemi = false;\n let forceSingleLine = DEFAULT_FORCE_SINGLE_LINE;\n let allowBlankLines = DEFAULT_ALLOW_BLANK_LINES;\n type ObjectOption = {\n items?: number; 'max-len'?: number; semi?: boolean; forceSingleLine?: boolean; allowBlankLines?: boolean;\n };\n const firstOption = context.options[0] as ObjectOption | number | undefined;\n if (typeof firstOption === 'object') {\n // Supported object-based configuration\n ({\n items: maxItems = DEFAULT_ITEMS,\n 'max-len': maxLineLength = DEFAULT_MAX_LENGTH,\n semi: includeSemi = DEFAULT_SEMICOLON,\n forceSingleLine = DEFAULT_FORCE_SINGLE_LINE,\n allowBlankLines = DEFAULT_ALLOW_BLANK_LINES,\n } = firstOption);\n } else {\n // Legacy array-based configuration\n [\n maxItems = DEFAULT_ITEMS,\n maxLineLength = DEFAULT_MAX_LENGTH,\n ] = context.options as [number?, number?];\n }\n if (maxItems < MIN_ITEMS) {\n throw new Error(`Minimum items must not be less than ${MIN_MAX_LENGTH}`);\n }\n if (maxLineLength < MIN_MAX_LENGTH) {\n throw new Error(`Maximum line length must not be less than ${MIN_MAX_LENGTH}`);\n }\n return {\n ImportDeclaration(node: Node<'ImportDeclaration'>) {\n const { specifiers } = node;\n\n // If blank lines are allowed, skip checking them by setting this to true\n let blankLinesChecked = allowBlankLines;\n const startColumn = node.loc!.start.column;\n const commentsInsideImport = getCommentsInsideImport(node);\n const commentBeforeLastLine = findCommentBeforeLastLine(node, commentsInsideImport);\n // # of lines between the start of the import statement and the end of the import source\n const linesOfActualImport = node.source.loc!.end.line - node.loc!.start.line;\n // We include the line count of the last line comment in the calculation\n const commentsCompensation = commentBeforeLastLine\n ? getCommentLineCount(commentBeforeLastLine)\n : 0;\n const importLineCount = 1 + linesOfActualImport - commentsCompensation;\n const importedItems = specifiers.reduce(\n (count, specifier) => count + (specifier.type === SPEC_IMPORT ? 1 : 0),\n 0,\n );\n\n for (let specifierIndex = 1; specifierIndex < specifiers.length; specifierIndex += 1) {\n const currentItem = specifiers[specifierIndex]!;\n const previousItem = specifiers[specifierIndex - 1]!;\n const previousEndLine = previousItem.loc!.end.line;\n const currentStartLine = currentItem.loc!.start.line;\n const lineDifference = currentStartLine - previousEndLine;\n if (!blankLinesChecked && lineDifference > 1) {\n context.report({\n node,\n messageId: 'noBlankBetween',\n fix: fixer(node, includeSemi),\n });\n blankLinesChecked = true;\n }\n }\n\n if (!blankLinesChecked) {\n const singleLine = importLineCount === 1;\n if (singleLine) {\n const line = context.sourceCode.getText(node);\n if (line.length > maxLineLength) {\n const canBeSplit = specifiers.length > 2\n || specifiers.some((specifier) => !nonSplittableImportTypes.has(specifier.type));\n // There's nothing we can really do about a very long line\n // that has only default and namespace imports (barring a\n // refactor of the import statement itself) so we'll just\n // ignore it.\n if (canBeSplit) {\n context.report({\n node,\n messageId: 'mustSplitLong',\n data: { maxLineLength, lineLength: line.length },\n fix: fixer(node, includeSemi),\n });\n }\n return;\n }\n if (importedItems > maxItems) {\n context.report({\n node,\n messageId: 'mustSplitMany',\n data: { maxItems },\n fix: fixer(node, includeSemi),\n });\n }\n return;\n }\n\n // One item per line + line with import + line with from\n const expectedLineCount = importedItems + 2;\n if (importLineCount !== expectedLineCount) {\n context.report({\n node,\n messageId: 'limitLineCount',\n data: { expectedLineCount, importLineCount },\n fix: fixer(node, includeSemi),\n });\n return;\n }\n\n if (forceSingleLine && importedItems <= maxItems) {\n let fixedValue: string | undefined;\n const fix = fixer(node, includeSemi, ' ');\n fix({\n replaceText: (_node: unknown, value: string) => {\n fixedValue = value;\n },\n } as never);\n // Only enforce this rule if fixing it would not cause going over the line length limit\n if ((fixedValue?.length ?? 0) + startColumn <= maxLineLength) {\n context.report({\n node,\n messageId: 'mustNotSplit',\n data: { maxItems },\n fix,\n });\n }\n }\n }\n },\n };\n },\n};\n\nexport default rule;\n" + "after": "import type {\n Comment, Fixer, LegacyRule, Node, RuleContext,\n} from '../../types';\n\nconst SPEC_IMPORT = 'ImportSpecifier';\nconst SPEC_DEFAULT_IMPORT = 'ImportDefaultSpecifier';\nconst SPEC_NAMESPACE_IMPORT = 'ImportNamespaceSpecifier';\nconst nonSplittableImportTypes = new Set([SPEC_DEFAULT_IMPORT, SPEC_NAMESPACE_IMPORT]);\n\n/**\n * Render the local binding with its alias and type prefix.\n * @param currentNode The import specifier node.\n * @returns The rendered specifier text.\n */\nfunction applyAliasAndType(currentNode: Node<'ImportSpecifier' | 'ImportDefaultSpecifier'>): string {\n const { local } = currentNode;\n const localName = (local as Node<'Identifier'>).name;\n const { imported } = (currentNode as Node<'ImportSpecifier'>);\n if (!imported) {\n return localName;\n }\n const importedName = (imported as Node<'Identifier'>).name;\n // The imported name's parent is this specifier; only an inline `type` modifier sets its importKind.\n const { parent } = (imported as unknown as { parent: { importKind?: string } });\n const importedNameWithPrefix = parent.importKind === 'type'\n ? `type ${importedName}`\n : importedName;\n return importedName !== localName\n ? `${importedNameWithPrefix} as ${localName}`\n : importedNameWithPrefix;\n}\n\n/**\n * Render comments attached to the import statement.\n * @param commentNode The comment node.\n * @returns The rendered comment text.\n */\nfunction outputComment(commentNode: Comment | null | undefined): string {\n const commentNodeType = commentNode && commentNode.type;\n switch (commentNodeType) {\n case 'Block':\n return `\\n\\n/*${(commentNode as Comment).value}*/\\n`;\n case 'Line':\n return `\\n\\n//${(commentNode as Comment).value}\\n`;\n default:\n return '';\n }\n}\n\n/**\n * Count the lines occupied by a comment node.\n * @param commentNode The comment node.\n * @returns The line count including the comment itself.\n */\nfunction getCommentLineCount(commentNode: Comment | null | undefined): number {\n let newLineCount = 0;\n if (commentNode) {\n const newLinesInComment = (commentNode as Comment).value.match(/\\n/g);\n if (Array.isArray(newLinesInComment)) {\n newLineCount = newLinesInComment.length;\n }\n }\n return 1 + newLineCount;\n}\n\n/**\n * Parent with optional attached comments.\n */\ntype ImportParent = { parent: { comments?: ((Comment & { value: string }) & { range: [number, number] })[] } };\n\n/**\n * Collect comments starting inside the import statement.\n * @param node The import declaration node.\n * @returns The comments inside the import.\n */\nfunction getCommentsInsideImport(node: Node<'ImportDeclaration'>): (Comment & { value: string })[] {\n const comments: (Comment & { value: string })[] = [];\n const { parent } = (node as unknown as ImportParent);\n if (parent.comments) {\n const [nodeStartPos, nodeEndPos] = (node as unknown as { range: [number, number] }).range;\n parent.comments.some((comment) => {\n const [commentStartPos] = (comment as unknown as { range: [number, number] }).range;\n if (commentStartPos > nodeEndPos) {\n // Comments after the import are ignored\n return true;\n }\n\n if (commentStartPos > nodeStartPos) {\n // The comment starts inside the import\n comments.push(comment as Comment | Comment);\n }\n return false;\n });\n }\n return comments;\n}\n\n/**\n * Special case for when semicolons are omitted in code style but\n * there is an IIFE with a comment and a semicolon before it on\n * the next token, resulting in the comment being treated as part\n * of the import statement.\n * @param node The import declaration node.\n * @param comments The comments inside the import.\n * @returns The trailing comment when present.\n */\nfunction findTrailingCommentInImport(\n node: Node<'ImportDeclaration'>,\n comments: (Comment & { value: string })[],\n): (Comment & { value: string }) | null {\n let trailingCommentBeforeEnd: (Comment & { value: string }) | null = null;\n const [, nodeSourceEndPos] = ((node.source as unknown as { range: [number, number] }).range);\n comments.some((comment) => {\n const [commentStartPos] = ((comment as unknown as { range: [number, number] }).range);\n if (commentStartPos > nodeSourceEndPos) {\n // The comment starts after the import source token\n trailingCommentBeforeEnd = comment;\n return true;\n }\n return false;\n });\n return trailingCommentBeforeEnd;\n}\n\n/**\n * Special case for when there is a comment inside the import\n * between the last specifier and the source line.\n * @param node The import declaration node.\n * @param comments The comments inside the import.\n * @returns The comment before the last line when present.\n */\nfunction findCommentBeforeLastLine(\n node: Node<'ImportDeclaration'>,\n comments: (Comment & { value: string })[],\n): (Comment & { value: string }) | null {\n let commentBeforeLastLine: (Comment & { value: string }) | null = null;\n if (node.specifiers.length > 0) {\n const lastSpecifierEndLine = node.specifiers[node.specifiers.length - 1]!.loc!.end.line;\n const sourceLine = node.source.loc!.start.line;\n comments.some((comment) => {\n const commentStartLine = comment.loc!.start.line;\n if (commentStartLine > lastSpecifierEndLine) {\n const commentEndLine = comment.loc!.end.line;\n if (commentEndLine < sourceLine) {\n // The comment is between the last specifier and the source line\n commentBeforeLastLine = comment;\n return true;\n }\n }\n return false;\n });\n }\n return commentBeforeLastLine;\n}\n\ntype ImportNewlinesOptions =\n | [{ items?: number; 'max-len'?: number; semi?: boolean; forceSingleLine?: boolean; allowBlankLines?: boolean }?]\n | [number?, number?];\n\n/**\n * Build the fixer that rewrites the import statement.\n * @param node The import declaration node.\n * @param semi Whether to include a semicolon.\n * @param spacer The separator between specifiers.\n * @returns The fixer function.\n */\nfunction fixer(node: Node<'ImportDeclaration'>, semi: boolean, spacer = '\\n') {\n return (eslintFixer: Fixer) => {\n const comments = getCommentsInsideImport(node);\n const trailingComment = findTrailingCommentInImport(node, comments);\n const lastLineComment = findCommentBeforeLastLine(node, comments);\n let defaultImport = '';\n let namespaceImport = '';\n const objectImports: string[] = [];\n const {\n specifiers,\n importKind,\n } = node as Node<'ImportDeclaration'> & { importKind?: string };\n for (let specifierIndex = 0; specifierIndex < specifiers.length; specifierIndex += 1) {\n const currentNode = specifiers[specifierIndex]!;\n switch (currentNode.type) {\n case SPEC_DEFAULT_IMPORT:\n defaultImport = applyAliasAndType(currentNode as Node<'ImportDefaultSpecifier'>);\n break;\n case SPEC_NAMESPACE_IMPORT:\n namespaceImport = `* as ${(currentNode.local as Node<'Identifier'>).name}`;\n break;\n case SPEC_IMPORT:\n objectImports.push(applyAliasAndType(currentNode as Node<'ImportSpecifier'>));\n break;\n default:\n break;\n }\n }\n const hasObjectImports = objectImports.length > 0;\n let namespaceImportValue = namespaceImport;\n if (namespaceImport.length > 0 && hasObjectImports) {\n namespaceImportValue = `${namespaceImport}, `;\n }\n let defaultImportValue = defaultImport;\n if (defaultImport.length > 0 && (hasObjectImports || namespaceImportValue.length > 0)) {\n defaultImportValue = `${defaultImport}, `;\n }\n const objectImportsValue = hasObjectImports\n ? `{${spacer}${objectImports.join(`,${spacer}`)}${spacer}}`\n : '';\n\n let lastLineCommentForSingleLineImport = '';\n if (lastLineComment) {\n if (hasObjectImports && spacer === '\\n') {\n // If there is a last line comment, add it to the object imports\n objectImports.push(outputComment(lastLineComment).trim());\n } else {\n // or if there are no object imports, as a prefix for the whole import\n lastLineCommentForSingleLineImport = outputComment(lastLineComment).trimStart();\n }\n }\n\n const importKeyword = importKind === 'type' ? 'import type' : 'import';\n const newValue = [\n lastLineCommentForSingleLineImport,\n importKeyword,\n ' ',\n defaultImportValue,\n namespaceImportValue,\n objectImportsValue,\n ' from ',\n (node.source as unknown as { raw: string }).raw,\n outputComment(trailingComment),\n semi || trailingComment ? ';' : '',\n ].join('');\n return eslintFixer.replaceText(node, newValue);\n };\n}\n\nconst DEFAULT_ITEMS = 4;\nconst MIN_ITEMS = 0;\nconst DEFAULT_MAX_LENGTH = Infinity;\nconst MIN_MAX_LENGTH = 17;\nconst DEFAULT_SEMICOLON = true;\nconst DEFAULT_FORCE_SINGLE_LINE = true;\nconst DEFAULT_ALLOW_BLANK_LINES = false;\n\nconst rule: LegacyRule = {\n meta: {\n type: 'layout',\n docs: {\n description: 'enforce multiple lines for import statements past a certain number of items',\n category: 'Stylistic Issues',\n url: 'https://github.com/SeinopSys/eslint-plugin-import-newlines',\n },\n fixable: 'whitespace',\n schema: {\n oneOf: [\n {\n type: 'array',\n minItems: 1,\n maxItems: 1,\n items: {\n type: 'object',\n properties: {\n items: {\n type: 'number',\n minimum: 0,\n },\n 'max-len': {\n type: 'number',\n minimum: 17,\n },\n semi: {\n type: 'boolean',\n },\n forceSingleLine: {\n type: 'boolean',\n },\n allowBlankLines: {\n type: 'boolean',\n },\n },\n },\n },\n {\n type: 'array',\n minItems: 0,\n maxItems: 2,\n items: {\n type: 'number',\n },\n },\n ],\n },\n messages: {\n mustSplitMany: 'Imports must be broken into multiple lines if there are more than {{maxItems}} elements.',\n mustSplitLong: 'Imports must be broken into multiple lines if the line length exceeds {{maxLineLength}} characters, saw {{lineLength}}.',\n mustNotSplit: 'Imports must not be broken into multiple lines if there are {{maxItems}} or less elements.',\n noBlankBetween: 'Import lines cannot have blank lines between them.',\n limitLineCount: 'Import lines must have one element per line. (Expected import to span {{expectedLineCount}} lines, saw {{importLineCount}})',\n },\n },\n create(context: RuleContext) {\n let maxItems = DEFAULT_ITEMS;\n let maxLineLength = DEFAULT_MAX_LENGTH;\n // Legacy default, prior to a refactoring this value was not set for array type configurations\n let includeSemi = false;\n let forceSingleLine = DEFAULT_FORCE_SINGLE_LINE;\n let allowBlankLines = DEFAULT_ALLOW_BLANK_LINES;\n type ObjectOption = {\n items?: number; 'max-len'?: number; semi?: boolean; forceSingleLine?: boolean; allowBlankLines?: boolean;\n };\n const firstOption = context.options[0] as ObjectOption | number | undefined;\n if (typeof firstOption === 'object') {\n // Supported object-based configuration\n ({\n items: maxItems = DEFAULT_ITEMS,\n 'max-len': maxLineLength = DEFAULT_MAX_LENGTH,\n semi: includeSemi = DEFAULT_SEMICOLON,\n forceSingleLine = DEFAULT_FORCE_SINGLE_LINE,\n allowBlankLines = DEFAULT_ALLOW_BLANK_LINES,\n } = firstOption);\n } else {\n // Legacy array-based configuration\n [\n maxItems = DEFAULT_ITEMS,\n maxLineLength = DEFAULT_MAX_LENGTH,\n ] = context.options as [number?, number?];\n }\n if (maxItems < MIN_ITEMS) {\n throw new Error(`Minimum items must not be less than ${MIN_MAX_LENGTH}`);\n }\n if (maxLineLength < MIN_MAX_LENGTH) {\n throw new Error(`Maximum line length must not be less than ${MIN_MAX_LENGTH}`);\n }\n return {\n ImportDeclaration(node: Node<'ImportDeclaration'>) {\n const { specifiers } = node;\n\n // If blank lines are allowed, skip checking them by setting this to true\n let blankLinesChecked = allowBlankLines;\n const startColumn = node.loc!.start.column;\n const commentsInsideImport = getCommentsInsideImport(node);\n const commentBeforeLastLine = findCommentBeforeLastLine(node, commentsInsideImport);\n // # of lines between the start of the import statement and the end of the import source\n const linesOfActualImport = node.source.loc!.end.line - node.loc!.start.line;\n // We include the line count of the last line comment in the calculation\n const commentsCompensation = commentBeforeLastLine\n ? getCommentLineCount(commentBeforeLastLine)\n : 0;\n const importLineCount = 1 + linesOfActualImport - commentsCompensation;\n const importedItems = specifiers.reduce(\n (count, specifier) => count + (specifier.type === SPEC_IMPORT ? 1 : 0),\n 0,\n );\n\n for (let specifierIndex = 1; specifierIndex < specifiers.length; specifierIndex += 1) {\n const currentItem = specifiers[specifierIndex]!;\n const previousItem = specifiers[specifierIndex - 1]!;\n const previousEndLine = previousItem.loc!.end.line;\n const currentStartLine = currentItem.loc!.start.line;\n const lineDifference = currentStartLine - previousEndLine;\n if (!blankLinesChecked && lineDifference > 1) {\n context.report({\n node,\n messageId: 'noBlankBetween',\n fix: fixer(node, includeSemi),\n });\n blankLinesChecked = true;\n }\n }\n\n if (!blankLinesChecked) {\n const singleLine = importLineCount === 1;\n if (singleLine) {\n const line = context.sourceCode.getText(node);\n if (line.length > maxLineLength) {\n const canBeSplit = specifiers.length > 2\n || specifiers.some((specifier) => !nonSplittableImportTypes.has(specifier.type));\n // There's nothing we can really do about a very long line\n // that has only default and namespace imports (barring a\n // refactor of the import statement itself) so we'll just\n // ignore it.\n if (canBeSplit) {\n context.report({\n node,\n messageId: 'mustSplitLong',\n data: { maxLineLength, lineLength: line.length },\n fix: fixer(node, includeSemi),\n });\n }\n return;\n }\n if (importedItems > maxItems) {\n context.report({\n node,\n messageId: 'mustSplitMany',\n data: { maxItems },\n fix: fixer(node, includeSemi),\n });\n }\n return;\n }\n\n // One item per line + line with import + line with from\n const expectedLineCount = importedItems + 2;\n if (importLineCount !== expectedLineCount) {\n context.report({\n node,\n messageId: 'limitLineCount',\n data: { expectedLineCount, importLineCount },\n fix: fixer(node, includeSemi),\n });\n return;\n }\n\n if (forceSingleLine && importedItems <= maxItems) {\n let fixedValue: string | undefined;\n const fix = fixer(node, includeSemi, ' ');\n fix({\n replaceText: (_node: unknown, value: string) => {\n fixedValue = value;\n },\n } as never);\n // Only enforce this rule if fixing it would not cause going over the line length limit\n if ((fixedValue?.length ?? 0) + startColumn <= maxLineLength) {\n context.report({\n node,\n messageId: 'mustNotSplit',\n data: { maxItems },\n fix,\n });\n }\n }\n }\n },\n };\n },\n};\n\nexport default rule;\n" } ] diff --git a/tests/behavior.test.ts b/tests/behavior.test.ts index eda3263..e2ddadb 100644 --- a/tests/behavior.test.ts +++ b/tests/behavior.test.ts @@ -34,6 +34,46 @@ const COMPILER_CASES = [ ['bounded tuple', 'const values: [number] = [1]; const value: number = values[0];', null], ] as const; +// The rule emits unindented members, as upstream does; the indent rule owns their indentation. +const TYPE_IMPORT_FIXES: Record = { + 'type-only.ts': { + source: "import type { Alpha, Beta, Gamma, Delta } from './types';\n", + fixed: "import type {\nAlpha,\nBeta,\nGamma,\nDelta\n} from './types';\n", + }, + 'type-only-alias.ts': { + source: "import type { Alpha as A, Beta, Gamma, Delta as D } from './types';\n", + fixed: "import type {\nAlpha as A,\nBeta,\nGamma,\nDelta as D\n} from './types';\n", + }, + 'type-only-joined.ts': { + source: "import type {\n Alpha,\n Beta,\n} from './types';\n", + fixed: "import type { Alpha, Beta } from './types';\n", + }, + 'inline.ts': { + source: "import { type Alpha, Beta, Gamma, Delta } from './types';\n", + fixed: "import {\ntype Alpha,\nBeta,\nGamma,\nDelta\n} from './types';\n", + }, + 'inline-alias.ts': { + source: "import { type Alpha as A, Beta, type Gamma, Delta } from './types';\n", + fixed: "import {\ntype Alpha as A,\nBeta,\ntype Gamma,\nDelta\n} from './types';\n", + }, + 'inline-joined.ts': { + source: "import {\n type Alpha,\n Beta as B,\n} from './types';\n", + fixed: "import { type Alpha, Beta as B } from './types';\n", + }, + 'default-inline.ts': { + source: "import Main, { type Alpha, Beta, Gamma, Delta } from './types';\n", + fixed: "import Main, {\ntype Alpha,\nBeta,\nGamma,\nDelta\n} from './types';\n", + }, + 'export-from.ts': { + source: "export type { Alpha, Beta, Gamma, Delta } from './types';\n", + fixed: "export type { Alpha, Beta, Gamma, Delta } from './types';\n", + }, + 'export-local.ts': { + source: 'type Alpha = 1;\ntype Beta = 2;\ntype Gamma = 3;\ntype Delta = 4;\nexport type { Alpha, Beta, Gamma, Delta };\n', + fixed: 'type Alpha = 1;\ntype Beta = 2;\ntype Gamma = 3;\ntype Delta = 4;\nexport type { Alpha, Beta, Gamma, Delta };\n', + }, +}; + describe('TypeScript 7 compiler contract', () => { it.each(COMPILER_CASES)('%s', async (_name, code, diagnostic) => { const directory = await mkdtemp(join(tmpdir(), 'ag-compiler-')); @@ -124,6 +164,35 @@ it('safe formatting fixes preserve comments and converge after repeated CLI pass } }); +it('import-newlines fixes keep type-only and inline type modifiers valid', async () => { + const directory = await mkdtemp(join(tmpdir(), 'ag-newlines-')); + try { + await writeFile( + join(directory, '.oxlintrc.json'), + JSON.stringify({ + categories: { correctness: 'off' }, + jsPlugins: [ + { name: 'ag-newlines', specifier: REQUIRE.resolve('@agcodeguidelines/oxlint-plugin/newlines') }, + ], + rules: { 'ag-newlines/enforce': ['error', { items: 3, 'max-len': 120 }] }, + }), + ); + const files = Object.entries(TYPE_IMPORT_FIXES); + await Promise.all(files.map(([name, { source }]) => writeFile(join(directory, name), source))); + await runOxlint(['--fix', ...Object.keys(TYPE_IMPORT_FIXES)], directory); + const fixed = Object.fromEntries(await Promise.all( + files.map(async ([name]) => [name, await readFile(join(directory, name), 'utf8')]), + )); + expect(fixed).toEqual(Object.fromEntries(files.map(([name, expected]) => [name, expected.fixed]))); + // Oxc reports misplaced type modifiers (TS1363, TS2206) as parser diagnostics. + const result = await runOxlint(Object.keys(TYPE_IMPORT_FIXES), directory); + expect(result.diagnostics).toEqual([]); + expect(result.status).toBe(0); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); + it('uses the compiler for type safety without requiring a typed lint service', () => { const config = createConfig('typescript'); expect(config.options?.typeAware).not.toBe(true); diff --git a/vendor/import-newlines/src/enforce.ts b/vendor/import-newlines/src/enforce.ts index e728668..0636df7 100644 --- a/vendor/import-newlines/src/enforce.ts +++ b/vendor/import-newlines/src/enforce.ts @@ -20,7 +20,8 @@ function applyAliasAndType(currentNode: Node<'ImportSpecifier' | 'ImportDefaultS return localName; } const importedName = (imported as Node<'Identifier'>).name; - const { parent } = (currentNode as unknown as { parent: { importKind?: string } }); + // The imported name's parent is this specifier; only an inline `type` modifier sets its importKind. + const { parent } = (imported as unknown as { parent: { importKind?: string } }); const importedNameWithPrefix = parent.importKind === 'type' ? `type ${importedName}` : importedName; diff --git a/vendor/manifest.json b/vendor/manifest.json index 049a62e..f5618dc 100644 --- a/vendor/manifest.json +++ b/vendor/manifest.json @@ -399,7 +399,7 @@ "scripts/vendor-patches/core/lib/shared/string-utils.ts.json": "06a3fd4abea0165ca2c7b727b6dfac118888a056b9a87b54b9fc298b2d1abfe4", "scripts/vendor-patches/core/lib/shared/traverser.ts.json": "03dd10443e2606f97cef67a2fb7f19237eb382b8e213314e983e96b336ae9523", "scripts/vendor-patches/core/package.json.json": "55902c9edd671631ee79a89d3f97d536fdb102c0d2ed3e13877d16b4156b90b6", - "scripts/vendor-patches/import-newlines/src/enforce.ts.json": "fcdfebedfa973123ac3b5129ca9df903e0021fffc53c5537f9b9237f28e0fa77", + "scripts/vendor-patches/import-newlines/src/enforce.ts.json": "0a6b40c45e619e16f71859293ca0f2f722144d865c77963b16163a7f82b25971", "scripts/vendor-patches/import/index.ts.json": "6ad5439b074996b890ede06412ea246c9c887e7efe2fde459224599ee052beb9", "scripts/vendor-patches/import/package.json.json": "6cf196ad25f97f95eeec3169dadb734e3546b50c4d5f5468551d6e9a3192a6be", "scripts/vendor-patches/import/src/core/importType.ts.json": "132f93b6f253d74581f434064e54996583e61c70b797baaaf646f5389dafa6c5", @@ -976,7 +976,7 @@ "vendor/core/lib/shared/traverser.ts": "134770a7d6a33c128a65f0b95926fc8838290738723ce4f236c43bd0992960b9", "vendor/core/package.json": "bc2425fee2b46d542a322891e777743aaab1be8e32146e24853cd8851d70f67f", "vendor/import-newlines/index.ts": "786a2d03ad439895450f14c87ce4f43e0adcb97cc178af0bb49b63fee0bfa003", - "vendor/import-newlines/src/enforce.ts": "8b800939ebf58fac6df3b3b3081b7432a4c0dfa4987976f1db57b085330ad057", + "vendor/import-newlines/src/enforce.ts": "2d7b0281756b6289b341bc1da523e0ae3db5b78e533d223fbe19390f3b7bf498", "vendor/import/LICENSE": "7829efc4266fad5f57f7fc489429a3b35d0c62183c0bd617ef6591f64c980c92", "vendor/import/compat/import-resolver.ts": "6f05ed55c5e54d957ceb66773d0d3b96bcc95f855a34dc463faa24317ff62f94", "vendor/import/compat/remote-source.ts": "c38fdc9004536067430c3c25281d0415497893f713f2e313852d17df60a99b1c", From c14feddcbb04a795a663138e313393e505263a85 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 06:56:36 +0000 Subject: [PATCH 3/4] fix: address clause-by-clause audit of guideline coverage An independent audit ran every good and bad example of the guide under both policies in JavaScript and TypeScript, and linted three AdGuard codebases with the guideline policy. It found clauses marked enforced whose examples passed, rules that rejected the guide's own good examples, and TypeScript syntax that pinned rules still ignored. Dispositions: mark 3.3, 6.3, 7.4, 7.6, 7.10, 7.12, 9.1, 9.2, 14.1, 17.3, 21.2, 21.3, and 22.2 partial where the configured rules check only part of the clause, 18.13 overridden under compatibility, and explain 6.2, 18.14, 19.2, and 21.4. Rule lists now match what enforces them. Guideline policy: add ag/prefer-array-from (4.3), ag/prefer-template-over-join (6.3), ag/no-arguments (7.4), and ag/docblock-spacing (17.3); report prototype changes through calls (9.1, 9.2); drop Airbnb's exemptions from no-param-reassign (7.10) and eqeqeq (14.1); enable no-implicit-coercion (21.2, 21.3), inline duplicate-import merging (10.4), and TypeScript no-require-imports (10.1); turn off func-names, which rejects the guide's examples. Keep built-in imports first in the `example` import grouping and put aliases after packages; allow braced cases back to back, TODO/FIXME comment runs, and tool directives next to code; report multiline conditions only when their operands span lines. Both policies: no-default-side-effects reports `delete`; require-docblock keeps `/*!` banners and directive blocks; enum-name accepts V1-style names; unknown-catch reports unions with `any`. TypeScript files use Stylistic brace-style, keyword-spacing, and space-before-function-paren and native class-methods-use-this that exempts overrides. The adguard-projects profile adds member-delimiter-style. --check-tsconfig resolves `extends` as TypeScript does (existing files as written, package tsconfig fields, never `main`) and reports disabled strict-family options. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MCeDV2VhbUtLyAP9XRG1zL --- docs/conformance.md | 2 +- docs/coverage.md | 82 ++-- packages/oxlint-config/README.md | 30 +- packages/oxlint-config/src/compiler.ts | 52 ++- packages/oxlint-config/src/index.ts | 4 +- packages/oxlint-plugin/README.md | 6 + packages/oxlint-plugin/src/helpers.ts | 69 ++- packages/oxlint-plugin/src/index.ts | 128 +++++- packages/rule-catalog/src/catalog.json | 564 ++++++++++++++++++++----- scripts/generate.ts | 178 +++++++- tests/clause-cases.ts | 271 ++++++++++++ tests/clauses.test.ts | 1 + tests/compiler.test.ts | 28 ++ tests/custom-cases.ts | 105 +++++ tests/custom.test.ts | 7 +- tests/gap-cases.ts | 22 + tests/integration.test.ts | 8 +- 17 files changed, 1386 insertions(+), 171 deletions(-) diff --git a/docs/conformance.md b/docs/conformance.md index 60c31fc..b4cb9e1 100644 --- a/docs/conformance.md +++ b/docs/conformance.md @@ -12,6 +12,6 @@ The handwritten adapters address standalone scope metadata and comment globals, Vitest coverage includes every handwritten TypeScript runtime file under `packages/*/src`, including files not otherwise imported. Thresholds are 95% for statements, lines, and functions and 90% for branches. Vendored source is assessed through conformance rather than included in that percentage. Repeated CLI executions used to check stability are not counted as distinct corpus scenarios. -The guide applies the JavaScript rules to TypeScript. For TypeScript files, the preset replaces pinned JavaScript implementations that report valid TypeScript or ignore TypeScript syntax with TypeScript-aware implementations of the same rules and options: Oxlint's native `no-unused-vars`, `no-shadow`, `no-useless-constructor`, `no-empty-function`, `no-dupe-class-members`, `no-array-constructor`, `default-param-last`, and `no-use-before-define`; Stylistic's `indent`, `comma-dangle` (extended to enums, type parameters, and tuples as airbnb-typescript does), `comma-spacing`, `key-spacing`, `lines-between-class-members`, `lines-around-comment`, `object-curly-spacing`, `semi`, `space-before-blocks`, and `space-infix-ops`; and `ag-ts/no-redeclare`, which runs the pinned rule but accepts overloads and the declaration merges that TypeScript permits. Import and React settings gain TypeScript extensions and development globs. The generator records each replacement and its reason in the catalog; `tests/typescript-preset.test.ts` requires idiomatic TypeScript to pass the complete preset under both policies and requires each replacement to report TypeScript-only violations. Replacements are limited to rules with an observed difference: TypeScript syntax the pinned rules neither misreport nor check, such as generic call spacing, remains outside the preset. The pinned `import/export` rule reports exported interface merges, as upstream does. Exact upstream JavaScript conformance continues to use the pinned implementations. +The guide applies the JavaScript rules to TypeScript. For TypeScript files, the preset replaces pinned JavaScript implementations that report valid TypeScript or ignore TypeScript syntax with TypeScript-aware implementations of the same rules and options: Oxlint's native `no-unused-vars`, `no-shadow`, `no-useless-constructor`, `no-empty-function`, `no-dupe-class-members`, `no-array-constructor`, `default-param-last`, `no-use-before-define`, and `class-methods-use-this` (exempting `override` methods); Stylistic's `indent`, `brace-style`, `keyword-spacing`, `space-before-function-paren`, `comma-dangle` (extended to enums, type parameters, and tuples as airbnb-typescript does), `comma-spacing`, `key-spacing`, `lines-between-class-members`, `lines-around-comment`, `object-curly-spacing`, `semi`, `space-before-blocks`, and `space-infix-ops`; and `ag-ts/no-redeclare`, which runs the pinned rule but accepts overloads and the declaration merges that TypeScript permits. Import and React settings gain TypeScript extensions and development globs. The generator records each replacement and its reason in the catalog; `tests/typescript-preset.test.ts` requires idiomatic TypeScript to pass the complete preset under both policies and requires each replacement to report TypeScript-only violations. Replacements are limited to rules with an observed difference: TypeScript syntax the pinned rules neither misreport nor check, such as generic call spacing, remains outside the preset. The pinned `import/export` rule reports exported interface merges, as upstream does. Exact upstream JavaScript conformance continues to use the pinned implementations. The default presets remain syntax-only. `createConfig({ language: 'typescript', typeAware: true })` opts into mappings marked `requiresTypeInfo` using the optional `oxlint-tsgolint@7.0.2002` peer. Currently this adds `typescript/consistent-type-exports`, part of the `adguard-projects` profile, only in the TypeScript file override. A real multi-file project and a packed-package consumer test check both an invalid re-export and its type-only correction. This establishes execution of that rule, not complete Airbnb TypeScript parity, compatibility with a project's nested policies, or full guideline-prose compliance. Compiler checking, declaration emission, and manual review remain separate requirements. diff --git a/docs/coverage.md b/docs/coverage.md index 30bce8b..29dcaaa 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -6,58 +6,58 @@ Generated by `pnpm catalog:generate`. The default `compatibility` policy gives t | --- | --- | --- | --- | --- | | [1.1 types--primitives](reference/Javascript.md#types--primitives) | manual | manual | | Educational semantics; supported browser targets must be selected by the consumer. | | [1.2 types--complex](reference/Javascript.md#types--complex) | manual | manual | | Educational explanation of reference values. | -| [2.1 references--prefer-const](reference/Javascript.md#references--prefer-const) | javascript | javascript | `prefer-const`, `no-const-assign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [2.1 references--prefer-const](reference/Javascript.md#references--prefer-const) | javascript | javascript | `prefer-const`, `no-const-assign`, `no-var` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [2.2 references--disallow-var](reference/Javascript.md#references--disallow-var) | javascript | javascript | `no-var` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [2.3 references--block-scope](reference/Javascript.md#references--block-scope) | javascript | javascript | `no-undef` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [3.1 objects--quoted-props](reference/Javascript.md#objects--quoted-props) | javascript | javascript | `quote-props` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [3.2 objects--prototype-builtins](reference/Javascript.md#objects--prototype-builtins) | javascript | javascript | `no-prototype-builtins` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [3.3 objects--rest-spread](reference/Javascript.md#objects--rest-spread) | javascript | javascript | `prefer-object-spread` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [3.3 objects--rest-spread](reference/Javascript.md#objects--rest-spread) | partial | partial | `prefer-object-spread` | prefer-object-spread reports `Object.assign` whose first argument is an object literal; mutating `Object.assign(target, ...)` and omitting properties with `delete` instead of rest syntax remain reviewable. | | [4.1 es6-array-spreads](reference/Javascript.md#es6-array-spreads) | manual | manual | | Recognizing arbitrary copying algorithms requires semantic intent; reviewed manually. | | [4.2 arrays--from-iterable](reference/Javascript.md#arrays--from-iterable) | manual | manual | | An unknown JavaScript value may be array-like rather than iterable; no speculative rewrite. | -| [4.3 arrays--from-array-like](reference/Javascript.md#arrays--from-array-like) | manual | manual | | Whether an arbitrary runtime value is array-like requires runtime information. | +| [4.3 arrays--from-array-like](reference/Javascript.md#arrays--from-array-like) | manual | custom | guideline: `ag/prefer-array-from` | Array.from for array-like values is not checked by the sample; the guideline policy reports `Array.prototype.slice.call(value)`. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [4.4 arrays--mapping](reference/Javascript.md#arrays--mapping) | custom | custom | `ag/prefer-array-from-map` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [4.5 arrays--callback-return](reference/Javascript.md#arrays--callback-return) | javascript | javascript | `array-callback-return` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [5.1 destructuring--object](reference/Javascript.md#destructuring--object) | javascript | javascript | `prefer-destructuring` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [5.2 destructuring--array](reference/Javascript.md#destructuring--array) | partial | javascript | `prefer-destructuring` | Inherited prefer-destructuring checks assignments but not declarations such as `const first = arr[0]`; the guideline policy checks both. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [5.3 destructuring--object-over-array](reference/Javascript.md#destructuring--object-over-array) | manual | manual | | Distinguishing multiple return values from an intentional array API requires review. | | [6.1 strings--quotes](reference/Javascript.md#strings--quotes) | javascript | javascript | `quotes` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [6.2 strings--line-length](reference/Javascript.md#strings--line-length) | overridden | custom | `max-len`, `no-useless-concat`; guideline: `max-len`, `ag/no-multiline-string-concat` | Sample precedence: max-len, no-useless-concat retains its inherited/explicit options and severity. Guideline: max-len ignores strings and template literals; ag/no-multiline-string-concat rejects strings broken across lines with concatenation. | -| [6.3 es6-template-literals](reference/Javascript.md#es6-template-literals) | javascript | javascript | `prefer-template`, `template-curly-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [6.2 strings--line-length](reference/Javascript.md#strings--line-length) | overridden | custom | `max-len`, `no-useless-concat`, `no-multi-str`; guideline: `max-len`, `no-multi-str`, `ag/no-multiline-string-concat` | The sample max-len has no ignoreStrings, so the clause's good example (a long single-line string) is rejected while strings broken with concatenation pass; backslash continuations are rejected by no-multi-str. The guideline policy enforces the clause. Guideline: max-len ignores strings and template literals; ag/no-multiline-string-concat rejects strings broken across lines with concatenation. | +| [6.3 es6-template-literals](reference/Javascript.md#es6-template-literals) | partial | custom | `prefer-template`, `template-curly-spacing`; guideline: `prefer-template`, `template-curly-spacing`, `ag/prefer-template-over-join` | prefer-template reports concatenation; building strings with `[...].join()` is reported only by the guideline policy. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [6.4 strings--eval](reference/Javascript.md#strings--eval) | javascript | javascript | `no-eval` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [6.5 strings--escaping](reference/Javascript.md#strings--escaping) | javascript | javascript | `no-useless-escape` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [7.1 functions--iife](reference/Javascript.md#functions--iife) | javascript | javascript | `wrap-iife` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [7.2 functions--in-blocks](reference/Javascript.md#functions--in-blocks) | javascript | javascript | `no-loop-func`, `no-inner-declarations` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [7.3 functions--arguments-shadow](reference/Javascript.md#functions--arguments-shadow) | javascript | javascript | `no-shadow-restricted-names` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.4 es6-rest](reference/Javascript.md#es6-rest) | javascript | javascript | `prefer-rest-params` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.4 es6-rest](reference/Javascript.md#es6-rest) | partial | custom | `prefer-rest-params`; guideline: `prefer-rest-params`, `ag/no-arguments` | prefer-rest-params allows property access such as `arguments.length`; the guideline policy reports it. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [7.5 es6-default-parameters](reference/Javascript.md#es6-default-parameters) | javascript | javascript | `no-param-reassign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.6 functions--default-side-effects](reference/Javascript.md#functions--default-side-effects) | custom | custom | `ag/no-default-side-effects` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.6 functions--default-side-effects](reference/Javascript.md#functions--default-side-effects) | partial | partial | `ag/no-default-side-effects` | ag/no-default-side-effects reports assignments, updates, and `delete` in default values; side effects of calls are not analyzed. | | [7.7 functions--defaults-last](reference/Javascript.md#functions--defaults-last) | javascript | javascript | `default-param-last` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [7.8 functions--constructor](reference/Javascript.md#functions--constructor) | javascript | javascript | `no-new-func` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [7.9 functions--signature-spacing](reference/Javascript.md#functions--signature-spacing) | javascript | javascript | `space-before-function-paren`, `space-before-blocks` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.10 functions--mutate-params](reference/Javascript.md#functions--mutate-params) | javascript | javascript | `no-param-reassign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.10 functions--mutate-params](reference/Javascript.md#functions--mutate-params) | partial | partial | `no-param-reassign` | Inherited no-param-reassign exempts parameters such as `acc`, `e`, and `res` and cannot see mutation through method calls; the guideline policy removes the exemptions. Guideline: no-param-reassign reports property writes to any parameter; mutation through method calls such as `list.push()` remains reviewable. | | [7.11 functions--reassign-params](reference/Javascript.md#functions--reassign-params) | javascript | javascript | `no-param-reassign` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [7.12 functions--spread-vs-apply](reference/Javascript.md#functions--spread-vs-apply) | javascript | javascript | `prefer-spread` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [7.12 functions--spread-vs-apply](reference/Javascript.md#functions--spread-vs-apply) | partial | partial | `prefer-spread` | prefer-spread reports `fn.apply(receiver, args)`; constructor application through `Function.prototype.bind.apply` remains reviewable. | | [7.13 functions--signature-invocation-indentation](reference/Javascript.md#functions--signature-invocation-indentation) | javascript | javascript | `function-paren-newline` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [8.1 arrows--use-them](reference/Javascript.md#arrows--use-them) | javascript | javascript | `prefer-arrow-callback`, `arrow-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [8.2 arrows--one-arg-parens](reference/Javascript.md#arrows--one-arg-parens) | javascript | javascript | `arrow-parens` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [8.3 whitespace--implicit-arrow-linebreak](reference/Javascript.md#whitespace--implicit-arrow-linebreak) | javascript | javascript | `implicit-arrow-linebreak` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [9.1 constructors--use-class](reference/Javascript.md#constructors--use-class) | custom | custom | `ag/no-prototype-mutation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [9.2 constructors--extends](reference/Javascript.md#constructors--extends) | custom | custom | `ag/no-prototype-mutation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [9.1 constructors--use-class](reference/Javascript.md#constructors--use-class) | partial | partial | `ag/no-prototype-mutation` | ag/no-prototype-mutation reports assignments to prototypes; the guideline policy also reports `Object.assign` or `Object.defineProperty` on a prototype. Plain constructor functions remain reviewable. Guideline: ag/no-prototype-mutation reports prototype writes, including `Object.assign` and `Object.defineProperty` on a prototype; plain constructor functions remain reviewable. | +| [9.2 constructors--extends](reference/Javascript.md#constructors--extends) | partial | partial | `ag/no-prototype-mutation` | ag/no-prototype-mutation reports prototype assignments; the guideline policy also reports `Object.setPrototypeOf` and `inherits`. Other manual inheritance remains reviewable. Guideline: ag/no-prototype-mutation reports prototype writes, `Object.setPrototypeOf`, and `inherits`; other manual inheritance remains reviewable. | | [9.3 constructors--chaining](reference/Javascript.md#constructors--chaining) | manual | manual | | Method chaining is optional API design guidance. | | [9.4 constructors--no-useless](reference/Javascript.md#constructors--no-useless) | javascript | javascript | `no-useless-constructor` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [9.5 classes--methods-use-this](reference/Javascript.md#classes--methods-use-this) | javascript | javascript | `class-methods-use-this` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.1 modules--use-them](reference/Javascript.md#modules--use-them) | overridden | javascript | `import/no-commonjs`, `import/no-amd` | Sample precedence: import/no-commonjs, import/no-amd retains its inherited/explicit options and severity. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.1 modules--use-them](reference/Javascript.md#modules--use-them) | overridden | partial | `import/no-commonjs`, `import/no-amd`; guideline: `import/no-commonjs`, `import/no-amd`, `@typescript-eslint/no-require-imports` | Sample precedence: import/no-commonjs, import/no-amd retains its inherited/explicit options and severity. Guideline: import/no-commonjs rejects `require` and `module.exports`, and typescript/no-require-imports rejects `import x = require()`; TypeScript `export =` remains reviewable. | | [10.2 modules--no-wildcard](reference/Javascript.md#modules--no-wildcard) | overridden | native | `import/no-namespace` | Sample precedence: import/no-namespace retains its inherited/explicit options and severity. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [10.3 modules--no-export-from-import](reference/Javascript.md#modules--no-export-from-import) | custom | custom | `ag/no-direct-reexport` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.4 modules--no-duplicate-imports](reference/Javascript.md#modules--no-duplicate-imports) | javascript | javascript | `no-duplicate-imports`, `import/no-duplicates` | Airbnb disables no-duplicate-imports in favor of import/no-duplicates, which enforces the clause. | +| [10.4 modules--no-duplicate-imports](reference/Javascript.md#modules--no-duplicate-imports) | javascript | javascript | `no-duplicate-imports`, `import/no-duplicates` | Airbnb disables no-duplicate-imports in favor of import/no-duplicates, which enforces the clause. Guideline: import/no-duplicates with prefer-inline also merges type-only and value imports of one path. | | [10.5 modules--no-mutable-exports](reference/Javascript.md#modules--no-mutable-exports) | javascript | javascript | `import/no-mutable-exports` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [10.6 modules--prefer-named-export](reference/Javascript.md#modules--prefer-named-export) | overridden | native | `import/prefer-default-export`; guideline: `import/no-default-export` | Sample precedence: import/prefer-default-export retains its inherited/explicit options and severity. Guideline: import/prefer-default-export is disabled and import/no-default-export rejects default exports. | | [10.7 modules--imports-first](reference/Javascript.md#modules--imports-first) | javascript | javascript | `import/first` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [10.8 modules--multiline-imports-over-newlines](reference/Javascript.md#modules--multiline-imports-over-newlines) | javascript | javascript | `object-curly-newline` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [10.9 modules--import-extensions](reference/Javascript.md#modules--import-extensions) | javascript | javascript | `import/extensions` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [10.10 modules--import-grouping](reference/Javascript.md#modules--import-grouping) | partial | javascript | `import/order` | Inherited import/order places built-in, package, and internal imports first, but does not require blank lines between groups or separate parent from sibling imports; use the guideline policy. Guideline: import/order requires blank lines between groups. `importGroups: example` follows the clause example (built-in and package imports share a block); `prose` separates every listed category. | +| [10.10 modules--import-grouping](reference/Javascript.md#modules--import-grouping) | partial | javascript | `import/order` | Inherited import/order places built-in, package, and internal imports first, but does not require blank lines between groups or separate parent from sibling imports; use the guideline policy. Guideline: import/order requires blank lines between groups. `importGroups: example` follows the clause example (built-in imports first, then packages, in one block); `prose` separates every listed category. | | [10.11 modules--import-order](reference/Javascript.md#modules--import-order) | overridden | javascript | `import/order` | Inherited import/order options do not alphabetize; the guideline policy orders imports by path. Guideline: import/order alphabetizes imports by path within each group. | -| [10.12 modules--import-member-order](reference/Javascript.md#modules--import-member-order) | javascript | javascript | `import/order`, `sort-imports` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [10.12 modules--import-member-order](reference/Javascript.md#modules--import-member-order) | javascript | javascript | `sort-imports` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [10.13 modules--import-node-protocol](reference/Javascript.md#modules--import-node-protocol) | native | native | `unicorn/prefer-node-protocol` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [11.1 properties--dot](reference/Javascript.md#properties--dot) | javascript | javascript | `dot-notation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [11.2 properties--bracket](reference/Javascript.md#properties--bracket) | javascript | javascript | `dot-notation` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | @@ -73,7 +73,7 @@ Generated by `pnpm catalog:generate`. The default `compatibility` policy gives t | [13.2 hoisting--anon-expressions](reference/Javascript.md#hoisting--anon-expressions) | manual | manual | | Educational explanation of anonymous function hoisting. | | [13.3 hoisting--named-expresions](reference/Javascript.md#hoisting--named-expresions) | manual | manual | | Educational explanation of named function expression hoisting. | | [13.4 hoisting--declarations](reference/Javascript.md#hoisting--declarations) | manual | manual | | Educational explanation of function declaration hoisting. | -| [14.1 comparison--eqeqeq](reference/Javascript.md#comparison--eqeqeq) | javascript | javascript | `eqeqeq` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [14.1 comparison--eqeqeq](reference/Javascript.md#comparison--eqeqeq) | partial | javascript | `eqeqeq` | Inherited eqeqeq allows `== null` and `!= null`; the guideline policy does not. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [14.2 comparison--if](reference/Javascript.md#comparison--if) | manual | manual | | Educational explanation of boolean coercion. | | [14.3 comparison--shortcuts](reference/Javascript.md#comparison--shortcuts) | manual | manual | | Requires semantic types for arbitrary JavaScript expressions; review manually. | | [14.4 comparison--moreinfo](reference/Javascript.md#comparison--moreinfo) | manual | manual | | Further-reading reference, not a code requirement. | @@ -84,11 +84,11 @@ Generated by `pnpm catalog:generate`. The default `compatibility` policy gives t | [15.1 blocks--braces](reference/Javascript.md#blocks--braces) | javascript | javascript | `curly`, `brace-style` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [15.2 blocks--cuddled-elses](reference/Javascript.md#blocks--cuddled-elses) | javascript | javascript | `brace-style` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [15.3 blocks--no-else-return](reference/Javascript.md#blocks--no-else-return) | javascript | javascript | `no-else-return` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [16.1 control-statements](reference/Javascript.md#control-statements) | partial | custom | `operator-linebreak`; guideline: `operator-linebreak`, `ag/multiline-condition-layout` | operator-linebreak puts logical operators first; starting the condition on its own line is enforced by the guideline policy. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [16.1 control-statements](reference/Javascript.md#control-statements) | partial | partial | `operator-linebreak`; guideline: `operator-linebreak`, `ag/multiline-condition-layout` | operator-linebreak puts logical operators first; starting the condition on its own line is enforced by the guideline policy. Guideline: ag/multiline-condition-layout checks multiline conditions; whether a single-line condition is too long to read remains reviewable. | | [16.2 control-statement--value-selection](reference/Javascript.md#control-statement--value-selection) | javascript | javascript | `no-unused-expressions` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [17.1 comments--multiline](reference/Javascript.md#comments--multiline) | partial | custom | `ag/require-docblock` | Multiline /* */ blocks are rejected; runs of // line comments (the clause example) are rejected only by the guideline policy. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [17.2 comments--singleline](reference/Javascript.md#comments--singleline) | overridden | javascript | `line-comment-position`, `lines-around-comment` | Sample precedence: line-comment-position, lines-around-comment retains its inherited/explicit options and severity. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [17.3 comments--spaces](reference/Javascript.md#comments--spaces) | javascript | javascript | `spaced-comment` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [17.2 comments--singleline](reference/Javascript.md#comments--singleline) | overridden | partial | `line-comment-position`, `lines-around-comment` | Sample precedence: line-comment-position, lines-around-comment retains its inherited/explicit options and severity. Guideline: line-comment-position and lines-around-comment place `//` comments; single-line `/* */` comments remain reviewable. | +| [17.3 comments--spaces](reference/Javascript.md#comments--spaces) | partial | custom | `spaced-comment`; guideline: `spaced-comment`, `ag/docblock-spacing` | spaced-comment checks the comment opener; text directly after the `*` of a JSDoc line is reported only by the guideline policy. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [17.4 comments--actionitems](reference/Javascript.md#comments--actionitems) | manual | manual | | TODO/FIXME classify work urgency; the author annotation is explicitly optional. | | [17.6 comments-jsdoc](reference/Javascript.md#comments-jsdoc) | javascript | javascript | `jsdoc/require-file-overview` | The recommended eslint-plugin-jsdoc configuration plus jsdoc/require-file-overview. | | [18.1 whitespace--spaces](reference/Javascript.md#whitespace--spaces) | partial | javascript | `indent`, `react/jsx-indent`, `react/jsx-indent-props` | indent requires four spaces; inherited react/jsx-indent and react/jsx-indent-props require two-space JSX, which the guideline policy changes to four. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | @@ -96,15 +96,15 @@ Generated by `pnpm catalog:generate`. The default `compatibility` policy gives t | [18.3 whitespace--around-keywords](reference/Javascript.md#whitespace--around-keywords) | javascript | javascript | `keyword-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [18.4 whitespace--infix-ops](reference/Javascript.md#whitespace--infix-ops) | javascript | javascript | `space-infix-ops` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [18.5 whitespace--newline-at-end](reference/Javascript.md#whitespace--newline-at-end) | javascript | javascript | `eol-last` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.6 whitespace--chains](reference/Javascript.md#whitespace--chains) | partial | javascript | `newline-per-chained-call`, `no-whitespace-before-property` | Inherited newline-per-chained-call allows four calls on one line; the guideline policy allows two. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.6 whitespace--chains](reference/Javascript.md#whitespace--chains) | partial | partial | `newline-per-chained-call`, `no-whitespace-before-property` | Inherited newline-per-chained-call allows four calls on one line; the guideline policy allows two. Guideline: newline-per-chained-call breaks chains longer than two calls; the clause's own d3 example keeps `.enter().append()` on one line, which the rule rejects. | | [18.7 whitespace--after-blocks](reference/Javascript.md#whitespace--after-blocks) | overridden | partial | `padding-line-between-statements` | Sample precedence: padding-line-between-statements retains its inherited/explicit options and severity. Guideline: padding-line-between-statements requires a blank line after block-like statements; blank lines between multiline members of object and array literals remain reviewable. | | [18.8 whitespace--padded-blocks](reference/Javascript.md#whitespace--padded-blocks) | javascript | javascript | `padded-blocks` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [18.9 whitespace--no-multiple-blanks](reference/Javascript.md#whitespace--no-multiple-blanks) | javascript | javascript | `no-multiple-empty-lines` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [18.10 whitespace--in-parens](reference/Javascript.md#whitespace--in-parens) | javascript | javascript | `space-in-parens` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [18.11 whitespace--in-brackets](reference/Javascript.md#whitespace--in-brackets) | javascript | javascript | `array-bracket-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [18.12 whitespace--in-braces](reference/Javascript.md#whitespace--in-braces) | javascript | javascript | `object-curly-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [18.13 whitespace--max-len](reference/Javascript.md#whitespace--max-len) | javascript | javascript | `max-len` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. Guideline: max-len exempts strings and template literals, as the clause notes. | -| [18.14 whitespace--block-spacing](reference/Javascript.md#whitespace--block-spacing) | javascript | javascript | `block-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [18.13 whitespace--max-len](reference/Javascript.md#whitespace--max-len) | overridden | partial | `max-len` | The sample max-len does not exempt long strings as the clause notes; the guideline policy does. Guideline: max-len exempts strings and template literals, but it exempts the whole line that contains one, so long code on such a line is not reported. | +| [18.14 whitespace--block-spacing](reference/Javascript.md#whitespace--block-spacing) | javascript | javascript | `block-spacing` | block-spacing requires spaces inside single-line blocks; the sample brace-style (allowSingleLine: false, clause 15.1) rejects single-line blocks, including this clause's good example. | | [18.15 whitespace--comma-spacing](reference/Javascript.md#whitespace--comma-spacing) | javascript | javascript | `comma-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [18.16 whitespace--computed-property-spacing](reference/Javascript.md#whitespace--computed-property-spacing) | javascript | javascript | `computed-property-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [18.17 whitespace--func-call-spacing](reference/Javascript.md#whitespace--func-call-spacing) | javascript | javascript | `func-call-spacing` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | @@ -112,22 +112,22 @@ Generated by `pnpm catalog:generate`. The default `compatibility` policy gives t | [18.19 whitespace--no-trailing-spaces](reference/Javascript.md#whitespace--no-trailing-spaces) | javascript | javascript | `no-trailing-spaces` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [18.20 whitespace--no-multiple-empty-lines](reference/Javascript.md#whitespace--no-multiple-empty-lines) | javascript | javascript | `no-multiple-empty-lines` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [19.1 commas--leading-trailing](reference/Javascript.md#commas--leading-trailing) | javascript | javascript | `comma-style` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [19.2 commas--dangling](reference/Javascript.md#commas--dangling) | javascript | javascript | `comma-dangle` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [19.2 commas--dangling](reference/Javascript.md#commas--dangling) | javascript | javascript | `comma-dangle` | comma-dangle requires trailing commas in multiline lists; like upstream, it also requires one after a trailing spread argument, which the clause's last example omits. | | [20.1 semicolons--required](reference/Javascript.md#semicolons--required) | javascript | javascript | `semi` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [21.1 coercion--explicit](reference/Javascript.md#coercion--explicit) | manual | manual | | Determining where a semantic conversion belongs requires review. | -| [21.2 coercion--strings](reference/Javascript.md#coercion--strings) | javascript | javascript | `no-new-wrappers` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [21.3 coercion--numbers](reference/Javascript.md#coercion--numbers) | javascript | javascript | `radix`, `no-new-wrappers` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [21.4 coercion--comment-deviations](reference/Javascript.md#coercion--comment-deviations) | overridden | overridden | `no-bitwise` | Sample precedence: no-bitwise retains its inherited/explicit options and severity. | +| [21.2 coercion--strings](reference/Javascript.md#coercion--strings) | partial | javascript | `no-new-wrappers`; guideline: `no-new-wrappers`, `no-implicit-coercion` | no-new-wrappers rejects `new String()`; Airbnb disables no-implicit-coercion, which the guideline policy enables for `'' + value`. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [21.3 coercion--numbers](reference/Javascript.md#coercion--numbers) | partial | javascript | `radix`, `no-new-wrappers`; guideline: `radix`, `no-new-wrappers`, `no-implicit-coercion` | radix and no-new-wrappers are enforced; Airbnb disables no-implicit-coercion, which the guideline policy enables for `+value`. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [21.4 coercion--comment-deviations](reference/Javascript.md#coercion--comment-deviations) | overridden | overridden | `no-bitwise` | no-bitwise reports every bitwise operation; a justified deviation is a directive that names the implementation, e.g. `// oxlint-disable-next-line ag-compat/no-bitwise -- reason`. | | [21.5 coercion--bitwise](reference/Javascript.md#coercion--bitwise) | manual | manual | | Educational warning about bitwise numeric range. | | [21.6 coercion--booleans](reference/Javascript.md#coercion--booleans) | javascript | javascript | `no-new-wrappers` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [22.1 naming--descriptive](reference/Javascript.md#naming--descriptive) | overridden | javascript | `id-length` | Sample precedence: id-length retains its inherited/explicit options and severity. Guideline: Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | -| [22.2 naming--camelCase](reference/Javascript.md#naming--camelCase) | javascript | javascript | `camelcase` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | +| [22.2 naming--camelCase](reference/Javascript.md#naming--camelCase) | partial | partial | `camelcase` | camelcase reports underscores; the capitalization of functions and instances remains reviewable. | | [22.3 naming--PascalCase](reference/Javascript.md#naming--PascalCase) | javascript | javascript | `new-cap` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [22.4 naming--leading-underscore](reference/Javascript.md#naming--leading-underscore) | javascript | javascript | `no-underscore-dangle` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [22.5 naming--self-this](reference/Javascript.md#naming--self-this) | native | native | `unicorn/no-this-assignment` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [22.6 naming--PascalCase-singleton](reference/Javascript.md#naming--PascalCase-singleton) | manual | manual | | Identifying a singleton or function library requires API-level intent. | | [22.7 naming--Acronyms-and-Initialisms](reference/Javascript.md#naming--Acronyms-and-Initialisms) | manual | manual | | Recognizing domain acronyms requires a project vocabulary. | -| [22.7 naming--constants](reference/Javascript.md#naming--constants) | manual | partial | ; guideline: `ag/constant-name` | The guide distinguishes semantic constants from ordinary const bindings without defining a boundary. Guideline: ag/constant-name requires UPPER_SNAKE_CASE for module-level const bindings initialized with primitive literals; other semantic constants remain reviewable. | +| [22.7 naming--constants](reference/Javascript.md#naming--constants) | manual | partial | guideline: `ag/constant-name` | The guide distinguishes semantic constants from ordinary const bindings without defining a boundary. Guideline: ag/constant-name requires UPPER_SNAKE_CASE for module-level const bindings initialized with primitive literals; other semantic constants remain reviewable. | | [23.1 accessors--not-required](reference/Javascript.md#accessors--not-required) | manual | manual | | Accessor functions are explicitly optional. | | [23.2 accessors--no-getters-setters](reference/Javascript.md#accessors--no-getters-setters) | custom | custom | `ag/no-accessors` | Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable. | | [23.3 accessors--boolean-prefix](reference/Javascript.md#accessors--boolean-prefix) | manual | manual | | Boolean API intent cannot be inferred reliably for arbitrary JavaScript methods. | @@ -149,7 +149,7 @@ Scope `base` is always active, `guideline` is added by `policy: 'guideline'`, an | array-callback-return | ag-compat/array-callback-return | javascript | `["error",{"allowImplicit":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | | block-scoped-var | ag-compat/block-scoped-var | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | | complexity | disabled | disabled | `["off",20]` | airbnb@19.0.4/base@15.0.0 | base | | | -| class-methods-use-this | ag-compat/class-methods-use-this | javascript | `["error",{"exceptMethods":["render","getInitialState","getDefaultProps","getChildContext","componentWillMount","UNSAFE_componentWillMount","componentDidMount","componentWillReceiveProps","UNSAFE_componentWillReceiveProps","shouldComponentUpdate","componentWillUpdate","UNSAFE_componentWillUpdate","componentDidUpdate","componentWillUnmount","componentDidCatch","getSnapshotBeforeUpdate"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| class-methods-use-this | ag-compat/class-methods-use-this | javascript | `["error",{"exceptMethods":["render","getInitialState","getDefaultProps","getChildContext","componentWillMount","UNSAFE_componentWillMount","componentDidMount","componentWillReceiveProps","UNSAFE_componentWillReceiveProps","shouldComponentUpdate","componentWillUpdate","UNSAFE_componentWillUpdate","componentDidUpdate","componentWillUnmount","componentDidCatch","getSnapshotBeforeUpdate"]}]` | airbnb@19.0.4/base@15.0.0 | base | eslint/class-methods-use-this: Exempts override methods, which a base class requires to be instance methods. | | | consistent-return | ag-compat/consistent-return | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | | curly | ag-compat/curly | javascript | `["error","all"]` | eslintrc.cjs | base | | | | default-case | ag-compat/default-case | javascript | `["error",{"commentPattern":"^no default$"}]` | airbnb@19.0.4/base@15.0.0 | base | | | @@ -288,7 +288,7 @@ Scope `base` is always active, `guideline` is added by `policy: 'guideline'`, an | array-element-newline | disabled | disabled | `["off",{"multiline":true,"minItems":3}]` | airbnb@19.0.4/base@15.0.0 | base | | | | array-bracket-spacing | ag-compat/array-bracket-spacing | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | | block-spacing | ag-compat/block-spacing | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | -| brace-style | ag-compat/brace-style | javascript | `["error","1tbs",{"allowSingleLine":false}]` | eslintrc.cjs | base | | | +| brace-style | ag-compat/brace-style | javascript | `["error","1tbs",{"allowSingleLine":false}]` | eslintrc.cjs | base | ag-style/brace-style: Checks namespace bodies. | | | camelcase | ag-compat/camelcase | javascript | `["error",{"properties":"never","ignoreDestructuring":false}]` | airbnb@19.0.4/base@15.0.0 | base | | | | capitalized-comments | disabled | disabled | `["off","never",{"line":{"ignorePattern":".*","ignoreInlineComments":true,"ignoreConsecutiveComments":true},"block":{"ignorePattern":".*","ignoreInlineComments":true,"ignoreConsecutiveComments":true}}]` | airbnb@19.0.4/base@15.0.0 | base | | | | comma-dangle | ag-compat/comma-dangle | javascript | `["error",{"arrays":"always-multiline","objects":"always-multiline","imports":"always-multiline","exports":"always-multiline","functions":"always-multiline"}]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/comma-dangle: Also checks enums, type parameters, and tuples with the array setting, as airbnb-typescript does. | | @@ -310,7 +310,7 @@ Scope `base` is always active, `guideline` is added by `policy: 'guideline'`, an | indent | ag-compat/indent | javascript | `["error",4,{"SwitchCase":1}]` | eslintrc.cjs | base | ag-style/indent: Indents interfaces, enums, and type annotations. | | | jsx-quotes | ag-compat/jsx-quotes | javascript | `["error","prefer-double"]` | airbnb@19.0.4/base@15.0.0 | base | | | | key-spacing | ag-compat/key-spacing | javascript | `["error",{"beforeColon":false,"afterColon":true}]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/key-spacing: Checks interface and type literal members. | | -| keyword-spacing | ag-compat/keyword-spacing | javascript | `["error",{"before":true,"after":true,"overrides":{"return":{"after":true},"throw":{"after":true},"case":{"after":true}}}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| keyword-spacing | ag-compat/keyword-spacing | javascript | `["error",{"before":true,"after":true,"overrides":{"return":{"after":true},"throw":{"after":true},"case":{"after":true}}}]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/keyword-spacing: Checks `as` and `satisfies`. | | | line-comment-position | disabled | disabled | `["off",{"position":"above","ignorePattern":"","applyDefaultPatterns":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | | linebreak-style | ag-compat/linebreak-style | javascript | `["error","unix"]` | airbnb@19.0.4/base@15.0.0 | base | | | | lines-between-class-members | ag-compat/lines-between-class-members | javascript | `["error","always",{"exceptAfterSingleLine":false}]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/lines-between-class-members: Accepts adjacent overload signatures. | | @@ -373,7 +373,7 @@ Scope `base` is always active, `guideline` is added by `policy: 'guideline'`, an | sort-keys | disabled | disabled | `["off","asc",{"caseSensitive":false,"natural":true}]` | airbnb@19.0.4/base@15.0.0 | base | | | | sort-vars | disabled | disabled | `"off"` | airbnb@19.0.4/base@15.0.0 | base | | | | space-before-blocks | ag-compat/space-before-blocks | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | ag-style/space-before-blocks: Checks interface, enum, and module bodies. | | -| space-before-function-paren | ag-compat/space-before-function-paren | javascript | `["error",{"anonymous":"always","named":"never","asyncArrow":"always"}]` | airbnb@19.0.4/base@15.0.0 | base | | | +| space-before-function-paren | ag-compat/space-before-function-paren | javascript | `["error",{"anonymous":"always","named":"never","asyncArrow":"always"}]` | airbnb@19.0.4/base@15.0.0 | base | ag-style/space-before-function-paren: Checks overload signatures, ambient functions, and abstract methods. | | | space-in-parens | ag-compat/space-in-parens | javascript | `["error","never"]` | airbnb@19.0.4/base@15.0.0 | base | | | | space-infix-ops | ag-compat/space-infix-ops | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | ag-style/space-infix-ops: Checks union and intersection types and enum initializers. | | | space-unary-ops | ag-compat/space-unary-ops | javascript | `["error",{"words":true,"nonwords":false,"overrides":{}}]` | airbnb@19.0.4/base@15.0.0 | base | | | @@ -694,20 +694,31 @@ Scope `base` is always active, `guideline` is added by `policy: 'guideline'`, an | import/no-namespace | import/no-namespace | native | `"error"` | Javascript.md | guideline | | | | import/prefer-default-export | ag-import/prefer-default-export | disabled | `"off"` | Javascript.md | guideline | | | | import/no-default-export | import/no-default-export | native | `"error"` | Javascript.md | guideline | | | -| import/order | ag-import/order | javascript | `["error",{"groups":[["builtin","external","internal"],"parent",["sibling","index"]],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true}}]` | Javascript.md | guideline | | | +| import/order | ag-import/order | javascript | `["error",{"groups":[["builtin","external"],"internal","parent",["sibling","index"]],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true},"pathGroups":[{"pattern":"node:*","group":"external","position":"before"},{"pattern":"node:*/**","group":"external","position":"before"}],"pathGroupsExcludedImportTypes":[],"distinctGroup":false}]` | Javascript.md | guideline | | | | line-comment-position | ag-compat/line-comment-position | javascript | `["error",{"position":"above","ignorePattern":"^\\s*(?:oxlint-\|@ts-)","applyDefaultPatterns":true}]` | Javascript.md | guideline | | | -| lines-around-comment | ag-compat/lines-around-comment | javascript | `["error",{"beforeBlockComment":false,"beforeLineComment":true,"allowBlockStart":true,"allowObjectStart":true,"allowArrayStart":true,"allowClassStart":true}]` | Javascript.md | guideline | ag-style/lines-around-comment: Treats interface, type literal, enum, and namespace bodies like blocks. | | +| lines-around-comment | ag-compat/lines-around-comment | javascript | `["error",{"beforeBlockComment":false,"beforeLineComment":true,"allowBlockStart":true,"allowObjectStart":true,"allowArrayStart":true,"allowClassStart":true,"ignorePattern":"^\\s*(?:oxlint-\|@ts-)"}]` | Javascript.md | guideline | ag-style/lines-around-comment: Treats interface, type literal, enum, and namespace bodies like blocks. | | | prefer-destructuring | ag-compat/prefer-destructuring | javascript | `["error",{"VariableDeclarator":{"array":true,"object":true},"AssignmentExpression":{"array":true,"object":false}},{"enforceForRenamedProperties":false}]` | Javascript.md | guideline | | | | react/jsx-indent | ag-react/jsx-indent | javascript | `["error",4]` | Javascript.md | guideline | | | | react/jsx-indent-props | ag-react/jsx-indent-props | javascript | `["error",4]` | Javascript.md | guideline | | | | newline-per-chained-call | ag-compat/newline-per-chained-call | javascript | `["error",{"ignoreChainWithDepth":2}]` | Javascript.md | guideline | | | -| padding-line-between-statements | ag-compat/padding-line-between-statements | javascript | `["error",{"blankLine":"always","prev":"block-like","next":"*"}]` | Javascript.md | guideline | | | +| padding-line-between-statements | ag-compat/padding-line-between-statements | javascript | `["error",{"blankLine":"always","prev":"block-like","next":"*"},{"blankLine":"any","prev":"*","next":["case","default"]}]` | Javascript.md | guideline | | | | id-length | ag-compat/id-length | javascript | `["error",{"min":2,"properties":"never"}]` | Javascript.md | guideline | | | | max-len | ag-compat/max-len | javascript | `["error",{"code":120,"comments":120,"tabWidth":4,"ignoreUrls":false,"ignoreTrailingComments":false,"ignoreComments":false,"ignoreStrings":true,"ignoreTemplateLiterals":true}]` | Javascript.md | guideline | | | | ag/no-multiline-string-concat | ag/no-multiline-string-concat | custom | `"error"` | Javascript.md | guideline | | | | ag/multiline-condition-layout | ag/multiline-condition-layout | custom | `"error"` | Javascript.md | guideline | | | | ag/require-docblock | ag/require-docblock | custom | `["error",{"lineCommentRuns":true}]` | Javascript.md | guideline | | | | ag/constant-name | ag/constant-name | custom | `"error"` | Javascript.md | guideline | | | +| ag/prefer-array-from | ag/prefer-array-from | custom | `"error"` | Javascript.md | guideline | | | +| ag/prefer-template-over-join | ag/prefer-template-over-join | custom | `"error"` | Javascript.md | guideline | | | +| ag/no-arguments | ag/no-arguments | custom | `"error"` | Javascript.md | guideline | | | +| ag/no-prototype-mutation | ag/no-prototype-mutation | custom | `["error",{"calls":true}]` | Javascript.md | guideline | | | +| ag/docblock-spacing | ag/docblock-spacing | custom | `"error"` | Javascript.md | guideline | | | +| no-param-reassign | ag-compat/no-param-reassign | javascript | `["error",{"props":true}]` | Javascript.md | guideline | | | +| eqeqeq | ag-compat/eqeqeq | javascript | `["error","always"]` | Javascript.md | guideline | | | +| no-implicit-coercion | ag-compat/no-implicit-coercion | javascript | `["error",{"boolean":false,"number":true,"string":true,"allow":[]}]` | Javascript.md | guideline | | | +| import/no-duplicates | ag-import/no-duplicates | javascript | `["error",{"prefer-inline":true}]` | Javascript.md | guideline | | | +| func-names | ag-compat/func-names | disabled | `"off"` | Javascript.md | guideline | | | +| @typescript-eslint/no-require-imports | typescript/no-require-imports | native | `"error"` | Javascript.md | guideline | | | | jsdoc/require-description | ag-jsdoc/require-description | javascript | `"error"` | adguard-projects | adguard-projects | | | | jsdoc/require-description-complete-sentence | ag-jsdoc/require-description-complete-sentence | javascript | `"error"` | adguard-projects | adguard-projects | | | | jsdoc/require-hyphen-before-param-description | ag-jsdoc/require-hyphen-before-param-description | javascript | `["error","never"]` | adguard-projects | adguard-projects | | | @@ -719,6 +730,7 @@ Scope `base` is always active, `guideline` is added by `policy: 'guideline'`, an | boundaries/element-types | ag-boundaries/element-types | javascript | `["error",{"default":"allow","rules":[{"from":"test-folder","disallow":["src-index"],"message":"Do not import directly from src/. Use specific submodules like src/utils instead."}]}]` | adguard-projects | adguard-projects | | | | notice/notice | ag-notice/notice | disabled | `"off"` | adguard-projects | adguard-projects | | | | @adguard/logger-context/require-logger-context | ag-logger/require-logger-context | javascript | `["error",{"contextModuleName":"ext"}]` | adguard-projects | adguard-projects | | | +| member-delimiter-style | ag-style/member-delimiter-style | javascript | `"error"` | adguard-projects | adguard-projects | | | | @typescript-eslint/consistent-type-imports | typescript/consistent-type-imports | native | `["error",{"fixStyle":"inline-type-imports"}]` | adguard-projects | adguard-projects | | | | @typescript-eslint/consistent-type-exports | typescript/consistent-type-exports | native | `"error"` | adguard-projects | adguard-projects | | opt-in required | | @typescript-eslint/explicit-function-return-type | typescript/explicit-function-return-type | native | `"error"` | adguard-projects | adguard-projects | | | diff --git a/packages/oxlint-config/README.md b/packages/oxlint-config/README.md index dafc4f9..639bd65 100644 --- a/packages/oxlint-config/README.md +++ b/packages/oxlint-config/README.md @@ -19,26 +19,46 @@ The default `compatibility` policy reproduces the sample `.eslintrc.js`, includi | Clause | Guideline policy | | --- | --- | +| 4.3 array-like conversion | `ag/prefer-array-from` for `Array.prototype.slice.call(value)` | | 5.2 array destructuring | `prefer-destructuring` also checks declarations | | 6.2, 18.13 long strings | `max-len` ignores strings and template literals; `ag/no-multiline-string-concat` | -| 10.1 modules | `import/no-commonjs` for `module` sources (off for `.cjs`/`.cts`) | +| 6.3 template strings | `ag/prefer-template-over-join` for `[...].join()` string building | +| 7.1, 7.9 function expressions | `func-names` off; the guide's examples use anonymous function expressions | +| 7.4 `arguments` | `ag/no-arguments` for `arguments.length` and other property access | +| 7.10 parameter mutation | `no-param-reassign` with `props: true` and no exempt parameter names | +| 9.1, 9.2 prototypes | `ag/no-prototype-mutation` also reports `Object.assign`/`defineProperty` on prototypes, `setPrototypeOf`, and `inherits` | +| 10.1 modules | `import/no-commonjs` for `module` sources (off for `.cjs`/`.cts` and `.eslintrc.js`); TypeScript `no-require-imports` | +| 10.4 duplicate imports | `import/no-duplicates` with `prefer-inline` merges type and value imports | | 10.2 wildcard imports | `import/no-namespace` | | 10.6 named exports | `import/prefer-default-export` off; `import/no-default-export` | | 10.10, 10.11 import groups and order | `import/order` with blank lines between groups and path alphabetization | +| 14.1 equality | `eqeqeq` without the `null` exception | | 16.1 multiline conditions | `ag/multiline-condition-layout` | -| 17.1 multiline comments | `ag/require-docblock` also rejects runs of `//` comments | -| 17.2 single-line comments | `line-comment-position` and `lines-around-comment` | +| 17.1 multiline comments | `ag/require-docblock` also rejects runs of `//` comments (except `TODO`/`FIXME` action items) | +| 17.2 single-line comments | `line-comment-position` and `lines-around-comment` (tool directives excepted) | +| 17.3 comment spacing | `ag/docblock-spacing` for `/**` lines without a space after `*` | | 18.1 indentation | four-space `react/jsx-indent` and `react/jsx-indent-props` | | 18.6 method chains | `newline-per-chained-call` breaks chains longer than two calls | | 18.7 blank line after blocks | `padding-line-between-statements` | +| 21.2, 21.3 coercion | `no-implicit-coercion` for `'' + value` and `+value` (Airbnb's options) | | 22.1 descriptive names | `id-length` with a minimum of two characters | | 22.7 constants | `ag/constant-name` for module-level primitive constants | -Clause 10.10's prose separates built-in, package, parent, and sibling imports, while its example keeps built-in and package imports together. `importGroups: 'example'` (the default) follows the example; `importGroups: 'prose'` separates every category. Configuration files that must use a default export, such as bundler configurations, can turn off `import/no-default-export` in an override. +Clause 10.10's prose separates built-in, package, parent, and sibling imports, while its example keeps built-in and package imports together. `importGroups: 'example'` (the default) follows the example: built-in imports, then packages, in one block, followed by internal aliases, parent, and sibling imports. `importGroups: 'prose'` separates every category. Aliases count as internal only when the resolver settings or `import/internal-regex` identify them. Configuration files that must use a default export, such as bundler configurations, can turn off `import/no-default-export` in an override. + +Some examples in the guide break other clauses' rules under the guideline policy: default exports in 10.1 and 10.3 (10.6), single-letter arrow parameters in 8.1 and 12.5 (22.1), the trailing comment in the 25.1 enum example (17.2), the one-line `.enter().append()` in the 18.6 chain example, and the functions without a final return in 15.3 (`consistent-return`). The policy follows each clause's prose; `docs/coverage.md` records the remaining partial clauses. + +## Suppression directives + +Rules run under this package's plugin namespaces, so directives must name the Oxlint rule, for example `// oxlint-disable-next-line ag-compat/no-bitwise -- reason` rather than `no-bitwise` or `import/order`. Rename existing ESLint directives when migrating; directives that name the upstream rule have no effect. + +## Environments + +`environment` defaults to `browser`. The sample configuration enables browser globals and inherits Node globals from Airbnb; use `environment: 'both'` to reproduce both. ## AdGuard projects profile -`profile: 'adguard-projects'` adds conventions shared by AdGuard extension projects that the guide does not require: stricter JSDoc rules, MV2/MV3 import restrictions, `.pcss` side-effect imports, `import-newlines`, `boundaries` (with default elements in `settings`), logger context checks, and TypeScript rules such as `explicit-function-return-type`, `explicit-member-accessibility`, `consistent-type-imports`, `no-explicit-any`, and `ban-ts-comment`. The catalog labels these mappings with the `adguard-projects` origin. +`profile: 'adguard-projects'` adds conventions shared by AdGuard extension projects that the guide does not require: stricter JSDoc rules, MV2/MV3 import restrictions, `.pcss` side-effect imports, `import-newlines`, `boundaries` (with default elements in `settings`), logger context checks, and TypeScript rules such as `explicit-function-return-type`, `explicit-member-accessibility`, `consistent-type-imports`, `member-delimiter-style`, `no-explicit-any`, and `ban-ts-comment`. The catalog labels these mappings with the `adguard-projects` origin. ## TypeScript diff --git a/packages/oxlint-config/src/compiler.ts b/packages/oxlint-config/src/compiler.ts index f454ea8..e383722 100644 --- a/packages/oxlint-config/src/compiler.ts +++ b/packages/oxlint-config/src/compiler.ts @@ -1,7 +1,25 @@ /** @file Verify that a consumer tsconfig enables the compiler options required by the guide. */ +import { existsSync, readFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; -import { dirname, isAbsolute, resolve } from 'node:path'; +import { + dirname, + isAbsolute, + join, + resolve, +} from 'node:path'; + +// Options that `strict: true` enables and a project can still turn off individually. +const STRICT_FAMILY = [ + 'alwaysStrict', + 'noImplicitAny', + 'noImplicitThis', + 'strictBindCallApply', + 'strictBuiltinIteratorReturn', + 'strictFunctionTypes', + 'strictNullChecks', + 'strictPropertyInitialization', +]; export interface TsconfigCheck { /** Resolved configuration files, starting with the checked file. */ @@ -64,7 +82,7 @@ export function parseJsonc(text: string): unknown { } /** - * Resolve an `extends` entry the way TypeScript does for relative paths and packages. + * Resolve an `extends` entry the way TypeScript does. * @param specifier - Value from the `extends` field. * @param from - Configuration file that declares it. * @returns Absolute path of the extended configuration. @@ -72,14 +90,31 @@ export function parseJsonc(text: string): unknown { function resolveExtends(specifier: string, from: string): string { if (specifier.startsWith('.') || isAbsolute(specifier)) { const path = resolve(dirname(from), specifier); - return path.endsWith('.json') ? path : `${path}.json`; + return existsSync(path) || path.endsWith('.json') ? path : `${path}.json`; } const require = createRequire(from); - try { - return require.resolve(specifier); - } catch { - return require.resolve(`${specifier}${specifier.endsWith('.json') ? '' : '.json'}`); + const bare = /^(?:@[^/]+\/)?[^/]+$/u.test(specifier); + // A bare package name uses its package.json `tsconfig` field or its root tsconfig.json, never `main`. + const candidates = bare ? [`${specifier}/tsconfig.json`] : [specifier, `${specifier}.json`]; + if (bare) { + try { + const manifest = require.resolve(`${specifier}/package.json`); + const { tsconfig } = JSON.parse(readFileSync(manifest, 'utf8')) as { tsconfig?: unknown }; + if (typeof tsconfig === 'string') { + candidates.unshift(join(dirname(manifest), tsconfig)); + } + } catch { + // Packages may hide package.json behind `exports`; the root tsconfig.json still applies. + } + } + for (const candidate of candidates) { + try { + return require.resolve(candidate); + } catch { + // Try the next form TypeScript accepts. + } } + throw new Error(`Cannot resolve tsconfig extends "${specifier}" from ${from}`); } /** @@ -124,5 +159,8 @@ export async function checkTsconfig(path = 'tsconfig.json'): Promise compilerOptions[option] === false).forEach((option) => { + problems.push(`26.1: do not disable "${option}", which "strict" enables`); + }); return { files, compilerOptions, problems }; } diff --git a/packages/oxlint-config/src/index.ts b/packages/oxlint-config/src/index.ts index 8a914af..3bf05b4 100644 --- a/packages/oxlint-config/src/index.ts +++ b/packages/oxlint-config/src/index.ts @@ -141,8 +141,8 @@ export function createConfig(options: ConfigOptions | Language = {}): OxlintConf config.options = { typeAware: true }; } if (policy === 'guideline' && sourceType === 'module') { - // CommonJS filename extensions keep CommonJS semantics. - config.overrides!.push({ files: ['**/*.{cjs,cts}'], rules: { 'import/no-commonjs': 'off' } }); + // CommonJS filename extensions and legacy ESLint configuration files keep CommonJS semantics. + config.overrides!.push({ files: ['**/*.{cjs,cts}', '**/.eslintrc.js'], rules: { 'import/no-commonjs': 'off' } }); } if (language === 'typescript') { settings['import/extensions'] = ['.js', '.mjs', '.jsx', '.ts', '.tsx', '.mts', '.cts']; diff --git a/packages/oxlint-plugin/README.md b/packages/oxlint-plugin/README.md index ba62eb0..fd5ba33 100644 --- a/packages/oxlint-plugin/README.md +++ b/packages/oxlint-plugin/README.md @@ -15,6 +15,12 @@ Use `@agcodeguidelines/oxlint-config` to materialize the full preset. The defaul - `no-multiline-string-concat` - `multiline-condition-layout` - `constant-name` +- `prefer-array-from` +- `prefer-template-over-join` +- `no-arguments` +- `docblock-spacing` + +`no-prototype-mutation` accepts `{ calls: true }` to also report prototype changes through `Object.assign`, `Object.defineProperty`, `Object.setPrototypeOf`, and `inherits`; the guideline policy enables it. The `/typescript` export provides TypeScript-aware variants of pinned core rules: `no-redeclare` accepts overloads and the declaration merges that TypeScript permits. diff --git a/packages/oxlint-plugin/src/helpers.ts b/packages/oxlint-plugin/src/helpers.ts index 14c2a20..a24c993 100644 --- a/packages/oxlint-plugin/src/helpers.ts +++ b/packages/oxlint-plugin/src/helpers.ts @@ -36,7 +36,8 @@ export function writesPrototype(node: ESTree.Node): boolean { * @returns Whether the name has PascalCase form rather than constant casing. */ export function isPascalCase(name: string): boolean { - return /^[A-Z][A-Za-z0-9]*$/u.test(name) && (name.length === 1 || /[a-z]/u.test(name)); + // A capital followed only by digits (V1, H2) is PascalCase; all-capital words are constant casing. + return /^[A-Z][A-Za-z0-9]*$/u.test(name) && (name.length === 1 || /[a-z]/u.test(name) || /^[A-Z]\d+$/u.test(name)); } /** @@ -136,3 +137,69 @@ export function isDirectiveComment(value: string): boolean { || /^\s*(?:eslint|oxlint|@ts-|global\s|globals\s|exported\s|istanbul\s|c8\s|v8\s|prettier-ignore|#(?:end)?region\b)/u .test(value); } + +/** + * Read a static property chain such as `Array.prototype.slice`. + * @param node - Expression to read. + * @returns Dotted path, or undefined when a segment is computed dynamically. + */ +export function staticPath(node: ESTree.Node): string | undefined { + if (node.type === 'Identifier') { + return node.name; + } + if (node.type === 'ArrayExpression' && node.elements.length === 0) { + return '[]'; + } + if (node.type === 'MemberExpression') { + const object = staticPath(node.object); + const property = memberName(node); + return object !== undefined && property !== undefined ? `${object}.${property}` : undefined; + } + return undefined; +} + +/** + * Recognize prototype writes performed through reflection or inheritance helpers. + * @param node - Call expression. + * @returns Whether the call defines prototype members or changes a prototype chain. + */ +export function mutatesPrototypeByCall(node: ESTree.CallExpression): boolean { + const callee = staticPath(node.callee); + const [target] = node.arguments; + if (['Object.setPrototypeOf', 'Reflect.setPrototypeOf', 'inherits', 'util.inherits'].includes(callee ?? '')) { + return true; + } + return ['Object.assign', 'Object.defineProperty', 'Object.defineProperties', 'Reflect.defineProperty'] + .includes(callee ?? '') + && target !== undefined + && writesPrototype(target); +} + +/** + * Find the nearest function that has its own `arguments` binding. + * @param node - Node inside the function. + * @returns Whether a non-arrow function encloses the node. + */ +export function insideRegularFunction(node: ESTree.Node): boolean { + let current: ESTree.Node | null = node.parent; + while (current) { + if (current.type === 'FunctionDeclaration' || current.type === 'FunctionExpression') { + return true; + } + current = current.parent; + } + return false; +} + +/** + * Flatten nested logical expressions into their operands in source order. + * @param node - Condition. + * @returns Operands that are not logical expressions. + */ +export function logicalOperands(node: ESTree.Node): ESTree.Node[] { + const value = unwrapTypeExpression(node); + if (value.type === 'LogicalExpression') { + return [...logicalOperands(value.left), ...logicalOperands(value.right)]; + } + return [value]; +} diff --git a/packages/oxlint-plugin/src/index.ts b/packages/oxlint-plugin/src/index.ts index 6f037d0..a2568d8 100644 --- a/packages/oxlint-plugin/src/index.ts +++ b/packages/oxlint-plugin/src/index.ts @@ -6,13 +6,17 @@ import { definePlugin, defineRule } from '@oxlint/plugins'; import type { ESTree, RuleMeta } from '@oxlint/plugins'; import { concatenationOperands, + insideRegularFunction, isDefaultMutation, isDirectiveComment, isPascalCase, isPrimitiveConstant, isStaticString, isUpperSnakeCase, + logicalOperands, memberName, + mutatesPrototypeByCall, + staticPath, unwrapTypeExpression, writesPrototype, } from './helpers'; @@ -101,8 +105,13 @@ export const rules = { const annotation = node.param && 'typeAnnotation' in node.param ? node.param.typeAnnotation : null; - if (annotation?.typeAnnotation.type === 'TSAnyKeyword') { - context.report({ node: annotation, messageId: 'guideline' }); + const type = annotation?.typeAnnotation; + // Syntactic `any`, alone or in a union; aliases of `any` need type information. + if ( + type?.type === 'TSAnyKeyword' + || (type?.type === 'TSUnionType' && type.types.some((member) => member.type === 'TSAnyKeyword')) + ) { + context.report({ node: annotation!, messageId: 'guideline' }); } }, }; @@ -124,14 +133,24 @@ export const rules = { }, }), 'no-prototype-mutation': defineRule({ - meta: metadata('constructors--use-class', 'Use class declarations instead of directly modifying prototypes.'), + meta: metadata('constructors--use-class', 'Use class declarations instead of directly modifying prototypes.', [{ + type: 'object', + properties: { calls: { type: 'boolean' } }, + additionalProperties: false, + }]), create(context) { + const { calls = false } = (context.options[0] ?? {}) as { calls?: boolean }; const report = (node: ESTree.Node) => { if (writesPrototype(node)) { context.report({ node, messageId: 'guideline' }); } }; return { + CallExpression(node) { + if (calls && mutatesPrototypeByCall(node)) { + context.report({ node, messageId: 'guideline' }); + } + }, AssignmentExpression(node) { report(node.left); }, @@ -157,7 +176,77 @@ export const rules = { context.report({ node, messageId: 'guideline' }); } }; - return { AssignmentExpression: report, UpdateExpression: report }; + return { + AssignmentExpression: report, + UpdateExpression: report, + UnaryExpression(node) { + if (node.operator === 'delete') { + report(node); + } + }, + }; + }, + }), + 'prefer-array-from': defineRule({ + meta: metadata('arrays--from-array-like', 'Use Array.from to convert an array-like object to an array.'), + create(context) { + return { + CallExpression(node) { + if ( + node.callee.type === 'MemberExpression' + && ['call', 'apply'].includes(memberName(node.callee) ?? '') + && ['Array.prototype.slice', '[].slice'].includes(staticPath(node.callee.object) ?? '') + && node.arguments.length === 1 + ) { + context.report({ node, messageId: 'guideline' }); + } + }, + }; + }, + }), + 'prefer-template-over-join': defineRule({ + meta: metadata('es6-template-literals', 'Use a template literal instead of joining string parts.'), + create(context) { + return { + CallExpression(node) { + const [separator, ...rest] = node.arguments; + if ( + node.callee.type !== 'MemberExpression' + || memberName(node.callee) !== 'join' + || node.callee.object.type !== 'ArrayExpression' + || rest.length > 0 + || (separator !== undefined && !(separator.type === 'Literal' && separator.value === '')) + ) { + return; + } + const { elements } = node.callee.object; + if ( + elements.every((element) => element !== null && element.type !== 'SpreadElement') + && elements.some((element) => isStaticString(element!)) + && elements.some((element) => !isStaticString(element!)) + ) { + context.report({ node, messageId: 'guideline' }); + } + }, + }; + }, + }), + 'no-arguments': defineRule({ + meta: metadata('es6-rest', 'Use rest parameters instead of the arguments object.'), + create(context) { + return { + MemberExpression(node) { + // prefer-rest-params reports other uses; it allows property access such as `arguments.length`. + if ( + node.object.type === 'Identifier' + && node.object.name === 'arguments' + && !node.computed + && insideRegularFunction(node) + ) { + context.report({ node, messageId: 'guideline' }); + } + }, + }; }, }), 'prefer-array-from-map': defineRule({ @@ -197,7 +286,8 @@ export const rules = { Program() { let run: ESTree.Comment[] = []; const flush = () => { - if (run.length > 1) { + // Action items (17.4) may span several // lines. + if (run.length > 1 && !run.some((comment) => /^\s*(?:TODO|FIXME)\b/u.test(comment.value))) { context.report({ loc: { start: run[0]!.loc.start, end: run.at(-1)!.loc.end }, messageId: 'guideline', @@ -208,7 +298,12 @@ export const rules = { context.sourceCode.getAllComments().forEach((comment) => { if (comment.type === 'Block') { flush(); - if (/\r|\n/u.test(comment.value) && !comment.value.startsWith('*')) { + // `/*!` preserves license text in bundles; directives keep their syntax. + if ( + /\r|\n/u.test(comment.value) + && !/^[*!]/u.test(comment.value) + && !isDirectiveComment(comment.value) + ) { context.report({ loc: comment.loc, messageId: 'guideline' }); } return; @@ -230,6 +325,22 @@ export const rules = { }; }, }), + 'docblock-spacing': defineRule({ + meta: metadata('comments--spaces', 'Start each line of a /** ... */ comment with "* " followed by the text.'), + create(context) { + return { + Program() { + context.sourceCode.getAllComments().forEach((comment) => { + // A continuation line whose text starts right after the asterisk: ` *text`. + if (comment.type === 'Block' && comment.value.startsWith('*') + && comment.value.split(/\r\n|\r|\n/u).slice(1).some((line) => /^\s*\*[^\s*/]/u.test(line))) { + context.report({ loc: comment.loc, messageId: 'guideline' }); + } + }); + }, + }; + }, + }), 'no-multiline-string-concat': defineRule({ meta: metadata( 'strings--line-length', @@ -269,6 +380,11 @@ export const rules = { } const first = sourceCode.getTokenAfter(open)!; const last = sourceCode.getTokenBefore(close)!; + // Only conditions split between their operands; a wrapped call argument is not a new condition. + const operands = logicalOperands(unwrapTypeExpression(test)); + if (new Set(operands.map((operand) => operand.loc.start.line)).size < 2) { + return; + } if ( first.loc.start.line !== last.loc.end.line && (first.loc.start.line === open.loc.end.line || last.loc.end.line === close.loc.start.line) diff --git a/packages/rule-catalog/src/catalog.json b/packages/rule-catalog/src/catalog.json index c060504..3ba35db 100644 --- a/packages/rule-catalog/src/catalog.json +++ b/packages/rule-catalog/src/catalog.json @@ -2148,7 +2148,36 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "eslint/class-methods-use-this", + "setting": [ + "error", + { + "exceptMethods": [ + "render", + "getInitialState", + "getDefaultProps", + "getChildContext", + "componentWillMount", + "UNSAFE_componentWillMount", + "componentDidMount", + "componentWillReceiveProps", + "UNSAFE_componentWillReceiveProps", + "shouldComponentUpdate", + "componentWillUpdate", + "UNSAFE_componentWillUpdate", + "componentDidUpdate", + "componentWillUnmount", + "componentDidCatch", + "getSnapshotBeforeUpdate" + ], + "ignoreOverrideMethods": true + } + ], + "implementation": "native", + "reason": "Exempts override methods, which a base class requires to be instance methods." + } }, { "source": "consistent-return", @@ -3385,7 +3414,19 @@ } ], "implementation": "javascript", - "origin": "eslintrc.cjs" + "origin": "eslintrc.cjs", + "typescript": { + "target": "ag-style/brace-style", + "setting": [ + "error", + "1tbs", + { + "allowSingleLine": false + } + ], + "implementation": "javascript", + "reason": "Checks namespace bodies." + } }, { "source": "camelcase", @@ -3707,7 +3748,30 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/keyword-spacing", + "setting": [ + "error", + { + "before": true, + "after": true, + "overrides": { + "return": { + "after": true + }, + "throw": { + "after": true + }, + "case": { + "after": true + } + } + } + ], + "implementation": "javascript", + "reason": "Checks `as` and `satisfies`." + } }, { "source": "line-comment-position", @@ -4484,7 +4548,20 @@ } ], "implementation": "javascript", - "origin": "airbnb@19.0.4/base@15.0.0" + "origin": "airbnb@19.0.4/base@15.0.0", + "typescript": { + "target": "ag-style/space-before-function-paren", + "setting": [ + "error", + { + "anonymous": "always", + "named": "never", + "asyncArrow": "always" + } + ], + "implementation": "javascript", + "reason": "Checks overload signatures, ambient functions, and abstract methods." + } }, { "source": "space-in-parens", @@ -7795,9 +7872,9 @@ "groups": [ [ "builtin", - "external", - "internal" + "external" ], + "internal", "parent", [ "sibling", @@ -7808,7 +7885,21 @@ "alphabetize": { "order": "asc", "caseInsensitive": true - } + }, + "pathGroups": [ + { + "pattern": "node:*", + "group": "external", + "position": "before" + }, + { + "pattern": "node:*/**", + "group": "external", + "position": "before" + } + ], + "pathGroupsExcludedImportTypes": [], + "distinctGroup": false } ], "implementation": "javascript", @@ -7841,7 +7932,8 @@ "allowBlockStart": true, "allowObjectStart": true, "allowArrayStart": true, - "allowClassStart": true + "allowClassStart": true, + "ignorePattern": "^\\s*(?:oxlint-|@ts-)" } ], "implementation": "javascript", @@ -7858,6 +7950,7 @@ "allowObjectStart": true, "allowArrayStart": true, "allowClassStart": true, + "ignorePattern": "^\\s*(?:oxlint-|@ts-)", "allowInterfaceStart": true, "allowTypeStart": true, "allowEnumStart": true, @@ -7935,6 +8028,14 @@ "blankLine": "always", "prev": "block-like", "next": "*" + }, + { + "blankLine": "any", + "prev": "*", + "next": [ + "case", + "default" + ] } ], "implementation": "javascript", @@ -8012,6 +8113,121 @@ "origin": "Javascript.md", "scope": "guideline" }, + { + "source": "ag/prefer-array-from", + "target": "ag/prefer-array-from", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "ag/prefer-template-over-join", + "target": "ag/prefer-template-over-join", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "ag/no-arguments", + "target": "ag/no-arguments", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "ag/no-prototype-mutation", + "target": "ag/no-prototype-mutation", + "setting": [ + "error", + { + "calls": true + } + ], + "implementation": "custom", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "ag/docblock-spacing", + "target": "ag/docblock-spacing", + "setting": "error", + "implementation": "custom", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "no-param-reassign", + "target": "ag-compat/no-param-reassign", + "setting": [ + "error", + { + "props": true + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "eqeqeq", + "target": "ag-compat/eqeqeq", + "setting": [ + "error", + "always" + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "no-implicit-coercion", + "target": "ag-compat/no-implicit-coercion", + "setting": [ + "error", + { + "boolean": false, + "number": true, + "string": true, + "allow": [] + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "import/no-duplicates", + "target": "ag-import/no-duplicates", + "setting": [ + "error", + { + "prefer-inline": true + } + ], + "implementation": "javascript", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "func-names", + "target": "ag-compat/func-names", + "setting": "off", + "implementation": "disabled", + "origin": "Javascript.md", + "scope": "guideline" + }, + { + "source": "@typescript-eslint/no-require-imports", + "target": "typescript/no-require-imports", + "setting": "error", + "implementation": "native", + "origin": "Javascript.md", + "scope": "guideline", + "language": "typescript" + }, { "source": "jsdoc/require-description", "target": "ag-jsdoc/require-description", @@ -8148,6 +8364,15 @@ "origin": "adguard-projects", "scope": "adguard-projects" }, + { + "source": "member-delimiter-style", + "target": "ag-style/member-delimiter-style", + "setting": "error", + "implementation": "javascript", + "origin": "adguard-projects", + "scope": "adguard-projects", + "language": "typescript" + }, { "source": "@typescript-eslint/consistent-type-imports", "target": "typescript/consistent-type-imports", @@ -8285,14 +8510,16 @@ "line": 85, "rules": [ "prefer-const", - "no-const-assign" + "no-const-assign", + "no-var" ], "enforcement": "javascript", "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", "guideline": { "rules": [ "prefer-const", - "no-const-assign" + "no-const-assign", + "no-var" ], "enforcement": "javascript", "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." @@ -8378,14 +8605,14 @@ "rules": [ "prefer-object-spread" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "prefer-object-spread reports `Object.assign` whose first argument is an object literal; mutating `Object.assign(target, ...)` and omitting properties with `delete` instead of rest syntax remain reviewable.", "guideline": { "rules": [ "prefer-object-spread" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "prefer-object-spread reports `Object.assign` whose first argument is an object literal; mutating `Object.assign(target, ...)` and omitting properties with `delete` instead of rest syntax remain reviewable." } }, { @@ -8423,11 +8650,13 @@ "line": 235, "rules": [], "enforcement": "manual", - "reason": "Whether an arbitrary runtime value is array-like requires runtime information.", + "reason": "Array.from for array-like values is not checked by the sample; the guideline policy reports `Array.prototype.slice.call(value)`.", "guideline": { - "rules": [], - "enforcement": "manual", - "reason": "Whether an arbitrary runtime value is array-like requires runtime information." + "rules": [ + "ag/prefer-array-from" + ], + "enforcement": "custom", + "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." } }, { @@ -8541,13 +8770,15 @@ "line": 371, "rules": [ "max-len", - "no-useless-concat" + "no-useless-concat", + "no-multi-str" ], "enforcement": "overridden", - "reason": "Sample precedence: max-len, no-useless-concat retains its inherited/explicit options and severity.", + "reason": "The sample max-len has no ignoreStrings, so the clause's good example (a long single-line string) is rejected while strings broken with concatenation pass; backslash continuations are rejected by no-multi-str. The guideline policy enforces the clause.", "guideline": { "rules": [ "max-len", + "no-multi-str", "ag/no-multiline-string-concat" ], "enforcement": "custom", @@ -8563,14 +8794,15 @@ "prefer-template", "template-curly-spacing" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "prefer-template reports concatenation; building strings with `[...].join()` is reported only by the guideline policy.", "guideline": { "rules": [ "prefer-template", - "template-curly-spacing" + "template-curly-spacing", + "ag/prefer-template-over-join" ], - "enforcement": "javascript", + "enforcement": "custom", "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." } }, @@ -8674,13 +8906,14 @@ "rules": [ "prefer-rest-params" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "prefer-rest-params allows property access such as `arguments.length`; the guideline policy reports it.", "guideline": { "rules": [ - "prefer-rest-params" + "prefer-rest-params", + "ag/no-arguments" ], - "enforcement": "javascript", + "enforcement": "custom", "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." } }, @@ -8710,14 +8943,14 @@ "rules": [ "ag/no-default-side-effects" ], - "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "ag/no-default-side-effects reports assignments, updates, and `delete` in default values; side effects of calls are not analyzed.", "guideline": { "rules": [ "ag/no-default-side-effects" ], - "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "ag/no-default-side-effects reports assignments, updates, and `delete` in default values; side effects of calls are not analyzed." } }, { @@ -8784,14 +9017,14 @@ "rules": [ "no-param-reassign" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "Inherited no-param-reassign exempts parameters such as `acc`, `e`, and `res` and cannot see mutation through method calls; the guideline policy removes the exemptions.", "guideline": { "rules": [ "no-param-reassign" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "no-param-reassign reports property writes to any parameter; mutation through method calls such as `list.push()` remains reviewable." } }, { @@ -8820,14 +9053,14 @@ "rules": [ "prefer-spread" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "prefer-spread reports `fn.apply(receiver, args)`; constructor application through `Function.prototype.bind.apply` remains reviewable.", "guideline": { "rules": [ "prefer-spread" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "prefer-spread reports `fn.apply(receiver, args)`; constructor application through `Function.prototype.bind.apply` remains reviewable." } }, { @@ -8912,14 +9145,14 @@ "rules": [ "ag/no-prototype-mutation" ], - "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "ag/no-prototype-mutation reports assignments to prototypes; the guideline policy also reports `Object.assign` or `Object.defineProperty` on a prototype. Plain constructor functions remain reviewable.", "guideline": { "rules": [ "ag/no-prototype-mutation" ], - "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "ag/no-prototype-mutation reports prototype writes, including `Object.assign` and `Object.defineProperty` on a prototype; plain constructor functions remain reviewable." } }, { @@ -8930,14 +9163,14 @@ "rules": [ "ag/no-prototype-mutation" ], - "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "ag/no-prototype-mutation reports prototype assignments; the guideline policy also reports `Object.setPrototypeOf` and `inherits`. Other manual inheritance remains reviewable.", "guideline": { "rules": [ "ag/no-prototype-mutation" ], - "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "ag/no-prototype-mutation reports prototype writes, `Object.setPrototypeOf`, and `inherits`; other manual inheritance remains reviewable." } }, { @@ -9004,10 +9237,11 @@ "guideline": { "rules": [ "import/no-commonjs", - "import/no-amd" + "import/no-amd", + "@typescript-eslint/no-require-imports" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "import/no-commonjs rejects `require` and `module.exports`, and typescript/no-require-imports rejects `import x = require()`; TypeScript `export =` remains reviewable." } }, { @@ -9063,7 +9297,7 @@ "import/no-duplicates" ], "enforcement": "javascript", - "reason": "Airbnb disables no-duplicate-imports in favor of import/no-duplicates, which enforces the clause." + "reason": "import/no-duplicates with prefer-inline also merges type-only and value imports of one path." } }, { @@ -9171,7 +9405,7 @@ "import/order" ], "enforcement": "javascript", - "reason": "import/order requires blank lines between groups. `importGroups: example` follows the clause example (built-in and package imports share a block); `prose` separates every listed category." + "reason": "import/order requires blank lines between groups. `importGroups: example` follows the clause example (built-in imports first, then packages, in one block); `prose` separates every listed category." } }, { @@ -9198,14 +9432,12 @@ "text": "Order named imports alphabetically.", "line": 1118, "rules": [ - "import/order", "sort-imports" ], "enforcement": "javascript", "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", "guideline": { "rules": [ - "import/order", "sort-imports" ], "enforcement": "javascript", @@ -9474,8 +9706,8 @@ "rules": [ "eqeqeq" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "Inherited eqeqeq allows `== null` and `!= null`; the guideline policy does not.", "guideline": { "rules": [ "eqeqeq" @@ -9669,8 +9901,8 @@ "operator-linebreak", "ag/multiline-condition-layout" ], - "enforcement": "custom", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "ag/multiline-condition-layout checks multiline conditions; whether a single-line condition is too long to read remains reviewable." } }, { @@ -9725,8 +9957,8 @@ "line-comment-position", "lines-around-comment" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "line-comment-position and lines-around-comment place `//` comments; single-line `/* */` comments remain reviewable." } }, { @@ -9737,13 +9969,14 @@ "rules": [ "spaced-comment" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "spaced-comment checks the comment opener; text directly after the `*` of a JSDoc line is reported only by the guideline policy.", "guideline": { "rules": [ - "spaced-comment" + "spaced-comment", + "ag/docblock-spacing" ], - "enforcement": "javascript", + "enforcement": "custom", "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." } }, @@ -9889,8 +10122,8 @@ "newline-per-chained-call", "no-whitespace-before-property" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "newline-per-chained-call breaks chains longer than two calls; the clause's own d3 example keeps `.enter().append()` on one line, which the rule rejects." } }, { @@ -10009,14 +10242,14 @@ "rules": [ "max-len" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "overridden", + "reason": "The sample max-len does not exempt long strings as the clause notes; the guideline policy does.", "guideline": { "rules": [ "max-len" ], - "enforcement": "javascript", - "reason": "max-len exempts strings and template literals, as the clause notes." + "enforcement": "partial", + "reason": "max-len exempts strings and template literals, but it exempts the whole line that contains one, so long code on such a line is not reported." } }, { @@ -10028,13 +10261,13 @@ "block-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "reason": "block-spacing requires spaces inside single-line blocks; the sample brace-style (allowSingleLine: false, clause 15.1) rejects single-line blocks, including this clause's good example.", "guideline": { "rules": [ "block-spacing" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "block-spacing requires spaces inside single-line blocks; the sample brace-style (allowSingleLine: false, clause 15.1) rejects single-line blocks, including this clause's good example." } }, { @@ -10172,13 +10405,13 @@ "comma-dangle" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "reason": "comma-dangle requires trailing commas in multiline lists; like upstream, it also requires one after a trailing spread argument, which the clause's last example omits.", "guideline": { "rules": [ "comma-dangle" ], "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "reason": "comma-dangle requires trailing commas in multiline lists; like upstream, it also requires one after a trailing spread argument, which the clause's last example omits." } }, { @@ -10221,11 +10454,12 @@ "rules": [ "no-new-wrappers" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "no-new-wrappers rejects `new String()`; Airbnb disables no-implicit-coercion, which the guideline policy enables for `'' + value`.", "guideline": { "rules": [ - "no-new-wrappers" + "no-new-wrappers", + "no-implicit-coercion" ], "enforcement": "javascript", "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." @@ -10240,12 +10474,13 @@ "radix", "no-new-wrappers" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "radix and no-new-wrappers are enforced; Airbnb disables no-implicit-coercion, which the guideline policy enables for `+value`.", "guideline": { "rules": [ "radix", - "no-new-wrappers" + "no-new-wrappers", + "no-implicit-coercion" ], "enforcement": "javascript", "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." @@ -10260,13 +10495,13 @@ "no-bitwise" ], "enforcement": "overridden", - "reason": "Sample precedence: no-bitwise retains its inherited/explicit options and severity.", + "reason": "no-bitwise reports every bitwise operation; a justified deviation is a directive that names the implementation, e.g. `// oxlint-disable-next-line ag-compat/no-bitwise -- reason`.", "guideline": { "rules": [ "no-bitwise" ], "enforcement": "overridden", - "reason": "Sample precedence: no-bitwise retains its inherited/explicit options and severity." + "reason": "no-bitwise reports every bitwise operation; a justified deviation is a directive that names the implementation, e.g. `// oxlint-disable-next-line ag-compat/no-bitwise -- reason`." } }, { @@ -10327,14 +10562,14 @@ "rules": [ "camelcase" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable.", + "enforcement": "partial", + "reason": "camelcase reports underscores; the capitalization of functions and instances remains reviewable.", "guideline": { "rules": [ "camelcase" ], - "enforcement": "javascript", - "reason": "Configured rule enforces the linked syntactic requirement; semantic intent remains reviewable." + "enforcement": "partial", + "reason": "camelcase reports underscores; the capitalization of functions and instances remains reviewable." } }, { @@ -12258,6 +12493,31 @@ "ag/require-docblock": "error" }, "typescriptRules": { + "ag-compat/class-methods-use-this": "off", + "eslint/class-methods-use-this": [ + "error", + { + "exceptMethods": [ + "render", + "getInitialState", + "getDefaultProps", + "getChildContext", + "componentWillMount", + "UNSAFE_componentWillMount", + "componentDidMount", + "componentWillReceiveProps", + "UNSAFE_componentWillReceiveProps", + "shouldComponentUpdate", + "componentWillUpdate", + "UNSAFE_componentWillUpdate", + "componentDidUpdate", + "componentWillUnmount", + "componentDidCatch", + "getSnapshotBeforeUpdate" + ], + "ignoreOverrideMethods": true + } + ], "ag-compat/default-param-last": "off", "eslint/default-param-last": "error", "ag-compat/no-empty-function": "off", @@ -12273,6 +12533,14 @@ ], "ag-compat/no-redeclare": "off", "ag-ts/no-redeclare": "error", + "ag-compat/brace-style": "off", + "ag-style/brace-style": [ + "error", + "1tbs", + { + "allowSingleLine": false + } + ], "ag-compat/comma-dangle": "off", "ag-style/comma-dangle": [ "error", @@ -12311,6 +12579,25 @@ "afterColon": true } ], + "ag-compat/keyword-spacing": "off", + "ag-style/keyword-spacing": [ + "error", + { + "before": true, + "after": true, + "overrides": { + "return": { + "after": true + }, + "throw": { + "after": true + }, + "case": { + "after": true + } + } + } + ], "ag-compat/lines-between-class-members": "off", "ag-style/lines-between-class-members": [ "error", @@ -12333,6 +12620,15 @@ ], "ag-compat/space-before-blocks": "off", "ag-style/space-before-blocks": "error", + "ag-compat/space-before-function-paren": "off", + "ag-style/space-before-function-paren": [ + "error", + { + "anonymous": "always", + "named": "never", + "asyncArrow": "always" + } + ], "ag-compat/space-infix-ops": "off", "ag-style/space-infix-ops": "error", "ag-compat/no-shadow": "off", @@ -12460,9 +12756,9 @@ "groups": [ [ "builtin", - "external", - "internal" + "external" ], + "internal", "parent", [ "sibling", @@ -12473,7 +12769,21 @@ "alphabetize": { "order": "asc", "caseInsensitive": true - } + }, + "pathGroups": [ + { + "pattern": "node:*", + "group": "external", + "position": "before" + }, + { + "pattern": "node:*/**", + "group": "external", + "position": "before" + } + ], + "pathGroupsExcludedImportTypes": [], + "distinctGroup": false } ], "ag-compat/line-comment-position": [ @@ -12492,7 +12802,8 @@ "allowBlockStart": true, "allowObjectStart": true, "allowArrayStart": true, - "allowClassStart": true + "allowClassStart": true, + "ignorePattern": "^\\s*(?:oxlint-|@ts-)" } ], "ag-compat/prefer-destructuring": [ @@ -12531,6 +12842,14 @@ "blankLine": "always", "prev": "block-like", "next": "*" + }, + { + "blankLine": "any", + "prev": "*", + "next": [ + "case", + "default" + ] } ], "ag-compat/id-length": [ @@ -12561,7 +12880,43 @@ "lineCommentRuns": true } ], - "ag/constant-name": "error" + "ag/constant-name": "error", + "ag/prefer-array-from": "error", + "ag/prefer-template-over-join": "error", + "ag/no-arguments": "error", + "ag/no-prototype-mutation": [ + "error", + { + "calls": true + } + ], + "ag/docblock-spacing": "error", + "ag-compat/no-param-reassign": [ + "error", + { + "props": true + } + ], + "ag-compat/eqeqeq": [ + "error", + "always" + ], + "ag-compat/no-implicit-coercion": [ + "error", + { + "boolean": false, + "number": true, + "string": true, + "allow": [] + } + ], + "ag-import/no-duplicates": [ + "error", + { + "prefer-inline": true + } + ], + "ag-compat/func-names": "off" }, "typescriptRules": { "ag-compat/lines-around-comment": "off", @@ -12574,12 +12929,14 @@ "allowObjectStart": true, "allowArrayStart": true, "allowClassStart": true, + "ignorePattern": "^\\s*(?:oxlint-|@ts-)", "allowInterfaceStart": true, "allowTypeStart": true, "allowEnumStart": true, "allowModuleStart": true } - ] + ], + "typescript/no-require-imports": "error" }, "settings": {}, "importGroups": { @@ -12589,9 +12946,9 @@ "groups": [ [ "builtin", - "external", - "internal" + "external" ], + "internal", "parent", [ "sibling", @@ -12602,7 +12959,21 @@ "alphabetize": { "order": "asc", "caseInsensitive": true - } + }, + "pathGroups": [ + { + "pattern": "node:*", + "group": "external", + "position": "before" + }, + { + "pattern": "node:*/**", + "group": "external", + "position": "before" + } + ], + "pathGroupsExcludedImportTypes": [], + "distinctGroup": false } ], "prose": [ @@ -12692,6 +13063,7 @@ ] }, "typescriptRules": { + "ag-style/member-delimiter-style": "error", "typescript/consistent-type-imports": [ "error", { diff --git a/scripts/generate.ts b/scripts/generate.ts index f8cf384..711a0f6 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -150,17 +150,28 @@ const guideTypeScriptAdditions: RuleMap = { }; // The opt-in guideline policy resolves documented conflicts between guide prose and the sample in favor of prose. -const importOrder = (groups: unknown[]): RuleSetting => ['error', { +const importOrder = (groups: unknown[], options: object = {}): RuleSetting => ['error', { groups, 'newlines-between': 'always', alphabetize: { order: 'asc', caseInsensitive: true }, + ...options, }]; const importGroups: Record = { - // The guide's example keeps built-in and package imports in one block. - example: importOrder([['builtin', 'external', 'internal'], 'parent', ['sibling', 'index']]), + // The guide's example keeps built-in and package imports in one block, built-in imports first. + // Project aliases ("internal") are not npm packages; they follow them, as in the "most far" to "most close" order. + example: importOrder([['builtin', 'external'], 'internal', 'parent', ['sibling', 'index']], { + pathGroups: [ + { pattern: 'node:*', group: 'external', position: 'before' }, + { pattern: 'node:*/**', group: 'external', position: 'before' }, + ], + pathGroupsExcludedImportTypes: [], + distinctGroup: false, + }), // The guide's prose separates every listed category. prose: importOrder(['builtin', 'external', 'internal', 'parent', ['sibling', 'index']]), }; +// Tool directives are not comments that describe code (clause 17.2). +const COMMENT_DIRECTIVES = '^\\s*(?:oxlint-|@ts-)'; const guidelineAdditions: RuleMap = { 'import/no-commonjs': 'error', 'import/no-namespace': 'error', @@ -169,7 +180,7 @@ const guidelineAdditions: RuleMap = { 'import/order': importGroups.example, 'line-comment-position': ['error', { ...(inheritedOptions('line-comment-position')[0] as object), - ignorePattern: '^\\s*(?:oxlint-|@ts-)', + ignorePattern: COMMENT_DIRECTIVES, }], // Clause 17.2 governs single-line comments; block comments keep their inherited freedom. 'lines-around-comment': ['error', { @@ -179,6 +190,7 @@ const guidelineAdditions: RuleMap = { allowObjectStart: true, allowArrayStart: true, allowClassStart: true, + ignorePattern: COMMENT_DIRECTIVES, }], 'prefer-destructuring': ['error', { ...(inheritedOptions('prefer-destructuring')[0] as object), @@ -187,7 +199,10 @@ const guidelineAdditions: RuleMap = { 'react/jsx-indent': ['error', 4], 'react/jsx-indent-props': ['error', 4], 'newline-per-chained-call': ['error', { ignoreChainWithDepth: 2 }], - 'padding-line-between-statements': ['error', { blankLine: 'always', prev: 'block-like', next: '*' }], + // Case labels are not statements; the clause 14.5 example puts braced cases back to back. + 'padding-line-between-statements': ['error', + { blankLine: 'always', prev: 'block-like', next: '*' }, + { blankLine: 'any', prev: '*', next: ['case', 'default'] }], 'id-length': ['error', { min: 2, properties: 'never' }], 'max-len': ['error', { ...(inheritedOptions('max-len')[0] as object), @@ -198,6 +213,21 @@ const guidelineAdditions: RuleMap = { 'ag/multiline-condition-layout': 'error', 'ag/require-docblock': ['error', { lineCommentRuns: true }], 'ag/constant-name': 'error', + 'ag/prefer-array-from': 'error', + 'ag/prefer-template-over-join': 'error', + 'ag/no-arguments': 'error', + 'ag/no-prototype-mutation': ['error', { calls: true }], + 'ag/docblock-spacing': 'error', + // The prose has no exemptions: "Never mutate parameters" and "Use === and !==". + 'no-param-reassign': ['error', { props: true }], + eqeqeq: ['error', 'always'], + 'no-implicit-coercion': ['error', ...inheritedOptions('no-implicit-coercion')], + 'import/no-duplicates': ['error', { 'prefer-inline': true }], + // No clause asks for named function expressions; the guide's own examples use anonymous ones. + 'func-names': 'off', +}; +const guidelineTypeScriptAdditions: RuleMap = { + '@typescript-eslint/no-require-imports': 'error', }; // Rules used by AdGuard projects that the guide does not require; opt-in through the adguard-projects profile. @@ -227,6 +257,7 @@ const projectAdditions: RuleMap = { '@adguard/logger-context/require-logger-context': ['error', { contextModuleName: 'ext' }], }; const projectTypeScriptAdditions: RuleMap = { + 'member-delimiter-style': 'error', '@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports' }], '@typescript-eslint/consistent-type-exports': 'error', '@typescript-eslint/explicit-function-return-type': 'error', @@ -297,6 +328,20 @@ const TYPESCRIPT_EQUIVALENTS: Record = { reason: 'Understands parameter-property constructors and overload signatures.', }, 'no-dupe-class-members': { target: 'eslint/no-dupe-class-members', reason: 'Accepts method overload signatures.' }, + 'class-methods-use-this': { + target: 'eslint/class-methods-use-this', + reason: 'Exempts override methods, which a base class requires to be instance methods.', + adapt: (setting) => withOptions(setting, ([options]) => [{ + ...(options as object), + ignoreOverrideMethods: true, + }]), + }, + 'space-before-function-paren': { + target: 'ag-style/space-before-function-paren', + reason: 'Checks overload signatures, ambient functions, and abstract methods.', + }, + 'keyword-spacing': { target: 'ag-style/keyword-spacing', reason: 'Checks `as` and `satisfies`.' }, + 'brace-style': { target: 'ag-style/brace-style', reason: 'Checks namespace bodies.' }, 'no-redeclare': { target: 'ag-ts/no-redeclare', reason: 'Accepts overloads and declaration merges that TypeScript permits.', @@ -416,9 +461,10 @@ const typescriptTargets = new Set([ const extraClauses: Record = { 'types--primitives': [], + 'references--prefer-const': ['prefer-const', 'no-const-assign', 'no-var'], 'references--block-scope': ['no-undef'], 'arrays--mapping': ['ag/prefer-array-from-map'], - 'strings--line-length': ['max-len', 'no-useless-concat'], + 'strings--line-length': ['max-len', 'no-useless-concat', 'no-multi-str'], 'functions--in-blocks': ['no-loop-func', 'no-inner-declarations'], 'functions--arguments-shadow': ['no-shadow-restricted-names'], 'es6-default-parameters': ['no-param-reassign'], @@ -431,6 +477,7 @@ const extraClauses: Record = { 'modules--no-duplicate-imports': ['no-duplicate-imports', 'import/no-duplicates'], 'modules--prefer-named-export': ['import/prefer-default-export'], 'modules--import-node-protocol': ['unicorn/prefer-node-protocol'], + 'modules--import-member-order': ['sort-imports'], 'properties--bracket': ['dot-notation'], 'control-statements': ['operator-linebreak'], 'control-statement--value-selection': ['no-unused-expressions'], @@ -448,9 +495,16 @@ const extraClauses: Record = { // Rules that the guideline policy adds to a clause's compatibility rules. const guidelineClauses: Record = { - 'strings--line-length': ['max-len', 'ag/no-multiline-string-concat'], + 'arrays--from-array-like': ['ag/prefer-array-from'], + 'strings--line-length': ['max-len', 'no-multi-str', 'ag/no-multiline-string-concat'], + 'es6-template-literals': ['prefer-template', 'template-curly-spacing', 'ag/prefer-template-over-join'], + 'es6-rest': ['prefer-rest-params', 'ag/no-arguments'], + 'modules--use-them': ['import/no-commonjs', 'import/no-amd', '@typescript-eslint/no-require-imports'], 'modules--prefer-named-export': ['import/no-default-export'], 'control-statements': ['operator-linebreak', 'ag/multiline-condition-layout'], + 'comments--spaces': ['spaced-comment', 'ag/docblock-spacing'], + 'coercion--strings': ['no-new-wrappers', 'no-implicit-coercion'], + 'coercion--numbers': ['radix', 'no-new-wrappers', 'no-implicit-coercion'], 'naming--constants': ['ag/constant-name'], }; @@ -460,7 +514,9 @@ const manual: Record = { 'es6-array-spreads': 'Recognizing arbitrary copying algorithms requires semantic intent; reviewed manually.', 'arrays--from-iterable': 'An unknown JavaScript value may be array-like rather than iterable; no speculative rewrite.', - 'arrays--from-array-like': 'Whether an arbitrary runtime value is array-like requires runtime information.', + 'arrays--from-array-like': + 'Array.from for array-like values is not checked by the sample; the guideline policy reports ' + + '`Array.prototype.slice.call(value)`.', 'destructuring--object-over-array': 'Distinguishing multiple return values from an intentional array API requires review.', 'constructors--chaining': 'Method chaining is optional API design guidance.', @@ -487,6 +543,45 @@ const manual: Record = { // Clauses whose linked rules are active but do not enforce every stated requirement. const partial: Record> = { compatibility: { + 'objects--rest-spread': + 'prefer-object-spread reports `Object.assign` whose first argument is an object literal; mutating ' + + '`Object.assign(target, ...)` and omitting properties with `delete` instead of rest syntax remain ' + + 'reviewable.', + 'functions--default-side-effects': + 'ag/no-default-side-effects reports assignments, updates, and `delete` in default values; side ' + + 'effects of calls are not analyzed.', + 'functions--spread-vs-apply': + 'prefer-spread reports `fn.apply(receiver, args)`; constructor application through ' + + '`Function.prototype.bind.apply` remains reviewable.', + 'naming--camelCase': + 'camelcase reports underscores; the capitalization of functions and instances remains reviewable.', + 'es6-template-literals': + 'prefer-template reports concatenation; building strings with `[...].join()` is reported only by the ' + + 'guideline policy.', + 'es6-rest': + 'prefer-rest-params allows property access such as `arguments.length`; the guideline policy reports ' + + 'it.', + 'functions--mutate-params': + 'Inherited no-param-reassign exempts parameters such as `acc`, `e`, and `res` and cannot see ' + + 'mutation through method calls; the guideline policy removes the exemptions.', + 'constructors--use-class': + 'ag/no-prototype-mutation reports assignments to prototypes; the guideline policy also reports ' + + '`Object.assign` or `Object.defineProperty` on a prototype. Plain constructor functions remain ' + + 'reviewable.', + 'constructors--extends': + 'ag/no-prototype-mutation reports prototype assignments; the guideline policy also reports ' + + '`Object.setPrototypeOf` and `inherits`. Other manual inheritance remains reviewable.', + 'comparison--eqeqeq': + 'Inherited eqeqeq allows `== null` and `!= null`; the guideline policy does not.', + 'comments--spaces': + 'spaced-comment checks the comment opener; text directly after the `*` of a JSDoc line is reported ' + + 'only by the guideline policy.', + 'coercion--strings': + 'no-new-wrappers rejects `new String()`; Airbnb disables no-implicit-coercion, which the guideline ' + + 'policy enables for `\'\' + value`.', + 'coercion--numbers': + 'radix and no-new-wrappers are enforced; Airbnb disables no-implicit-coercion, which the guideline ' + + 'policy enables for `+value`.', 'destructuring--array': 'Inherited prefer-destructuring checks assignments but not declarations such as `const first = arr[0]`; ' + 'the guideline policy checks both.', @@ -506,6 +601,42 @@ const partial: Record> = { 'Inherited newline-per-chained-call allows four calls on one line; the guideline policy allows two.', }, guideline: { + 'objects--rest-spread': + 'prefer-object-spread reports `Object.assign` whose first argument is an object literal; mutating ' + + '`Object.assign(target, ...)` and omitting properties with `delete` instead of rest syntax remain ' + + 'reviewable.', + 'functions--default-side-effects': + 'ag/no-default-side-effects reports assignments, updates, and `delete` in default values; side ' + + 'effects of calls are not analyzed.', + 'functions--spread-vs-apply': + 'prefer-spread reports `fn.apply(receiver, args)`; constructor application through ' + + '`Function.prototype.bind.apply` remains reviewable.', + 'naming--camelCase': + 'camelcase reports underscores; the capitalization of functions and instances remains reviewable.', + 'functions--mutate-params': + 'no-param-reassign reports property writes to any parameter; mutation through method calls such as ' + + '`list.push()` remains reviewable.', + 'constructors--use-class': + 'ag/no-prototype-mutation reports prototype writes, including `Object.assign` and ' + + '`Object.defineProperty` on a prototype; plain constructor functions remain reviewable.', + 'constructors--extends': + 'ag/no-prototype-mutation reports prototype writes, `Object.setPrototypeOf`, and `inherits`; other ' + + 'manual inheritance remains reviewable.', + 'modules--use-them': + 'import/no-commonjs rejects `require` and `module.exports`, and typescript/no-require-imports ' + + 'rejects `import x = require()`; TypeScript `export =` remains reviewable.', + 'control-statements': + 'ag/multiline-condition-layout checks multiline conditions; whether a single-line condition is too ' + + 'long to read remains reviewable.', + 'comments--singleline': + 'line-comment-position and lines-around-comment place `//` comments; single-line `/* */` comments ' + + 'remain reviewable.', + 'whitespace--chains': + 'newline-per-chained-call breaks chains longer than two calls; the clause\'s own d3 example keeps ' + + '`.enter().append()` on one line, which the rule rejects.', + 'whitespace--max-len': + 'max-len exempts strings and template literals, but it exempts the whole line that contains one, so ' + + 'long code on such a line is not reported.', 'whitespace--after-blocks': 'padding-line-between-statements requires a blank line after block-like statements; blank lines between ' + 'multiline members of object and array literals remain reviewable.', @@ -523,6 +654,21 @@ const reasons: Record> = { 'modules--import-order': 'Inherited import/order options do not alphabetize; the guideline policy orders imports by path.', 'comments-jsdoc': 'The recommended eslint-plugin-jsdoc configuration plus jsdoc/require-file-overview.', + 'strings--line-length': + 'The sample max-len has no ignoreStrings, so the clause\'s good example (a long single-line string) is ' + + 'rejected while strings broken with concatenation pass; backslash continuations are rejected by ' + + 'no-multi-str. The guideline policy enforces the clause.', + 'whitespace--max-len': + 'The sample max-len does not exempt long strings as the clause notes; the guideline policy does.', + 'whitespace--block-spacing': + 'block-spacing requires spaces inside single-line blocks; the sample brace-style (allowSingleLine: false, ' + + 'clause 15.1) rejects single-line blocks, including this clause\'s good example.', + 'commas--dangling': + 'comma-dangle requires trailing commas in multiline lists; like upstream, it also requires one after a ' + + 'trailing spread argument, which the clause\'s last example omits.', + 'coercion--comment-deviations': + 'no-bitwise reports every bitwise operation; a justified deviation is a directive that names the ' + + 'implementation, e.g. `// oxlint-disable-next-line ag-compat/no-bitwise -- reason`.', 'typescript--enum-naming-conventions': 'Enum and member casing is enforced. Singular English names require review; values are unrestricted.', 'typescript--caught-error-type': @@ -530,9 +676,11 @@ const reasons: Record> = { + 'strict compiler settings.', }, guideline: { + 'modules--no-duplicate-imports': + 'import/no-duplicates with prefer-inline also merges type-only and value imports of one path.', 'modules--import-grouping': 'import/order requires blank lines between groups. `importGroups: example` follows the clause example ' - + '(built-in and package imports share a block); `prose` separates every listed category.', + + '(built-in imports first, then packages, in one block); `prose` separates every listed category.', 'modules--import-order': 'import/order alphabetizes imports by path within each group.', 'modules--prefer-named-export': 'import/prefer-default-export is disabled and import/no-default-export rejects default exports.', @@ -573,6 +721,7 @@ const known = new Set([ ...Object.keys(guideAdditions), ...Object.keys(guideTypeScriptAdditions), ...Object.keys(guidelineAdditions), + ...Object.keys(guidelineTypeScriptAdditions), ]); clauses.forEach((clause) => [...clause.rules, ...clause.guideline.rules].forEach((rule) => { if (!known.has(rule)) { @@ -717,6 +866,9 @@ Object.entries(guideTypeScriptAdditions).forEach(([name, setting]) => { Object.entries(guidelineAdditions).forEach(([name, setting]) => { mapRule(name, setting, 'Javascript.md', 'guideline'); }); +Object.entries(guidelineTypeScriptAdditions).forEach(([name, setting]) => { + mapRule(name, setting, 'Javascript.md', 'guideline', 'typescript'); +}); Object.entries(projectAdditions).forEach(([name, setting]) => { mapRule(name, setting, 'adguard-projects', 'adguard-projects'); }); @@ -769,7 +921,8 @@ function classify(clause: Clause, disposition: Disposition, policy: Policy): Dis } // The comment-deviation clause is satisfied by a justified directive; the others lose to sample options. const forced = clause.id === 'coercion--comment-deviations' || (policy === 'compatibility' - && ['modules--prefer-named-export', 'strings--line-length', 'modules--import-order'].includes(clause.id)); + && ['modules--prefer-named-export', 'strings--line-length', 'modules--import-order', 'whitespace--max-len'] + .includes(clause.id)); if (forced || (enabled.length < rules.length && !override)) { return { rules, @@ -827,9 +980,8 @@ const report = [ '| --- | --- | --- | --- | --- |', ...clauses.map((clause) => { const sameRules = JSON.stringify(clause.rules) === JSON.stringify(clause.guideline.rules); - const rules = sameRules - ? ruleList(clause.rules) - : `${ruleList(clause.rules)}; guideline: ${ruleList(clause.guideline.rules)}`; + const guidelineRules = `guideline: ${ruleList(clause.guideline.rules)}`; + const rules = sameRules ? ruleList(clause.rules) : [ruleList(clause.rules), guidelineRules].filter(Boolean).join('; '); const interpretation = clause.reason === clause.guideline.reason ? clause.reason : `${clause.reason} Guideline: ${clause.guideline.reason}`; diff --git a/tests/clause-cases.ts b/tests/clause-cases.ts index f1d2503..c64e503 100644 --- a/tests/clause-cases.ts +++ b/tests/clause-cases.ts @@ -1078,4 +1078,275 @@ export const clauseCases: ClauseCase[] = [ good: 'try {\n report(1);\n} catch (error: unknown) {\n report(error);\n}', rules: ['ag/unknown-catch'], }, + // Gaps and additions found by the clause-by-clause audit. + { + clause: 'objects--rest-spread', + policy: 'both', + bad: 'const original = { first: 1 };\nconst copy = Object.assign(original, { second: 2 });\ndelete copy.first;\nreport(copy);', + rules: ['ag-compat/prefer-object-spread'], + expectMiss: true, + }, + { + clause: 'arrays--from-array-like', + policy: 'guideline', + bad: 'report(Array.prototype.slice.call(source));', + good: 'report(Array.from(source));', + rules: ['ag/prefer-array-from'], + }, + { + clause: 'es6-template-literals', + policy: 'compatibility', + bad: "report(['How are you, ', userName, '?'].join());", + rules: ['ag-compat/prefer-template', 'ag/prefer-template-over-join'], + expectMiss: true, + }, + { + clause: 'es6-template-literals', + policy: 'guideline', + bad: "report(['How are you, ', userName, '?'].join());", + rules: ['ag/prefer-template-over-join'], + }, + { + clause: 'es6-rest', + policy: 'compatibility', + bad: 'report(function count() {\n return arguments.length;\n});', + rules: ['ag-compat/prefer-rest-params', 'ag/no-arguments'], + expectMiss: true, + }, + { + clause: 'es6-rest', + policy: 'guideline', + bad: 'report(function count() {\n return arguments.length;\n});', + good: 'report((...args) => args.length);', + rules: ['ag/no-arguments'], + }, + { + clause: 'functions--default-side-effects', + policy: 'both', + bad: 'report((next = items.push(1)) => next);', + rules: ['ag/no-default-side-effects'], + expectMiss: true, + }, + { + clause: 'functions--default-side-effects', + policy: 'both', + bad: 'report((next = delete source.key) => next);', + rules: ['ag/no-default-side-effects'], + }, + { + clause: 'functions--mutate-params', + policy: 'compatibility', + bad: 'report((acc) => {\n acc.key = 1;\n return acc;\n});', + rules: ['ag-compat/no-param-reassign'], + expectMiss: true, + }, + { + clause: 'functions--mutate-params', + policy: 'guideline', + bad: 'report((acc) => {\n acc.key = 1;\n return acc;\n});', + rules: ['ag-compat/no-param-reassign'], + }, + { + clause: 'functions--mutate-params', + policy: 'guideline', + bad: 'report((list) => {\n list.push(1);\n return list;\n});', + rules: ['ag-compat/no-param-reassign'], + expectMiss: true, + }, + { + clause: 'functions--spread-vs-apply', + policy: 'both', + bad: 'report(new (Function.prototype.bind.apply(Date, [null, 2016, 8, 5]))());', + rules: ['ag-compat/prefer-spread'], + expectMiss: true, + }, + { + clause: 'constructors--use-class', + policy: 'compatibility', + bad: 'function Queue() {}\nObject.assign(Queue.prototype, { pop() {\n return this.last;\n} });\nreport(Queue);', + rules: ['ag/no-prototype-mutation'], + expectMiss: true, + }, + { + clause: 'constructors--use-class', + policy: 'guideline', + bad: 'function Queue() {}\nObject.assign(Queue.prototype, { pop() {\n return this.last;\n} });\nreport(Queue);', + rules: ['ag/no-prototype-mutation'], + }, + { + clause: 'constructors--use-class', + policy: 'guideline', + bad: 'function Queue(contents = []) {\n this.queue = [...contents];\n}\nreport(new Queue());', + rules: ['ag/no-prototype-mutation'], + expectMiss: true, + }, + { + clause: 'constructors--extends', + policy: 'compatibility', + bad: 'function Queue() {}\nfunction PeekableQueue() {}\nObject.setPrototypeOf(PeekableQueue, Queue);\nreport(PeekableQueue);', + rules: ['ag/no-prototype-mutation'], + expectMiss: true, + }, + { + clause: 'constructors--extends', + policy: 'guideline', + bad: 'function Queue() {}\nfunction PeekableQueue() {}\nObject.setPrototypeOf(PeekableQueue, Queue);\nreport(PeekableQueue);', + rules: ['ag/no-prototype-mutation'], + }, + { + clause: 'constructors--extends', + policy: 'guideline', + bad: 'function Queue() {}\nfunction PeekableQueue(...contents) {\n Queue.apply(this, contents);\n}\nreport(PeekableQueue);', + rules: ['ag/no-prototype-mutation'], + expectMiss: true, + }, + { + clause: 'modules--use-them', + policy: 'guideline', + extension: 'ts', + bad: "import fs = require('node:fs');\n\nreport(fs);", + rules: ['typescript/no-require-imports', 'import/no-commonjs'], + }, + { + clause: 'modules--use-them', + policy: 'guideline', + extension: 'ts', + bad: 'const value = 1;\n\nexport = value;', + rules: ['typescript/no-require-imports', 'import/no-commonjs', 'ag-import/no-amd'], + expectMiss: true, + }, + { + clause: 'modules--no-duplicate-imports', + policy: 'guideline', + extension: 'ts', + bad: "import type { Shape } from './shapes';\nimport { area } from './shapes';\n\nreport(area, null as Shape | null);", + good: "import { area, type Shape } from './shapes';\n\nreport(area, null as Shape | null);", + rules: ['ag-import/no-duplicates'], + }, + { + clause: 'modules--import-grouping', + policy: 'guideline', + importGroups: 'example', + directory: 'src', + bad: "import axios from 'axios';\nimport fs from 'node:fs';\n\nreport(axios, fs);", + good: "import fs from 'node:fs';\nimport axios from 'axios';\n\nimport bar from '../bar';\n\nreport(fs, axios, bar);", + rules: ['ag-import/order'], + }, + { + clause: 'comparison--eqeqeq', + policy: 'compatibility', + bad: 'report(source == null);', + rules: ['ag-compat/eqeqeq'], + expectMiss: true, + }, + { + clause: 'comparison--eqeqeq', + policy: 'guideline', + bad: 'report(source == null);', + good: 'report(source === null || source === undefined);', + rules: ['ag-compat/eqeqeq'], + }, + { + clause: 'comparison--switch-blocks', + policy: 'guideline', + good: 'switch (source) {\n case 1: {\n report(1);\n break;\n }\n case 2: {\n report(2);\n break;\n }\n' + + ' default:\n break;\n}', + rules: ['ag-compat/padding-line-between-statements'], + }, + { + clause: 'control-statements', + policy: 'guideline', + bad: 'if ((first === 123 || second === 1) && check(third) && convert(flag)) {\n report(1);\n}', + rules: ['ag/multiline-condition-layout'], + expectMiss: true, + }, + { + clause: 'comments--singleline', + policy: 'guideline', + bad: 'report(1);\n/* is current tab */\nreport(2);', + rules: ['ag-compat/line-comment-position', 'ag-compat/lines-around-comment'], + expectMiss: true, + }, + { + clause: 'comments--spaces', + policy: 'compatibility', + bad: '/**\n *make() returns a new element\n */\nreport(1);', + rules: ['ag-compat/spaced-comment', 'ag/docblock-spacing'], + expectMiss: true, + }, + { + clause: 'comments--spaces', + policy: 'guideline', + bad: '/**\n *make() returns a new element\n */\nreport(1);', + good: '/**\n * make() returns a new element\n */\nreport(1);', + rules: ['ag/docblock-spacing'], + }, + { + clause: 'comments--actionitems', + policy: 'guideline', + good: '// author name is not required\n// for future changes\n// TODO(author): blablabla\nreport(1);', + rules: ['ag/require-docblock'], + }, + { + clause: 'whitespace--chains', + policy: 'guideline', + good: "report($('#items')\n .find('.selected')\n .enter().append('svg'));", + rules: ['ag-compat/newline-per-chained-call'], + expectConflict: true, + }, + { + clause: 'whitespace--max-len', + policy: 'compatibility', + good: `report('${LONG}');`, + rules: ['ag-compat/max-len'], + expectConflict: true, + }, + { + clause: 'whitespace--max-len', + policy: 'guideline', + bad: `report(first, second, third, '', ${Array.from({ length: 12 }, () => 'first + second').join(', ')});`, + rules: ['ag-compat/max-len'], + expectMiss: true, + }, + { + clause: 'coercion--strings', + policy: 'compatibility', + bad: "report(source + '');", + rules: ['ag-compat/no-implicit-coercion'], + expectMiss: true, + }, + { + clause: 'coercion--strings', + policy: 'guideline', + bad: "report('' + source);", + good: 'report(String(source));', + rules: ['ag-compat/no-implicit-coercion'], + }, + { + clause: 'coercion--numbers', + policy: 'compatibility', + bad: 'report(+source);', + rules: ['ag-compat/no-implicit-coercion'], + expectMiss: true, + }, + { + clause: 'coercion--numbers', + policy: 'guideline', + bad: 'report(+source);', + good: 'report(Number(source));', + rules: ['ag-compat/no-implicit-coercion'], + }, + { + clause: 'naming--camelCase', + policy: 'both', + bad: 'const MyInstance = compute();\nreport(MyInstance);', + rules: ['ag-compat/camelcase'], + expectMiss: true, + }, + { + clause: 'functions--iife', + policy: 'guideline', + good: '(function () {\n report(1);\n}());', + rules: ['ag-compat/func-names'], + }, ]; diff --git a/tests/clauses.test.ts b/tests/clauses.test.ts index fa6061b..07ca0b7 100644 --- a/tests/clauses.test.ts +++ b/tests/clauses.test.ts @@ -27,6 +27,7 @@ const SHARED_FILES = { 'helpers.js': 'export const first = 1;\nexport const second = 2;\nexport const third = 3;\nexport const fourth = 4;\n', 'bar.js': 'export default 1;\n', 'src/foo.js': 'export default 1;\n', + 'shapes.ts': 'export interface Shape {\n area: number;\n}\n\nexport const area = 1;\n', }; interface Run { diff --git a/tests/compiler.test.ts b/tests/compiler.test.ts index a3c92fa..6cffe09 100644 --- a/tests/compiler.test.ts +++ b/tests/compiler.test.ts @@ -95,6 +95,34 @@ describe('tsconfig requirements', () => { expect((await checkTsconfig(join(directory, 'tsconfig.json'))).problems).toEqual([]); }); + it('resolves bare package names like TypeScript and never through main', async () => { + await write('node_modules/strictest/package.json', { name: 'strictest', main: 'index.js' }); + await write('node_modules/strictest/index.js', 'module.exports = {};'); + await write('node_modules/strictest/tsconfig.json', { compilerOptions: { strict: true, noUncheckedIndexedAccess: true } }); + await write('node_modules/fielded/package.json', { name: 'fielded', tsconfig: 'configs/base.json' }); + await write('node_modules/fielded/configs/base.json', { compilerOptions: { strict: true, noUncheckedIndexedAccess: true } }); + await write('first.json', { extends: 'strictest' }); + await write('second.json', { extends: 'fielded' }); + expect((await checkTsconfig(join(directory, 'first.json'))).problems).toEqual([]); + expect((await checkTsconfig(join(directory, 'second.json'))).files.at(-1)).toContain('configs'); + }); + + it('uses an existing relative file as written and reports unresolved packages', async () => { + await write('base.jsonc', { compilerOptions: { strict: true, noUncheckedIndexedAccess: true } }); + await write('tsconfig.json', { extends: './base.jsonc' }); + expect((await checkTsconfig(join(directory, 'tsconfig.json'))).problems).toEqual([]); + await write('missing.json', { extends: 'no-such-config' }); + await expect(checkTsconfig(join(directory, 'missing.json'))).rejects.toThrow(/Cannot resolve tsconfig extends/u); + }); + + it('reports strict-family options disabled next to strict', async () => { + await write('tsconfig.json', { + compilerOptions: { strict: true, noUncheckedIndexedAccess: true, strictNullChecks: false }, + }); + expect((await checkTsconfig(join(directory, 'tsconfig.json'))).problems) + .toEqual(['26.1: do not disable "strictNullChecks", which "strict" enables']); + }); + it('lets a project disable an inherited requirement and reports it', async () => { await write('base.json', { compilerOptions: { strict: true, noUncheckedIndexedAccess: true } }); await write('tsconfig.json', { extends: './base.json', compilerOptions: { strict: false } }); diff --git a/tests/custom-cases.ts b/tests/custom-cases.ts index 0889871..3719ae1 100644 --- a/tests/custom-cases.ts +++ b/tests/custom-cases.ts @@ -177,6 +177,10 @@ const enumNames = [ ['$Color', false], ['URL', false], ['C0L0R', false], + ['V1', true], + ['H2', true], + ['IO', false], + ['MV3', false], ['c', false], ['C_o_l_o_r', false], ['color_mode', false], @@ -215,6 +219,9 @@ const catchForms = [ ['catch ({ message }: any) {}', 1], ['catch ([first]: any) {}', 1], ['catch ({ message }: unknown) {}', 0], + ['catch (error: any | unknown) {}', 1], + ['catch (error: unknown | any) {}', 1], + ['catch (error: unknown | Error) {}', 0], ] as const; for (const [form, errors] of catchForms) { for (const [context, wrap] of [ @@ -428,3 +435,101 @@ for (const [name, code, errors] of [ add('require-docblock', `line runs: ${name}`, code, errors, 'jsx', [runs]); } add('require-docblock', 'line runs disabled explicitly', '// alpha\n// beta\nwork();', 0, 'jsx', [{ lineCommentRuns: false }]); + +for (const [code, errors] of [ + ['function count(first = delete source.key) { return first; }', 1], + ['function count(first = typeof source) { return first; }', 0], +] as const) { + add('no-default-side-effects', `delete: ${code}`, code, errors); +} + +const prototypeCalls = { calls: true }; +for (const [code, errors] of [ + ['Object.assign(Queue.prototype, { pop() {} });', 1], + ["Object.defineProperty(Queue.prototype, 'size', { value: 1 });", 1], + ['Object.defineProperties(Queue.prototype, {});', 1], + ["Reflect.defineProperty(Queue.prototype, 'size', { value: 1 });", 1], + ['Object.setPrototypeOf(PeekableQueue, Queue);', 1], + ['Reflect.setPrototypeOf(PeekableQueue, Queue);', 1], + ['inherits(PeekableQueue, Queue);', 1], + ['util.inherits(PeekableQueue, Queue);', 1], + ['Object.assign(target, { pop() {} });', 0], + ["Object.defineProperty(target, 'size', { value: 1 });", 0], + ['Object.assign();', 0], + ['queue[method](Queue.prototype);', 0], +] as const) { + add('no-prototype-mutation', `calls: ${code}`, code, errors, 'jsx', [prototypeCalls]); +} +add('no-prototype-mutation', 'calls disabled by default', 'Object.setPrototypeOf(Child, Parent);', 0); + +for (const [code, errors] of [ + ['const list = Array.prototype.slice.call(arrayLike);', 1], + ['const list = [].slice.call(arrayLike);', 1], + ['const list = Array.prototype.slice.apply(arrayLike);', 1], + ["const list = Array.prototype['slice'].call(arrayLike);", 1], + ['const list = Array.prototype.slice.call(arrayLike, 1);', 0], + ['const list = Array.from(arrayLike);', 0], + ['const list = items.slice.call(arrayLike);', 0], + ['const list = [1].slice.call(arrayLike);', 0], + ['const list = Array.prototype.map.call(arrayLike, convert);', 0], + ['const list = factory().slice.call(arrayLike);', 0], +] as const) { + add('prefer-array-from', code, code, errors); +} + +for (const [code, errors] of [ + ["const text = ['How are you, ', name, '?'].join();", 1], + ["const text = ['How are you, ', name, '?'].join('');", 1], + ["const text = [greeting, '!'].join('');", 1], + ["const text = ['How are you, ', name, '?'].join(' ');", 0], + ["const text = ['a', 'b'].join('');", 0], + ['const text = [first, second].join();', 0], + ["const text = ['a', ...rest].join('');", 0], + ["const text = ['a', , first].join('');", 0], + ["const text = parts.join('');", 0], + ["const text = ['a', first].concat();", 0], + ["const text = ['a', first].join('', extra);", 0], +] as const) { + add('prefer-template-over-join', code, code, errors); +} + +for (const [code, errors] of [ + ['function count() { return arguments.length; }', 1], + ['const count = function () { return arguments.callee; };', 1], + ['function outer() { return () => arguments.length; }', 1], + ['const count = (...args) => args.length;', 0], + ['function first() { return arguments[0]; }', 0], + ['const size = arguments.length;', 0], + ['function run(options) { return options.arguments.length; }', 0], +] as const) { + add('no-arguments', code, code, errors); +} + +for (const [code, errors] of [ + ['/**\n *make() returns a new element\n */', 1], + ['/**\n * make() returns a new element\n *based on the tag\n */', 1], + ['/**\n * make() returns a new element\n */', 0], + ['/**\n *\n * Blank line above.\n */', 0], + ['/**\n **** banner ****\n */', 0], + ['/** single line */', 0], + ['/*\n *not a doc comment\n */', 0], + ['// line comment', 0], +] as const) { + add('docblock-spacing', JSON.stringify(code), code, errors); +} + +for (const [name, code, errors] of [ + ['action items', '// TODO(author): first\n// continues\nwork();', 0], + ['fix item later in run', '// Explains the next line.\n// FIXME: broken\nwork();', 0], + ['preserved banner', '/*!\n * Copyright\n */\nwork();', 0], + ['block directive', '/* eslint-disable\n no-console,\n no-alert\n*/\nwork();', 0], +] as const) { + add('require-docblock', `line runs: ${name}`, code, errors, 'jsx', [runs]); +} + +for (const [name, code, errors] of [ + ['operands on one line', 'if (first && check(\n second,\n)) {\n work();\n}', 0], + ['nested operands on separate lines', 'if (first && (second\n || third)) {\n work();\n}', 1], +] as const) { + add('multiline-condition-layout', name, code, errors); +} diff --git a/tests/custom.test.ts b/tests/custom.test.ts index 7f33961..c06c3be 100644 --- a/tests/custom.test.ts +++ b/tests/custom.test.ts @@ -36,9 +36,10 @@ describe('custom plugin contract', () => { expect(plugin.meta?.name).toBe('ag'); expect(Object.keys(rules)).toEqual(Object.keys(rules)); Object.entries(rules).forEach(([name, rule]) => { - // Only the multiline comment rule has an option: the guideline policy's line-comment runs. - expect(rule.meta?.schema).toEqual(name === 'require-docblock' - ? [{ type: 'object', properties: { lineCommentRuns: { type: 'boolean' } }, additionalProperties: false }] + // Options only switch on the guideline policy's stricter checks. + const options: Record = { 'require-docblock': 'lineCommentRuns', 'no-prototype-mutation': 'calls' }; + expect(rule.meta?.schema).toEqual(options[name] + ? [{ type: 'object', properties: { [options[name]]: { type: 'boolean' } }, additionalProperties: false }] : []); expect(rule.meta?.docs?.url).toContain('CodeGuidelines/blob/master/JavaScript/Javascript.md#'); expect(rule.meta?.fixable).toBeUndefined(); diff --git a/tests/gap-cases.ts b/tests/gap-cases.ts index 1e96ee8..65caebd 100644 --- a/tests/gap-cases.ts +++ b/tests/gap-cases.ts @@ -396,4 +396,26 @@ export const gapCases: GapCase[] = [ valid: 'const query = 1;', invalid: 'const q = 1;', }, + { + rule: 'no-implicit-coercion', + scope: 'guideline', + valid: 'const value = Number(source);', + invalid: 'const value = +source;', + }, + { + rule: '@typescript-eslint/no-require-imports', + scope: 'guideline', + language: 'typescript', + extension: 'ts', + valid: 'import fs from "node:fs";\n\nexport { fs };', + invalid: 'import fs = require("node:fs");\n\nexport { fs };', + }, + { + rule: 'member-delimiter-style', + scope: 'adguard-projects', + language: 'typescript', + extension: 'ts', + valid: 'export interface Shape {\n area: number;\n}', + invalid: 'export interface Shape {\n area: number\n}', + }, ]; diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 92ed249..9c93dc9 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -134,7 +134,9 @@ describe('public configuration API', () => { expect(guideline.rules![rule as keyof typeof guideline.rules]).toBe('error'); }); expect(guideline.plugins).toContain('import'); - expect(guideline.overrides).toEqual([{ files: ['**/*.{cjs,cts}'], rules: { 'import/no-commonjs': 'off' } }]); + expect(guideline.overrides).toEqual([ + { files: ['**/*.{cjs,cts}', '**/.eslintrc.js'], rules: { 'import/no-commonjs': 'off' } }, + ]); const script = createConfig({ policy: 'guideline', sourceType: 'commonjs' }); expect(script.rules!['import/no-commonjs']).toBe('off'); expect(script.overrides).toEqual([]); @@ -149,7 +151,9 @@ describe('public configuration API', () => { expect(typescript.rules!['ag-compat/lines-around-comment']).toBe('off'); expect(typescript.rules!['ag-style/lines-around-comment']).toMatchObject(['error', { allowInterfaceStart: true }]); expect(typescript.rules!['ag-ts/no-redeclare']).toBe('error'); - expect(guideline.overrides![1]!.files).toEqual(['**/*.{cjs,cts}']); + expect(guideline.overrides![1]!.files).toEqual(['**/*.{cjs,cts}', '**/.eslintrc.js']); + expect(typescript.rules!['typescript/no-require-imports']).toBe('error'); + expect(typescript.rules!['eslint/class-methods-use-this']).toMatchObject(['error', { ignoreOverrideMethods: true }]); }); it('keeps AdGuard project conventions in an opt-in profile', () => { From 074fae9b6e9c447373fd15e75e711ce99b39eaa8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 07:21:32 +0000 Subject: [PATCH 4/4] fix(typescript): accept only the declaration merges typescript-eslint accepts ag-ts/no-redeclare suppressed reports for every declaration merge that TypeScript permits, including enum + enum, type alias + value, and interface + value. The TypeScript version of the rule, @typescript-eslint/no-redeclare (used by AdGuard projects through airbnb-typescript), accepts fewer merges with its default ignoreDeclarationMerge: interfaces, namespaces, one class with interfaces and namespaces, one function implementation with namespaces, and one enum with namespaces. It reports the rest, although TypeScript itself allows enum and type/value merges. Match that set so the TypeScript replacement enforces the same rule as its reference implementation. Overload signatures and ambient function declarations still merge with their implementation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MCeDV2VhbUtLyAP9XRG1zL --- docs/conformance.md | 2 +- docs/coverage.md | 2 +- packages/oxlint-plugin/README.md | 2 +- packages/oxlint-plugin/src/typescript.ts | 41 +++++++++++++----------- packages/rule-catalog/src/catalog.json | 2 +- scripts/generate.ts | 2 +- tests/fixtures/typescript/merging.ts | 9 +----- tests/typescript-rules.test.ts | 17 ++++++++-- 8 files changed, 43 insertions(+), 34 deletions(-) diff --git a/docs/conformance.md b/docs/conformance.md index b4cb9e1..711faf7 100644 --- a/docs/conformance.md +++ b/docs/conformance.md @@ -12,6 +12,6 @@ The handwritten adapters address standalone scope metadata and comment globals, Vitest coverage includes every handwritten TypeScript runtime file under `packages/*/src`, including files not otherwise imported. Thresholds are 95% for statements, lines, and functions and 90% for branches. Vendored source is assessed through conformance rather than included in that percentage. Repeated CLI executions used to check stability are not counted as distinct corpus scenarios. -The guide applies the JavaScript rules to TypeScript. For TypeScript files, the preset replaces pinned JavaScript implementations that report valid TypeScript or ignore TypeScript syntax with TypeScript-aware implementations of the same rules and options: Oxlint's native `no-unused-vars`, `no-shadow`, `no-useless-constructor`, `no-empty-function`, `no-dupe-class-members`, `no-array-constructor`, `default-param-last`, `no-use-before-define`, and `class-methods-use-this` (exempting `override` methods); Stylistic's `indent`, `brace-style`, `keyword-spacing`, `space-before-function-paren`, `comma-dangle` (extended to enums, type parameters, and tuples as airbnb-typescript does), `comma-spacing`, `key-spacing`, `lines-between-class-members`, `lines-around-comment`, `object-curly-spacing`, `semi`, `space-before-blocks`, and `space-infix-ops`; and `ag-ts/no-redeclare`, which runs the pinned rule but accepts overloads and the declaration merges that TypeScript permits. Import and React settings gain TypeScript extensions and development globs. The generator records each replacement and its reason in the catalog; `tests/typescript-preset.test.ts` requires idiomatic TypeScript to pass the complete preset under both policies and requires each replacement to report TypeScript-only violations. Replacements are limited to rules with an observed difference: TypeScript syntax the pinned rules neither misreport nor check, such as generic call spacing, remains outside the preset. The pinned `import/export` rule reports exported interface merges, as upstream does. Exact upstream JavaScript conformance continues to use the pinned implementations. +The guide applies the JavaScript rules to TypeScript. For TypeScript files, the preset replaces pinned JavaScript implementations that report valid TypeScript or ignore TypeScript syntax with TypeScript-aware implementations of the same rules and options: Oxlint's native `no-unused-vars`, `no-shadow`, `no-useless-constructor`, `no-empty-function`, `no-dupe-class-members`, `no-array-constructor`, `default-param-last`, `no-use-before-define`, and `class-methods-use-this` (exempting `override` methods); Stylistic's `indent`, `brace-style`, `keyword-spacing`, `space-before-function-paren`, `comma-dangle` (extended to enums, type parameters, and tuples as airbnb-typescript does), `comma-spacing`, `key-spacing`, `lines-between-class-members`, `lines-around-comment`, `object-curly-spacing`, `semi`, `space-before-blocks`, and `space-infix-ops`; and `ag-ts/no-redeclare`, which runs the pinned rule but accepts overloads and the declaration merges that `@typescript-eslint/no-redeclare` accepts by default (interfaces, namespaces, and one class, function, or enum with them). Import and React settings gain TypeScript extensions and development globs. The generator records each replacement and its reason in the catalog; `tests/typescript-preset.test.ts` requires idiomatic TypeScript to pass the complete preset under both policies and requires each replacement to report TypeScript-only violations. Replacements are limited to rules with an observed difference: TypeScript syntax the pinned rules neither misreport nor check, such as generic call spacing, remains outside the preset. The pinned `import/export` rule reports exported interface merges, as upstream does. Exact upstream JavaScript conformance continues to use the pinned implementations. The default presets remain syntax-only. `createConfig({ language: 'typescript', typeAware: true })` opts into mappings marked `requiresTypeInfo` using the optional `oxlint-tsgolint@7.0.2002` peer. Currently this adds `typescript/consistent-type-exports`, part of the `adguard-projects` profile, only in the TypeScript file override. A real multi-file project and a packed-package consumer test check both an invalid re-export and its type-only correction. This establishes execution of that rule, not complete Airbnb TypeScript parity, compatibility with a project's nested policies, or full guideline-prose compliance. Compiler checking, declaration emission, and manual review remain separate requirements. diff --git a/docs/coverage.md b/docs/coverage.md index 29dcaaa..b41b944 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -197,7 +197,7 @@ Scope `base` is always active, `guideline` is added by `policy: 'guideline'`, an | no-octal-escape | ag-compat/no-octal-escape | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | | no-param-reassign | ag-compat/no-param-reassign | javascript | `["error",{"props":true,"ignorePropertyModificationsFor":["acc","accumulator","e","ctx","context","req","request","res","response","$scope","staticContext"]}]` | airbnb@19.0.4/base@15.0.0 | base | | | | no-proto | ag-compat/no-proto | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | -| no-redeclare | ag-compat/no-redeclare | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | ag-ts/no-redeclare: Accepts overloads and declaration merges that TypeScript permits. | | +| no-redeclare | ag-compat/no-redeclare | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | ag-ts/no-redeclare: Accepts overloads and the declaration merges that @typescript-eslint/no-redeclare accepts. | | | no-restricted-properties | ag-compat/no-restricted-properties | javascript | `["error",{"object":"arguments","property":"callee","message":"arguments.callee is deprecated"},{"object":"global","property":"isFinite","message":"Please use Number.isFinite instead"},{"object":"self","property":"isFinite","message":"Please use Number.isFinite instead"},{"object":"window","property":"isFinite","message":"Please use Number.isFinite instead"},{"object":"global","property":"isNaN","message":"Please use Number.isNaN instead"},{"object":"self","property":"isNaN","message":"Please use Number.isNaN instead"},{"object":"window","property":"isNaN","message":"Please use Number.isNaN instead"},{"property":"__defineGetter__","message":"Please use Object.defineProperty instead."},{"property":"__defineSetter__","message":"Please use Object.defineProperty instead."},{"object":"Math","property":"pow","message":"Use the exponentiation operator (**) instead."}]` | airbnb@19.0.4/base@15.0.0 | base | | | | no-return-assign | ag-compat/no-return-assign | javascript | `["error","always"]` | airbnb@19.0.4/base@15.0.0 | base | | | | no-return-await | ag-compat/no-return-await | javascript | `"error"` | airbnb@19.0.4/base@15.0.0 | base | | | diff --git a/packages/oxlint-plugin/README.md b/packages/oxlint-plugin/README.md index fd5ba33..cf319e5 100644 --- a/packages/oxlint-plugin/README.md +++ b/packages/oxlint-plugin/README.md @@ -22,7 +22,7 @@ Use `@agcodeguidelines/oxlint-config` to materialize the full preset. The defaul `no-prototype-mutation` accepts `{ calls: true }` to also report prototype changes through `Object.assign`, `Object.defineProperty`, `Object.setPrototypeOf`, and `inherits`; the guideline policy enables it. -The `/typescript` export provides TypeScript-aware variants of pinned core rules: `no-redeclare` accepts overloads and the declaration merges that TypeScript permits. +The `/typescript` export provides TypeScript-aware variants of pinned core rules: `no-redeclare` accepts overloads and the declaration merges that `@typescript-eslint/no-redeclare` accepts. The `/compat`, `/jsdoc`, `/react`, `/jsx-a11y`, `/import`, and `/stylistic` exports provide isolated upstream rules through Oxlint's JavaScript plugin API. They are not an ESLint plugin distribution and do not depend on the ESLint engine. diff --git a/packages/oxlint-plugin/src/typescript.ts b/packages/oxlint-plugin/src/typescript.ts index 0254558..294274e 100644 --- a/packages/oxlint-plugin/src/typescript.ts +++ b/packages/oxlint-plugin/src/typescript.ts @@ -83,28 +83,33 @@ function containerOf(identifier: ESTree.Node): Container | null { } /** - * Decide whether declarations form a merge that TypeScript permits. + * Decide whether declarations form a merge that `@typescript-eslint/no-redeclare` accepts with its default + * `ignoreDeclarationMerge`: interfaces, namespaces, one class with interfaces and namespaces, one function + * implementation with namespaces, or one enum with namespaces. TypeScript permits more (for example enum or + * type and value merges), but the TypeScript version of the rule still reports them. * @param declarations - Every same-name declaration in one container. - * @returns Whether the declarations are an allowed TypeScript declaration merge. + * @returns Whether the declarations are an accepted declaration merge. */ export function isAllowedMerge(declarations: Declaration[]): boolean { - const count = (kind: DeclarationKind) => declarations.filter((declaration) => declaration.kind === kind).length; - if (declarations.length < 2 || count('other') > 0) { + if (declarations.length < 2) { return false; } - const values = new Set( - declarations - .map((declaration) => declaration.kind) - .filter((kind) => ['class', 'function', 'enum', 'variable'].includes(kind)), - ); - const typeAliases = count('type'); - return values.size <= 1 - && typeAliases <= 1 - && (typeAliases === 0 || count('interface') + count('class') + count('enum') === 0) - && count('class') <= 1 - && count('variable') <= 1 - && (count('variable') === 0 || count('namespace') === 0) - && declarations.filter((declaration) => declaration.kind === 'function' && declaration.implementation).length <= 1; + // Overload signatures and ambient declarations belong to their implementation. + const kinds = declarations + .filter((declaration) => declaration.kind !== 'function' || declaration.implementation) + .map((declaration) => declaration.kind); + const count = (kind: DeclarationKind) => kinds.filter((entry) => entry === kind).length; + const only = (...allowed: DeclarationKind[]) => kinds.every((kind) => allowed.includes(kind)); + if (kinds.length < 2 || only('interface') || only('namespace')) { + return true; + } + if (only('class', 'interface', 'namespace')) { + return count('class') <= 1; + } + if (only('function', 'namespace')) { + return count('function') <= 1; + } + return only('enum', 'namespace') && count('enum') === 1; } const noRedeclare = builtinRules.get('no-redeclare')!; @@ -124,7 +129,7 @@ export const rules = { .flatMap(declarationsOf) .filter(([name]) => name === (node as ESTree.IdentifierReference).name) .map(([, declaration]) => declaration) ?? []; - // Overloads and permitted interface, namespace, class, function, or enum merges + // Overloads and accepted interface, namespace, class, function, or enum merges // declare one TypeScript symbol. if (!isAllowedMerge(declarations)) { context.report(descriptor); diff --git a/packages/rule-catalog/src/catalog.json b/packages/rule-catalog/src/catalog.json index 3ba35db..662bf71 100644 --- a/packages/rule-catalog/src/catalog.json +++ b/packages/rule-catalog/src/catalog.json @@ -2628,7 +2628,7 @@ "target": "ag-ts/no-redeclare", "setting": "error", "implementation": "javascript", - "reason": "Accepts overloads and declaration merges that TypeScript permits." + "reason": "Accepts overloads and the declaration merges that @typescript-eslint/no-redeclare accepts." } }, { diff --git a/scripts/generate.ts b/scripts/generate.ts index 711a0f6..9123896 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -344,7 +344,7 @@ const TYPESCRIPT_EQUIVALENTS: Record = { 'brace-style': { target: 'ag-style/brace-style', reason: 'Checks namespace bodies.' }, 'no-redeclare': { target: 'ag-ts/no-redeclare', - reason: 'Accepts overloads and declaration merges that TypeScript permits.', + reason: 'Accepts overloads and the declaration merges that @typescript-eslint/no-redeclare accepts.', }, 'no-array-constructor': { target: 'eslint/no-array-constructor', diff --git a/tests/fixtures/typescript/merging.ts b/tests/fixtures/typescript/merging.ts index 876e054..425fc6c 100644 --- a/tests/fixtures/typescript/merging.ts +++ b/tests/fixtures/typescript/merging.ts @@ -1,5 +1,5 @@ /** - * @file Declaration merges that TypeScript permits. + * @file Declaration merges that `@typescript-eslint/no-redeclare` accepts. */ interface Size { @@ -38,10 +38,3 @@ export enum Direction { export namespace Direction { export const DEFAULT = Direction.Up; } - -export type Mode = 'light' | 'dark'; - -export const Mode = { - Light: 'light', - Dark: 'dark', -} as const; diff --git a/tests/typescript-rules.test.ts b/tests/typescript-rules.test.ts index 9797488..112f201 100644 --- a/tests/typescript-rules.test.ts +++ b/tests/typescript-rules.test.ts @@ -20,10 +20,9 @@ const valid = [ ['class and namespace', 'class Box {}\nnamespace Box { export const SIZE = 1; }'], ['function and namespace', 'function build(): number { return 1; }\nnamespace build { export const SIZE = 1; }'], ['enum and namespace', 'enum Color { Red }\nnamespace Color { export const DEFAULT = 1; }'], - ['enum merge', 'enum Color { Red }\nenum Color { Green = 1 }'], ['namespace merge', 'namespace Space { export const A = 1; }\nnamespace Space { export const B = 2; }'], - ['type and value', 'type Mode = "light" | "dark";\nconst Mode = { Light: "light" };'], - ['interface and value', 'interface Mode { name: string }\nconst Mode = { name: "light" };'], + ['interface and namespace', 'interface Box { width: number }\nnamespace Box { export const SIZE = 1; }'], + ['overloads with namespace', 'function build(): number;\nfunction build(): number { return 1; }\nnamespace build { export const SIZE = 1; }'], ['nested block merge', 'function run() {\n interface Box { width: number }\n interface Box { height: number }\n}'], ['namespace body merge', 'namespace Outer {\n export interface Box { width: number }\n export interface Box { height: number }\n}'], ['distinct names', 'let first = 1;\nlet second = 2;'], @@ -33,6 +32,10 @@ const invalid = [ ['var redeclaration', 'var value = 1;\nvar value = 2;', 1], ['two implementations', 'function run() {}\nfunction run() {}', 1], ['class and function', 'class Box {}\nfunction Box() {}', 1], + // TypeScript merges these, but @typescript-eslint/no-redeclare reports them. + ['enum merge', 'enum Color { Red }\nenum Color { Green = 1 }', 1], + ['type and value', 'type Mode = "light" | "dark";\nconst Mode = { Light: "light" };', 1], + ['interface and value', 'interface Mode { name: string }\nconst Mode = { name: "light" };', 1], ['import and interface', 'import { Box } from "./box";\ninterface Box { width: number }', 1], ['hoisted var in block', 'var value = 1;\nif (value) {\n var value = 2;\n}', 1], ['switch case', 'switch (mode) {\n case 1:\n var value = 1;\n var value = 2;\n}', 1], @@ -79,6 +82,14 @@ describe('TypeScript rule plugin contract', () => { expect(isAllowedMerge([declaration('function', true), declaration('function', true)])).toBe(false); expect(isAllowedMerge([declaration('class'), declaration('enum')])).toBe(false); expect(isAllowedMerge([declaration('function'), declaration('function', true)])).toBe(true); + expect(isAllowedMerge([declaration('function'), declaration('function')])).toBe(true); + expect(isAllowedMerge([declaration('enum'), declaration('enum')])).toBe(false); + expect(isAllowedMerge([declaration('enum'), declaration('enum'), declaration('namespace')])).toBe(false); + expect(isAllowedMerge([declaration('type'), declaration('variable')])).toBe(false); + expect(isAllowedMerge([declaration('interface'), declaration('variable')])).toBe(false); + expect(isAllowedMerge([declaration('class'), declaration('class'), declaration('interface')])).toBe(false); + expect(isAllowedMerge([declaration('function', true), declaration('function', true), declaration('namespace')])) + .toBe(false); }); it('requires at least two declarations to describe a merge', () => {