From 2ec7e028ec228696a2514c10b703ad8d978cc90b Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Tue, 12 May 2026 10:39:37 +0800 Subject: [PATCH 1/2] refactor(eslint): modernize plugin deps and rule typing - Bump versions of @typescript-eslint/utils, @typescript-eslint/rule-tester, and related packages to 8.59.2 in pnpm-lock.yaml and package.json. - Refactor eslint-plugin-qwik rules to utilize utility functions for JSX attribute checks and improve code readability. - Remove redundant code and enhance type safety in various rule implementations. - Ensure consistent handling of JSX attributes across rules. --- .changeset/bright-impalas-fetch.md | 5 + packages/eslint-plugin-qwik/index.ts | 25 +-- packages/eslint-plugin-qwik/package.json | 4 +- packages/eslint-plugin-qwik/src/jsxAtag.ts | 11 +- packages/eslint-plugin-qwik/src/jsxImg.ts | 24 +-- packages/eslint-plugin-qwik/src/jsxKey.ts | 70 +++--- .../eslint-plugin-qwik/src/loaderLocation.ts | 2 +- .../src/noAwaitNavigateInUseTask.ts | 76 ++----- .../eslint-plugin-qwik/src/noReactProps.ts | 6 +- .../eslint-plugin-qwik/src/preferClasslist.ts | 6 +- .../eslint-plugin-qwik/src/scope-use-task.ts | 125 +++-------- .../src/serializerSignalUsage.ts | 200 +++++++---------- .../eslint-plugin-qwik/src/useAsyncTop.ts | 102 ++------- .../eslint-plugin-qwik/src/useMethodUsage.ts | 7 +- packages/eslint-plugin-qwik/src/utils.ts | 203 ++++++++++++++++++ .../src/validLexicalScope.ts | 52 ++--- pnpm-lock.yaml | 137 +++++++----- 17 files changed, 510 insertions(+), 545 deletions(-) create mode 100644 .changeset/bright-impalas-fetch.md create mode 100644 packages/eslint-plugin-qwik/src/utils.ts diff --git a/.changeset/bright-impalas-fetch.md b/.changeset/bright-impalas-fetch.md new file mode 100644 index 00000000000..f0b33c987e9 --- /dev/null +++ b/.changeset/bright-impalas-fetch.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-qwik': patch +--- + +chore: refactor(eslint): modernize plugin deps and rule typing diff --git a/packages/eslint-plugin-qwik/index.ts b/packages/eslint-plugin-qwik/index.ts index 0b71aaa76e8..b1a63b887d3 100644 --- a/packages/eslint-plugin-qwik/index.ts +++ b/packages/eslint-plugin-qwik/index.ts @@ -103,22 +103,17 @@ const qwikEslint9Plugin = { rules, } as const; -const recommendedConfig = [ - { - plugins: { - qwik: qwikEslint9Plugin, +const createFlatConfig = (rules: TSESLint.FlatConfig.Rules) => + [ + { + plugins: { + qwik: qwikEslint9Plugin, + }, + rules, }, - rules: recommendedRulesLevels, - }, -] satisfies TSESLint.FlatConfig.ConfigArray; + ] satisfies TSESLint.FlatConfig.ConfigArray; -const strictConfig = [ - { - plugins: { - qwik: qwikEslint9Plugin, - }, - rules: strictRulesLevels, - }, -] satisfies TSESLint.FlatConfig.ConfigArray; +const recommendedConfig = createFlatConfig(recommendedRulesLevels); +const strictConfig = createFlatConfig(strictRulesLevels); export { configs, qwikEslint9Plugin, rules }; diff --git a/packages/eslint-plugin-qwik/package.json b/packages/eslint-plugin-qwik/package.json index 882a3f7e4b6..f061c34d61d 100644 --- a/packages/eslint-plugin-qwik/package.json +++ b/packages/eslint-plugin-qwik/package.json @@ -5,14 +5,14 @@ "author": "Qwik Team", "bugs": "https://github.com/QwikDev/qwik/issues", "dependencies": { - "@typescript-eslint/utils": "^8.56.1", + "@typescript-eslint/utils": "^8.59.2", "jsx-ast-utils": "^3.3.5" }, "devDependencies": { "@qwik.dev/core": "workspace:*", "@qwik.dev/router": "workspace:*", "@types/estree": "1.0.8", - "@typescript-eslint/rule-tester": "8.56.1", + "@typescript-eslint/rule-tester": "8.59.2", "redent": "4.0.0" }, "engines": { diff --git a/packages/eslint-plugin-qwik/src/jsxAtag.ts b/packages/eslint-plugin-qwik/src/jsxAtag.ts index 2974f961016..d7fde7045ee 100644 --- a/packages/eslint-plugin-qwik/src/jsxAtag.ts +++ b/packages/eslint-plugin-qwik/src/jsxAtag.ts @@ -1,4 +1,5 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import { hasJsxAttribute } from './utils'; const createRule = ESLintUtils.RuleCreator(() => 'https://qwik.dev/docs/advanced/dollar/'); @@ -9,7 +10,6 @@ export const jsxAtag = createRule({ type: 'problem', docs: { description: 'For a perfect SEO score, always provide href attribute for elements.', - recommended: 'warn', }, fixable: 'code', schema: [], @@ -29,15 +29,10 @@ export const jsxAtag = createRule({ ); if (!hasSpread) { - const hasHref = node.openingElement.attributes.some( - (attr) => - attr.type === 'JSXAttribute' && - attr.name.type === 'JSXIdentifier' && - attr.name.name === 'href' - ); + const hasHref = hasJsxAttribute(node.openingElement.attributes, 'href'); if (!hasHref) { context.report({ - node: node as any, + node, messageId: 'noHref', }); } diff --git a/packages/eslint-plugin-qwik/src/jsxImg.ts b/packages/eslint-plugin-qwik/src/jsxImg.ts index 70139b59115..279eed7cc37 100644 --- a/packages/eslint-plugin-qwik/src/jsxImg.ts +++ b/packages/eslint-plugin-qwik/src/jsxImg.ts @@ -1,5 +1,6 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import { QwikEslintExamples } from '../examples'; +import { findJsxAttribute, hasJsxAttribute } from './utils'; const createRule = ESLintUtils.RuleCreator( (name) => `https://qwik.dev/docs/advanced/eslint/#${name}` @@ -40,12 +41,7 @@ See https://qwik.dev/docs/integrations/image-optimization/#responsive-images`, ); if (!hasSpread) { - const src = node.openingElement.attributes.find( - (attr) => - attr.type === 'JSXAttribute' && - attr.name.type === 'JSXIdentifier' && - attr.name.name === 'src' - ) as TSESTree.JSXAttribute | undefined; + const src = findJsxAttribute(node.openingElement.attributes, 'src'); if (src && src.value) { const literal: TSESTree.Literal | undefined = src.value.type === 'Literal' @@ -72,21 +68,11 @@ See https://qwik.dev/docs/integrations/image-optimization/#responsive-images`, } } - const hasWidth = node.openingElement.attributes.some( - (attr) => - attr.type === 'JSXAttribute' && - attr.name.type === 'JSXIdentifier' && - attr.name.name === 'width' - ); - const hasHeight = node.openingElement.attributes.some( - (attr) => - attr.type === 'JSXAttribute' && - attr.name.type === 'JSXIdentifier' && - attr.name.name === 'height' - ); + const hasWidth = hasJsxAttribute(node.openingElement.attributes, 'width'); + const hasHeight = hasJsxAttribute(node.openingElement.attributes, 'height'); if (!hasWidth || !hasHeight) { context.report({ - node: node as any, + node, messageId: 'noWidthHeight', }); } diff --git a/packages/eslint-plugin-qwik/src/jsxKey.ts b/packages/eslint-plugin-qwik/src/jsxKey.ts index 47279dc8d5c..73eab006a0d 100644 --- a/packages/eslint-plugin-qwik/src/jsxKey.ts +++ b/packages/eslint-plugin-qwik/src/jsxKey.ts @@ -1,12 +1,16 @@ import jsxAstUtils from 'jsx-ast-utils'; +import type { TSESTree } from '@typescript-eslint/utils'; import { QwikEslintExamples } from '../examples'; +import { hasJsxImportSourceComment } from './utils'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ -function isFunctionLikeExpression(node) { - return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression'; +function isFunctionLikeExpression( + node: TSESTree.Node | false | null | undefined +): node is TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression { + return node?.type === 'FunctionExpression' || node?.type === 'ArrowFunctionExpression'; } const defaultOptions = { @@ -60,19 +64,14 @@ export const jsxKey = { }, create(context) { - const sourceCode = context.sourceCode ?? context.getSourceCode(); - const modifyJsxSource = sourceCode - .getAllComments() - .some((c) => c.value.includes('@jsxImportSource')); - if (modifyJsxSource) { + if (hasJsxImportSourceComment(context)) { return {}; } const options = Object.assign({}, defaultOptions, context.options[0]); const checkFragmentShorthand = options.checkFragmentShorthand; - const checkKeyMustBeforeSpread = options.checkKeyMustBeforeSpread; const warnOnDuplicates = options.warnOnDuplicates; - function checkIteratorElement(node) { + function checkIteratorElement(node: TSESTree.Node) { if ( node.type === 'JSXElement' && !jsxAstUtils.hasProp(node.openingElement.attributes, 'key') @@ -89,7 +88,10 @@ export const jsxKey = { } } - function getReturnStatements(node, returnStatements: any[] = []) { + function getReturnStatements( + node: TSESTree.Node, + returnStatements: TSESTree.ReturnStatement[] = [] + ) { if (node.type === 'IfStatement') { if (node.consequent) { getReturnStatements(node.consequent, returnStatements); @@ -97,7 +99,7 @@ export const jsxKey = { if (node.alternate) { getReturnStatements(node.alternate, returnStatements); } - } else if (Array.isArray(node.body)) { + } else if (node.type === 'BlockStatement') { node.body.forEach((item) => { if (item.type === 'IfStatement') { getReturnStatements(item, returnStatements); @@ -112,27 +114,15 @@ export const jsxKey = { return returnStatements; } - function isKeyAfterSpread(attributes) { - let hasFoundSpread = false; - return attributes.some((attribute) => { - if (attribute.type === 'JSXSpreadAttribute') { - hasFoundSpread = true; - return false; - } - if (attribute.type !== 'JSXAttribute') { - return false; - } - return hasFoundSpread && jsxAstUtils.propName(attribute) === 'key'; - }); - } - /** * Checks if the given node is a function expression or arrow function, and checks if there is a * missing key prop in return statement's arguments * * @param {ASTNode} node */ - function checkFunctionsBlockStatement(node) { + function checkFunctionsBlockStatement( + node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression + ) { if (isFunctionLikeExpression(node)) { if (node.body.type === 'BlockStatement') { getReturnStatements(node.body) @@ -150,9 +140,12 @@ export const jsxKey = { * * @param {ASTNode} node */ - function checkArrowFunctionWithJSX(node) { - const isArrFn = node && node.type === 'ArrowFunctionExpression'; - const shouldCheckNode = (n) => n && (n.type === 'JSXElement' || n.type === 'JSXFragment'); + function checkArrowFunctionWithJSX( + node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression + ) { + const isArrFn = node.type === 'ArrowFunctionExpression'; + const shouldCheckNode = (n: TSESTree.Node | null | undefined) => + !!n && (n.type === 'JSXElement' || n.type === 'JSXFragment'); if (isArrFn && shouldCheckNode(node.body)) { checkIteratorElement(node.body); } @@ -196,16 +189,19 @@ export const jsxKey = { } const jsx = (node.type === 'ArrayExpression' ? node.elements : node.parent.children).filter( - (x) => x && x.type === 'JSXElement' + (x): x is TSESTree.JSXElement => !!x && x.type === 'JSXElement' ); if (jsx.length === 0) { return; } - const map = {}; + const keyGroups = new Map(); jsx.forEach((element) => { const attrs = element.openingElement.attributes; - const keys = attrs.filter((x) => x.name && x.name.name === 'key'); + const keys = attrs.filter( + (x): x is TSESTree.JSXAttribute => + x.type === 'JSXAttribute' && x.name.type === 'JSXIdentifier' && x.name.name === 'key' + ); if (keys.length === 0) { if (node.type === 'ArrayExpression') { @@ -218,10 +214,9 @@ export const jsxKey = { }); if (warnOnDuplicates) { - Object.values(map) - .filter((v: any) => v.length > 1) - .forEach((v: any) => { - v.forEach((n) => { + for (const group of keyGroups.values()) { + if (group.length > 1) { + group.forEach((n) => { if (!seen.has(n)) { seen.add(n); context.report({ @@ -230,7 +225,8 @@ export const jsxKey = { }); } }); - }); + } + } } }, diff --git a/packages/eslint-plugin-qwik/src/loaderLocation.ts b/packages/eslint-plugin-qwik/src/loaderLocation.ts index baf13e589e7..2f8c7a21739 100644 --- a/packages/eslint-plugin-qwik/src/loaderLocation.ts +++ b/packages/eslint-plugin-qwik/src/loaderLocation.ts @@ -54,7 +54,7 @@ If you understand this, you can disable this warning with: }, create(context) { const routesDir = context.options?.[0]?.routesDir ?? 'src/routes'; - const path = normalizePath(context.filename ?? context.getFilename()); + const path = normalizePath(context.filename); const isLayout = /\/layout(|!|-[^/]+)\.(j|t)sx?$/.test(path); const isIndex = /\/index(|!|@[^/]+)\.(j|t)sx?$/.test(path); const isPlugin = /\/plugin(|@[^/]+)\.(j|t)sx?$/.test(path); diff --git a/packages/eslint-plugin-qwik/src/noAwaitNavigateInUseTask.ts b/packages/eslint-plugin-qwik/src/noAwaitNavigateInUseTask.ts index 3096b64cf38..4435812739c 100644 --- a/packages/eslint-plugin-qwik/src/noAwaitNavigateInUseTask.ts +++ b/packages/eslint-plugin-qwik/src/noAwaitNavigateInUseTask.ts @@ -1,22 +1,16 @@ import type { Rule } from 'eslint'; -import type { - ArrowFunctionExpression, - CallExpression, - Expression, - FunctionExpression, - Node, - Pattern, -} from 'estree'; +import type { TSESTree } from '@typescript-eslint/utils'; +import { traverse } from './utils'; const USE_TASK_CALLEES = new Set(['useTask$', 'useTaskQrl']); -function isUseTaskCall(node: CallExpression): boolean { +function isUseTaskCall(node: TSESTree.CallExpression): boolean { return node.callee.type === 'Identifier' && USE_TASK_CALLEES.has(node.callee.name); } function getTaskCallback( - node: CallExpression -): ArrowFunctionExpression | FunctionExpression | null { + node: TSESTree.CallExpression +): TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | null { const arg0 = node.arguments[0]; if (arg0?.type === 'ArrowFunctionExpression' || arg0?.type === 'FunctionExpression') { return arg0; @@ -24,7 +18,7 @@ function getTaskCallback( return null; } -function isDeferUpdatesFalse(node: Expression | Pattern | undefined): boolean { +function isDeferUpdatesFalse(node: TSESTree.Expression | TSESTree.Pattern | undefined): boolean { if (!node) { return false; } @@ -35,12 +29,12 @@ function isDeferUpdatesFalse(node: Expression | Pattern | undefined): boolean { return true; } if (node.type === 'TSAsExpression') { - return isDeferUpdatesFalse(node.expression as Expression); + return isDeferUpdatesFalse(node.expression); } return false; } -function hasDeferUpdatesFalseOption(node: CallExpression): boolean { +function hasDeferUpdatesFalseOption(node: TSESTree.CallExpression): boolean { const opts = node.arguments[1]; if (!opts || opts.type !== 'ObjectExpression') { return false; @@ -55,18 +49,16 @@ function hasDeferUpdatesFalseOption(node: CallExpression): boolean { if (name !== 'deferUpdates') { continue; } - if (isDeferUpdatesFalse(prop.value as Expression | Pattern)) { + if (isDeferUpdatesFalse(prop.value)) { return true; } } return false; } -function collectUseNavigateBoundNamesFromNode(root: Node): Set { +function collectUseNavigateBoundNamesFromNode(root: TSESTree.Node): Set { const ids = new Set(); - const stack: Node[] = [root]; - while (stack.length) { - const n = stack.pop()!; + traverse(root, (n) => { if (n.type === 'VariableDeclarator' && n.id.type === 'Identifier' && n.init) { if ( n.init.type === 'CallExpression' && @@ -76,28 +68,13 @@ function collectUseNavigateBoundNamesFromNode(root: Node): Set { ids.add(n.id.name); } } - for (const key of Object.keys(n) as (keyof Node)[]) { - if (key === 'parent') { - continue; - } - const child = (n as unknown as Record)[key as string]; - if (Array.isArray(child)) { - for (const c of child) { - if (c && typeof c === 'object' && c !== null && 'type' in (c as object)) { - stack.push(c as Node); - } - } - } else if (child && typeof child === 'object' && 'type' in (child as object)) { - stack.push(child as Node); - } - } - } + }); return ids; } -function collectNavigateBindingsForUseTask(useTaskCall: CallExpression): Set { +function collectNavigateBindingsForUseTask(useTaskCall: TSESTree.CallExpression): Set { const ids = new Set(); - let current: Node | null = useTaskCall.parent; + let current: TSESTree.Node | null = useTaskCall.parent; while (current) { if (current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') { const p = current.parent; @@ -117,19 +94,17 @@ function collectNavigateBindingsForUseTask(useTaskCall: CallExpression): Set ) { - const stack: Node[] = [root]; - while (stack.length) { - const n = stack.pop()!; + traverse(root, (n) => { if (n.type === 'AwaitExpression') { const arg = n.argument; if (arg.type === 'CallExpression' && arg.callee.type === 'Identifier') { @@ -142,22 +117,7 @@ function reportAwaitedNavigateCalls( } } } - for (const key of Object.keys(n) as (keyof Node)[]) { - if (key === 'parent') { - continue; - } - const child = (n as unknown as Record)[key as string]; - if (Array.isArray(child)) { - for (const c of child) { - if (c && typeof c === 'object' && c !== null && 'type' in (c as object)) { - stack.push(c as Node); - } - } - } else if (child && typeof child === 'object' && 'type' in (child as object)) { - stack.push(child as Node); - } - } - } + }); } export const noAwaitNavigateInUseTask: Rule.RuleModule = { diff --git a/packages/eslint-plugin-qwik/src/noReactProps.ts b/packages/eslint-plugin-qwik/src/noReactProps.ts index f7cd57e20fa..e5f3ab7d9b0 100644 --- a/packages/eslint-plugin-qwik/src/noReactProps.ts +++ b/packages/eslint-plugin-qwik/src/noReactProps.ts @@ -1,6 +1,7 @@ import type { TSESLint } from '@typescript-eslint/utils'; import jsxAstUtils from 'jsx-ast-utils'; import { QwikEslintExamples } from '../examples'; +import { hasJsxImportSourceComment } from './utils'; const reactSpecificProps = [ { from: 'className', to: 'class' }, @@ -25,10 +26,7 @@ export const noReactProps = { }, }, create(context) { - const modifyJsxSource = context.sourceCode - .getAllComments() - .some((c) => c.value.includes('@jsxImportSource')); - if (modifyJsxSource) { + if (hasJsxImportSourceComment(context)) { return {}; } return { diff --git a/packages/eslint-plugin-qwik/src/preferClasslist.ts b/packages/eslint-plugin-qwik/src/preferClasslist.ts index 8efe1f4d178..d34036fad8e 100644 --- a/packages/eslint-plugin-qwik/src/preferClasslist.ts +++ b/packages/eslint-plugin-qwik/src/preferClasslist.ts @@ -3,6 +3,7 @@ import type { TSESTree as T } from '@typescript-eslint/utils'; import jsxAstUtils from 'jsx-ast-utils'; import { QwikEslintExamples } from '../examples'; +import { hasJsxImportSourceComment } from './utils'; export const preferClasslist = { meta: { @@ -39,10 +40,7 @@ export const preferClasslist = { }, }, create(context) { - const modifyJsxSource = context.sourceCode - .getAllComments() - .some((c) => c.value.includes('@jsxImportSource')); - if (modifyJsxSource) { + if (hasJsxImportSourceComment(context)) { return {}; } const classnames = context.options[0]?.classnames ?? ['cn', 'clsx', 'classnames']; diff --git a/packages/eslint-plugin-qwik/src/scope-use-task.ts b/packages/eslint-plugin-qwik/src/scope-use-task.ts index bb9bbce2fb1..622234e7de8 100644 --- a/packages/eslint-plugin-qwik/src/scope-use-task.ts +++ b/packages/eslint-plugin-qwik/src/scope-use-task.ts @@ -1,23 +1,11 @@ -import { Rule } from 'eslint'; +import type { Rule } from 'eslint'; import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils'; -import * as eslint from 'eslint'; // For Scope types +import type { TSESLint } from '@typescript-eslint/utils'; +import { isDescendantOf, resolveVariableForIdentifier, traverse } from './utils'; + const ISSERVER = 'isServer'; const GLOBALAPIS = ['process', '__dirname', '__filename', 'module']; const PRESETNODEAPIS = ['fs', 'os', 'path', 'child_process', 'http', 'https', 'Buffer']; -// Helper function: checks if a node is a descendant of another node -function isNodeDescendantOf(descendantNode, ancestorNode): boolean { - if (!ancestorNode) { - return false; - } - let current: TSESTree.Node | undefined = descendantNode.parent; - while (current) { - if (current === ancestorNode) { - return true; - } - current = current.parent; - } - return false; -} export const scopeUseTask: Rule.RuleModule = { meta: { @@ -25,8 +13,6 @@ export const scopeUseTask: Rule.RuleModule = { docs: { description: 'Disallow direct or indirect (via one-level function call) Node.js API usage in useTask$ without a server guard (e.g., isServer).', - category: 'Best Practices', - recommended: true, url: '', // Optional: URL to your rule's documentation }, fixable: undefined, @@ -71,7 +57,10 @@ export const scopeUseTask: Rule.RuleModule = { * @param functionContextNode - The function context node where the API or call resides. * @returns True if the node is properly guarded, false otherwise. */ - function isApiUsageGuarded(apiOrCallNode, functionContextNode): boolean { + function isApiUsageGuarded( + apiOrCallNode: TSESTree.Node, + functionContextNode: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression + ): boolean { let currentParentNode: TSESTree.Node | undefined = apiOrCallNode.parent; while ( currentParentNode && @@ -81,7 +70,7 @@ export const scopeUseTask: Rule.RuleModule = { if (currentParentNode.type === AST_NODE_TYPES.IfStatement) { const ifStatement = currentParentNode; if ( - isNodeDescendantOf(apiOrCallNode, ifStatement.consequent) || + isDescendantOf(apiOrCallNode, ifStatement.consequent) || apiOrCallNode === ifStatement.consequent ) { const testExpression = ifStatement.test; @@ -134,12 +123,12 @@ export const scopeUseTask: Rule.RuleModule = { * @returns True if the identifier is shadowed by such a declaration, false otherwise (implying * it might be a global API). */ - function isIdentifierShadowedByDeclaration(identifierNode): boolean { + function isIdentifierShadowedByDeclaration(identifierNode: TSESTree.Identifier): boolean { const scope = sourceCode.getScope(identifierNode); - let variable: eslint.Scope.Variable | undefined | null = null; + let variable: TSESLint.Scope.Variable | undefined | null = null; // Try to find the variable starting from the current scope and going upwards - let currentScopeForSearch: eslint.Scope.Scope | null = scope; + let currentScopeForSearch: TSESLint.Scope.Scope | null = scope; if (!GLOBALAPIS.includes(identifierNode.name)) { return true; @@ -192,20 +181,22 @@ export const scopeUseTask: Rule.RuleModule = { * @param callSiteNode The node where this function/expression was called/used from within * useTask$ (for reporting). */ - function analyzeNodeContent(nodeToAnalyze, functionContextForGuardCheck, callSiteNode) { + function analyzeNodeContent( + nodeToAnalyze: TSESTree.Node, + functionContextForGuardCheck: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, + callSiteNode: TSESTree.CallExpression + ) { // Internal recursive visitor - function internalVisitor(currentNode: TSESTree.Node): boolean { - // Returns true if an error was reported - if (!currentNode) { - return false; - } - + traverse(nodeToAnalyze, (currentNode) => { if (currentNode.type === AST_NODE_TYPES.Identifier) { if (forbiddenApis.has(currentNode.name)) { if (!isIdentifierShadowedByDeclaration(currentNode)) { if (!isApiUsageGuarded(currentNode, functionContextForGuardCheck)) { context.report({ - node: callSiteNode as any, + node: callSiteNode, messageId: 'unsafeApiUsageInCalledFunction', data: { apiName: currentNode.name, @@ -222,31 +213,8 @@ export const scopeUseTask: Rule.RuleModule = { } } } - - // Traverse child nodes - for (const key in currentNode) { - if (key === 'parent' || key === 'range' || key === 'loc') { - continue; - } - - const child = (currentNode as any)[key]; - if (Array.isArray(child)) { - for (const item of child) { - if (item && typeof item === 'object' && 'type' in item) { - if (internalVisitor(item as TSESTree.Node)) { - return true; - } - } - } - } else if (child && typeof child === 'object' && 'type' in child) { - if (internalVisitor(child as TSESTree.Node)) { - return true; - } - } - } return false; - } - internalVisitor(nodeToAnalyze); + }); } return { @@ -288,7 +256,7 @@ export const scopeUseTask: Rule.RuleModule = { // Ensure this identifier is directly within the useTask$ callback body. if ( currentUseTaskFunction.body !== node && - !isNodeDescendantOf(node, currentUseTaskFunction.body) + !isDescendantOf(node, currentUseTaskFunction.body) ) { return; } @@ -319,7 +287,7 @@ export const scopeUseTask: Rule.RuleModule = { if (!currentUseTaskFunction) { return; } - if (!isNodeDescendantOf(callNode, currentUseTaskFunction.body)) { + if (!isDescendantOf(callNode, currentUseTaskFunction.body)) { return; } if (isApiUsageGuarded(callNode, currentUseTaskFunction)) { @@ -328,48 +296,7 @@ export const scopeUseTask: Rule.RuleModule = { if (callNode.callee.type === AST_NODE_TYPES.Identifier) { const calleeIdentifierNode = callNode.callee; - const scopeOfCallee = sourceCode.getScope(calleeIdentifierNode); - let resolvedVariable: eslint.Scope.Variable | null = null; - - // Find the reference object for the calleeIdentifierNode - const ref = scopeOfCallee.references.find((r) => r.identifier === calleeIdentifierNode); - if (ref && ref.resolved) { - resolvedVariable = ref.resolved; - } else { - // Fallback: If not found as a direct reference (e.g. declared in same scope or complex cases), - // try to find variable by name walking up the scope chain. - let currentScopeToSearch: eslint.Scope.Scope | null = scopeOfCallee; - while (currentScopeToSearch) { - const v = currentScopeToSearch.variables.find( - (va) => va.name === calleeIdentifierNode.name - ); - if (v && v.defs.length > 0) { - // Ensure it has definitions - resolvedVariable = v; - break; - } - if (currentScopeToSearch.type === 'global' && !v) { - // If global and still not found, break - // Check globalScope explicitly for built-ins if not found as a declared variable - const globalVar = - currentScopeToSearch.type === 'global' - ? currentScopeToSearch.variables.find( - (gv) => gv.name === calleeIdentifierNode.name - ) - : null; - if (globalVar) { - resolvedVariable = globalVar; - } - break; - } - if (!currentScopeToSearch.upper) { - break; - } // No more upper scopes - currentScopeToSearch = currentScopeToSearch.upper; - } - } - - const variable = resolvedVariable; + const variable = resolveVariableForIdentifier(context, calleeIdentifierNode); if (variable && variable.defs.length > 0) { const definition = variable.defs[0]; // Assuming the first definition is the relevant one diff --git a/packages/eslint-plugin-qwik/src/serializerSignalUsage.ts b/packages/eslint-plugin-qwik/src/serializerSignalUsage.ts index 66aaaa6684e..8ae25ea070a 100644 --- a/packages/eslint-plugin-qwik/src/serializerSignalUsage.ts +++ b/packages/eslint-plugin-qwik/src/serializerSignalUsage.ts @@ -1,6 +1,6 @@ -// This was vibe-coded with AI import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import { QwikEslintExamples } from '../examples'; +import { findObjectProperty, isFunctionExpression, traverseWithParents } from './utils'; const createRule = ESLintUtils.RuleCreator( (name) => `https://qwik.dev/docs/advanced/eslint/#${name}` @@ -21,59 +21,34 @@ export const serializerSignalUsage = createRule({ }, defaultOptions: [], create(context) { - // Helper: check if an identifier looks like a signal/store by name function isSignalOrStoreName(name: string): boolean { return ( name.endsWith('Signal') || name.endsWith('Store') || name === 'signal' || name === 'store' ); } - // Helper: collect signals/stores used in a function body + function isAssignmentTarget(node: TSESTree.Node, parent: TSESTree.Node | null): boolean { + if (!parent) { + return false; + } + return ( + (parent.type === 'AssignmentExpression' && parent.left === node) || + (parent.type === 'UpdateExpression' && parent.argument === node) + ); + } + function collectSignalsFromFunction( - node: TSESTree.BlockStatement | TSESTree.Expression + fn: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression ): Set { const signals = new Set(); - const visited = new WeakSet(); const functionParams = new Set(); - // Collect function parameters if available - if ('params' in node && Array.isArray(node.params)) { - node.params.forEach((param) => { - if (param.type === 'Identifier') { - functionParams.add(param.name); - } - }); - } - - function isAssignmentTarget(node: TSESTree.Node): boolean { - const parent = (node as any).parent; - if (!parent) { - return false; - } - if (parent.type === 'AssignmentExpression' && parent.left === node) { - return true; - } - if (parent.type === 'UpdateExpression' && parent.argument === node) { - return true; - } - return false; - } - - function visit(n: any) { - if (!n || typeof n !== 'object' || visited.has(n)) { - return; - } - visited.add(n); - - // MemberExpression: obj.value or obj.prop - if (n.type === 'MemberExpression' && !isAssignmentTarget(n)) { - // Only support simple identifiers for now + traverseWithParents(fn.body, (n, parent) => { + if (n.type === 'MemberExpression' && !isAssignmentTarget(n, parent)) { if (n.object.type === 'Identifier' && !functionParams.has(n.object.name)) { - // Heuristic: treat as signal if .value, or as store if .prop if (n.property.type === 'Identifier' && n.property.name === 'value') { signals.add(n.object.name); } else if (n.property.type === 'Identifier') { - // If the object name looks like a store, track property if (isSignalOrStoreName(n.object.name)) { signals.add(`${n.object.name}.${n.property.name}`); } @@ -81,108 +56,77 @@ export const serializerSignalUsage = createRule({ } } - // Identifier: countSignal, myStore, etc. if (n.type === 'Identifier' && !functionParams.has(n.name) && isSignalOrStoreName(n.name)) { signals.add(n.name); } + }); - // Visit child nodes - for (const key in n) { - if (key === 'parent') { - continue; - } - const value = n[key]; - if (Array.isArray(value)) { - value.forEach(visit); - } else if (value && typeof value === 'object') { - (value as any).parent = n; // for assignment checking - visit(value); - } - } - } + return signals; + } - // If node is a block, visit all statements; else, visit the expression - if (node.type === 'BlockStatement') { - node.body.forEach(visit); - } else { - visit(node); + function getReturnedSerializerObject( + fn: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression + ): TSESTree.ObjectExpression | undefined { + if (fn.body.type === 'ObjectExpression') { + return fn.body; } - return signals; + if (fn.body.type !== 'BlockStatement') { + return undefined; + } + const returnStatement = fn.body.body.find( + (stmt): stmt is TSESTree.ReturnStatement => stmt.type === 'ReturnStatement' + ); + return returnStatement?.argument?.type === 'ObjectExpression' + ? returnStatement.argument + : undefined; } return { CallExpression(node: TSESTree.CallExpression) { if ( - node.callee.type === 'Identifier' && - (node.callee.name === 'useSerializer$' || node.callee.name === 'createSerializer$') + node.callee.type !== 'Identifier' || + (node.callee.name !== 'useSerializer$' && node.callee.name !== 'createSerializer$') ) { - const arg = node.arguments[0]; - if ( - arg && - (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression') - ) { - // Find the returned object - let returnValue: TSESTree.ObjectExpression | undefined; - if (arg.body.type === 'BlockStatement') { - const ret = arg.body.body.find((stmt) => stmt.type === 'ReturnStatement') as - | TSESTree.ReturnStatement - | undefined; - if (ret && ret.argument && ret.argument.type === 'ObjectExpression') { - returnValue = ret.argument; - } - } else if (arg.body.type === 'ObjectExpression') { - returnValue = arg.body; - } + return; + } - if (returnValue) { - const updateProp = returnValue.properties.find( - (p) => - p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'update' - ) as TSESTree.Property | undefined; - const deserializeProp = returnValue.properties.find( - (p) => - p.type === 'Property' && - p.key.type === 'Identifier' && - p.key.name === 'deserialize' - ) as TSESTree.Property | undefined; - - if (updateProp && deserializeProp) { - const updateFn = updateProp.value; - const deserializeFn = deserializeProp.value; - - // Only support function expressions for now - if ( - (updateFn.type === 'ArrowFunctionExpression' || - updateFn.type === 'FunctionExpression') && - (deserializeFn.type === 'ArrowFunctionExpression' || - deserializeFn.type === 'FunctionExpression') - ) { - const updateSignals = collectSignalsFromFunction(updateFn.body); - const deserializeSignals = collectSignalsFromFunction(deserializeFn.body); - - // Check both directions - const missingInDeserialize = Array.from(updateSignals).filter( - (signal) => !deserializeSignals.has(signal) - ); - const missingInUpdate = Array.from(deserializeSignals).filter( - (signal) => !updateSignals.has(signal) - ); - - const allMissingSignals = [...missingInDeserialize, ...missingInUpdate]; - - if (allMissingSignals.length > 0) { - context.report({ - node: updateProp, - messageId: 'serializerSignalMismatch', - data: { - signals: allMissingSignals.join(', '), - }, - }); - } - } - } - } - } + const arg = node.arguments[0]; + if (!isFunctionExpression(arg)) { + return; + } + + const returnValue = getReturnedSerializerObject(arg); + if (!returnValue) { + return; + } + + const updateProp = findObjectProperty(returnValue, 'update'); + const deserializeProp = findObjectProperty(returnValue, 'deserialize'); + const updateFn = updateProp?.value; + const deserializeFn = deserializeProp?.value; + if ( + !updateProp || + !isFunctionExpression(updateFn) || + !isFunctionExpression(deserializeFn) + ) { + return; + } + + const updateSignals = collectSignalsFromFunction(updateFn); + const deserializeSignals = collectSignalsFromFunction(deserializeFn); + const allMissingSignals = [ + ...Array.from(updateSignals).filter((signal) => !deserializeSignals.has(signal)), + ...Array.from(deserializeSignals).filter((signal) => !updateSignals.has(signal)), + ]; + + if (allMissingSignals.length > 0) { + context.report({ + node: updateProp, + messageId: 'serializerSignalMismatch', + data: { + signals: allMissingSignals.join(', '), + }, + }); } }, }; diff --git a/packages/eslint-plugin-qwik/src/useAsyncTop.ts b/packages/eslint-plugin-qwik/src/useAsyncTop.ts index 95eda57b28e..46660dfaafc 100644 --- a/packages/eslint-plugin-qwik/src/useAsyncTop.ts +++ b/packages/eslint-plugin-qwik/src/useAsyncTop.ts @@ -1,58 +1,16 @@ -import { Rule } from 'eslint'; -import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils'; +import type { Rule } from 'eslint'; +import { ESLintUtils, TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils'; +import type { TSESLint } from '@typescript-eslint/utils'; import ts from 'typescript'; - -// Utility type to handle nodes from Rule handlers which may come from @types/estree -// The nodes from Rule handlers are compatible with TSESTree at runtime but not at the type level -// because @types/estree uses string literals while TSESTree uses AST_NODE_TYPES enum -type RuleNode = { - type: any; - parent?: any; - [key: string]: any; -}; - -function isFromQwikModule(resolvedVar: any): boolean { - return resolvedVar?.defs?.some((def: any) => { - if (def.type !== 'ImportBinding') { - return false; - } - const importSource = def.parent.source.value; - return ( - importSource.startsWith('@qwik.dev/core') || - importSource.startsWith('@qwik.dev/router') || - importSource.startsWith('@builder.io/qwik') || - importSource.startsWith('@builder.io/qwik-city') - ); - }); -} - -function resolveVariableForIdentifier(context: Rule.RuleContext, ident: any) { - const scope = context.sourceCode.getScope(ident); - const ref = scope.references.find((r) => r.identifier === ident); - if (ref && ref.resolved) { - return ref.resolved; - } - // Fallback lookup walking up scopes by name - let current: any = scope; - while (current) { - const found = current.variables.find((v: any) => v.name === ident.name); - if (found) { - return found; - } - current = current.upper; - } - return null; -} +import { + findContainingFunction, + isFromQwikModule, + isQwikQrlCallee, + resolveVariableForIdentifier, +} from './utils'; function isQrlCallee(context: Rule.RuleContext, callee: TSESTree.Identifier): boolean { - if (!callee || callee.type !== AST_NODE_TYPES.Identifier) { - return false; - } - if (!callee.name.endsWith('$')) { - return false; - } - const resolved = resolveVariableForIdentifier(context, callee); - return isFromQwikModule(resolved); + return isQwikQrlCallee(context, callee); } function getFirstStatementIfValueRead( @@ -74,13 +32,15 @@ function getFirstStatementIfValueRead( return null; } -function isAsyncIdentifier(context: Rule.RuleContext, ident: any): boolean { +function isAsyncIdentifier(context: Rule.RuleContext, ident: TSESTree.Identifier): boolean { const variable = resolveVariableForIdentifier(context, ident); if (!variable || (variable.defs && variable.defs.length === 0)) { return false; } - const services: any = (context as any).sourceCode.parserServices; + const services = ESLintUtils.getParserServices( + context as unknown as TSESLint.RuleContext + ); const checker: ts.TypeChecker | undefined = services?.program?.getTypeChecker(); const esTreeNodeToTSNodeMap = services?.esTreeNodeToTSNodeMap; @@ -151,7 +111,7 @@ function isAsyncIdentifier(context: Rule.RuleContext, ident: any): boolean { if (def.type === 'ImportBinding' && checker && esTreeNodeToTSNodeMap) { try { // Map the identifier to TS node & promise symbol (following re-exports) - const tsNode = esTreeNodeToTSNodeMap.get(ident as any); + const tsNode = esTreeNodeToTSNodeMap.get(ident); if (tsNode) { let symbol = checker.getSymbolAtLocation(tsNode); if (symbol && symbol.flags & ts.SymbolFlags.Alias) { @@ -188,7 +148,7 @@ function isAsyncIdentifier(context: Rule.RuleContext, ident: any): boolean { // As a fallback, try using TypeScript type information if available if (checker && esTreeNodeToTSNodeMap) { try { - const tsNode = esTreeNodeToTSNodeMap.get(ident as any); + const tsNode = esTreeNodeToTSNodeMap.get(ident); const type = checker.getTypeAtLocation(tsNode); const typeStr = checker.typeToString(type.getNonNullableType()); // Heuristic: type name includes Async or LoaderSignal @@ -205,7 +165,7 @@ function isAsyncIdentifier(context: Rule.RuleContext, ident: any): boolean { function hasAwaitPromiseBefore( body: TSESTree.BlockStatement, - beforeStmt: RuleNode | RuleNode, + beforeStmt: TSESTree.ExpressionStatement | TSESTree.ReturnStatement, identifierName: string ): boolean { for (const stmt of body.body) { @@ -241,34 +201,10 @@ function hasAwaitPromiseBefore( return false; } -function findContainingFunction( - node: RuleNode -): - | RuleNode - | RuleNode - | RuleNode - | null { - let current: any = node; - while (current) { - if ( - current.type === AST_NODE_TYPES.FunctionDeclaration || - current.type === AST_NODE_TYPES.FunctionExpression || - current.type === AST_NODE_TYPES.ArrowFunctionExpression - ) { - return current; - } - current = current.parent; - } - return null; -} - function shouldCheckFunction( context: Rule.RuleContext, - fn: - | RuleNode - | RuleNode - | RuleNode, - signalIdent: RuleNode + fn: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, + signalIdent: TSESTree.Identifier ): boolean { // Check if this function is directly passed to a QRL (e.g., component$, $, etc.) const parent = fn.parent; diff --git a/packages/eslint-plugin-qwik/src/useMethodUsage.ts b/packages/eslint-plugin-qwik/src/useMethodUsage.ts index 8bc7522b27b..8237b798c90 100644 --- a/packages/eslint-plugin-qwik/src/useMethodUsage.ts +++ b/packages/eslint-plugin-qwik/src/useMethodUsage.ts @@ -1,6 +1,7 @@ import type { Rule } from 'eslint'; import type { CallExpression } from 'estree'; import type { QwikEslintExamples } from '../examples'; +import { hasJsxImportSourceComment } from './utils'; export const useMethodUsage: Rule.RuleModule = { meta: { @@ -16,11 +17,7 @@ export const useMethodUsage: Rule.RuleModule = { }, }, create(context) { - const sourceCode = context.sourceCode ?? context.getSourceCode(); - const modifyJsxSource = sourceCode - .getAllComments() - .some((c) => c.value.includes('@jsxImportSource')); - if (modifyJsxSource) { + if (hasJsxImportSourceComment(context)) { return {}; } return { diff --git a/packages/eslint-plugin-qwik/src/utils.ts b/packages/eslint-plugin-qwik/src/utils.ts new file mode 100644 index 00000000000..55acb1fdff3 --- /dev/null +++ b/packages/eslint-plugin-qwik/src/utils.ts @@ -0,0 +1,203 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +type NodeWithParent = TSESTree.Node & { + parent?: NodeWithParent; + [key: string]: unknown; +}; + +type RuleContextLike = { + sourceCode: TSESLint.SourceCode; +}; + +const QWIK_MODULE_PREFIXES = [ + '@qwik.dev/core', + '@qwik.dev/router', + '@builder.io/qwik', + '@builder.io/qwik-city', +]; + +export function hasJsxImportSourceComment(context: RuleContextLike): boolean { + return context.sourceCode + .getAllComments() + .some((comment) => comment.value.includes('@jsxImportSource')); +} + +export function isAstNode(value: unknown): value is TSESTree.Node { + return ( + !!value && typeof value === 'object' && typeof (value as { type?: unknown }).type === 'string' + ); +} + +export function forEachChild( + node: TSESTree.Node, + visit: (child: TSESTree.Node) => boolean | void +): boolean { + for (const [key, value] of Object.entries(node as Record)) { + if (key === 'parent' || key === 'range' || key === 'loc') { + continue; + } + if (Array.isArray(value)) { + for (const item of value) { + if (isAstNode(item) && visit(item)) { + return true; + } + } + continue; + } + if (isAstNode(value) && visit(value)) { + return true; + } + } + return false; +} + +export function traverse( + node: TSESTree.Node, + visit: (node: TSESTree.Node) => boolean | void +): boolean { + if (visit(node)) { + return true; + } + return forEachChild(node, (child) => traverse(child, visit)); +} + +export function traverseWithParents( + node: TSESTree.Node, + visit: (node: TSESTree.Node, parent: TSESTree.Node | null) => boolean | void, + parent: TSESTree.Node | null = null +): boolean { + if (visit(node, parent)) { + return true; + } + return forEachChild(node, (child) => traverseWithParents(child, visit, node)); +} + +export function isDescendantOf( + descendant: TSESTree.Node, + ancestor: TSESTree.Node | null | undefined +): boolean { + let current = (descendant as NodeWithParent).parent; + while (current) { + if (current === ancestor) { + return true; + } + current = current.parent; + } + return false; +} + +export function resolveVariableForIdentifier( + context: RuleContextLike, + identifier: TSESTree.Identifier +): TSESLint.Scope.Variable | null { + const scope = context.sourceCode.getScope(identifier); + const reference = scope.references.find((ref) => ref.identifier === identifier); + if (reference?.resolved) { + return reference.resolved; + } + + let currentScope: TSESLint.Scope.Scope | null = scope; + while (currentScope) { + const variable = currentScope.variables.find((item) => item.name === identifier.name); + if (variable) { + return variable; + } + currentScope = currentScope.upper; + } + return null; +} + +export function isFromQwikModule(variable: TSESLint.Scope.Variable | null | undefined): boolean { + return !!variable?.defs.some((def) => { + if (def.type !== 'ImportBinding') { + return false; + } + const source = def.parent.source.value; + return ( + typeof source === 'string' && QWIK_MODULE_PREFIXES.some((prefix) => source.startsWith(prefix)) + ); + }); +} + +export function isQwikQrlCallee( + context: RuleContextLike, + callee: TSESTree.Node | null | undefined +): callee is TSESTree.Identifier { + return ( + callee?.type === 'Identifier' && + callee.name.endsWith('$') && + isFromQwikModule(resolveVariableForIdentifier(context, callee)) + ); +} + +export function getStaticPropertyName( + property: TSESTree.Property | TSESTree.MethodDefinition | TSESTree.JSXAttribute +): string | null { + const key = property.type === 'JSXAttribute' ? property.name : property.key; + if (key.type === 'Identifier' || key.type === 'JSXIdentifier') { + return key.name; + } + if (key.type === 'Literal') { + return String(key.value); + } + return null; +} + +export function findObjectProperty( + objectExpression: TSESTree.ObjectExpression, + name: string +): TSESTree.Property | undefined { + return objectExpression.properties.find( + (property): property is TSESTree.Property => + property.type === 'Property' && !property.computed && getStaticPropertyName(property) === name + ); +} + +export function isFunctionExpression( + node: TSESTree.Node | null | undefined +): node is TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression { + return node?.type === 'ArrowFunctionExpression' || node?.type === 'FunctionExpression'; +} + +export function findJsxAttribute( + attributes: TSESTree.JSXOpeningElement['attributes'], + name: string +): TSESTree.JSXAttribute | undefined { + return attributes.find( + (attribute): attribute is TSESTree.JSXAttribute => + attribute.type === 'JSXAttribute' && + attribute.name.type === 'JSXIdentifier' && + attribute.name.name === name + ); +} + +export function hasJsxAttribute( + attributes: TSESTree.JSXOpeningElement['attributes'], + name: string +): boolean { + return !!findJsxAttribute(attributes, name); +} + +export function findContainingFunction( + node: TSESTree.Node +): + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | null { + let current: NodeWithParent | undefined = node as NodeWithParent; + while (current) { + if ( + current.type === 'ArrowFunctionExpression' || + current.type === 'FunctionDeclaration' || + current.type === 'FunctionExpression' + ) { + return current as + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression; + } + current = current.parent; + } + return null; +} diff --git a/packages/eslint-plugin-qwik/src/validLexicalScope.ts b/packages/eslint-plugin-qwik/src/validLexicalScope.ts index 2d948c74eaf..503ad724513 100644 --- a/packages/eslint-plugin-qwik/src/validLexicalScope.ts +++ b/packages/eslint-plugin-qwik/src/validLexicalScope.ts @@ -1,8 +1,7 @@ import * as ESLintUtils from '@typescript-eslint/utils/eslint-utils'; import ts from 'typescript'; -import type { Identifier } from 'estree'; import redent from 'redent'; -import type { RuleContext, Scope } from '@typescript-eslint/utils/dist/ts-eslint'; +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { QwikEslintExamples } from '../examples'; const createRule = ESLintUtils.RuleCreator( @@ -12,6 +11,13 @@ const createRule = ESLintUtils.RuleCreator( interface DetectorOptions { allowAny: boolean; } + +type ValidLexicalScopeMessage = 'invalidJsxDollar' | 'mutableIdentifier' | 'referencesOutside'; +type SourceTextContext = Pick< + TSESLint.RuleContext, + 'sourceCode' +>; + export const validLexicalScope = createRule({ name: 'valid-lexical-scope', defaultOptions: [ @@ -60,12 +66,12 @@ export const validLexicalScope = createRule({ const services = ESLintUtils.getParserServices(context); const esTreeNodeToTSNodeMap = services.esTreeNodeToTSNodeMap; const typeChecker = services.program.getTypeChecker(); - const relevantScopes: Map = new Map(); + const relevantScopes = new Map(); - function walkScope(scope: Scope.Scope) { + function walkScope(scope: TSESLint.Scope.Scope) { scope.references.forEach((ref) => { const declaredVariable = ref.resolved; - const declaredScope = ref.resolved?.scope as Scope.Scope; + const declaredScope = ref.resolved?.scope as TSESLint.Scope.Scope; if (declaredVariable && declaredScope) { const variableType = declaredVariable.defs.at(0)?.type; if (variableType === 'Type') { @@ -79,7 +85,7 @@ export const validLexicalScope = createRule({ return; } - let dollarScope: Scope.Scope | null = ref.from; + let dollarScope: TSESLint.Scope.Scope | null = ref.from; let dollarIdentifier: string | undefined; while (dollarScope) { dollarIdentifier = relevantScopes.get(dollarScope); @@ -99,7 +105,7 @@ export const validLexicalScope = createRule({ } const identifier = ref.identifier; const tsNode = esTreeNodeToTSNodeMap.get(identifier); - let ownerDeclared: Scope.Scope | null = declaredScope; + let ownerDeclared: TSESLint.Scope.Scope | null = declaredScope; while (ownerDeclared) { if (relevantScopes.has(ownerDeclared)) { break; @@ -222,21 +228,21 @@ export const validLexicalScope = createRule({ } }, 'Program:exit'() { - walkScope(scopeManager.globalScope! as any); + walkScope(scopeManager.globalScope!); }, }; }, }); function canCapture( - context: RuleContext, + context: SourceTextContext, checker: ts.TypeChecker, node: ts.Node, - ident: Identifier, + ident: TSESTree.Identifier, opts: DetectorOptions ) { const type = checker.getTypeAtLocation(node); - const seen = new Set(); + const seen = new Set(); return isTypeCapturable(context, checker, type, node, ident, opts, seen); } @@ -259,13 +265,13 @@ function humanizeTypeReason(reason: TypeReason) { } function isTypeCapturable( - context: RuleContext, + context: SourceTextContext, checker: ts.TypeChecker, type: ts.Type, tsnode: ts.Node, - ident: Identifier, + ident: TSESTree.Identifier, opts: DetectorOptions, - seen: Set + seen: Set ): TypeReason | undefined { const result = _isTypeCapturable(context, checker, type, tsnode, opts, 0, seen); if (result) { @@ -278,13 +284,13 @@ function isTypeCapturable( return result; } function _isTypeCapturable( - context: RuleContext, + context: SourceTextContext, checker: ts.TypeChecker, type: ts.Type, node: ts.Node, opts: DetectorOptions, level: number, - seen: Set + seen: Set ): TypeReason | undefined { // NoSerialize is ok if (seen.has(type)) { @@ -451,13 +457,13 @@ function _isTypeCapturable( } function isSymbolCapturable( - context: RuleContext, + context: SourceTextContext, checker: ts.TypeChecker, symbol: ts.Symbol, node: ts.Node, opts: DetectorOptions, level: number, - seen: Set + seen: Set ) { const type = checker.getTypeOfSymbolAtLocation(symbol, node); return _isTypeCapturable(context, checker, type, node, opts, level, seen); @@ -488,16 +494,14 @@ function getSignalValueProperty(type: ts.Type): ts.Symbol | undefined { } function getElementTypeOfArrayType(type: ts.Type, checker: ts.TypeChecker): ts.Type | undefined { - return (checker as any).getElementTypeOfArrayType(type); + return checker.getElementTypeOfArrayType(type); } function getTypesOfTupleType( type: ts.Type, checker: ts.TypeChecker ): readonly ts.Type[] | undefined { - return (checker as any).isTupleType(type) - ? checker.getTypeArguments(type as ts.TupleType) - : undefined; + return checker.isTupleType(type) ? checker.getTypeArguments(type as ts.TupleType) : undefined; } function isTypeQRL(type: ts.Type): boolean { @@ -516,7 +520,7 @@ function getContent(symbol: ts.Symbol, sourceCode: string) { return ''; } -function isQwikHook(variable, context) { +function isQwikHook(variable: TSESLint.Scope.Variable, context: SourceTextContext) { const def = variable.defs[0]; if (!def || def.type !== 'Variable') { return false; @@ -537,7 +541,7 @@ function isQwikHook(variable, context) { return false; } -function isFromQwikModule(resolvedVar) { +function isFromQwikModule(resolvedVar: TSESLint.Scope.Variable) { return resolvedVar.defs.some((def) => { if (def.type !== 'ImportBinding') { return false; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f86018f64a..fcd2c07e21f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -630,8 +630,8 @@ importers: packages/eslint-plugin-qwik: dependencies: '@typescript-eslint/utils': - specifier: ^8.56.1 - version: 8.58.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.59.2 + version: 8.59.2(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) jsx-ast-utils: specifier: ^3.3.5 version: 3.3.5 @@ -646,8 +646,8 @@ importers: specifier: 1.0.8 version: 1.0.8 '@typescript-eslint/rule-tester': - specifier: 8.56.1 - version: 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.59.2 + version: 8.59.2(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) redent: specifier: 4.0.0 version: 4.0.0 @@ -4602,6 +4602,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: 5.9.3 + '@typescript-eslint/parser@8.59.2': + resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: 5.9.3 + '@typescript-eslint/project-service@8.56.1': resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4614,17 +4621,18 @@ packages: peerDependencies: typescript: 5.9.3 - '@typescript-eslint/project-service@8.58.1': - resolution: {integrity: sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==} + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: 5.9.3 - '@typescript-eslint/rule-tester@8.56.1': - resolution: {integrity: sha512-EWuV5Vq1EFYJEOVcILyWPO35PjnT0c6tv99PCpD12PgfZae5/Jo+F17hGjsEs2Moe+Dy1J7KIr8y037cK8+/rQ==} + '@typescript-eslint/rule-tester@8.59.2': + resolution: {integrity: sha512-u6yY503P7E76xIzIQw2R6FCJwwifh0fOJsOWtkpEPeUUVmUApi1Hdnahz5mKSqRDi5wUN+iiUBedM0qZ41owYw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: 5.9.3 '@typescript-eslint/scope-manager@8.56.1': resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} @@ -4634,8 +4642,8 @@ packages: resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.58.1': - resolution: {integrity: sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==} + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/tsconfig-utils@8.56.1': @@ -4650,8 +4658,8 @@ packages: peerDependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.58.1': - resolution: {integrity: sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==} + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: 5.9.3 @@ -4678,8 +4686,8 @@ packages: resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.58.1': - resolution: {integrity: sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==} + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.56.1': @@ -4694,8 +4702,8 @@ packages: peerDependencies: typescript: 5.9.3 - '@typescript-eslint/typescript-estree@8.58.1': - resolution: {integrity: sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==} + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: 5.9.3 @@ -4714,8 +4722,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: 5.9.3 - '@typescript-eslint/utils@8.58.1': - resolution: {integrity: sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==} + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4729,8 +4737,8 @@ packages: resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.58.1': - resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript/analyze-trace@0.10.1': @@ -4739,6 +4747,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unpic/core@0.0.42': resolution: {integrity: sha512-K5Di+P8Bijl7doGDBGU+5VqX44e2iXMEm7G/AVla+9Hgxb5rOm/OXZoOjCUNjx5BOnjsVyegP6vlgsTfBtymkQ==} @@ -15227,10 +15236,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.59.2(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + eslint: 10.0.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) - '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -15238,44 +15259,44 @@ snapshots: '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) - '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.1(supports-color@10.2.2)(typescript@5.9.3)': + '@typescript-eslint/project-service@8.59.2(supports-color@10.2.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) - '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 debug: 4.4.3(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.59.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) - '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/rule-tester@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/rule-tester@8.59.2(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/parser': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.2(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) ajv: 6.14.0 eslint: 10.0.3(jiti@2.6.1) json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 semver: 7.7.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript '@typescript-eslint/scope-manager@8.56.1': dependencies: @@ -15287,10 +15308,10 @@ snapshots: '@typescript-eslint/types': 8.57.1 '@typescript-eslint/visitor-keys': 8.57.1 - '@typescript-eslint/scope-manager@8.58.1': + '@typescript-eslint/scope-manager@8.59.2': dependencies: - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/visitor-keys': 8.58.1 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': dependencies: @@ -15300,7 +15321,7 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.58.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -15332,7 +15353,7 @@ snapshots: '@typescript-eslint/types@8.57.1': {} - '@typescript-eslint/types@8.58.1': {} + '@typescript-eslint/types@8.59.2': {} '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': dependencies: @@ -15364,12 +15385,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.58.1(supports-color@10.2.2)(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.59.2(supports-color@10.2.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.58.1(supports-color@10.2.2)(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/visitor-keys': 8.58.1 + '@typescript-eslint/project-service': 8.59.2(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.5 semver: 7.7.3 @@ -15379,12 +15400,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.58.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.59.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.58.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/visitor-keys': 8.58.1 + '@typescript-eslint/project-service': 8.59.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 minimatch: 10.2.5 semver: 7.7.3 @@ -15416,12 +15437,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.58.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.59.2(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.58.1 - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) eslint: 10.0.3(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -15437,9 +15458,9 @@ snapshots: '@typescript-eslint/types': 8.57.1 eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.58.1': + '@typescript-eslint/visitor-keys@8.59.2': dependencies: - '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/types': 8.59.2 eslint-visitor-keys: 5.0.1 '@typescript/analyze-trace@0.10.1': @@ -16796,7 +16817,7 @@ snapshots: detective-typescript@14.0.0(supports-color@10.2.2)(typescript@5.9.3): dependencies: - '@typescript-eslint/typescript-estree': 8.58.1(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.2(supports-color@10.2.2)(typescript@5.9.3) ast-module-types: 6.0.1 node-source-walk: 7.0.1 typescript: 5.9.3 @@ -16805,7 +16826,7 @@ snapshots: detective-typescript@14.0.0(typescript@5.9.3): dependencies: - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) ast-module-types: 6.0.1 node-source-walk: 7.0.1 typescript: 5.9.3 From 061058d7b92f9c59a8b88dadf2af1d032f94ef16 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Tue, 12 May 2026 11:13:57 +0800 Subject: [PATCH 2/2] fix Error fetching release: Request failed with status code 404 --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c448f5737be..7055e1d1a59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -316,8 +316,12 @@ jobs: registry-url: https://registry.npmjs.org/ - run: pnpm install + # Pin wasm-pack: npm 0.14.0+ downloads from drager/wasm-pack releases, which are + # currently missing on GitHub → "Error fetching release: 404". 0.13.1 uses rustwasm + # URLs that redirect to wasm-bindgen/wasm-pack assets. Local dev often uses + # `cargo install wasm-pack` (CONTRIBUTING) and avoids this path. - if: matrix.settings.wasm - run: pnpm install -w wasm-pack + run: pnpm install -w wasm-pack@0.13.1 - name: Lint check if: matrix.settings.wasm