From fd81a540af9ba385b7436fcc33df34feb482a143 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:07:06 -0600 Subject: [PATCH 1/7] feat: add baseline-target rule --- src/main.ts | 4 ++- src/rules/baseline-target.test.ts | 23 +++++++++++++ src/rules/baseline-target.ts | 54 +++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/rules/baseline-target.test.ts create mode 100644 src/rules/baseline-target.ts diff --git a/src/main.ts b/src/main.ts index b5fac37..d5e8717 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,6 +33,7 @@ import {preferGetOrInsert} from './rules/prefer-get-or-insert.js'; import {preferCharCodeAtInLoop} from './rules/prefer-charcode-at-in-loop.js'; import {preferSliceOverSplitIndex} from './rules/prefer-slice-over-split-index.js'; import {preferThrowIfNoEntry} from './rules/prefer-throw-if-no-entry.js'; +import { baselineTarget } from './rules/baseline-target.js'; const plugin: ESLint.Plugin = { meta: { @@ -71,7 +72,8 @@ const plugin: ESLint.Plugin = { preferCharCodeAtInLoop as never as Rule.RuleModule, 'prefer-slice-over-split-index': preferSliceOverSplitIndex, 'prefer-throw-if-no-entry': preferThrowIfNoEntry, - 'ban-dependencies': banDependencies + 'ban-dependencies': banDependencies, + 'baseline-target': baselineTarget as never as Rule.RuleModule } }; diff --git a/src/rules/baseline-target.test.ts b/src/rules/baseline-target.test.ts new file mode 100644 index 0000000..26711a0 --- /dev/null +++ b/src/rules/baseline-target.test.ts @@ -0,0 +1,23 @@ +import {RuleTester} from 'eslint'; +import {baselineTarget} from './baseline-target.js'; + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module' + } +}); + +ruleTester.run('baseline-target', baselineTarget as never, { + valid: ['const x = a ?? b;', 'x = 1;'], + invalid: [ + { + code: 'new URLPattern();', + errors: [{messageId: 'exceedsYear', data: {year: 2025, floor: 2023}}] + }, + { + code: 'Temporal.Now;', + errors: [{messageId: 'notBaseline', data: {feature: 'temporal'}}] + } + ] +}); \ No newline at end of file diff --git a/src/rules/baseline-target.ts b/src/rules/baseline-target.ts new file mode 100644 index 0000000..b0597a1 --- /dev/null +++ b/src/rules/baseline-target.ts @@ -0,0 +1,54 @@ +import type {TSESLint} from '@typescript-eslint/utils'; +import { + detectBaselineTargetForFeatures, + detectBaselineYearForFeatures, + detectFeaturesForSource +} from 'baseline-detector'; + +type MessageIds = 'notBaseline' | 'exceedsYear'; + +const BASELINE_YEAR = 2023; + +export const baselineTarget: TSESLint.RuleModule = { + meta: { + type: 'problem', + docs: { + description: `Disallow features newer than Baseline ${BASELINE_YEAR}` + }, + schema: [], + messages: { + notBaseline: '`{{feature}}` is not Baseline yet', + exceedsYear: 'This file needs Baseline {{year}}, above the {{floor}} floor' + } + }, + defaultOptions: [], + create(context) { + return { + Program(node) { + const features = detectFeaturesForSource(context.sourceCode.text, { + fileName: context.filename + }); + const target = detectBaselineTargetForFeatures(features); + const year = detectBaselineYearForFeatures(features); + // the order of the checks matters, a feature that is not baseline makes year come back null, and so does a file + // with no features at all. + if(target.status === false){ + context.report({ + node, + messageId: 'notBaseline', + data: { feature: target.reason } + }); + return; + } + + if (year !== null && year > BASELINE_YEAR) { + context.report({ + node, + messageId: 'exceedsYear', + data: { year, floor: BASELINE_YEAR } + }); + } + } + }; + } +}; From a5aa6fbdcba89f435dcee59ed850a670f27246b4 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:56:47 -0600 Subject: [PATCH 2/7] feat: pass typescriptContext to baseline-target --- src/rules/baseline-target.test.ts | 30 +++++++++++++++++++++++++++++- src/rules/baseline-target.ts | 25 ++++++++++++++++++++----- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/rules/baseline-target.test.ts b/src/rules/baseline-target.test.ts index 26711a0..f7b49f3 100644 --- a/src/rules/baseline-target.test.ts +++ b/src/rules/baseline-target.test.ts @@ -1,6 +1,24 @@ import {RuleTester} from 'eslint'; +import {RuleTester as TSRuleTester} from '@typescript-eslint/rule-tester'; import {baselineTarget} from './baseline-target.js'; +import * as path from 'node:path'; +import {fileURLToPath} from 'node:url'; +const rootDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../..' +); +const typedRuleTester = new TSRuleTester({ + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.ts'], + defaultProject: './tsconfig.json' + }, + tsconfigRootDir: rootDir + } + } +}); const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 2022, @@ -20,4 +38,14 @@ ruleTester.run('baseline-target', baselineTarget as never, { errors: [{messageId: 'notBaseline', data: {feature: 'temporal'}}] } ] -}); \ No newline at end of file +}); + +typedRuleTester.run('baseline-target (typed)', baselineTarget, { + valid: ['const x = [1, 2, 3].at(-1);'], + invalid: [ + { + code: 'const {promise} = Promise.withResolvers();', + errors: [{messageId: 'exceedsYear', data: {year: 2024, floor: 2023}}] + } + ] +}); diff --git a/src/rules/baseline-target.ts b/src/rules/baseline-target.ts index b0597a1..3c79cd1 100644 --- a/src/rules/baseline-target.ts +++ b/src/rules/baseline-target.ts @@ -1,9 +1,12 @@ +import {createRequire} from 'node:module'; +import path from 'node:path'; import type {TSESLint} from '@typescript-eslint/utils'; import { detectBaselineTargetForFeatures, detectBaselineYearForFeatures, detectFeaturesForSource } from 'baseline-detector'; +import {tryGetTypedParserServices} from '../utils/typescript.js'; type MessageIds = 'notBaseline' | 'exceedsYear'; @@ -18,25 +21,37 @@ export const baselineTarget: TSESLint.RuleModule = { schema: [], messages: { notBaseline: '`{{feature}}` is not Baseline yet', - exceedsYear: 'This file needs Baseline {{year}}, above the {{floor}} floor' + exceedsYear: + 'This file needs Baseline {{year}}, above the {{floor}} floor' } }, defaultOptions: [], create(context) { + const services = tryGetTypedParserServices(context); + return { Program(node) { const features = detectFeaturesForSource(context.sourceCode.text, { - fileName: context.filename + fileName: context.filename, + ...(services && { + typescriptContext: { + ts: createRequire(path.join(context.cwd, 'package.json'))( + 'typescript' + ), + program: services.program, + checker: services.program.getTypeChecker() + } + }) }); const target = detectBaselineTargetForFeatures(features); const year = detectBaselineYearForFeatures(features); // the order of the checks matters, a feature that is not baseline makes year come back null, and so does a file // with no features at all. - if(target.status === false){ + if (target.status === false) { context.report({ node, messageId: 'notBaseline', - data: { feature: target.reason } + data: {feature: target.reason} }); return; } @@ -45,7 +60,7 @@ export const baselineTarget: TSESLint.RuleModule = { context.report({ node, messageId: 'exceedsYear', - data: { year, floor: BASELINE_YEAR } + data: {year, floor: BASELINE_YEAR} }); } } From c94d9fb603eb7b4577f647300a20dfa021bb63f9 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:51:23 -0600 Subject: [PATCH 3/7] style: format baseline-target import --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index d5e8717..71cbe2e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,7 +33,7 @@ import {preferGetOrInsert} from './rules/prefer-get-or-insert.js'; import {preferCharCodeAtInLoop} from './rules/prefer-charcode-at-in-loop.js'; import {preferSliceOverSplitIndex} from './rules/prefer-slice-over-split-index.js'; import {preferThrowIfNoEntry} from './rules/prefer-throw-if-no-entry.js'; -import { baselineTarget } from './rules/baseline-target.js'; +import {baselineTarget} from './rules/baseline-target.js'; const plugin: ESLint.Plugin = { meta: { From 1cb1f7b9d51f242d25ec4efa3045b5c7db94f7f1 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:57:03 -0600 Subject: [PATCH 4/7] feat: report baseline-target per usage --- src/rules/baseline-target.test.ts | 40 ++++++++++++++++++++++-- src/rules/baseline-target.ts | 52 +++++++++++++++++-------------- 2 files changed, 66 insertions(+), 26 deletions(-) diff --git a/src/rules/baseline-target.test.ts b/src/rules/baseline-target.test.ts index f7b49f3..194b930 100644 --- a/src/rules/baseline-target.test.ts +++ b/src/rules/baseline-target.test.ts @@ -31,11 +31,36 @@ ruleTester.run('baseline-target', baselineTarget as never, { invalid: [ { code: 'new URLPattern();', - errors: [{messageId: 'exceedsYear', data: {year: 2025, floor: 2023}}] + errors: [ + { + messageId: 'exceedsYear', + data: {feature: 'urlpattern', year: 2025, floor: 2023}, + line: 1, + column: 5, + endLine: 1, + endColumn: 15 + } + ] }, { code: 'Temporal.Now;', - errors: [{messageId: 'notBaseline', data: {feature: 'temporal'}}] + errors: [ + { + messageId: 'notBaseline', + data: {feature: 'temporal'}, + line: 1, + column: 1, + endLine: 1, + endColumn: 9 + } + ] + }, + { + code: 'const a = 1;\nnew URLPattern();\nnew URLPattern();', + errors: [ + {messageId: 'exceedsYear', line: 2, column: 5}, + {messageId: 'exceedsYear', line: 3, column: 5} + ] } ] }); @@ -45,7 +70,16 @@ typedRuleTester.run('baseline-target (typed)', baselineTarget, { invalid: [ { code: 'const {promise} = Promise.withResolvers();', - errors: [{messageId: 'exceedsYear', data: {year: 2024, floor: 2023}}] + errors: [ + { + messageId: 'exceedsYear', + data: {feature: 'promise-withresolvers', year: 2024, floor: 2023}, + line: 1, + column: 27, + endLine: 1, + endColumn: 40 + } + ] } ] }); diff --git a/src/rules/baseline-target.ts b/src/rules/baseline-target.ts index 3c79cd1..670e765 100644 --- a/src/rules/baseline-target.ts +++ b/src/rules/baseline-target.ts @@ -2,9 +2,9 @@ import {createRequire} from 'node:module'; import path from 'node:path'; import type {TSESLint} from '@typescript-eslint/utils'; import { - detectBaselineTargetForFeatures, - detectBaselineYearForFeatures, - detectFeaturesForSource + baselineStatusOf, + detectFeaturesForSource, + yearOf } from 'baseline-detector'; import {tryGetTypedParserServices} from '../utils/typescript.js'; @@ -22,7 +22,7 @@ export const baselineTarget: TSESLint.RuleModule = { messages: { notBaseline: '`{{feature}}` is not Baseline yet', exceedsYear: - 'This file needs Baseline {{year}}, above the {{floor}} floor' + '`{{feature}}` is Baseline {{year}}, above the {{floor}} floor' } }, defaultOptions: [], @@ -30,7 +30,7 @@ export const baselineTarget: TSESLint.RuleModule = { const services = tryGetTypedParserServices(context); return { - Program(node) { + Program() { const features = detectFeaturesForSource(context.sourceCode.text, { fileName: context.filename, ...(services && { @@ -43,25 +43,31 @@ export const baselineTarget: TSESLint.RuleModule = { } }) }); - const target = detectBaselineTargetForFeatures(features); - const year = detectBaselineYearForFeatures(features); - // the order of the checks matters, a feature that is not baseline makes year come back null, and so does a file - // with no features at all. - if (target.status === false) { - context.report({ - node, - messageId: 'notBaseline', - data: {feature: target.reason} - }); - return; - } - if (year !== null && year > BASELINE_YEAR) { - context.report({ - node, - messageId: 'exceedsYear', - data: {year, floor: BASELINE_YEAR} - }); + for (const feature of features.get(context.filename) ?? []) { + const status = baselineStatusOf(feature.id); + const year = yearOf(feature.id); + + for (const [start, end] of feature.ranges) { + const loc = { + start: context.sourceCode.getLocFromIndex(start), + end: context.sourceCode.getLocFromIndex(end) + }; + + if (status === false) { + context.report({ + loc, + messageId: 'notBaseline', + data: {feature: feature.id} + }); + } else if (year !== null && year > BASELINE_YEAR) { + context.report({ + loc, + messageId: 'exceedsYear', + data: {feature: feature.id, year, floor: BASELINE_YEAR} + }); + } + } } } }; From 2d6ab625f9696671c3c1d8853ff76802c32a8df9 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:57:54 -0600 Subject: [PATCH 5/7] chore(deps): bump typescript-eslint to 8.72 --- package-lock.json | 146 +++++++++++++++++++++++----------------------- package.json | 6 +- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5eb6dc5..b41893a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,8 @@ "@eslint/js": "^10.0.1", "@eslint/json": "^2.0.1", "@types/node": "^26.1.0", - "@typescript-eslint/rule-tester": "^8.62.1", - "@typescript-eslint/typescript-estree": "^8.62.1", + "@typescript-eslint/rule-tester": "^8.72.0", + "@typescript-eslint/typescript-estree": "^8.72.0", "@vitest/coverage-v8": "^4.1.9", "eslint": "^10.6.0", "eslint-plugin-eslint-plugin": "^7.4.1", @@ -26,7 +26,7 @@ "oxlint": "^1.72.0", "prettier": "^3.9.4", "typescript": "^6.0.3", - "typescript-eslint": "^8.62.1", + "typescript-eslint": "^8.72.0", "vitest": "^4.0.14" }, "peerDependencies": { @@ -1096,17 +1096,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", - "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz", + "integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/type-utils": "8.62.1", - "@typescript-eslint/utils": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/type-utils": "8.72.0", + "@typescript-eslint/utils": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1119,15 +1119,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.62.1", + "@typescript-eslint/parser": "^8.72.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz", + "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==", "dev": true, "license": "MIT", "engines": { @@ -1135,16 +1135,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", - "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz", + "integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", "debug": "^4.4.3" }, "engines": { @@ -1160,14 +1160,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", - "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz", + "integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.62.1", - "@typescript-eslint/types": "^8.62.1", + "@typescript-eslint/tsconfig-utils": "^8.72.0", + "@typescript-eslint/types": "^8.72.0", "debug": "^4.4.3" }, "engines": { @@ -1182,15 +1182,15 @@ } }, "node_modules/@typescript-eslint/rule-tester": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.62.1.tgz", - "integrity": "sha512-qq2Io5qOVAyKsXfcgpqc5qIef++7bFMit3R8gNLNYlxrivwWtZH5pfIuU0nc69mUHAxowv4sUGfbpwykfBGHYQ==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.72.0.tgz", + "integrity": "sha512-3DkhIwJJ1EgssS9r613P8mqraY/c0uSg9e5rQdYPKf71poFqIrN/6agYEtTzI9SvVLgFqfbCwpKLr0zRBij0UQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/parser": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/parser": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", @@ -1209,14 +1209,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", - "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz", + "integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1" + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1227,9 +1227,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", - "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz", + "integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==", "dev": true, "license": "MIT", "engines": { @@ -1244,15 +1244,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", - "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz", + "integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1269,9 +1269,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", - "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz", + "integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==", "dev": true, "license": "MIT", "engines": { @@ -1283,16 +1283,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", - "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz", + "integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.62.1", - "@typescript-eslint/tsconfig-utils": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/project-service": "8.72.0", + "@typescript-eslint/tsconfig-utils": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1311,16 +1311,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", - "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz", + "integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1" + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1335,13 +1335,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", - "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz", + "integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/types": "8.72.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2981,16 +2981,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz", - "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.0.tgz", + "integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.62.1", - "@typescript-eslint/parser": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/utils": "8.62.1" + "@typescript-eslint/eslint-plugin": "8.72.0", + "@typescript-eslint/parser": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 643142e..2379f90 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "@eslint/js": "^10.0.1", "@eslint/json": "^2.0.1", "@types/node": "^26.1.0", - "@typescript-eslint/rule-tester": "^8.62.1", - "@typescript-eslint/typescript-estree": "^8.62.1", + "@typescript-eslint/rule-tester": "^8.72.0", + "@typescript-eslint/typescript-estree": "^8.72.0", "@vitest/coverage-v8": "^4.1.9", "eslint": "^10.6.0", "eslint-plugin-eslint-plugin": "^7.4.1", @@ -47,7 +47,7 @@ "oxlint": "^1.72.0", "prettier": "^3.9.4", "typescript": "^6.0.3", - "typescript-eslint": "^8.62.1", + "typescript-eslint": "^8.72.0", "vitest": "^4.0.14" }, "peerDependencies": { From 5c757d78bcb2fa900a884a84139223966113f47b Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:57:54 -0600 Subject: [PATCH 6/7] feat: support css in baseline-target --- package-lock.json | 43 ++++++++++++++++-- package.json | 1 + src/rules/baseline-target.test.ts | 51 +++++++++++++++++++++ src/rules/baseline-target.ts | 74 ++++++++++++++++--------------- 4 files changed, 131 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index b41893a..81ca961 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "verkit": "^0.5.0" }, "devDependencies": { + "@eslint/css": "^2.0.1", "@eslint/js": "^10.0.1", "@eslint/json": "^2.0.1", "@types/node": "^26.1.0", @@ -219,6 +220,35 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@eslint/css/-/css-2.0.1.tgz", + "integrity": "sha512-1qIQFzuFeTaHF3ewzsj/HUDC9RTDwfBHGWdFvpmOG0R/Tb3Mt+17yltbE10/LK+oglS1fP9+tiG5UzMvWgQY9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "@eslint/css-tree": "^4.1.1", + "@eslint/plugin-kit": "^0.7.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/css-tree": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@eslint/css-tree/-/css-tree-4.1.2.tgz", + "integrity": "sha512-rVcxLmwSkEL3aWS3vAgsTRrQHoYE1b0XcgDtKHDJ92XOkyUpcbS/g0tUJKhvCtlEhYzgKBWsgDd+Z/aPhd166w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.37.2", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/js": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", @@ -267,9 +297,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2497,6 +2527,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mdn-data": { + "version": "2.37.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.37.2.tgz", + "integrity": "sha512-rz9YOBGItdSbe7HcSArUbhiAv4SB/4DKRYg96CSFS2CcIYMbUvBtZlzA1NCbk2AdgbmYwMqwNblLpR441lLeiQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", diff --git a/package.json b/package.json index 2379f90..90cec6b 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "lint:format": "prettier --check src" }, "devDependencies": { + "@eslint/css": "^2.0.1", "@eslint/js": "^10.0.1", "@eslint/json": "^2.0.1", "@types/node": "^26.1.0", diff --git a/src/rules/baseline-target.test.ts b/src/rules/baseline-target.test.ts index 194b930..8fc2833 100644 --- a/src/rules/baseline-target.test.ts +++ b/src/rules/baseline-target.test.ts @@ -1,4 +1,5 @@ import {RuleTester} from 'eslint'; +import css from '@eslint/css'; import {RuleTester as TSRuleTester} from '@typescript-eslint/rule-tester'; import {baselineTarget} from './baseline-target.js'; import * as path from 'node:path'; @@ -83,3 +84,53 @@ typedRuleTester.run('baseline-target (typed)', baselineTarget, { } ] }); + +const cssRuleTester = new RuleTester({ + plugins: {css}, + language: 'css/css' +}); + +cssRuleTester.run('baseline-target (css)', baselineTarget as never, { + valid: [ + {code: 'a { color: red; }', filename: 'test.css'}, + {code: 'b { height: 100dvh; }', filename: 'test.css'} + ], + invalid: [ + { + code: 'a { text-wrap: balance; }', + filename: 'test.css', + errors: [ + { + messageId: 'exceedsYear', + data: {feature: 'text-wrap', year: 2024, floor: 2023}, + line: 1, + column: 5, + endLine: 1, + endColumn: 14 + }, + { + messageId: 'exceedsYear', + data: {feature: 'text-wrap-balance', year: 2024, floor: 2023}, + line: 1, + column: 16, + endLine: 1, + endColumn: 23 + } + ] + }, + { + code: '.a { anchor-name: --a; }', + filename: 'test.css', + errors: [ + { + messageId: 'notBaseline', + data: {feature: 'anchor-positioning'}, + line: 1, + column: 6, + endLine: 1, + endColumn: 17 + } + ] + } + ] +}); diff --git a/src/rules/baseline-target.ts b/src/rules/baseline-target.ts index 670e765..2c00812 100644 --- a/src/rules/baseline-target.ts +++ b/src/rules/baseline-target.ts @@ -15,6 +15,7 @@ const BASELINE_YEAR = 2023; export const baselineTarget: TSESLint.RuleModule = { meta: { type: 'problem', + languages: ['js/js', 'css/css'], docs: { description: `Disallow features newer than Baseline ${BASELINE_YEAR}` }, @@ -29,47 +30,50 @@ export const baselineTarget: TSESLint.RuleModule = { create(context) { const services = tryGetTypedParserServices(context); - return { - Program() { - const features = detectFeaturesForSource(context.sourceCode.text, { - fileName: context.filename, - ...(services && { - typescriptContext: { - ts: createRequire(path.join(context.cwd, 'package.json'))( - 'typescript' - ), - program: services.program, - checker: services.program.getTypeChecker() - } - }) - }); + const check = (): void => { + const features = detectFeaturesForSource(context.sourceCode.text, { + fileName: context.filename, + ...(services && { + typescriptContext: { + ts: createRequire(path.join(context.cwd, 'package.json'))( + 'typescript' + ), + program: services.program, + checker: services.program.getTypeChecker() + } + }) + }); - for (const feature of features.get(context.filename) ?? []) { - const status = baselineStatusOf(feature.id); - const year = yearOf(feature.id); + for (const feature of features.get(context.filename) ?? []) { + const status = baselineStatusOf(feature.id); + const year = yearOf(feature.id); - for (const [start, end] of feature.ranges) { - const loc = { - start: context.sourceCode.getLocFromIndex(start), - end: context.sourceCode.getLocFromIndex(end) - }; + for (const [start, end] of feature.ranges) { + const loc = { + start: context.sourceCode.getLocFromIndex(start), + end: context.sourceCode.getLocFromIndex(end) + }; - if (status === false) { - context.report({ - loc, - messageId: 'notBaseline', - data: {feature: feature.id} - }); - } else if (year !== null && year > BASELINE_YEAR) { - context.report({ - loc, - messageId: 'exceedsYear', - data: {feature: feature.id, year, floor: BASELINE_YEAR} - }); - } + if (status === false) { + context.report({ + loc, + messageId: 'notBaseline', + data: {feature: feature.id} + }); + } else if (year !== null && year > BASELINE_YEAR) { + context.report({ + loc, + messageId: 'exceedsYear', + data: {feature: feature.id, year, floor: BASELINE_YEAR} + }); } } } }; + + return { + Program: check, + StyleSheet: check + }; } }; From 4d90e1e39b0a9f468a19f3579b8d51923fdc8554 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:04:19 -0600 Subject: [PATCH 7/7] feat: make the baseline-target floor configurable --- src/rules/baseline-target.test.ts | 69 +++++++++++++++++++++- src/rules/baseline-target.ts | 96 +++++++++++++++++++++++-------- 2 files changed, 141 insertions(+), 24 deletions(-) diff --git a/src/rules/baseline-target.test.ts b/src/rules/baseline-target.test.ts index 8fc2833..97da4d3 100644 --- a/src/rules/baseline-target.test.ts +++ b/src/rules/baseline-target.test.ts @@ -1,15 +1,19 @@ -import {RuleTester} from 'eslint'; +import {Linter, RuleTester} from 'eslint'; import css from '@eslint/css'; import {RuleTester as TSRuleTester} from '@typescript-eslint/rule-tester'; import {baselineTarget} from './baseline-target.js'; import * as path from 'node:path'; +import {describe, expect, it} from 'vitest'; import {fileURLToPath} from 'node:url'; const rootDir = path.resolve( path.dirname(fileURLToPath(import.meta.url)), '../..' ); +const yearSettings = {e18e: {baseline: {target: 2023}}}; + const typedRuleTester = new TSRuleTester({ + settings: yearSettings, languageOptions: { parserOptions: { projectService: { @@ -21,6 +25,7 @@ const typedRuleTester = new TSRuleTester({ } }); const ruleTester = new RuleTester({ + settings: yearSettings, languageOptions: { ecmaVersion: 2022, sourceType: 'module' @@ -86,6 +91,7 @@ typedRuleTester.run('baseline-target (typed)', baselineTarget, { }); const cssRuleTester = new RuleTester({ + settings: yearSettings, plugins: {css}, language: 'css/css' }); @@ -134,3 +140,64 @@ cssRuleTester.run('baseline-target (css)', baselineTarget as never, { } ] }); + +const targetRuleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module' + } +}); + +targetRuleTester.run('baseline-target (targets)', baselineTarget as never, { + valid: [ + 'const x = a ?? b;', + { + code: 'new URLPattern();', + settings: {e18e: {baseline: {target: 'newly available'}}} + }, + { + code: 'new URLPattern();', + settings: {e18e: {baseline: {target: 2025}}} + } + ], + invalid: [ + { + code: 'new URLPattern();', + errors: [ + { + messageId: 'notWidelyAvailable', + data: {feature: 'urlpattern'}, + line: 1, + column: 5 + } + ] + }, + { + code: 'new URLPattern();', + settings: {e18e: {baseline: {target: 'widely available'}}}, + errors: [{messageId: 'notWidelyAvailable'}] + }, + { + code: 'Temporal.Now;', + settings: {e18e: {baseline: {target: 'newly available'}}}, + errors: [{messageId: 'notBaseline', data: {feature: 'temporal'}}] + } + ] +}); + +describe('baseline-target settings', () => { + it('rejects an unknown target', () => { + const linter = new Linter(); + expect(() => + linter.verify( + 'x = 1;', + { + plugins: {e18e: {rules: {'baseline-target': baselineTarget}}}, + settings: {e18e: {baseline: {target: 'soon'}}}, + rules: {'e18e/baseline-target': 'error'} + } as never, + {filename: 'x.js'} + ) + ).toThrow('Invalid settings.e18e.baseline.target "soon"'); + }); +}); diff --git a/src/rules/baseline-target.ts b/src/rules/baseline-target.ts index 2c00812..d9d4f95 100644 --- a/src/rules/baseline-target.ts +++ b/src/rules/baseline-target.ts @@ -8,26 +8,85 @@ import { } from 'baseline-detector'; import {tryGetTypedParserServices} from '../utils/typescript.js'; -type MessageIds = 'notBaseline' | 'exceedsYear'; +type MessageIds = 'notBaseline' | 'notWidelyAvailable' | 'exceedsYear'; -const BASELINE_YEAR = 2023; +type BaselineTarget = + | {kind: 'widely available'} + | {kind: 'newly available'} + | {kind: 'year'; year: number}; + +interface Violation { + messageId: MessageIds; + data: Record; +} + +function parseTarget(value: unknown): BaselineTarget { + if (value === undefined || value === 'widely available') { + return {kind: 'widely available'}; + } + if (value === 'newly available') { + return {kind: 'newly available'}; + } + if (typeof value === 'number' && Number.isInteger(value)) { + return {kind: 'year', year: value}; + } + throw new Error( + `Invalid settings.e18e.baseline.target ${JSON.stringify(value)}. Expected "widely available", "newly available" or a year such as 2023.` + ); +} + +function checkFeature( + featureId: string, + target: BaselineTarget +): Violation | null { + const status = baselineStatusOf(featureId); + if (status === null) { + return null; + } + if (status === false) { + return {messageId: 'notBaseline', data: {feature: featureId}}; + } + + switch (target.kind) { + case 'newly available': + return null; + case 'widely available': + return status === 'high' + ? null + : {messageId: 'notWidelyAvailable', data: {feature: featureId}}; + case 'year': { + const year = yearOf(featureId); + return year !== null && year > target.year + ? { + messageId: 'exceedsYear', + data: {feature: featureId, year, floor: target.year} + } + : null; + } + } +} export const baselineTarget: TSESLint.RuleModule = { meta: { type: 'problem', languages: ['js/js', 'css/css'], docs: { - description: `Disallow features newer than Baseline ${BASELINE_YEAR}` + description: 'Disallow features newer than the configured Baseline target' }, schema: [], messages: { notBaseline: '`{{feature}}` is not Baseline yet', + notWidelyAvailable: '`{{feature}}` is not widely available yet', exceedsYear: '`{{feature}}` is Baseline {{year}}, above the {{floor}} floor' } }, defaultOptions: [], create(context) { + const settings = context.settings as { + e18e?: {baseline?: {target?: unknown}}; + }; + const target = parseTarget(settings.e18e?.baseline?.target); const services = tryGetTypedParserServices(context); const check = (): void => { @@ -45,28 +104,19 @@ export const baselineTarget: TSESLint.RuleModule = { }); for (const feature of features.get(context.filename) ?? []) { - const status = baselineStatusOf(feature.id); - const year = yearOf(feature.id); + const violation = checkFeature(feature.id, target); + if (!violation) { + continue; + } for (const [start, end] of feature.ranges) { - const loc = { - start: context.sourceCode.getLocFromIndex(start), - end: context.sourceCode.getLocFromIndex(end) - }; - - if (status === false) { - context.report({ - loc, - messageId: 'notBaseline', - data: {feature: feature.id} - }); - } else if (year !== null && year > BASELINE_YEAR) { - context.report({ - loc, - messageId: 'exceedsYear', - data: {feature: feature.id, year, floor: BASELINE_YEAR} - }); - } + context.report({ + loc: { + start: context.sourceCode.getLocFromIndex(start), + end: context.sourceCode.getLocFromIndex(end) + }, + ...violation + }); } } };