Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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
}
};

Expand Down
85 changes: 85 additions & 0 deletions src/rules/baseline-target.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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,
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<number>();',
errors: [
{
messageId: 'exceedsYear',
data: {feature: 'promise-withresolvers', year: 2024, floor: 2023},
line: 1,
column: 27,
endLine: 1,
endColumn: 40
}
]
}
]
});
75 changes: 75 additions & 0 deletions src/rules/baseline-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {createRequire} from 'node:module';
import path from 'node:path';
import type {TSESLint} from '@typescript-eslint/utils';
import {
baselineStatusOf,
detectFeaturesForSource,
yearOf
} from 'baseline-detector';

Check failure on line 8 in src/rules/baseline-target.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Cannot find module 'baseline-detector' or its corresponding type declarations.

Check failure on line 8 in src/rules/baseline-target.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

Cannot find module 'baseline-detector' or its corresponding type declarations.

Check failure on line 8 in src/rules/baseline-target.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Cannot find module 'baseline-detector' or its corresponding type declarations.
import {tryGetTypedParserServices} from '../utils/typescript.js';

type MessageIds = 'notBaseline' | 'exceedsYear';

const BASELINE_YEAR = 2023;

export const baselineTarget: TSESLint.RuleModule<MessageIds, []> = {
meta: {
type: 'problem',
docs: {
description: `Disallow features newer than Baseline ${BASELINE_YEAR}`
},
schema: [],
messages: {
notBaseline: '`{{feature}}` is not Baseline yet',
exceedsYear:
'`{{feature}}` is Baseline {{year}}, above the {{floor}} floor'
}
},
defaultOptions: [],
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()
}
})
});

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}
});
}
}
}
}
};
}
};
Loading