Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add more comprehensive test to type detection #57

Merged
merged 10 commits into from
Nov 24, 2024
2 changes: 0 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches:
- '**'
Expand Down
3 changes: 3 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
"recommended": true,
"complexity": {
"useLiteralKeys": "off"
},
"style": {
"noNonNullAssertion": "off"
}
}
},
Expand Down
6 changes: 6 additions & 0 deletions packages/data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,11 @@
},
"dependencies": {
"runtime-compat-data": "0.0.5"
},
"peerDependencies": {
"@cloudflare/workers-types": "*",
"@types/bun": "*",
"@types/deno": "*",
"@types/node": "*"
}
}
15 changes: 8 additions & 7 deletions packages/data/preprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { writeFileSync } from 'node:fs'
import type { CompatStatement, Identifier, StatusBlock } from 'runtime-compat-data'
import rawCompatData from 'runtime-compat-data'
import type { PreprocessCompatData, PreprocessCompatStatement } from './src/types'
import { parseJsonKeys, stringifyJsonKeys } from './src/utils'

/**
* Compress raw compat data to single level flatmap
Expand Down Expand Up @@ -48,7 +49,7 @@ const mapCompatData = new Map<string, PreprocessCompatStatement>()
const subData = compatData[key]
if (key === '__compat') {
const preprocessCompatStatement = extractPreprocessCompatStatement(subData as never)
mapCompatData.set(JSON.stringify(parentKeys), preprocessCompatStatement)
mapCompatData.set(stringifyJsonKeys(parentKeys), preprocessCompatStatement)
} else {
// Only chain keys if "__compat" exists
const nodeHasCompatData = !keys.includes('__compat')
Expand All @@ -61,7 +62,7 @@ const mapCompatData = new Map<string, PreprocessCompatStatement>()
}

/**
* Sort mapped compat data into different AST detection scenarios
* Sort mapped compat data into different AST detection apiContext
*/
const preprocessCompatData: PreprocessCompatData = {
class: {},
Expand All @@ -75,7 +76,7 @@ const preprocessCompatData: PreprocessCompatData = {
const isPascalCase = (s: string | undefined) => s?.match(/^[A-Z]+.*/)

for (const [jsonKeys, preprocessCompatStatement] of mapCompatData.entries()) {
const keys = JSON.parse(jsonKeys) as string[]
const keys = parseJsonKeys(jsonKeys)
if (keys.length === 1) {
if (isPascalCase(keys[0])) {
// PascalCase, hence a class
Expand All @@ -87,24 +88,24 @@ const preprocessCompatData: PreprocessCompatData = {
} else if (keys.length === 2) {
if (keys[0] === keys[1])
// Duplicate keys are class constructors
preprocessCompatData.class[JSON.stringify([keys[0]])] = preprocessCompatStatement
preprocessCompatData.class[stringifyJsonKeys([keys[0]!])] = preprocessCompatStatement
else if (keys[1]?.match('_static')) {
// Static methods have '_static'
const newKeys = JSON.stringify([keys[0], keys[1]?.replace('_static', '')])
const newKeys = stringifyJsonKeys([keys[0]!, keys[1]?.replace('_static', '')])
if (isPascalCase(keys[0]))
preprocessCompatData.classProperty[newKeys] = preprocessCompatStatement
else preprocessCompatData.globalClassProperty[newKeys] = preprocessCompatStatement
} else if (keys[1]?.match('_event')) {
// Events have '_event'
const newKeys = JSON.stringify([keys[0], keys[1]?.replace('_event', '')])
const newKeys = stringifyJsonKeys([keys[0]!, keys[1]?.replace('_event', '')])
preprocessCompatData.eventListener[newKeys] = preprocessCompatStatement
} else if (!keys[1]?.match('_'))
// Normal class property
preprocessCompatData.classProperty[jsonKeys] = preprocessCompatStatement
else preprocessCompatData.misc[jsonKeys] = preprocessCompatStatement
} else {
// Not sure how to analyse
preprocessCompatData.misc[JSON.stringify([keys[0]])] = preprocessCompatStatement
preprocessCompatData.misc[stringifyJsonKeys([keys[0]!])] = preprocessCompatStatement
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion packages/data/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { RuntimeName } from 'runtime-compat-data'
import { filterPreprocessCompatData, preprocessCompatData } from './runtime'
import { objectKeys, parseJsonKeys, stringifyJsonKeys } from './utils'

export type { RuntimeName }
export { filterPreprocessCompatData, preprocessCompatData }
export {
filterPreprocessCompatData,
preprocessCompatData,
objectKeys,
parseJsonKeys,
stringifyJsonKeys,
}
6 changes: 0 additions & 6 deletions packages/data/src/objectKeys.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/data/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { RuntimeName, StatusBlock } from 'runtime-compat-data'
import { objectKeys } from './objectKeys'
import _preprocessCompatData from './preprocessCompatData.json'
import type {
PreprocessCompatData,
PreprocessCompatStatement,
RuntimeCompatData,
RuntimeCompatStatement,
} from './types.js'
import { objectKeys } from './utils'

const preprocessCompatData: PreprocessCompatData = _preprocessCompatData
export { preprocessCompatData }
Expand Down
2 changes: 1 addition & 1 deletion packages/data/src/tests/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { RuntimeName } from 'runtime-compat-data'
import { describe, expect, it } from 'vitest'
import { objectKeys } from '../objectKeys'
import { filterPreprocessCompatData, preprocessCompatData } from '../runtime'
import { objectKeys } from '../utils'

describe('filterPreprocessCompatData', () => {
const filterRuntimes: RuntimeName[] = ['node']
Expand Down
2 changes: 2 additions & 0 deletions packages/data/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type ApiClassification =
| 'globalClassProperty'
| 'misc'

export type JsonKeys = string

/**
* Types for preprocessing
*/
Expand Down
22 changes: 22 additions & 0 deletions packages/data/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { JsonKeys } from './types'

/**
* Extracts the keys of an object to array with correct TypeScript.
* @param object - The object.
* @return Object key array.
*/
export const objectKeys = <T extends object>(object: T) => Object.keys(object) as (keyof T)[]

/**
* Parse stringified JSON string array.
* @param jsonKeys
* @returns
*/
export const parseJsonKeys = (jsonKeys: JsonKeys) => JSON.parse(jsonKeys) as string[]

/**
* Stringify JSON string array.
* @param stringArray
* @returns
*/
export const stringifyJsonKeys = (stringArray: string[]) => JSON.stringify(stringArray) as JsonKeys
6 changes: 4 additions & 2 deletions packages/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"format": "biome check --write --verbose",
"lint": "tsc --noEmit --incremental",
"test": "vitest",
"test:dev": "vitest --watch",
"build": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap && attw -P .",
"clean": "rm -rf node_modules"
},
Expand All @@ -25,10 +26,11 @@
"@typescript-eslint/rule-tester": "8.12.2"
},
"dependencies": {
"@typescript-eslint/utils": "8.12.2",
"@eslint-plugin-runtime-compat/data": "workspace:*"
"@eslint-plugin-runtime-compat/data": "workspace:*",
"@typescript-eslint/utils": "8.12.2"
},
"peerDependencies": {
"@eslint-plugin-runtime-compat/data": "workspace:*",
"eslint": ">=9.0.0",
"typescript": "^5.0.0"
}
Expand Down
17 changes: 8 additions & 9 deletions packages/plugin/src/rules/runtime-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import {
filterPreprocessCompatData,
preprocessCompatData,
} from '@eslint-plugin-runtime-compat/data'
import { ESLintUtils } from '@typescript-eslint/utils'
import type { Node } from 'typescript'
import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils'
import { createRule } from './utils'

/**
Expand All @@ -31,7 +30,7 @@ export const runtimeCompatRule = (filterRuntimes: RuntimeName[]) =>
const runtimeCompatData = filterPreprocessCompatData(preprocessCompatData, filterRuntimes)

const reportMatchingError = (
node: Node,
node: TSESTree.Expression,
apiContext: keyof typeof runtimeCompatData,
apiId: string,
) => {
Expand All @@ -48,18 +47,18 @@ export const runtimeCompatRule = (filterRuntimes: RuntimeName[]) =>
const className = checker.typeToString(classType)

const apiId = JSON.stringify([className])
reportMatchingError(node as never, 'class', apiId)
reportMatchingError(node, 'class', apiId)
},
// Check compat for class property access
MemberExpression: (node) => {
const classType = services.getTypeAtLocation(node.object)
const className = checker.typeToString(classType)

const propertyType = services.getTypeAtLocation(node.property)
const propertyName = propertyType.getSymbol()?.escapedName

const apiId = JSON.stringify([className, propertyName])
reportMatchingError(node as never, 'classProperty', apiId)
const apiId = JSON.stringify([
className,
(node.property as TSESTree.PrivateIdentifier).name,
])
reportMatchingError(node, 'classProperty', apiId)
},
}
},
Expand Down
45 changes: 0 additions & 45 deletions packages/plugin/src/rules/tests/runtime-compat.test.ts

This file was deleted.

90 changes: 90 additions & 0 deletions packages/plugin/src/rules/tests/runtime-compat.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
type RuntimeName,
filterPreprocessCompatData,
objectKeys,
parseJsonKeys,
preprocessCompatData,
stringifyJsonKeys,
} from '@eslint-plugin-runtime-compat/data'
import type { InvalidTestCase } from '@typescript-eslint/rule-tester'
import { runtimeCompatRule } from '..'
import { ruleTester } from './setup'
import unsupportedApis from './unsupportedApis.json'

const filterRuntimes: RuntimeName[] = [
'bun',
'deno',
'edge-light',
'fastly',
'netlify',
'node',
'workerd',
]

const invalidTests: InvalidTestCase<never, []>[] = []
const runtimeCompatData = filterPreprocessCompatData(preprocessCompatData, filterRuntimes)

for (const apiContext of objectKeys(runtimeCompatData)) {
for (const [jsonKeys, apiInfo] of runtimeCompatData[apiContext].entries()) {
if (!Object.hasOwn(unsupportedApis, jsonKeys)) {
const keys = parseJsonKeys(jsonKeys)
switch (apiContext) {
case 'class': {
const testComment = `// Class instantiation: [${keys}]`
const errors = [{ message: `${apiContext} - ${apiInfo.error}` }]
invalidTests.push({
code: `
${testComment}
const _ClassName = ${keys[0]}
const _classInstance = new _ClassName()
`,
// @ts-expect-error message is legacy
errors,
})
invalidTests.push({
code: `
${testComment}
const _classInstance = new ${keys[0]}()
`,
// @ts-expect-error message is legacy
errors,
})
break
}
case 'classProperty': {
const testComment = `// Class property: [${keys}]`
const errors = [{ message: `${apiContext} - ${apiInfo.error}` }]

const classApiInfo = runtimeCompatData['class'].get(stringifyJsonKeys([keys[0]!]))
if (classApiInfo) {
errors.unshift({ message: `class - ${classApiInfo?.error}` })
}

invalidTests.push({
code: `
${testComment}
const _classInstance = new ${keys[0]}()
const _classProperty = _classInstance.${keys[1]}
`,
// @ts-expect-error message is legacy
errors,
})
break
}
case 'eventListener':
break
case 'global':
break
case 'globalClassProperty':
break
case 'misc':
break
}
}
}
}

ruleTester.run('runtime-compat', runtimeCompatRule(filterRuntimes), {
valid: [],
invalid: invalidTests,
})
Loading