Skip to content

Commit

Permalink
chore: add type tests for core package (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
fasttime authored Dec 4, 2024
1 parent 44d812d commit f7cb7e2
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 1 deletion.
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;

0 comments on commit f7cb7e2

Please sign in to comment.