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
18 changes: 2 additions & 16 deletions packages/core/src/check-react-server-components-advisory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,11 @@ import {
import { findMonorepoRoot, isFile, readPackageJson } from "./project-info/index.js";
import { getWorkspacePatterns } from "./project-info/get-workspace-patterns.js";
import { resolveWorkspaceDirectories } from "./project-info/resolve-workspace-directories.js";
import { getDependencySpec } from "./project-info/utils/get-dependency-spec.js";
import type { Diagnostic, PackageJson, ProjectInfo } from "./types/index.js";

const RULE_KEY = "no-vulnerable-react-server-components";

const DEPENDENCY_SECTIONS = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
] as const;

// Per-minor advisory thresholds for React's Server Components runtime
// (`react-server-dom-*`, versioned in lockstep with `react`/`react-dom`).
// `rceFixedVersion` patched the critical unauthenticated RCE (CVE-2025-55182);
Expand Down Expand Up @@ -91,14 +85,6 @@ const enumerateWorkspaceDirectories = (workspaceRoot: string): string[] => {
return [...directories];
};

const readDeclaredSpec = (packageJson: PackageJson, packageName: string): string | null => {
for (const section of DEPENDENCY_SECTIONS) {
const spec = packageJson[section]?.[packageName];
if (typeof spec === "string") return spec;
}
return null;
};

// Resolves the concrete version a package runs *in a single directory*,
// preferring the installed manifest under that directory's `node_modules`
// (authoritative, always concrete) and falling back to an exact pin declared in
Expand All @@ -122,7 +108,7 @@ const resolveVersionInDirectory = (
// catalog-resolved `project.nextjsVersion`) — so an unparseable manifest spec
// like `catalog:` doesn't shadow an already-resolved concrete pin.
const candidateSpecs = [
readDeclaredSpec(readPackageJson(path.join(directory, "package.json")), packageName),
getDependencySpec(readPackageJson(path.join(directory, "package.json")), packageName),
declaredSpecOverride,
];
for (const spec of candidateSpecs) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/project-info/extract-dependency-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const pickConcreteVersion = (
): string | null => {
for (const section of sections) {
const version = packageJson[section]?.[packageName];
if (version === undefined) continue;
if (typeof version !== "string") continue;
if (isCatalogReference(version)) return null;
if (isConcreteDependencyVersion(version)) return version;
}
Expand Down
8 changes: 2 additions & 6 deletions packages/core/src/project-info/find-expo-version.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { PackageJson } from "../types/index.js";
import { findInWorkspacePackageJsons } from "./find-in-workspace-package-jsons.js";
import { getDependencySpec } from "./utils/get-dependency-spec.js";
import { findWorkspaceDependencySpec } from "./find-workspace-dependency-spec.js";

// The declared `expo` package version spec, looked up in the root manifest
// and then each workspace package — react-doctor's "is this an Expo
Expand All @@ -16,7 +15,4 @@ import { getDependencySpec } from "./utils/get-dependency-spec.js";
export const findExpoVersion = (
rootDirectory: string,
rootPackageJson: PackageJson,
): string | null =>
findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) =>
getDependencySpec(packageJson, "expo"),
);
): string | null => findWorkspaceDependencySpec(rootDirectory, rootPackageJson, "expo");
8 changes: 2 additions & 6 deletions packages/core/src/project-info/find-nextjs-version.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { PackageJson } from "../types/index.js";
import { findInWorkspacePackageJsons } from "./find-in-workspace-package-jsons.js";
import { getDependencySpec } from "./utils/get-dependency-spec.js";
import { findWorkspaceDependencySpec } from "./find-workspace-dependency-spec.js";

// The declared `next` package version spec, looked up in the root manifest and
// then each workspace package — the signal the `nextjs:15` capability gate
Expand All @@ -12,7 +11,4 @@ import { getDependencySpec } from "./utils/get-dependency-spec.js";
export const findNextjsVersion = (
rootDirectory: string,
rootPackageJson: PackageJson,
): string | null =>
findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) =>
getDependencySpec(packageJson, "next"),
);
): string | null => findWorkspaceDependencySpec(rootDirectory, rootPackageJson, "next");
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import type { PackageJson } from "../types/index.js";
import { findInWorkspacePackageJsons } from "./find-in-workspace-package-jsons.js";
import { getDependencySpec } from "./utils/get-dependency-spec.js";
import { findWorkspaceDependencySpec } from "./find-workspace-dependency-spec.js";

