Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ jobs:
env:
CI: true

test_types:
name: Test Types (core)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"

- name: npm install and test types
working-directory: packages/core
run: |
npm install
npm run build
npm run test:types

jsr_test:
name: Verify JSR Publish
runs-on: ubuntu-latest
Expand Down
6 changes: 5 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"scripts": {
"build:cts": "node -e \"fs.cpSync('dist/esm/types.d.ts', 'dist/cjs/types.d.cts')\"",
"build": "tsc && npm run build:cts",
"test:jsr": "npx jsr@latest publish --dry-run"
"test:jsr": "npx jsr@latest publish --dry-run",
"test:types": "tsc -p tests/types/tsconfig.json"
},
"repository": {
"type": "git",
Expand All @@ -35,6 +36,9 @@
"url": "https://github.com/eslint/rewrite/issues"
},
"homepage": "https://github.com/eslint/rewrite#readme",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"devDependencies": {
"json-schema": "^0.4.0",
"typescript": "^5.4.5"
Expand Down
9 changes: 9 additions & 0 deletions packages/core/tests/types/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"rootDir": "../..",
"strict": true
},
"files": ["../../dist/esm/types.d.ts", "types.test.ts"]
}
241 changes: 241 additions & 0 deletions packages/core/tests/types/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/**
* @fileoverview Type tests for ESLint Core.
* @author Francesco Trotta
*/

//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------

import type {
File,
FileProblem,
Language,
LanguageContext,
LanguageOptions,
OkParseResult,
ParseResult,
RuleContext,
RuleDefinition,
RulesConfig,
RulesMeta,
RuleTextEdit,
RuleTextEditor,
RuleVisitor,
SourceLocation,
SourceRange,
TextSourceCode,
TraversalStep,
} from "@eslint/core";

//-----------------------------------------------------------------------------
// Helper types
//-----------------------------------------------------------------------------

interface TestNode {
type: string;
start: number;
lenght: number;
}

interface TestRootNode {
type: "root";
start: number;
length: number;
}

//-----------------------------------------------------------------------------
// Tests for shared types
//-----------------------------------------------------------------------------

interface TestLanguageOptions extends LanguageOptions {
howMuch?: "yes" | "no" | boolean;
}

class TestSourceCode
implements
TextSourceCode<{
LangOptions: TestLanguageOptions;
RootNode: TestRootNode;
SyntaxElementWithLoc: unknown;
ConfigNode: unknown;
}>
{
text: string;
ast: TestRootNode;
notMuch: "no" | false;
visitorKeys?: Record<string, string[]> | undefined;

constructor(text: string, ast: TestRootNode) {
this.text = text;
this.ast = ast;
this.notMuch = false;
}

/* eslint-disable class-methods-use-this -- not all methods need `this` */

getLoc(syntaxElement: { start: number; length: number }): SourceLocation {
return {
start: { line: 1, column: syntaxElement.start + 1 },
end: {
line: 1,
column: syntaxElement.start + 1 + syntaxElement.length,
},
};
}

getRange(syntaxElement: { start: number; length: number }): SourceRange {
return [
syntaxElement.start,
syntaxElement.start + syntaxElement.length,
];
}

*traverse(): Iterable<TraversalStep> {
// To be implemented.
}

applyLanguageOptions(languageOptions: TestLanguageOptions): void {
if (languageOptions.howMuch === "yes") {
this.notMuch = "no";
}
}

applyInlineConfig(): {
configs: { loc: SourceLocation; config: { rules: RulesConfig } }[];
problems: FileProblem[];
} {
throw new Error("Method not implemented.");
}

/* eslint-enable class-methods-use-this -- not all methods need `this` */
}

//-----------------------------------------------------------------------------
// Tests for language-related types
//-----------------------------------------------------------------------------

interface TestNormalizedLanguageOptions extends TestLanguageOptions {
howMuch: boolean; // option is required and must be a boolean
}

const testLanguage: Language = {
fileType: "text",
lineStart: 1,
columnStart: 1,
nodeTypeKey: "type",

validateLanguageOptions(languageOptions: TestLanguageOptions): void {
if (
!["yes", "no", true, false, undefined].includes(
languageOptions.howMuch,
)
) {
throw Error("Invalid options.");
}
},

normalizeLanguageOptions(
languageOptions: TestLanguageOptions,
): TestNormalizedLanguageOptions {
const { howMuch } = languageOptions;
return { howMuch: howMuch === "yes" || howMuch === true };
},

parse(
file: File,
context: { languageOptions: TestNormalizedLanguageOptions },
): ParseResult<TestRootNode> {
context.languageOptions.howMuch satisfies boolean;
return {
ok: true,
ast: {
type: "root",
start: 0,
length: file.body.length,
},
};
},

createSourceCode(
file: File,
input: OkParseResult<TestRootNode>,
context: LanguageContext<TestNormalizedLanguageOptions>,
): TestSourceCode {
context.languageOptions.howMuch satisfies boolean;
return new TestSourceCode(String(file.body), input.ast);
},
};

testLanguage.defaultLanguageOptions satisfies LanguageOptions | undefined;

//-----------------------------------------------------------------------------
// Tests for rule-related types
//-----------------------------------------------------------------------------

interface TestRuleVisitor extends RuleVisitor {
Node?: (node: TestNode) => void;
}

type TestRuleContext = RuleContext<{
LangOptions: TestLanguageOptions;
Code: TestSourceCode;
RuleOptions: [{ foo: string; bar: number }];
Node: TestNode;
}>;

const testRule: RuleDefinition<{
LangOptions: TestLanguageOptions;
Code: TestSourceCode;
RuleOptions: [{ foo: string; bar: number }];
Visitor: TestRuleVisitor;
Node: TestNode;
MessageIds: "badFoo" | "wrongBar";
ExtRuleDocs: never;
}> = {
meta: {
type: "problem",
fixable: "code",
messages: {
badFoo: "change this foo",
wrongBar: "fix this bar",
},
},

create(context: TestRuleContext): TestRuleVisitor {
return {
Foo(node: TestNode) {
// node.type === "Foo"
context.report({
messageId: "badFoo",
loc: {
start: { line: node.start, column: 1 },
end: { line: node.start + 1, column: Infinity },
},
fix(fixer: RuleTextEditor): RuleTextEdit {
return fixer.replaceText(
node,
context.languageOptions.howMuch === "yes"
? "👍"
: "👎",
);
},
});
},
Bar(node: TestNode) {
// node.type === "Bar"
context.report({
message: "This bar is foobar",
node,
suggest: [
{
messageId: "Bar",
},
],
});
},
};
},
};

testRule.meta satisfies RulesMeta | undefined;
Loading