diff --git a/package-lock.json b/package-lock.json index 5eb6dc5..81ca961 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,12 @@ "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", - "@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 +27,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": { @@ -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": { @@ -1096,17 +1126,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 +1149,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 +1165,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 +1190,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 +1212,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 +1239,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 +1257,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 +1274,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 +1299,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 +1313,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 +1341,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 +1365,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": { @@ -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", @@ -2981,16 +3018,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..90cec6b 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,12 @@ "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", - "@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 +48,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": { diff --git a/src/main.ts b/src/main.ts index b5fac37..71cbe2e 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..97da4d3 --- /dev/null +++ b/src/rules/baseline-target.test.ts @@ -0,0 +1,203 @@ +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: { + allowDefaultProject: ['*.ts'], + defaultProject: './tsconfig.json' + }, + tsconfigRootDir: rootDir + } + } +}); +const ruleTester = new RuleTester({ + settings: yearSettings, + 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: {feature: 'urlpattern', year: 2025, floor: 2023}, + line: 1, + column: 5, + endLine: 1, + endColumn: 15 + } + ] + }, + { + code: 'Temporal.Now;', + 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} + ] + } + ] +}); + +typedRuleTester.run('baseline-target (typed)', baselineTarget, { + valid: ['const x = [1, 2, 3].at(-1);'], + invalid: [ + { + code: 'const {promise} = Promise.withResolvers();', + errors: [ + { + messageId: 'exceedsYear', + data: {feature: 'promise-withresolvers', year: 2024, floor: 2023}, + line: 1, + column: 27, + endLine: 1, + endColumn: 40 + } + ] + } + ] +}); + +const cssRuleTester = new RuleTester({ + settings: yearSettings, + 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 + } + ] + } + ] +}); + +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 new file mode 100644 index 0000000..d9d4f95 --- /dev/null +++ b/src/rules/baseline-target.ts @@ -0,0 +1,129 @@ +import {createRequire} from 'node:module'; +import path from 'node:path'; +import type {TSESLint} from '@typescript-eslint/utils'; +import { + baselineStatusOf, + detectFeaturesForSource, + yearOf +} from 'baseline-detector'; +import {tryGetTypedParserServices} from '../utils/typescript.js'; + +type MessageIds = 'notBaseline' | 'notWidelyAvailable' | 'exceedsYear'; + +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 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 => { + 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 violation = checkFeature(feature.id, target); + if (!violation) { + continue; + } + + for (const [start, end] of feature.ranges) { + context.report({ + loc: { + start: context.sourceCode.getLocFromIndex(start), + end: context.sourceCode.getLocFromIndex(end) + }, + ...violation + }); + } + } + }; + + return { + Program: check, + StyleSheet: check + }; + } +};