export const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";

export const findShopifyFlashListVersion = (
rootDirectory: string,
rootPackageJson: PackageJson,
): string | null =>
findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) =>
getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME),
);
findWorkspaceDependencySpec(rootDirectory, rootPackageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME);
12 changes: 12 additions & 0 deletions packages/core/src/project-info/find-workspace-dependency-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { PackageJson } from "../types/index.js";
import { findInWorkspacePackageJsons } from "./find-in-workspace-package-jsons.js";
import { getDependencySpec } from "./utils/get-dependency-spec.js";

export const findWorkspaceDependencySpec = (
rootDirectory: string,
rootPackageJson: PackageJson,
packageName: string,
): string | null =>
findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) =>
getDependencySpec(packageJson, packageName),
);
2 changes: 1 addition & 1 deletion packages/core/src/project-info/get-preact-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export const getPreactVersion = (packageJson: PackageJson): string | null => {
...packageJson.dependencies,
...packageJson.devDependencies,
};
return allDependencies.preact ?? null;
return typeof allDependencies.preact === "string" ? allDependencies.preact : null;
};
2 changes: 1 addition & 1 deletion packages/core/src/project-info/resolve-catalog-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export const resolveCatalogVersion = (
const hasExplicitCatalogReference = explicitCatalogReference !== undefined;
const catalogName = hasExplicitCatalogReference
? explicitCatalogReference
: rawVersion
: typeof rawVersion === "string"
? extractCatalogName(rawVersion)
: null;
const shouldSearchUnreferencedNamedCatalogs =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const getDependencyDeclaration = ({
}: GetDependencyDeclarationOptions): DependencyDeclaration => {
for (const section of sections) {
const version = packageJson[section]?.[packageName];
if (version === undefined) continue;
if (typeof version !== "string") continue;

return {
catalogReference: extractCatalogName(version) ?? null,
Expand Down
6 changes: 1 addition & 5 deletions packages/core/src/project-info/utils/get-dependency-spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import type { PackageJson } from "../../types/index.js";

// Reads a package's declared version spec from any of the four dependency
// sections (runtime → dev → peer → optional), so detection matches the
// framework / RN-workspace gates that also treat `peer`/`optional` entries as
// present. The `typeof` guard keeps a malformed non-string entry (e.g.
// `"expo": 54`) from reaching downstream `.trim()` parsing and aborting the scan.
// Malformed non-string entries must not reach downstream version parsing.
export const getDependencySpec = (packageJson: PackageJson, packageName: string): string | null => {
const spec =
packageJson.dependencies?.[packageName] ??
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PackageJson } from "../../types/index.js";
import { getDependencySpec } from "./get-dependency-spec.js";

// `react-native-reanimated` ships `.get()` / `.set()` accessors as the
// React Compiler-compatible alternative to `.value`. Detecting the
Expand All @@ -7,12 +8,5 @@ import type { PackageJson } from "../../types/index.js";
// the React Native gate so a reanimated dep in any section counts.
const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";

export const isPackageJsonReanimatedAware = (packageJson: PackageJson): boolean => {
const allDependencies = {
...packageJson.peerDependencies,
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.optionalDependencies,
};
return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
};
export const isPackageJsonReanimatedAware = (packageJson: PackageJson): boolean =>
getDependencySpec(packageJson, REANIMATED_DEPENDENCY_NAME) !== null;
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
]);

const isIgnorableReaddirError = (error: unknown): boolean =>
isErrnoException(error) &&
typeof error.code === "string" &&
IGNORABLE_READDIR_ERROR_CODES.has(error.code);
isErrnoException(error) && IGNORABLE_READDIR_ERROR_CODES.has(error.code);

export const readDirectoryEntries = (directoryPath: string): fs.Dirent[] => {
try {
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/read-ignore-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ export const readIgnoreFile = (filePath: string): string[] => {
try {
content = fs.readFileSync(filePath, "utf-8");
} catch (error) {
const errnoCode = isErrnoException(error) ? error.code : undefined;
if (errnoCode && errnoCode !== "ENOENT") {
Effect.runSync(Console.warn(`Could not read ignore file ${filePath}: ${errnoCode}`));
if (isErrnoException(error) && error.code !== "ENOENT") {
Effect.runSync(Console.warn(`Could not read ignore file ${filePath}: ${error.code}`));
}
return [];
}
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/utils/is-errno-exception.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export const isErrnoException = (error: unknown): error is NodeJS.ErrnoException =>
error instanceof Error && "code" in error;
export const isErrnoException = (
error: unknown,
): error is NodeJS.ErrnoException & { code: string } =>
error instanceof Error && "code" in error && typeof error.code === "string";
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ruleRegistry } from "./rule-registry.js";
import type { Rule } from "./utils/rule.js";
import type { HostRule } from "./utils/rule-plugin.js";
import type { RulePlugin } from "./utils/rule-plugin.js";
import type { HostRule, RulePlugin } from "./utils/rule-plugin.js";
import { wrapReactNativeRule } from "./utils/wrap-react-native-rule.js";
import { wrapWithSemanticContext } from "./utils/wrap-with-semantic-context.js";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as eslintVisitorKeys from "eslint-visitor-keys";

export const MAX_EXPRESSION_SNIPPET_ITEMS_COUNT = 3;

const TYPESCRIPT_VISITOR_KEYS: Readonly<Record<string, ReadonlyArray<string>>> = {
TSAsExpression: ["expression", "typeAnnotation"],
TSNonNullExpression: ["expression"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { EsTreeNode } from "../../../../utils/es-tree-node.js";
import { isAstNode } from "../../../../utils/is-ast-node.js";
import { isFunctionLike } from "../../../../utils/is-function-like.js";
import { isNodeOfType } from "../../../../utils/is-node-of-type.js";
import { isUppercaseName } from "../../../../utils/is-uppercase-name.js";
import {
getDownstreamRefs,
getRef,
Expand Down Expand Up @@ -45,17 +46,14 @@ const getOuterScopeContaining = (analysis: ProgramAnalysis, node: EsTreeNode): S

const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);

const startsWithUppercase = (name: string | undefined): boolean =>
Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");

const isReactFunctionalComponent = (node: EsTreeNode | null | undefined): boolean => {
if (!node) return false;
if (isNodeOfType(node, "FunctionDeclaration")) {
return Boolean(node.id && startsWithUppercase(node.id.name));
return Boolean(node.id && isUppercaseName(node.id.name));
}
if (isNodeOfType(node, "VariableDeclarator")) {
if (!isNodeOfType(node.id, "Identifier")) return false;
if (!startsWithUppercase(node.id.name)) return false;
if (!isUppercaseName(node.id.name)) return false;
const init = node.init;
if (!init) return false;
return isNodeOfType(init, "ArrowFunctionExpression") || isNodeOfType(init, "CallExpression");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ const visitDestructuringDeclarations = (
// BINDING position (declares a name) vs a REFERENCE position (uses a
// name). The walker tracks binding sites explicitly; everything else is
// treated as a reference.
const tagAsBinding = (state: BuilderState, identifier: EsTreeNode): void => {
const tagAsBinding = (identifier: EsTreeNode): void => {
// Currently a marker only — we already recorded the symbol, so we
// tag the identifier so the generic walk doesn't add it again as a
// reference. We use a dedicated WeakSet for this (built lazily).
Expand Down Expand Up @@ -389,8 +389,7 @@ const isFunctionBodyBlock = (block: EsTreeNode): boolean => {
// True for AST node types where the .body is a catch-clause body
// block. Same reasoning — the catch clause already pushed its own
// scope.
const isCatchClauseBlock = (block: EsTreeNode): boolean =>
block.parent !== null && block.parent !== undefined && block.parent.type === "CatchClause";
const isCatchClauseBlock = (block: EsTreeNode): boolean => block.parent?.type === "CatchClause";

const handleVariableDeclaration = (declaration: EsTreeNode, state: BuilderState): void => {
if (!isNodeOfType(declaration, "VariableDeclaration")) return;
Expand Down Expand Up @@ -421,7 +420,7 @@ const handleVariableDeclaration = (declaration: EsTreeNode, state: BuilderState)
for (const identifier of collectBindingNamesFromPattern(
(declarator as { id: EsTreeNode }).id,
)) {
tagAsBinding(state, identifier);
tagAsBinding(identifier);
}
}
};
Expand All @@ -437,7 +436,7 @@ const handleFunctionDeclaration = (fn: EsTreeNode, state: BuilderState): void =>
declarationNode: fn,
initializer: fn,
});
tagAsBinding(state, fn.id as EsTreeNode);
tagAsBinding(fn.id as EsTreeNode);
}
};

Expand All @@ -451,7 +450,7 @@ const handleClassDeclaration = (cls: EsTreeNode, state: BuilderState): void => {
declarationNode: cls,
initializer: cls,
});
tagAsBinding(state, cls.id as EsTreeNode);
tagAsBinding(cls.id as EsTreeNode);
}
};

Expand All @@ -468,7 +467,7 @@ const handleImportDeclaration = (importDeclaration: EsTreeNode, state: BuilderSt
declarationNode: specifier as EsTreeNode,
initializer: specifier as EsTreeNode,
});
tagAsBinding(state, local);
tagAsBinding(local);
}
};

Expand Down Expand Up @@ -502,7 +501,7 @@ const handleTsDeclarations = (node: EsTreeNode, state: BuilderState): void => {
declarationNode: node,
initializer: null,
});
tagAsBinding(state, idNode);
tagAsBinding(idNode);
};

