Skip to content

fix(eslint-plugin): detect node API usage more accurately #7664

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

Open
wants to merge 17 commits into
base: build/v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loud-mammals-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-qwik': patch
---

FIX: eslint-plugin: detect node API usage more accurately
47 changes: 13 additions & 34 deletions packages/eslint-plugin-qwik/src/scope-use-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Rule } from 'eslint';
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
import * as eslint from 'eslint'; // For Scope types
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) {
Expand Down Expand Up @@ -35,18 +37,7 @@ export const scopeUseTask: Rule.RuleModule = {
forbiddenApis: {
type: 'array',
items: { type: 'string' },
default: [
'process',
'fs',
'os',
'path',
'child_process',
'http',
'https',
'Buffer',
'__dirname',
'__filename',
],
default: PRESETNODEAPIS,
},
},
additionalProperties: false,
Expand All @@ -62,18 +53,7 @@ export const scopeUseTask: Rule.RuleModule = {
create(context: Rule.RuleContext): Rule.RuleListener {
const options = context.options[0] || {};
const forbiddenApis = new Set<string>(
options.forbiddenApis || [
'process',
'fs',
'os',
'path',
'child_process',
'http',
'https',
'Buffer',
'__dirname',
'__filename',
]
options.forbiddenApis || PRESETNODEAPIS.concat(GLOBALAPIS)
);
const serverGuardIdentifier: string = ISSERVER;
const sourceCode = context.sourceCode;
Expand All @@ -93,7 +73,6 @@ export const scopeUseTask: Rule.RuleModule = {
*/
function isApiUsageGuarded(apiOrCallNode, functionContextNode): boolean {
let currentParentNode: TSESTree.Node | undefined = apiOrCallNode.parent;

while (
currentParentNode &&
currentParentNode !== functionContextNode.body &&
Expand Down Expand Up @@ -161,6 +140,11 @@ export const scopeUseTask: Rule.RuleModule = {

// Try to find the variable starting from the current scope and going upwards
let currentScopeForSearch: eslint.Scope.Scope | null = scope;

if (!GLOBALAPIS.includes(identifierNode.name)) {
return true;
}

while (currentScopeForSearch) {
const foundVar = currentScopeForSearch.variables.find(
(v) => v.name === identifierNode.name
Expand All @@ -178,29 +162,25 @@ export const scopeUseTask: Rule.RuleModule = {
currentScopeForSearch = currentScopeForSearch.upper;
}

if (!variable) {
// Cannot find variable, assume it's not a shadowed global for safety,
// though this state implies an undeclared variable (another ESLint rule should catch this).
return false;
}
// If we didn't find a variable, it might be a global API or an undeclared variable.

if (variable.defs.length === 0) {
if (!variable || variable.defs.length === 0) {
// No definitions usually means it's an implicit global (e.g., 'process' in Node.js environment).
// Such a variable is NOT considered "shadowed by a user declaration".
return false;
}

// If there are definitions, check if any of them are standard declaration types.
// This means the identifier refers to a user-declared variable, parameter, function, class, or an import.
return variable.defs.some((def) => {
return variable?.defs.some((def) => {
return (
def.type === 'Variable' ||
def.type === 'Parameter' ||
def.type === 'FunctionName' ||
def.type === 'ClassName' ||
def.type === 'ImportBinding'
);
});
}) as boolean;
}

/**
Expand Down Expand Up @@ -418,7 +398,6 @@ export const scopeUseTask: Rule.RuleModule = {
targetFunctionNode.body.type === AST_NODE_TYPES.BlockStatement
? targetFunctionNode.body
: targetFunctionNode.body;

analyzeNodeContent(nodeToAnalyze, targetFunctionNode, callNode);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
// Expect error: { "messageId": "unsafeApiUsageInCalledFunction" }

import { component$, useTask$ } from '@qwik.dev/core';

export default component$(() => {
useTask$(() => {
function foo() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
// Expect error: { "messageId": "unsafeApiUsage" }

import { component$, useSignal, useTask$ } from '@qwik.dev/core';
// Expect error: { "messageId": "unsafeApiUsage" }
// Expect error: { "messageId": "unsafeApiUsage" }
// Expect error: { "messageId": "unsafeApiUsage" }
import { component$, isBrowser, useSignal, useTask$ } from '@qwik.dev/core';

export default component$(() => {
const s = useSignal(0);
useTask$(({ track }) => {
track(() => {
if (isBrowser) {
process.env;
const m = process;
}
process.env;
const m = process;
return s.value;
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { component$, isServer, useTask$ } from '@qwik.dev/core';

import path from 'path';
export default component$(() => {
useTask$(() => {
function child_process() {}
function foo() {
if (isServer) {
process.env;
const m = process;
const _path = path;
const pathJoin = path.join('foo', 'bar');
}
}
child_process();
const foo2 = () => {
if (isServer) {
process.env;
const m = process;
}
};
foo();
foo2();
});
return <></>;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { component$, useTask$, isBrowser, useSignal } from '@qwik.dev/core';

export default component$(() => {
const state = useSignal(true);
useTask$(({ track }) => {
if (isBrowser) {
track(() => {
if (state.value) {
const values = [
{
path: '1',
},
];
}
});
}
});
return <></>;
});
Loading