Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { readFileSync, statSync } from "node:fs";
import { readFileSync } from "node:fs";
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
import fg from "fast-glob";
import ts from "typescript";
import { isFile } from "../../project-info/fs-utils.js";
import { isRecord } from "../../utils/is-record.js";
import { unwrapTypescriptExpression } from "../../utils/unwrap-typescript-expression.js";
import { EXPO_CONFIG_SCAN_MAX_DEPTH, SOURCE_EXTENSIONS } from "../constants.js";

Expand All @@ -23,9 +25,6 @@ interface StaticConfigBindings {
readonly functions: Map<string, ts.FunctionDeclaration>;
}

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;

const isExpoOrReactNativeWorkspace = (dependencies: Record<string, string>): boolean =>
[...EXPO_REACT_NATIVE_DEPENDENCIES].some((dependencyName) => dependencyName in dependencies);

Expand All @@ -34,14 +33,6 @@ const isLocalExpoPluginPath = (value: string): boolean =>
!value.includes("*") &&
!value.includes("?");

const isFile = (filePath: string): boolean => {
try {
return statSync(filePath).isFile();
} catch {
return false;
}
};

const resolveExpoPluginPath = (configDirectory: string, pluginPath: string): string | undefined => {
const candidatePath = resolve(configDirectory, pluginPath);
if (isFile(candidatePath)) return candidatePath;
Expand Down
22 changes: 2 additions & 20 deletions packages/core/src/project-analysis/collect/workspaces.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { resolve, join, relative, dirname } from "node:path";
import { readFileSync, existsSync, statSync } from "node:fs";
import fg from "fast-glob";
import { parseYAML } from "confbox";
import { STANDALONE_PROJECT_LOCKFILES } from "../constants.js";
import { evaluateStaticConfig } from "../utils/evaluate-static-config.js";
import { toPosixPath } from "../utils/to-posix-path.js";
import { parsePnpmWorkspacePatternsFromContent } from "../../utils/parse-pnpm-workspace-patterns.js";
import { extractReactRouterRouteModuleEntries } from "./parse.js";

export interface WorkspacePackage {
Expand Down Expand Up @@ -129,7 +129,7 @@ const collectWorkspacePatterns = (rootDir: string): string[] => {
if (existsSync(pnpmWorkspacePath)) {
try {
const content = readFileSync(pnpmWorkspacePath, "utf-8");
const packageLines = extractPnpmWorkspacePackages(content);
const packageLines = parsePnpmWorkspacePatternsFromContent(content);
patterns.push(...packageLines);
} catch {}
}
Expand All @@ -153,24 +153,6 @@ const collectWorkspacePatterns = (rootDir: string): string[] => {
return patterns;
};

const extractPnpmWorkspacePackages = (yamlContent: string): string[] => {
const workspaceConfig = parseYAML<unknown>(yamlContent);
if (
!workspaceConfig ||
typeof workspaceConfig !== "object" ||
Array.isArray(workspaceConfig) ||
!("packages" in workspaceConfig) ||
!Array.isArray(workspaceConfig.packages)
) {
return [];
}

return workspaceConfig.packages.filter(
(packagePattern): packagePattern is string =>
typeof packagePattern === "string" && !packagePattern.startsWith("!"),
);
};

const expandWorkspaceGlobs = (patterns: string[], rootDir: string): string[] => {
const directories: string[] = [];

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/project-analysis/report/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from "../utils/collect-override-mappings-from-record.js";
import { collectPnpmWorkspaceOverrideMappings } from "../utils/parse-pnpm-workspace-overrides.js";
import { collectPackageLockPackageMetadata } from "../utils/collect-package-lock-package-metadata.js";
import { collectPackageImportNames } from "../utils/matches-package-import-reference.js";
import { collectPackageImportNames } from "../utils/collect-package-import-names.js";
import { collectPackageConfigReferences } from "../utils/matches-package-config-reference.js";
import { extractScriptBinaryNames } from "../utils/extract-script-binary-names.js";
import { extractLocalScriptFileReference } from "../utils/extract-local-script-file-reference.js";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,3 @@ export const collectPackageImportNames = (content: string): Set<string> => {
}
return packageNames;
};

export const matchesPackageImportReference = (content: string, packageName: string): boolean =>
collectPackageImportNames(content).has(packageName);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync } from "node:fs";
import { statSync } from "node:fs";
import { join, resolve } from "node:path";

const RESOLVABLE_EXTENSIONS = [
Expand All @@ -13,17 +13,25 @@ const RESOLVABLE_EXTENSIONS = [
".es6",
];

const isFile = (filePath: string): boolean => {
try {
return statSync(filePath, { throwIfNoEntry: false })?.isFile() ?? false;
} catch {
return false;
}
};

export const resolveEntryWithExtensions = (basePath: string): string | undefined => {
if (existsSync(basePath)) return basePath;
if (isFile(basePath)) return basePath;

for (const extension of RESOLVABLE_EXTENSIONS) {
const withExtension = basePath + extension;
if (existsSync(withExtension)) return withExtension;
if (isFile(withExtension)) return withExtension;
}

for (const extension of RESOLVABLE_EXTENSIONS) {
const indexCandidate = join(basePath, `index${extension}`);
if (existsSync(indexCandidate)) return indexCandidate;
if (isFile(indexCandidate)) return indexCandidate;
}

return undefined;
Expand Down
19 changes: 2 additions & 17 deletions packages/core/src/project-info/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { PackageJson, WorkspacePackage } from "../types/index.js";
import { hasSupportedProjectDependency } from "./dependencies.js";
import { isDirectory, isFile, readDirectoryEntries } from "./fs-utils.js";
import { readPackageJson } from "./package-json.js";
import { parsePnpmWorkspacePatternsFromContent } from "../utils/parse-pnpm-workspace-patterns.js";

export const getWorkspacePatterns = (rootDirectory: string, packageJson: PackageJson): string[] => {
const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);
Expand All @@ -28,23 +29,7 @@ export const parsePnpmWorkspacePatterns = (rootDirectory: string): string[] => {
if (!isFile(workspacePath)) return [];

const content = fs.readFileSync(workspacePath, "utf-8");
const patterns: string[] = [];
let isInsidePackagesBlock = false;

for (const line of content.split("\n")) {
const trimmed = line.trim();
if (trimmed === "packages:") {
isInsidePackagesBlock = true;
continue;
}
if (isInsidePackagesBlock && trimmed.startsWith("-")) {
patterns.push(trimmed.replace(/^-\s*/, "").replace(/["']/g, ""));
} else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith("#")) {
isInsidePackagesBlock = false;
}
}

return patterns;
return parsePnpmWorkspacePatternsFromContent(content);
};

const NX_PROJECT_DISCOVERY_DIRS = ["apps", "libs", "packages"];
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/run-oxlint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import { dedupeDiagnostics } from "./utils/dedupe-diagnostics.js";
import { collectProjectIndexModuleSources } from "./utils/collect-project-index-module-sources.js";
import { hashFileContents } from "./utils/hash-file-contents.js";
import { listSourceFilesWithSize } from "./utils/list-source-files.js";
import { mapWithConcurrency } from "./utils/map-with-concurrency.js";
import { planLintBatches } from "./utils/plan-lint-batches.js";
import { resolvePooledBatchCount } from "./utils/resolve-pooled-batch-count.js";
import { createDeferred } from "./utils/create-deferred.js";
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/utils/parse-pnpm-workspace-patterns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { parseYAML } from "confbox";
import { isRecord } from "./is-record.js";

export const parsePnpmWorkspacePatternsFromContent = (yamlContent: string): string[] => {
const workspaceConfig = parseYAML<unknown>(yamlContent);
if (!isRecord(workspaceConfig) || !Array.isArray(workspaceConfig.packages)) return [];

return workspaceConfig.packages.filter(
(packagePattern): packagePattern is string =>
typeof packagePattern === "string" && !packagePattern.startsWith("!"),
);
};
18 changes: 18 additions & 0 deletions packages/core/tests/discover-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3114,6 +3114,24 @@ describe("listWorkspacePackages", () => {
expect(packages).toEqual([{ name: "web", directory: appDirectory }]);
});

it("supports pnpm workspace flow sequence form", () => {
const rootDirectory = path.join(tempDirectory, "pnpm-workspace-flow-form");
const appDirectory = path.join(rootDirectory, "apps", "web");
fs.mkdirSync(appDirectory, { recursive: true });
fs.writeFileSync(
path.join(rootDirectory, "package.json"),
JSON.stringify({ name: "workspace-root", workspaces: ["packages/*"] }),
);
fs.writeFileSync(path.join(rootDirectory, "pnpm-workspace.yaml"), 'packages: ["apps/*"]\n');
fs.writeFileSync(
path.join(appDirectory, "package.json"),
JSON.stringify({ name: "web", dependencies: { react: "^19.0.0" } }),
);

const packages = listWorkspacePackages(rootDirectory);
expect(packages).toEqual([{ name: "web", directory: appDirectory }]);
});

// HACK: cal.com's workspace patterns include both `"packages/*"` AND
// `"packages/app-store"` — overlapping globs that resolve the same
// directory through two patterns. Without dedup-by-directory the
Expand Down
31 changes: 31 additions & 0 deletions packages/core/tests/resolve-entry-with-extensions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, describe, expect, it } from "vite-plus/test";
import { resolveEntryWithExtensions } from "../src/project-analysis/utils/resolve-entry-with-extensions.js";

const temporaryDirectories: string[] = [];

afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
fs.rmSync(directory, { recursive: true, force: true });
}
});

const createTemporaryDirectory = (): string => {
const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-entry-"));
temporaryDirectories.push(temporaryDirectory);
return temporaryDirectory;
};

describe("resolveEntryWithExtensions", () => {
it("continues to directory index resolution when the entry path is a directory", () => {
const rootDirectory = createTemporaryDirectory();
const distDirectory = path.join(rootDirectory, "dist");
fs.mkdirSync(distDirectory, { recursive: true });
const distIndexPath = path.join(distDirectory, "index.js");
fs.writeFileSync(distIndexPath, "export const value = 1;\n");

expect(resolveEntryWithExtensions(distDirectory)).toBe(distIndexPath);
});
});
7 changes: 6 additions & 1 deletion packages/evals/src/parse-evaluation-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export interface MatrixEvaluationOptions {
waveWidth: number;
}

const hasCliOption = (argumentsToParse: ReadonlyArray<string>, option: string): boolean =>
argumentsToParse.some(
(argumentToParse) => argumentToParse === option || argumentToParse.startsWith(`${option}=`),
);

export const parseEvaluationArguments = (
argumentsToParse: ReadonlyArray<string>,
): EvaluationOptions => {
Expand Down Expand Up @@ -145,7 +150,7 @@ export const parseEvaluationArguments = (
"--rule",
];
const incompatibleMatrixOption = hasMatrixOption
? matrixIncompatibleOptions.find((option) => argumentsToParse.includes(option))
? matrixIncompatibleOptions.find((option) => hasCliOption(argumentsToParse, option))
: undefined;
if (incompatibleMatrixOption) {
throw new Error(
Expand Down
10 changes: 8 additions & 2 deletions packages/evals/src/utils/create-concurrency-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ export const createConcurrencyLimit = (concurrency: number): ConcurrencyLimit =>
}

const pendingOperations: Array<() => void> = [];
let nextPendingOperationIndex = 0;
let activeOperationCount = 0;

const startNextOperations = (): void => {
while (activeOperationCount < concurrency) {
const startOperation = pendingOperations.shift();
if (!startOperation) return;
const startOperation = pendingOperations[nextPendingOperationIndex];
if (!startOperation) {
pendingOperations.length = 0;
nextPendingOperationIndex = 0;
return;
}
nextPendingOperationIndex += 1;
activeOperationCount += 1;
startOperation();
}
Expand Down
7 changes: 7 additions & 0 deletions packages/evals/tests/parse-evaluation-arguments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ describe("parseEvaluationArguments", () => {
"/tmp/other-corpus.json",
]),
).toThrow("descriptor-driven matrix evaluation");
expect(() =>
parseEvaluationArguments([
"--matrix-treatment",
"/tmp/pr-1.json",
"--repositories=/tmp/other-corpus.json",
]),
).toThrow("descriptor-driven matrix evaluation");
});

it("rejects unsafe paired output, execution, and rule arguments", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ const isStaticRenderKey = (key: EsTreeNode): boolean => {
return false;
};

const isFunctionExpressionLike = (node: EsTreeNode): boolean =>
isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");

interface RenderHostInfo {
reportNode: EsTreeNode;
isObjectPropertyRender: boolean;
Expand Down Expand Up @@ -101,13 +98,6 @@ export const requireRenderReturn = defineRule({
ArrowFunctionExpression(node: EsTreeNodeOfType<"ArrowFunctionExpression">) {
checkFunction(node);
},
FunctionDeclaration(node: EsTreeNodeOfType<"FunctionDeclaration">) {
// FunctionDeclaration can't appear inside a class/object literal as
// a render method, but the parent traversal in `resolveRenderHost`
// is cheap enough to also accept it here for robustness.
if (isFunctionExpressionLike(node)) return;
checkFunction(node);
},
};
},
});
Original file line number Diff line number Diff line change
@@ -1,45 +1,14 @@
import { FUNCTION_LIKE_TYPES } from "../constants/js.js";
import type { EsTreeNode } from "./es-tree-node.js";
import { isAstNode } from "./is-ast-node.js";
import { collectFunctionReturnStatements } from "./collect-function-return-statements.js";

// Visitor-based approximation of "the function body contains a non-empty
// `return X` somewhere reachable". We don't have a CFG, so instead we
// recursively walk descendants but stop crossing into nested function
// bodies (their `return` statements belong to that inner function, not
// the outer one). For arrow functions with an expression body the body
// IS the return value — those always count. Used by `require-render-return`.
// Used by `require-render-return`, where expression-bodied arrows count
// as a returned value and nested functions keep their own returns.
export const functionBodyHasReturnWithValue = (functionNode: EsTreeNode): boolean => {
if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
if (functionNode.body && functionNode.body.type !== "BlockStatement") return true;
}

const body = (functionNode as unknown as { body?: EsTreeNode | null }).body;
if (!body || body.type !== "BlockStatement") return false;

let didFindReturn = false;
const visit = (node: EsTreeNode): void => {
if (didFindReturn) return;
if (node.type === "ReturnStatement" && "argument" in node && node.argument != null) {
didFindReturn = true;
return;
}
const nodeRecord = node as unknown as Record<string, unknown>;
for (const key of Object.keys(nodeRecord)) {
if (key === "parent") continue;
const child = nodeRecord[key];
if (Array.isArray(child)) {
for (const item of child) {
if (!isAstNode(item)) continue;
if (FUNCTION_LIKE_TYPES.has(item.type)) continue;
visit(item);
if (didFindReturn) return;
}
} else if (isAstNode(child)) {
if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
visit(child);
}
}
};
visit(body);
return didFindReturn;
return collectFunctionReturnStatements(functionNode).some(
(returnStatement) => returnStatement.argument != null,
);
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import type { EsTreeNodeOfType } from "./es-tree-node-of-type.js";
import { isNodeOfType } from "./is-node-of-type.js";

/**
* True when a JSX opening element carries a `key={...}` attribute.
* Used by `jsx-key` and `jsx-no-useless-fragment` — both rules need to
* see through fragment wrappers that exist only to hold a key.
*
* Was duplicated in both rule files; consolidated here so adding
* variants (e.g. spread-attribute handling) propagates to both.
*/
export const hasJsxKeyAttribute = (
openingElement: EsTreeNodeOfType<"JSXOpeningElement">,
): boolean => {
Expand Down
Loading