const handleFunctionParameters = (
Expand All @@ -513,7 +512,7 @@ const handleFunctionParameters = (
for (const param of params) {
visitDestructuringDeclarations(param, null, scope, state, "parameter", param);
for (const identifier of collectBindingNamesFromPattern(param)) {
tagAsBinding(state, identifier);
tagAsBinding(identifier);
}
}
};
Expand Down Expand Up @@ -709,7 +708,7 @@ const walk = (node: EsTreeNode, state: BuilderState): void => {
declarationNode: node,
initializer: node,
});
tagAsBinding(state, node.id as EsTreeNode);
tagAsBinding(node.id as EsTreeNode);
}
const functionParams = (node as { params: ReadonlyArray<EsTreeNode> }).params ?? [];
handleFunctionParameters(functionParams, fnScope, state);
Expand Down Expand Up @@ -744,7 +743,7 @@ const walk = (node: EsTreeNode, state: BuilderState): void => {
declarationNode: node,
initializer: node,
});
tagAsBinding(state, node.id as EsTreeNode);
tagAsBinding(node.id as EsTreeNode);
}
if (node.superClass) walk(node.superClass as EsTreeNode, state);
if (node.body) walk(node.body as EsTreeNode, state);
Expand All @@ -765,7 +764,7 @@ const walk = (node: EsTreeNode, state: BuilderState): void => {
node as EsTreeNode,
);
for (const identifier of collectBindingNamesFromPattern(node.param as EsTreeNode)) {
tagAsBinding(state, identifier);
tagAsBinding(identifier);
}
}
if (node.body) walk(node.body as EsTreeNode, state);
Expand Down Expand Up @@ -821,7 +820,7 @@ const walk = (node: EsTreeNode, state: BuilderState): void => {
declarationNode: node,
initializer: null,
});
tagAsBinding(state, identifier);
tagAsBinding(identifier);
}
if (node.body) walk(node.body as EsTreeNode, state);
popScope(state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// target.

// Closed set of canonical Expo-managed dependency names.
export const EXPO_MANAGED_DEPENDENCY_NAMES: ReadonlySet<string> = new Set([
const EXPO_MANAGED_DEPENDENCY_NAMES: ReadonlySet<string> = new Set([
"expo",
"expo-router",
"@expo/cli",
Expand Down
Loading