Skip to content

Commit

Permalink
feat: switch to @typescript-eslint for type inferencing (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
MengLinMaker authored Nov 1, 2024
2 parents e7b9bcc + 9c9bfc9 commit de8f48c
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 177 deletions.
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"commit": "czg",
"format": "biome check --write --verbose",
"lint": "eslint --fix & tsc --noEmit --incremental",
"test": "vitest",
"test": "vitest --no-isolate",
"build": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap && attw -P .",
"version": "changeset version",
"release": "changeset publish"
Expand All @@ -34,20 +34,24 @@
"@eslint/js": "^9.9.1",
"@types/eslint": "^9.6.1",
"@types/node": "^22.5.2",
"@typescript-eslint/rule-tester": "8.12.2",
"@vitest/coverage-v8": "^2.0.5",
"cz-git": "1.10.1",
"czg": "1.10.0",
"simple-git-hooks": "^2.11.1",
"tsup": "^8.2.4",
"typescript": "5.6.3",
"typescript-eslint": "^8.3.0",
"vitepress": "^1.3.4",
"vitest": "^2.0.5"
},
"dependencies": {
"@typescript-eslint/utils": "8.12.2",
"runtime-compat-data": "^0.0.5"
},
"peerDependencies": {
"eslint": ">=9.0.0"
"eslint": ">=9.0.0",
"typescript": "^5.0.0"
},
"simple-git-hooks": {
"pre-commit": "pnpm run format",
Expand Down
295 changes: 194 additions & 101 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export type { RuntimeName } from 'runtime-compat-data'
import type { Linter } from 'eslint'
import type { RuntimeName } from 'runtime-compat-data'
import pkg from '../package.json'
import { supportedRuntimes } from './constants'
Expand All @@ -20,7 +19,7 @@ const defaultRuleConfig: RuleConfig = {
const runtimeCompatPlugin = (
filterRuntimes: RuntimeName[],
ruleConfig: RuleConfig = defaultRuleConfig,
): Linter.FlatConfig => ({
) => ({
plugins: {
'runtime-compat': {
meta: {
Expand Down
3 changes: 3 additions & 0 deletions src/rules/createRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ESLintUtils } from '@typescript-eslint/utils'

export const createRule = ESLintUtils.RuleCreator(() => '')
78 changes: 43 additions & 35 deletions src/rules/runtime-compat.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,61 @@
import type { Rule } from 'eslint'
import type { Identifier } from 'estree'
import type { RuntimeName } from 'runtime-compat-data'
import data from 'runtime-compat-data'
import type { RuleConfig } from '../types'
import { compatErrorMessage } from '../utils/compatErrorMessage'
import { filterSupportCompatData } from '../utils/filterSupportCompatData'
import { mapCompatData } from '../utils/mapCompatData'
import { createRule } from './createRule'

/**
* Creates a runtime-compat rule.
* @param filterRuntimes - List of runtimes to check.
* @returns ESLint rule.
*/
export const runtimeCompatRule = (
filterRuntimes: RuntimeName[],
ruleConfig: RuleConfig,
): Rule.RuleModule => ({
meta: {
docs: {
description: 'Ensure cross-runtime API compatibility',
export const runtimeCompatRule = (filterRuntimes: data.RuntimeName[], ruleConfig: RuleConfig) =>
createRule({
name: 'runtime-compat',
meta: {
docs: {
description: 'Ensure cross-runtime API compatibility',
},
type: 'problem',
schema: [],
messages: {},
},
type: 'problem',
},
create: (context) => {
const unsupportedApis = filterSupportCompatData(mapCompatData(data), filterRuntimes, ruleConfig)
defaultOptions: [],
create: (context) => {
const unsupportedApis = filterSupportCompatData(
mapCompatData(data),
filterRuntimes,
ruleConfig,
)

const reportError = (node: Identifier, unsupportesApiId: string) => {
const apiInfo = unsupportedApis[unsupportesApiId]
if (!apiInfo) return
const message = compatErrorMessage(unsupportesApiId, apiInfo)
context.report({ node, message })
}
const reportError = (node: Identifier, unsupportesApiId: string) => {
const apiInfo = unsupportedApis[unsupportesApiId]
if (!apiInfo) return
const message = compatErrorMessage(unsupportesApiId, apiInfo)
// @ts-expect-error using typescript-eslint
context.report({ node, message })
}

return {
Identifier: (identifierNode) => {
// Detect a class constructor
if (identifierNode.parent.type === 'NewExpression') {
const unsupportesApiId = JSON.stringify([identifierNode.name])
reportError(identifierNode, unsupportesApiId)
}
// Detect variable assignment from class
if (identifierNode.parent.type === 'VariableDeclarator') {
if (identifierNode.parent.init === identifierNode) {
console.log(identifierNode.name)
return {
TSInterfaceDeclaration: (node) => {
console.log(node)
},
Identifier: (identifierNode) => {
// Detect a class constructor
if (identifierNode.parent.type === 'NewExpression') {
const unsupportesApiId = JSON.stringify([identifierNode.name])
reportError(identifierNode, unsupportesApiId)
}
}
},
}
},
})
// Detect variable assignment from class
if (identifierNode.parent.type === 'VariableDeclarator') {
if (identifierNode.parent.init === identifierNode) {
const unsupportesApiId = JSON.stringify([identifierNode.name])
reportError(identifierNode, unsupportesApiId)
}
}
},
}
},
})
4 changes: 4 additions & 0 deletions tests/rules/resources/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/*
needed for eslint rule tests
https://typescript-eslint.io/developers/custom-rules#testing
*/
5 changes: 5 additions & 0 deletions tests/rules/resources/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"target": "ESNext"
}
}
80 changes: 43 additions & 37 deletions tests/rules/runtimeCompatRule.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,50 @@
import { RuleTester } from 'eslint'
import type { RuntimeName } from 'runtime-compat-data'
import { it } from 'vitest'
import { runtimeCompatRule } from '../../src/rules/runtime-compat'
import { ruleTester } from './setup'

const ruleTester = new RuleTester({
languageOptions: { ecmaVersion: 2015, sourceType: 'module' },
})
const filterRuntimes: RuntimeName[] = ['node']
const cacheErrorMsg =
"'Cache' - Unsupported API in node.\nDocs - https://developer.mozilla.org/docs/Web/API/Cache"

it('should pass eslint "runtime-compat" test', () => {
ruleTester.run(
'runtime-compat',
runtimeCompatRule(filterRuntimes, {
deprecated: false,
experimental: false,
}),
{
valid: [{ code: 'fetch("https://www.google.com")' }],
invalid: [
{
// Detect unsupported API constructor call.
code: 'const a = new Cache(); let b = new Cache(); b = new Cache()',
errors: [
{ message: cacheErrorMsg },
{ message: cacheErrorMsg },
{ message: cacheErrorMsg },
],
},
{
// Detect unsupported API variable assignment.
code: 'const n = Cache; let b = new Cache; b = new Cache',
errors: [
{ message: cacheErrorMsg },
{ message: cacheErrorMsg },
{ message: cacheErrorMsg },
],
},
],
},
)
})
ruleTester.run(
'runtime-compat',
runtimeCompatRule(filterRuntimes, {
deprecated: false,
experimental: false,
}),
{
valid: [
{
code: 'fetch("https://www.google.com")',
},
],
invalid: [
{
// Detect unsupported API constructor call.
code: /*javascript*/ `
const a = new Cache()
let b = new Cache()
b = new Cache()
`,
errors: [
{ message: cacheErrorMsg },
{ message: cacheErrorMsg },
{ message: cacheErrorMsg },
],
},
{
// Detect unsupported API variable assignment.
code: /*javascript*/ `
const n = Cache
let b = new Cache
b = new Cache
`,
errors: [
{ message: cacheErrorMsg },
{ message: cacheErrorMsg },
{ message: cacheErrorMsg },
],
},
],
},
)
17 changes: 17 additions & 0 deletions tests/rules/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as path from 'node:path'
import { RuleTester } from '@typescript-eslint/rule-tester'
import { afterAll, describe, it } from 'vitest'

RuleTester.afterAll = afterAll
RuleTester.it = it
RuleTester.itOnly = it.only
RuleTester.describe = describe

export const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
project: path.resolve(__dirname, 'resources', 'tsconfig.json'),
tsconfigRootDir: path.resolve(__dirname, 'resources'),
},
},
})

0 comments on commit de8f48c

Please sign in to comment.