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 16 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
43 changes: 10 additions & 33 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 @@ -178,29 +157,28 @@ 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 (!GLOBALAPIS.includes(identifierNode.name)) {
return true;
Comment on lines +160 to +162
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems wrong? It will almost always be true, and if it is correct then it should be befor the while loop because it's not using the result of the while loop

}

if (variable.defs.length === 0) {
if (variable?.defs.length === 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because you removed the !variable test, now you needed to add this. Just add the !varaible test again

// 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 +396,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,15 +4,16 @@
// Expect error: { "messageId": "unsafeApiUsageInCalledFunction" }

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

export default component$(() => {
function child_process() {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is valid code, because it shadows the global child_process, and shouldn't fail the eslint test

useTask$(() => {
function foo() {
process.env;
}
const foo2 = () => {
process.env;
};
child_process();
foo();
foo2();
});
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