e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
1568 lines
45 KiB
Python
1568 lines
45 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
"""Edge-case suite for scripts/check_frontend_dep_removal.py.
|
|
|
|
Each case patches a copy of package.json to remove/move a dependency,
|
|
runs the checker against the real lockfile, and asserts the verdict.
|
|
Run: `python tests/studio/test_frontend_dep_removal.py` (exit 0 iff all pass).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
HEAD_PKG = REPO / "studio/frontend/package.json"
|
|
HEAD_LOCK = REPO / "studio/frontend/package-lock.json"
|
|
SCRIPT = REPO / "scripts/check_frontend_dep_removal.py"
|
|
|
|
|
|
@dataclass
|
|
class Case:
|
|
id: str
|
|
desc: str
|
|
remove: list[str]
|
|
expected_status: str # "PASS" | "FAIL"
|
|
expected_failures: list[str]
|
|
move_to_dev: list[str] | None = None # rare: deps moved, not removed
|
|
|
|
|
|
CASES: list[Case] = [
|
|
Case(
|
|
"C1",
|
|
"removing next-themes breaks 2 src imports",
|
|
["next-themes"],
|
|
"FAIL",
|
|
["next-themes"],
|
|
),
|
|
Case(
|
|
"C2",
|
|
"removing @xyflow/react breaks recipe-studio src imports "
|
|
"(no other declared dep pulls @xyflow/react)",
|
|
["@xyflow/react"],
|
|
"FAIL",
|
|
["@xyflow/react"],
|
|
),
|
|
Case(
|
|
"C3",
|
|
"removing katex is safe: streamdown/math, mermaid, "
|
|
"rehype-katex all keep it at top level",
|
|
["katex"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case("C4", "removing clsx is safe: streamdown keeps it", ["clsx"], "PASS", []),
|
|
Case(
|
|
"C5",
|
|
"removing react is safe: peer of countless packages",
|
|
["react"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C6",
|
|
"removing @radix-ui/react-slot is safe: pulled by "
|
|
"radix-ui umbrella + @assistant-ui/react",
|
|
["@radix-ui/react-slot"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C7",
|
|
"removing zustand is safe: @assistant-ui/react keeps "
|
|
"top-level zustand@5.x (nested xyflow 4.x is irrelevant "
|
|
"to src imports)",
|
|
["zustand"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C8",
|
|
"multi-remove with mixed safety: next-themes + @huggingface/hub + dexie all unsafe",
|
|
["next-themes", "@huggingface/hub", "dexie"],
|
|
"FAIL",
|
|
["next-themes", "@huggingface/hub", "dexie"],
|
|
),
|
|
Case(
|
|
"C9",
|
|
"removing @huggingface/hub breaks 5+ src imports",
|
|
["@huggingface/hub"],
|
|
"FAIL",
|
|
["@huggingface/hub"],
|
|
),
|
|
Case(
|
|
"C10",
|
|
"removing tailwind-merge is safe: streamdown keeps it",
|
|
["tailwind-merge"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C11",
|
|
"removing a non-existent name is a no-op",
|
|
["__never_existed_in_pkg__"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C12",
|
|
"moving @hugeicons/react from deps to devDeps is NOT a removal (still declared)",
|
|
[],
|
|
"PASS",
|
|
[],
|
|
move_to_dev = ["@hugeicons/react"],
|
|
),
|
|
Case(
|
|
"C13",
|
|
"removing @huggingface/hub AND @xyflow/react together: both "
|
|
"are root-only deps with no other parents, so both should FAIL",
|
|
["@huggingface/hub", "@xyflow/react"],
|
|
"FAIL",
|
|
["@huggingface/hub", "@xyflow/react"],
|
|
),
|
|
Case(
|
|
"C14",
|
|
"removing dexie breaks src imports (no other declared dep needs it)",
|
|
["dexie"],
|
|
"FAIL",
|
|
["dexie"],
|
|
),
|
|
Case(
|
|
"C15",
|
|
"removing motion (used in 20+ src imports including "
|
|
"framer-motion-style animations); no transitive parent",
|
|
["motion"],
|
|
"FAIL",
|
|
["motion"],
|
|
),
|
|
Case(
|
|
"C16",
|
|
"removing canvas-confetti (imported in confetti.tsx); no transitive parent",
|
|
["canvas-confetti"],
|
|
"FAIL",
|
|
["canvas-confetti"],
|
|
),
|
|
Case(
|
|
"C17",
|
|
"removing recharts (imported in chart.tsx); no transitive parent",
|
|
["recharts"],
|
|
"FAIL",
|
|
["recharts"],
|
|
),
|
|
Case(
|
|
"C18",
|
|
"removing js-yaml is safe: @eslint/eslintrc keeps it "
|
|
"(triggers @types/js-yaml orphan warning, non-fatal)",
|
|
["js-yaml"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C19",
|
|
"removing node-forge (imported in providers-api.ts); no transitive parent",
|
|
["node-forge"],
|
|
"FAIL",
|
|
["node-forge"],
|
|
),
|
|
Case(
|
|
"C20",
|
|
"removing @tauri-apps/api is safe: all 5 @tauri-apps plugins declare it as a direct dep",
|
|
["@tauri-apps/api"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C21",
|
|
"removing mammoth (imported in runtime-provider.tsx); no transitive parent",
|
|
["mammoth"],
|
|
"FAIL",
|
|
["mammoth"],
|
|
),
|
|
Case(
|
|
"C22",
|
|
"removing unpdf (imported in runtime-provider.tsx); no transitive parent",
|
|
["unpdf"],
|
|
"FAIL",
|
|
["unpdf"],
|
|
),
|
|
Case(
|
|
"C23",
|
|
"removing remark-gfm is safe: streamdown declares it as a direct dep",
|
|
["remark-gfm"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C24",
|
|
"removing date-fns is safe: react-day-picker and "
|
|
"@base-ui/react both declare it as a direct dep",
|
|
["date-fns"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C25",
|
|
"removing vite is safe: @vitejs/plugin-react and @tailwindcss/vite "
|
|
"keep it via peer (bin still resolves)",
|
|
["vite"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C26",
|
|
"removing typescript is safe: 11 transitive @typescript-eslint/* "
|
|
"parents keep tsc bin alive",
|
|
["typescript"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C27",
|
|
"removing eslint is safe: typescript-eslint and eslint-plugin-* "
|
|
"peers keep eslint bin alive",
|
|
["eslint"],
|
|
"PASS",
|
|
[],
|
|
),
|
|
Case(
|
|
"C28",
|
|
"removing @biomejs/biome breaks scripts.biome:check / biome:fix "
|
|
"(no transitive parents, biome bin orphans)",
|
|
["@biomejs/biome"],
|
|
"FAIL",
|
|
["@biomejs/biome"],
|
|
),
|
|
Case(
|
|
"C29",
|
|
"removing both @biomejs/biome AND @vitejs/plugin-react together: "
|
|
"biome dies outright; vite loses one of its two retained peers "
|
|
"but @tailwindcss/vite still keeps it",
|
|
["@biomejs/biome", "@vitejs/plugin-react"],
|
|
"FAIL",
|
|
["@biomejs/biome", "@vitejs/plugin-react"],
|
|
),
|
|
]
|
|
|
|
|
|
def synth_head(head_pkg: dict, case: Case) -> dict:
|
|
out = json.loads(json.dumps(head_pkg))
|
|
for name in case.remove:
|
|
for field in (
|
|
"dependencies",
|
|
"devDependencies",
|
|
"peerDependencies",
|
|
"optionalDependencies",
|
|
):
|
|
(out.get(field) or {}).pop(name, None)
|
|
if case.move_to_dev:
|
|
for name in case.move_to_dev:
|
|
v = (out.get("dependencies") or {}).pop(name, None)
|
|
if v is not None:
|
|
out.setdefault("devDependencies", {})[name] = v
|
|
return out
|
|
|
|
|
|
def run_case(case: Case, head_pkg: dict) -> tuple[bool, str]:
|
|
synth = synth_head(head_pkg, case)
|
|
with tempfile.NamedTemporaryFile("w", suffix = ".json", delete = False) as f:
|
|
json.dump(synth, f, indent = 2)
|
|
synth_path = f.name
|
|
try:
|
|
proc = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--base-pkg",
|
|
str(HEAD_PKG),
|
|
"--head-pkg",
|
|
synth_path,
|
|
"--head-lock",
|
|
str(HEAD_LOCK),
|
|
],
|
|
capture_output = True,
|
|
text = True,
|
|
)
|
|
finally:
|
|
os.unlink(synth_path)
|
|
|
|
actual_status = {0: "PASS", 1: "FAIL"}.get(proc.returncode, f"RC{proc.returncode}")
|
|
failure_pkgs: list[str] = []
|
|
in_summary = False
|
|
for line in proc.stdout.splitlines():
|
|
if "FAIL:" in line and "removed package" in line:
|
|
in_summary = True
|
|
continue
|
|
if in_summary and line.strip().startswith("- "):
|
|
failure_pkgs.append(line.strip()[2:])
|
|
|
|
ok = actual_status == case.expected_status and set(failure_pkgs) == set(case.expected_failures)
|
|
return ok, (
|
|
f"expected: status={case.expected_status} fails={sorted(case.expected_failures)}\n"
|
|
f"actual: status={actual_status} fails={sorted(failure_pkgs)}\n"
|
|
f"--- stdout (first 30 lines) ---\n" + "\n".join(proc.stdout.splitlines()[:30])
|
|
)
|
|
|
|
|
|
# Classifier unit tests: feed snippets into classify(), assert the kind.
|
|
# Covers sneaky import shapes used to obscure a real usage.
|
|
|
|
# Import classify() by file path so this test needs no installed package.
|
|
import importlib.util as _ilu
|
|
|
|
_spec = _ilu.spec_from_file_location("_dep_check", str(SCRIPT))
|
|
_dep_check = _ilu.module_from_spec(_spec)
|
|
sys.modules["_dep_check"] = _dep_check # required so @dataclass can resolve annotations
|
|
_spec.loader.exec_module(_dep_check)
|
|
classify = _dep_check.classify
|
|
_next_real_bin = _dep_check._next_real_bin
|
|
scripts_bin_refs = _dep_check.scripts_bin_refs
|
|
|
|
|
|
@dataclass
|
|
class ClassifyCase:
|
|
id: str
|
|
desc: str
|
|
pkg: str
|
|
file: str
|
|
content: str
|
|
expected_kind: str | None # None means "no detection"
|
|
|
|
|
|
CLASSIFY_CASES: list[ClassifyCase] = [
|
|
# Bog-standard shapes
|
|
ClassifyCase(
|
|
"U01",
|
|
"single-line static import",
|
|
"next-themes",
|
|
"src/x.tsx",
|
|
'import { ThemeProvider } from "next-themes";',
|
|
"static_import",
|
|
),
|
|
ClassifyCase(
|
|
"U02",
|
|
"side-effect import",
|
|
"katex",
|
|
"src/x.tsx",
|
|
'import "katex/dist/katex.min.css";',
|
|
"side_effect_import",
|
|
),
|
|
ClassifyCase(
|
|
"U03",
|
|
"dynamic import",
|
|
"@tauri-apps/api",
|
|
"src/x.tsx",
|
|
'const { x } = await import("@tauri-apps/api/window");',
|
|
"dynamic_import",
|
|
),
|
|
ClassifyCase(
|
|
"U04",
|
|
"require()",
|
|
"lodash",
|
|
"src/x.js",
|
|
'const _ = require("lodash");',
|
|
"require",
|
|
),
|
|
ClassifyCase(
|
|
"U05",
|
|
"CSS @import",
|
|
"tailwindcss",
|
|
"src/x.css",
|
|
'@import "tailwindcss";',
|
|
"css_import",
|
|
),
|
|
# Sneaky shapes
|
|
ClassifyCase(
|
|
"U06",
|
|
"multi-line static import",
|
|
"next-themes",
|
|
"src/x.tsx",
|
|
'import {\n ThemeProvider,\n useTheme,\n} from "next-themes";',
|
|
"static_import",
|
|
),
|
|
ClassifyCase(
|
|
"U07",
|
|
"import type",
|
|
"@huggingface/hub",
|
|
"src/x.ts",
|
|
'import type { PipelineType } from "@huggingface/hub";',
|
|
"static_import",
|
|
),
|
|
ClassifyCase(
|
|
"U08",
|
|
"export * from re-export",
|
|
"@some-org/secrets",
|
|
"src/x.ts",
|
|
'export * from "@some-org/secrets";',
|
|
"re_export",
|
|
),
|
|
ClassifyCase(
|
|
"U09",
|
|
"export { x } from re-export",
|
|
"lodash-es",
|
|
"src/x.ts",
|
|
'export { foo, bar } from "lodash-es";',
|
|
"re_export",
|
|
),
|
|
ClassifyCase(
|
|
"U10",
|
|
"export type ... from re-export",
|
|
"@huggingface/hub",
|
|
"src/x.ts",
|
|
'export type { Foo } from "@huggingface/hub";',
|
|
"re_export",
|
|
),
|
|
ClassifyCase(
|
|
"U11",
|
|
"multi-line export from re-export",
|
|
"@some/pkg",
|
|
"src/x.ts",
|
|
'export {\n thing,\n other,\n} from "@some/pkg";',
|
|
"re_export",
|
|
),
|
|
ClassifyCase(
|
|
"U12",
|
|
"JSDoc @import",
|
|
"react",
|
|
"src/x.ts",
|
|
'/** @type {import("react").FC} */\nconst Foo = () => null;',
|
|
"dynamic_import",
|
|
),
|
|
ClassifyCase(
|
|
"U13",
|
|
"template literal package path",
|
|
"@assistant-ui/react",
|
|
"src/x.tsx",
|
|
"const url = `@assistant-ui/react`;",
|
|
"template_literal",
|
|
),
|
|
ClassifyCase(
|
|
"U14",
|
|
"new URL import-meta",
|
|
"monaco-editor",
|
|
"src/x.ts",
|
|
'new URL("monaco-editor/esm/vs/editor/editor.worker", import.meta.url);',
|
|
"new_url",
|
|
),
|
|
ClassifyCase(
|
|
"U15",
|
|
"tsc triple-slash type ref",
|
|
"@types/some-pkg",
|
|
"src/x.ts",
|
|
'/// <reference types="@types/some-pkg" />',
|
|
"tsc_triple_slash",
|
|
),
|
|
ClassifyCase(
|
|
"U16",
|
|
"HTML script src",
|
|
"alpinejs",
|
|
"index.html",
|
|
'<script src="/node_modules/alpinejs/dist/cdn.min.js"></script>',
|
|
"html_script",
|
|
),
|
|
ClassifyCase(
|
|
"U17",
|
|
"HTML link href",
|
|
"alpinejs",
|
|
"index.html",
|
|
'<link rel="stylesheet" href="/node_modules/alpinejs/dist/style.css">',
|
|
"html_link",
|
|
),
|
|
ClassifyCase(
|
|
"U18",
|
|
"bare quoted string in tsconfig paths",
|
|
"@huggingface/hub",
|
|
"tsconfig.json",
|
|
'"paths": { "hf": ["@huggingface/hub/*"] }',
|
|
"string_literal",
|
|
),
|
|
ClassifyCase(
|
|
"U19",
|
|
"vite alias key",
|
|
"@dagrejs/dagre",
|
|
"vite.config.ts",
|
|
'"@dagrejs/dagre": path.resolve(__dirname, "./..."),',
|
|
"string_literal",
|
|
),
|
|
# False-positive guards (these should NOT detect)
|
|
ClassifyCase(
|
|
"U20",
|
|
"different package with shared prefix",
|
|
"foo",
|
|
"src/x.ts",
|
|
'import { x } from "foobar";',
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U21",
|
|
"package mentioned in plain comment text",
|
|
"react",
|
|
"src/x.ts",
|
|
"// We migrated from react-router to tanstack-router",
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U22",
|
|
"package name as a URL path tail is NOT detected "
|
|
"(boundary rule: pkg must be followed by quote or `/`)",
|
|
"react",
|
|
"src/x.ts",
|
|
'const docs = "https://example.com/react";',
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U23",
|
|
"package name in Python file (ignored, Python can never import npm packages)",
|
|
"playwright",
|
|
"tests/x.py",
|
|
'label: str = "playwright"',
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U24",
|
|
"exact-prefix collision: pkg 'lodash' and 'lodash-es'",
|
|
"lodash",
|
|
"src/x.ts",
|
|
'import _ from "lodash-es";',
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U25",
|
|
"scoped pkg substring collision",
|
|
"@radix-ui/react-label",
|
|
"src/x.ts",
|
|
'import x from "@radix-ui/react-label-extra";',
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U26",
|
|
"package only mentioned in a markdown link",
|
|
"react",
|
|
"README.md",
|
|
"See [react](https://react.dev).",
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U27",
|
|
"side-effect import with subpath",
|
|
"katex",
|
|
"src/x.css",
|
|
'@import "katex/dist/katex.min.css";',
|
|
"css_import",
|
|
),
|
|
ClassifyCase(
|
|
"U28",
|
|
"require.resolve",
|
|
"lodash",
|
|
"build/x.cjs",
|
|
'const path = require.resolve("lodash/fp");',
|
|
"require",
|
|
),
|
|
ClassifyCase(
|
|
"U29",
|
|
"TypeScript ambient `declare module`",
|
|
"@tanstack/react-router",
|
|
"src/app/router.tsx",
|
|
'declare module "@tanstack/react-router" {\n interface X {}\n}',
|
|
"string_literal",
|
|
),
|
|
ClassifyCase(
|
|
"U30",
|
|
"namespace import `import * as X from pkg`",
|
|
"@radix-ui/react-slot",
|
|
"src/x.tsx",
|
|
'import * as Slot from "@radix-ui/react-slot";',
|
|
"static_import",
|
|
),
|
|
ClassifyCase(
|
|
"U31",
|
|
"combined default + named import",
|
|
"react",
|
|
"src/x.tsx",
|
|
'import React, { useState } from "react";',
|
|
"static_import",
|
|
),
|
|
ClassifyCase(
|
|
"U32",
|
|
"default-as-named import alias",
|
|
"react",
|
|
"src/x.tsx",
|
|
'import { default as R } from "react";',
|
|
"static_import",
|
|
),
|
|
ClassifyCase(
|
|
"U33",
|
|
"re-export default",
|
|
"lodash",
|
|
"src/x.ts",
|
|
'export { default } from "lodash";',
|
|
"re_export",
|
|
),
|
|
ClassifyCase(
|
|
"U34",
|
|
"re-export default as alias",
|
|
"lodash",
|
|
"src/x.ts",
|
|
'export { default as _ } from "lodash";',
|
|
"re_export",
|
|
),
|
|
ClassifyCase(
|
|
"U35",
|
|
".then() dynamic import (no await)",
|
|
"@tauri-apps/api",
|
|
"src/x.ts",
|
|
'import("@tauri-apps/api/window").then(m => m.x());',
|
|
"dynamic_import",
|
|
),
|
|
ClassifyCase(
|
|
"U36",
|
|
"TypeScript import() in type position",
|
|
"react",
|
|
"src/x.ts",
|
|
'type C = import("react").ComponentType;',
|
|
"dynamic_import",
|
|
),
|
|
# File-type gating: JS classifiers must not fire on non-script files
|
|
# (.py/.md/.sh/.yml), whose JS-shaped strings are docs/test data, not usages.
|
|
ClassifyCase(
|
|
"U37",
|
|
"JS import snippet inside a Python fixture string is NOT a usage",
|
|
"next-themes",
|
|
"tests/studio/something.py",
|
|
"snippet = 'import x from \"next-themes\";'",
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U38",
|
|
"JS import snippet inside a Markdown code fence is NOT a usage",
|
|
"next-themes",
|
|
"docs/example.md",
|
|
'```ts\nimport x from "next-themes";\n```',
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U39",
|
|
"JS import inside a shell script is NOT classified as a JS usage",
|
|
"next-themes",
|
|
"scripts/build.sh",
|
|
'echo "import x from \\"next-themes\\";"',
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U40",
|
|
"JS import inside a YAML workflow is NOT classified as a JS usage",
|
|
"next-themes",
|
|
".github/workflows/x.yml",
|
|
"run: echo 'import x from \"next-themes\";'",
|
|
None,
|
|
),
|
|
# HTML script/link must respect package-name boundaries: a
|
|
# `/node_modules/foo-extra/...` reference does NOT use `foo`.
|
|
ClassifyCase(
|
|
"U41",
|
|
"HTML <script src=...> with similar-prefix package is NOT a match",
|
|
"foo",
|
|
"index.html",
|
|
'<script src="/node_modules/foo-extra/dist/index.js"></script>',
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U42",
|
|
"HTML <link href=...> with similar-prefix package is NOT a match",
|
|
"foo",
|
|
"index.html",
|
|
'<link rel="stylesheet" href="/node_modules/foo-extra/dist/style.css">',
|
|
None,
|
|
),
|
|
ClassifyCase(
|
|
"U43",
|
|
"HTML <script src=...> with exact package match IS a match",
|
|
"foo",
|
|
"index.html",
|
|
'<script src="/node_modules/foo/dist/index.js"></script>',
|
|
"html_script",
|
|
),
|
|
# CSS url() unquoted variant must classify the same as the quoted one.
|
|
ClassifyCase(
|
|
"U44",
|
|
"CSS url() unquoted bare package path",
|
|
"katex",
|
|
"src/x.css",
|
|
"src: url(katex/dist/fonts/font.woff2);",
|
|
"css_url",
|
|
),
|
|
ClassifyCase(
|
|
"U45",
|
|
"CSS url() quoted bare package path still works",
|
|
"katex",
|
|
"src/x.css",
|
|
'src: url("katex/dist/fonts/font.woff2");',
|
|
"css_url",
|
|
),
|
|
]
|
|
|
|
|
|
def run_classify_unit_tests() -> int:
|
|
passed = 0
|
|
for c in CLASSIFY_CASES:
|
|
actual = classify(c.pkg, c.file, c.content)
|
|
ok = actual == c.expected_kind
|
|
mark = "PASS" if ok else "FAIL"
|
|
print(f" [{mark}] {c.id}: {c.desc}")
|
|
if not ok:
|
|
print(f" pkg={c.pkg!r} file={c.file!r}")
|
|
print(f" content={c.content!r}")
|
|
print(f" expected={c.expected_kind!r}, actual={actual!r}")
|
|
if ok:
|
|
passed += 1
|
|
print()
|
|
print(f"{passed}/{len(CLASSIFY_CASES)} classify-unit cases pass")
|
|
return 0 if passed == len(CLASSIFY_CASES) else 1
|
|
|
|
|
|
# Adversarial end-to-end cases: drop a synthetic file into src/, run the
|
|
# checker, clean up. Catches regressions in the full grep+classify pipeline.
|
|
|
|
ADVERSARIAL_TMP_DIR = REPO / "studio/frontend/src/__dep_check_adversarial__"
|
|
|
|
|
|
@dataclass
|
|
class AdvCase:
|
|
id: str
|
|
desc: str
|
|
filename: str
|
|
content: str
|
|
target_pkg: str
|
|
expected_status: str
|
|
expected_failures: list[str]
|
|
|
|
|
|
ADV_CASES: list[AdvCase] = [
|
|
AdvCase(
|
|
"A01",
|
|
"multi-line import of removed pkg should FAIL",
|
|
"adv01.ts",
|
|
'import {\n foo,\n bar,\n} from "__adv_only_pkg_a__";\n',
|
|
"__adv_only_pkg_a__",
|
|
"FAIL",
|
|
["__adv_only_pkg_a__"],
|
|
),
|
|
AdvCase(
|
|
"A02",
|
|
"export * from removed pkg should FAIL",
|
|
"adv02.ts",
|
|
'export * from "__adv_only_pkg_b__";\n',
|
|
"__adv_only_pkg_b__",
|
|
"FAIL",
|
|
["__adv_only_pkg_b__"],
|
|
),
|
|
AdvCase(
|
|
"A03",
|
|
"export { x } from removed pkg should FAIL",
|
|
"adv03.ts",
|
|
'export { foo, bar } from "__adv_only_pkg_c__";\n',
|
|
"__adv_only_pkg_c__",
|
|
"FAIL",
|
|
["__adv_only_pkg_c__"],
|
|
),
|
|
AdvCase(
|
|
"A04",
|
|
"export type ... from removed pkg should FAIL",
|
|
"adv04.ts",
|
|
'export type { Foo } from "__adv_only_pkg_d__";\n',
|
|
"__adv_only_pkg_d__",
|
|
"FAIL",
|
|
["__adv_only_pkg_d__"],
|
|
),
|
|
AdvCase(
|
|
"A05",
|
|
"package with similar prefix should NOT trigger FAIL",
|
|
"adv05.ts",
|
|
# Imports the *_extra* name; removing the shorter name is safe (zero usage).
|
|
'import x from "__adv_only_pkg_e_extra__";\n',
|
|
"__adv_only_pkg_e__",
|
|
"PASS",
|
|
[],
|
|
),
|
|
AdvCase(
|
|
"A06",
|
|
"dynamic import of removed pkg should FAIL",
|
|
"adv06.ts",
|
|
'const m = await import("__adv_only_pkg_f__");\n',
|
|
"__adv_only_pkg_f__",
|
|
"FAIL",
|
|
["__adv_only_pkg_f__"],
|
|
),
|
|
AdvCase(
|
|
"A07",
|
|
"new URL of removed pkg should FAIL",
|
|
"adv07.ts",
|
|
'const w = new URL("__adv_only_pkg_g__/worker.js", import.meta.url);\n',
|
|
"__adv_only_pkg_g__",
|
|
"FAIL",
|
|
["__adv_only_pkg_g__"],
|
|
),
|
|
AdvCase(
|
|
"A08",
|
|
"string-concat dynamic import is unanalyzable (PASS)",
|
|
"adv08.ts",
|
|
'const m = await import("__adv_only_" + "pkg_h__");\n',
|
|
"__adv_only_pkg_h__",
|
|
"PASS",
|
|
[],
|
|
),
|
|
AdvCase(
|
|
"A09",
|
|
"package referenced only inside a JS comment "
|
|
"is conservatively flagged via the string_literal fallback "
|
|
"(this is acceptable -- err on the side of caution)",
|
|
"adv09.ts",
|
|
'// TODO: import x from "__adv_only_pkg_i__"\n',
|
|
"__adv_only_pkg_i__",
|
|
"FAIL",
|
|
["__adv_only_pkg_i__"],
|
|
),
|
|
AdvCase(
|
|
"A10",
|
|
"package referenced only in a Python file should NOT trigger a JS FAIL",
|
|
"adv10.py",
|
|
'label = "__adv_only_pkg_j__"\n',
|
|
"__adv_only_pkg_j__",
|
|
"PASS",
|
|
[],
|
|
),
|
|
AdvCase(
|
|
"A11",
|
|
"package mentioned in a markdown doc file is ignored by JS-like-only string_literal",
|
|
"adv11.md",
|
|
"See [docs](https://example.com/__adv_only_pkg_k__).\n",
|
|
"__adv_only_pkg_k__",
|
|
"PASS",
|
|
[],
|
|
),
|
|
AdvCase(
|
|
"A12",
|
|
"JSDoc @import of removed pkg should FAIL",
|
|
"adv12.ts",
|
|
'/** @type {import("__adv_only_pkg_l__").Foo} */\n' "const x = null;\n",
|
|
"__adv_only_pkg_l__",
|
|
"FAIL",
|
|
["__adv_only_pkg_l__"],
|
|
),
|
|
# Prettier puts `import` ~22 lines from the `from "pkg"` clause; the old
|
|
# ±4-line classify fallback missed it. Exercises the widened (±25) window.
|
|
AdvCase(
|
|
"A13",
|
|
"Prettier-style 22-identifier multi-line import should FAIL "
|
|
"(exercises the widened multi-line classify window)",
|
|
"adv13.ts",
|
|
"import {\n"
|
|
+ "".join(f" ident_{i:02d},\n" for i in range(22))
|
|
+ '} from "__adv_only_pkg_m__";\n',
|
|
"__adv_only_pkg_m__",
|
|
"FAIL",
|
|
["__adv_only_pkg_m__"],
|
|
),
|
|
]
|
|
|
|
|
|
# package.json field-reference cases: simulate prettier/eslintConfig/overrides/
|
|
# peerDependenciesMeta etc., testing package_json_extra_refs() coverage.
|
|
|
|
|
|
@dataclass
|
|
class PkgFieldCase:
|
|
id: str
|
|
desc: str
|
|
field_patch: dict # extra fields to merge into synth_head package.json
|
|
target_pkg: str
|
|
expected_status: str
|
|
expected_failures: list[str]
|
|
|
|
|
|
PKG_FIELD_CASES: list[PkgFieldCase] = [
|
|
PkgFieldCase(
|
|
"P01",
|
|
"removing pkg referenced only in `prettier` string field",
|
|
{"prettier": "__pkg_prettier_config__"},
|
|
"__pkg_prettier_config__",
|
|
"FAIL",
|
|
["__pkg_prettier_config__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P02",
|
|
"removing pkg referenced in `eslintConfig.extends` array",
|
|
{"eslintConfig": {"extends": ["__pkg_eslint_cfg__"]}},
|
|
"__pkg_eslint_cfg__",
|
|
"FAIL",
|
|
["__pkg_eslint_cfg__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P03",
|
|
"removing pkg referenced in `stylelint.plugins`",
|
|
{"stylelint": {"plugins": ["__pkg_stylelint_plugin__"]}},
|
|
"__pkg_stylelint_plugin__",
|
|
"FAIL",
|
|
["__pkg_stylelint_plugin__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P04",
|
|
"removing pkg referenced in `babel.presets`",
|
|
{"babel": {"presets": [["__pkg_babel_preset__", {"opt": 1}]]}},
|
|
"__pkg_babel_preset__",
|
|
"FAIL",
|
|
["__pkg_babel_preset__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P05",
|
|
"removing pkg used as a key in `overrides`",
|
|
{"overrides": {"__pkg_overridden__": "^1.0.0"}},
|
|
"__pkg_overridden__",
|
|
"FAIL",
|
|
["__pkg_overridden__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P06",
|
|
"removing pkg used as a key in `pnpm.overrides`",
|
|
{"pnpm": {"overrides": {"__pkg_pnpm_override__": "^1.0.0"}}},
|
|
"__pkg_pnpm_override__",
|
|
"FAIL",
|
|
["__pkg_pnpm_override__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P07",
|
|
"removing pkg used as a key in `pnpm.patchedDependencies`",
|
|
{"pnpm": {"patchedDependencies": {"__pkg_patched__": "patches/x.patch"}}},
|
|
"__pkg_patched__",
|
|
"FAIL",
|
|
["__pkg_patched__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P08",
|
|
"removing pkg used as a key in `peerDependenciesMeta`",
|
|
{"peerDependenciesMeta": {"__pkg_peer_meta__": {"optional": True}}},
|
|
"__pkg_peer_meta__",
|
|
"FAIL",
|
|
["__pkg_peer_meta__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P09",
|
|
"removing pkg referenced in `jest.preset` string",
|
|
{"jest": {"preset": "__pkg_jest_preset__"}},
|
|
"__pkg_jest_preset__",
|
|
"FAIL",
|
|
["__pkg_jest_preset__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P10",
|
|
"removing pkg referenced in `commitlint.extends`",
|
|
{"commitlint": {"extends": ["__pkg_commitlint__"]}},
|
|
"__pkg_commitlint__",
|
|
"FAIL",
|
|
["__pkg_commitlint__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P11",
|
|
"removing pkg referenced in `renovate.extends`",
|
|
{"renovate": {"extends": ["__pkg_renovate__"]}},
|
|
"__pkg_renovate__",
|
|
"FAIL",
|
|
["__pkg_renovate__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P12",
|
|
"removing pkg referenced in `remarkConfig.plugins`",
|
|
{"remarkConfig": {"plugins": ["__pkg_remark__"]}},
|
|
"__pkg_remark__",
|
|
"FAIL",
|
|
["__pkg_remark__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P13",
|
|
"removing pkg with subpath ref in tool config (`pkg/config`)",
|
|
{"prettier": "__pkg_prettier_sub__/config"},
|
|
"__pkg_prettier_sub__",
|
|
"FAIL",
|
|
["__pkg_prettier_sub__"],
|
|
),
|
|
PkgFieldCase(
|
|
"P14",
|
|
"false-positive guard: similar-prefix package in tool config",
|
|
{"prettier": "__pkg_short_extra__/config"},
|
|
"__pkg_short__",
|
|
"PASS",
|
|
[],
|
|
),
|
|
PkgFieldCase(
|
|
"P15",
|
|
"false-positive guard: package-named string in `browserslist` "
|
|
"must NOT trigger (browserslist values are browser queries, "
|
|
"never package names)",
|
|
{"browserslist": ["last 2 versions", "__pkg_browserslist__"]},
|
|
"__pkg_browserslist__",
|
|
"PASS",
|
|
[],
|
|
),
|
|
PkgFieldCase(
|
|
"P16",
|
|
"false-positive guard: matching string in `keywords` field",
|
|
{"keywords": ["__pkg_keyword__", "foo"]},
|
|
"__pkg_keyword__",
|
|
"PASS",
|
|
[],
|
|
),
|
|
PkgFieldCase(
|
|
"P17",
|
|
"false-positive guard: matching string in `workspaces` (paths)",
|
|
{"workspaces": ["packages/__pkg_workspace_path__"]},
|
|
"__pkg_workspace_path__",
|
|
"PASS",
|
|
[],
|
|
),
|
|
PkgFieldCase(
|
|
"P18",
|
|
"false-positive guard: matching value in `files` field",
|
|
{"files": ["dist/__pkg_in_files__"]},
|
|
"__pkg_in_files__",
|
|
"PASS",
|
|
[],
|
|
),
|
|
PkgFieldCase(
|
|
"P19",
|
|
"false-positive guard: matching `packageManager` string",
|
|
{"packageManager": "__pkg_in_pm__@1.0.0"},
|
|
"__pkg_in_pm__",
|
|
"PASS",
|
|
[],
|
|
),
|
|
]
|
|
|
|
|
|
def run_pkg_field_cases() -> int:
|
|
head_pkg = json.loads(HEAD_PKG.read_text())
|
|
passed = 0
|
|
for pc in PKG_FIELD_CASES:
|
|
synth_head = json.loads(json.dumps(head_pkg))
|
|
# Apply the field patch (deep-merge isn't needed; we control the keys).
|
|
for k, v in pc.field_patch.items():
|
|
synth_head[k] = v
|
|
# Base declares the target; head drops it from deps but references it
|
|
# via the extra field.
|
|
synth_base = json.loads(json.dumps(head_pkg))
|
|
synth_base.setdefault("dependencies", {})[pc.target_pkg] = "^1.0.0"
|
|
with tempfile.NamedTemporaryFile("w", suffix = ".json", delete = False) as f:
|
|
json.dump(synth_base, f, indent = 2)
|
|
base_path = f.name
|
|
with tempfile.NamedTemporaryFile("w", suffix = ".json", delete = False) as f:
|
|
json.dump(synth_head, f, indent = 2)
|
|
head_path = f.name
|
|
try:
|
|
proc = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--base-pkg",
|
|
base_path,
|
|
"--head-pkg",
|
|
head_path,
|
|
"--head-lock",
|
|
str(HEAD_LOCK),
|
|
],
|
|
capture_output = True,
|
|
text = True,
|
|
cwd = str(REPO),
|
|
)
|
|
finally:
|
|
os.unlink(base_path)
|
|
os.unlink(head_path)
|
|
actual_status = {0: "PASS", 1: "FAIL"}.get(proc.returncode, f"RC{proc.returncode}")
|
|
fails: list[str] = []
|
|
in_summary = False
|
|
for line in proc.stdout.splitlines():
|
|
if "FAIL:" in line and "removed package" in line:
|
|
in_summary = True
|
|
continue
|
|
if in_summary and line.strip().startswith("- "):
|
|
fails.append(line.strip()[2:])
|
|
# Both status and failure set must match.
|
|
ok = actual_status == pc.expected_status and set(fails) == set(pc.expected_failures)
|
|
mark = "PASS" if ok else "FAIL"
|
|
print(f" [{mark}] {pc.id}: {pc.desc}")
|
|
if not ok:
|
|
print(f" expected: status={pc.expected_status} fails={pc.expected_failures}")
|
|
print(f" actual: status={actual_status} fails={fails}")
|
|
for ln in proc.stdout.splitlines()[:25]:
|
|
print(f" {ln}")
|
|
if ok:
|
|
passed += 1
|
|
print()
|
|
print(f"{passed}/{len(PKG_FIELD_CASES)} package.json-field cases pass")
|
|
return 0 if passed == len(PKG_FIELD_CASES) else 1
|
|
|
|
|
|
def run_adversarial_cases() -> int:
|
|
ADVERSARIAL_TMP_DIR.mkdir(parents = True, exist_ok = True)
|
|
head_pkg = json.loads(HEAD_PKG.read_text())
|
|
passed = 0
|
|
for ac in ADV_CASES:
|
|
# Drop the synthetic file.
|
|
fpath = ADVERSARIAL_TMP_DIR / ac.filename
|
|
try:
|
|
fpath.write_text(ac.content)
|
|
# Base adds the target pkg; real head lacks it, so the script
|
|
# treats it as removed and scans the repo (now with our file).
|
|
synth_base = json.loads(json.dumps(head_pkg))
|
|
synth_base.setdefault("dependencies", {})[ac.target_pkg] = "^1.0.0"
|
|
with tempfile.NamedTemporaryFile("w", suffix = ".json", delete = False) as f:
|
|
json.dump(synth_base, f, indent = 2)
|
|
base_path = f.name
|
|
try:
|
|
proc = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--base-pkg",
|
|
base_path,
|
|
"--head-pkg",
|
|
str(HEAD_PKG),
|
|
"--head-lock",
|
|
str(HEAD_LOCK),
|
|
],
|
|
capture_output = True,
|
|
text = True,
|
|
cwd = str(REPO),
|
|
)
|
|
finally:
|
|
os.unlink(base_path)
|
|
actual_status = {0: "PASS", 1: "FAIL"}.get(proc.returncode, f"RC{proc.returncode}")
|
|
fails = []
|
|
in_summary = False
|
|
for line in proc.stdout.splitlines():
|
|
if "FAIL:" in line and "removed package" in line:
|
|
in_summary = True
|
|
continue
|
|
if in_summary and line.strip().startswith("- "):
|
|
fails.append(line.strip()[2:])
|
|
ok = actual_status == ac.expected_status and set(fails) == set(ac.expected_failures)
|
|
mark = "PASS" if ok else "FAIL"
|
|
print(f" [{mark}] {ac.id}: {ac.desc}")
|
|
if not ok:
|
|
print(f" expected: status={ac.expected_status} fails={ac.expected_failures}")
|
|
print(f" actual: status={actual_status} fails={fails}")
|
|
for ln in proc.stdout.splitlines()[:20]:
|
|
print(f" {ln}")
|
|
if ok:
|
|
passed += 1
|
|
finally:
|
|
try:
|
|
fpath.unlink()
|
|
except FileNotFoundError:
|
|
pass
|
|
# Clean up the directory.
|
|
try:
|
|
ADVERSARIAL_TMP_DIR.rmdir()
|
|
except OSError:
|
|
pass
|
|
print()
|
|
print(f"{passed}/{len(ADV_CASES)} adversarial cases pass")
|
|
return 0 if passed == len(ADV_CASES) else 1
|
|
|
|
|
|
# Dead-dep enumeration cases
|
|
|
|
|
|
@dataclass
|
|
class EnumCase:
|
|
id: str
|
|
desc: str
|
|
add_deps: dict[str, str]
|
|
add_dev_deps: dict[str, str]
|
|
field_patch: dict
|
|
extra_file: tuple[str, str] | None # (relative_path, content) or None
|
|
expected_unused: set[str]
|
|
expected_used: set[str]
|
|
expected_orphan_types: set[str]
|
|
|
|
|
|
ENUM_CASES: list[EnumCase] = [
|
|
EnumCase(
|
|
"E01",
|
|
"fake dep with no usage anywhere is flagged unused",
|
|
{"__enum_fake_unused_pkg__": "^1.0.0"},
|
|
{},
|
|
{},
|
|
None,
|
|
{"__enum_fake_unused_pkg__"},
|
|
set(),
|
|
set(),
|
|
),
|
|
EnumCase(
|
|
"E02",
|
|
"fake dep referenced via vite.config-style import is flagged used "
|
|
"(uses a real adversarial file as the import site)",
|
|
{"__enum_used_via_src__": "^1.0.0"},
|
|
{},
|
|
{},
|
|
(
|
|
"src/__dep_check_adversarial__/enum_e02.ts",
|
|
'import x from "__enum_used_via_src__";\n',
|
|
),
|
|
set(),
|
|
{"__enum_used_via_src__"},
|
|
set(),
|
|
),
|
|
EnumCase(
|
|
"E03",
|
|
"fake dep referenced only in package.json `overrides` is flagged used",
|
|
{"__enum_used_via_overrides__": "^1.0.0"},
|
|
{},
|
|
{"overrides": {"__enum_used_via_overrides__": "^1.0.0"}},
|
|
None,
|
|
set(),
|
|
{"__enum_used_via_overrides__"},
|
|
set(),
|
|
),
|
|
EnumCase(
|
|
"E04",
|
|
"@types/X where X is declared -> kept (NOT orphan)",
|
|
{"__enum_real_pkg__": "^1.0.0"},
|
|
{"@types/__enum_real_pkg__": "^1.0.0"},
|
|
{},
|
|
(
|
|
"src/__dep_check_adversarial__/enum_e04.ts",
|
|
'import x from "__enum_real_pkg__";\n',
|
|
),
|
|
set(),
|
|
{"__enum_real_pkg__"},
|
|
set(),
|
|
),
|
|
EnumCase(
|
|
"E05",
|
|
"@types/X where X is NOT declared anywhere -> orphan",
|
|
{},
|
|
{"@types/__enum_orphan_pkg__": "^1.0.0"},
|
|
{},
|
|
None,
|
|
set(),
|
|
set(),
|
|
{"@types/__enum_orphan_pkg__"},
|
|
),
|
|
]
|
|
|
|
|
|
def run_enum_cases() -> int:
|
|
head_pkg = json.loads(HEAD_PKG.read_text())
|
|
passed = 0
|
|
ADVERSARIAL_TMP_DIR.mkdir(parents = True, exist_ok = True)
|
|
for ec in ENUM_CASES:
|
|
synth_head = json.loads(json.dumps(head_pkg))
|
|
synth_head.setdefault("dependencies", {}).update(ec.add_deps)
|
|
synth_head.setdefault("devDependencies", {}).update(ec.add_dev_deps)
|
|
for k, v in ec.field_patch.items():
|
|
synth_head[k] = v
|
|
# Drop any temp source file if needed.
|
|
fpath = None
|
|
if ec.extra_file:
|
|
rel, content = ec.extra_file
|
|
fpath = REPO / rel
|
|
fpath.parent.mkdir(parents = True, exist_ok = True)
|
|
fpath.write_text(content)
|
|
with tempfile.NamedTemporaryFile("w", suffix = ".json", delete = False) as f:
|
|
json.dump(synth_head, f, indent = 2)
|
|
head_path = f.name
|
|
try:
|
|
proc = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--base-pkg",
|
|
str(HEAD_PKG),
|
|
"--head-pkg",
|
|
head_path,
|
|
"--head-lock",
|
|
str(HEAD_LOCK),
|
|
"--enumerate-dead",
|
|
],
|
|
capture_output = True,
|
|
text = True,
|
|
cwd = str(REPO),
|
|
)
|
|
finally:
|
|
os.unlink(head_path)
|
|
if fpath:
|
|
try:
|
|
fpath.unlink()
|
|
except FileNotFoundError:
|
|
pass
|
|
# Parse the dead-dep enumeration output.
|
|
unused: set[str] = set()
|
|
orphans: set[str] = set()
|
|
in_unused = False
|
|
in_orphan = False
|
|
for line in proc.stdout.splitlines():
|
|
s = line.strip()
|
|
if s.startswith("unused ("):
|
|
in_unused = True
|
|
in_orphan = False
|
|
continue
|
|
if s.startswith("type_pkg_orphan ("):
|
|
in_unused = False
|
|
in_orphan = True
|
|
continue
|
|
if s.startswith("used:") or s.startswith("type_pkg_kept:"):
|
|
in_unused = in_orphan = False
|
|
continue
|
|
if s.startswith("- "):
|
|
if in_unused:
|
|
unused.add(s[2:])
|
|
elif in_orphan:
|
|
orphans.add(s[2:])
|
|
unused_ok = ec.expected_unused.issubset(unused) and (
|
|
not ec.expected_used or not (ec.expected_used & unused)
|
|
)
|
|
orphan_ok = ec.expected_orphan_types.issubset(orphans)
|
|
ok = unused_ok and orphan_ok
|
|
mark = "PASS" if ok else "FAIL"
|
|
print(f" [{mark}] {ec.id}: {ec.desc}")
|
|
if not ok:
|
|
print(f" expected unused superset: {sorted(ec.expected_unused)}")
|
|
print(f" expected used NOT in unused: {sorted(ec.expected_used)}")
|
|
print(f" expected orphans superset: {sorted(ec.expected_orphan_types)}")
|
|
print(f" actual unused: {sorted(unused)}")
|
|
print(f" actual orphans: {sorted(orphans)}")
|
|
for ln in proc.stdout.splitlines()[:30]:
|
|
print(f" {ln}")
|
|
if ok:
|
|
passed += 1
|
|
# Cleanup tmp dir if empty.
|
|
try:
|
|
ADVERSARIAL_TMP_DIR.rmdir()
|
|
except OSError:
|
|
pass
|
|
print()
|
|
print(f"{passed}/{len(ENUM_CASES)} enumeration cases pass")
|
|
return 0 if passed == len(ENUM_CASES) else 1
|
|
|
|
|
|
# Script-wrapper cases: scripts_bin_refs / _next_real_bin must credit the real
|
|
# bin (`biome` -> @biomejs/biome), not the wrapper. The old "first non-env
|
|
# token" heuristic missed cross-env / dotenv / etc.
|
|
|
|
|
|
@dataclass
|
|
class WrapperCase:
|
|
id: str
|
|
desc: str
|
|
raw_cmd: str
|
|
expected_bin: str | None # None means "no real bin (e.g. unwrappable)"
|
|
|
|
|
|
WRAPPER_CASES: list[WrapperCase] = [
|
|
WrapperCase(
|
|
"W01",
|
|
"cross-env wraps the real bin",
|
|
"cross-env CI=1 biome check .",
|
|
"biome",
|
|
),
|
|
WrapperCase(
|
|
"W02",
|
|
"cross-env with multiple env tokens after the wrapper",
|
|
"cross-env A=1 B=2 NODE_ENV=prod biome check",
|
|
"biome",
|
|
),
|
|
WrapperCase(
|
|
"W03",
|
|
"bare env-prefix run (no wrapper) still peels the env tokens",
|
|
"FOO=bar biome check",
|
|
"biome",
|
|
),
|
|
WrapperCase(
|
|
"W04",
|
|
"quoted env value with spaces (shlex preserves it as one word)",
|
|
'FOO="a b" biome check',
|
|
"biome",
|
|
),
|
|
WrapperCase(
|
|
"W05",
|
|
"npx + cross-env: runner peels, wrapper peels, real bin wins",
|
|
"npx cross-env CI=1 biome check",
|
|
"biome",
|
|
),
|
|
WrapperCase(
|
|
"W06",
|
|
"pnpm exec + cross-env",
|
|
"pnpm exec cross-env CI=1 biome check",
|
|
"biome",
|
|
),
|
|
WrapperCase(
|
|
"W07",
|
|
"dotenv with the `--` separator before the wrapped command",
|
|
"dotenv -- biome check",
|
|
"biome",
|
|
),
|
|
WrapperCase(
|
|
"W08",
|
|
"dotenv with a flag-arg pair and `--` separator",
|
|
"dotenv -e .env -- biome check",
|
|
"biome",
|
|
),
|
|
WrapperCase(
|
|
"W09",
|
|
"leading `./node_modules/.bin/` prefix is stripped",
|
|
"./node_modules/.bin/biome check",
|
|
"biome",
|
|
),
|
|
WrapperCase(
|
|
"W10",
|
|
"concurrently is NOT a script wrapper -- it dispatches by "
|
|
"script *name*, not bin, so the real bin is `concurrently` "
|
|
"itself (the wrapped script names are credited by their own "
|
|
"scripts entries, which scripts_bin_refs iterates separately)",
|
|
'concurrently "npm:dev" "npm:typecheck"',
|
|
"concurrently",
|
|
),
|
|
]
|
|
|
|
|
|
def run_wrapper_cases() -> int:
|
|
import shlex
|
|
|
|
passed = 0
|
|
for wc in WRAPPER_CASES:
|
|
try:
|
|
words = shlex.split(wc.raw_cmd, posix = True)
|
|
except ValueError:
|
|
words = wc.raw_cmd.split()
|
|
actual = _next_real_bin(words, 0)
|
|
ok = actual == wc.expected_bin
|
|
mark = "PASS" if ok else "FAIL"
|
|
print(f" [{mark}] {wc.id}: {wc.desc}")
|
|
if not ok:
|
|
print(f" raw_cmd={wc.raw_cmd!r}")
|
|
print(f" expected={wc.expected_bin!r}, actual={actual!r}")
|
|
if ok:
|
|
passed += 1
|
|
|
|
# End-to-end: feed scripts_bin_refs a head_pkg whose scripts use a wrapper
|
|
# and confirm the wrapped bin's owner is credited (the find_command_usage path).
|
|
int_total = 0
|
|
int_passed = 0
|
|
int_cases = [
|
|
(
|
|
"I01",
|
|
"cross-env wrapping `biome` credits @biomejs/biome",
|
|
{"lint": "cross-env CI=1 biome check"},
|
|
{"biome": "@biomejs/biome"},
|
|
"@biomejs/biome",
|
|
),
|
|
(
|
|
"I02",
|
|
"dotenv -- biome credits @biomejs/biome",
|
|
{"lint": "dotenv -- biome check"},
|
|
{"biome": "@biomejs/biome"},
|
|
"@biomejs/biome",
|
|
),
|
|
(
|
|
"I03",
|
|
"quoted env value before bin still credits the bin's owner",
|
|
{"lint": 'FOO="a b" biome check .'},
|
|
{"biome": "@biomejs/biome"},
|
|
"@biomejs/biome",
|
|
),
|
|
(
|
|
"I04",
|
|
"&& chain: both halves credit their owning packages",
|
|
{"build": "tsc -b && cross-env CI=1 biome check"},
|
|
{"tsc": "typescript", "biome": "@biomejs/biome"},
|
|
None, # checked via owning_pkgs below
|
|
),
|
|
]
|
|
for case_id, desc, scripts, bin_to_pkg, expect_owner in int_cases:
|
|
int_total += 1
|
|
refs = scripts_bin_refs({"scripts": scripts}, bin_to_pkg)
|
|
if case_id == "I04":
|
|
owners = set(refs.keys())
|
|
ok = owners == {"typescript", "@biomejs/biome"}
|
|
else:
|
|
ok = expect_owner in refs
|
|
mark = "PASS" if ok else "FAIL"
|
|
print(f" [{mark}] {case_id}: {desc}")
|
|
if not ok:
|
|
print(f" scripts={scripts!r} bin_to_pkg={bin_to_pkg!r}")
|
|
print(f" refs={refs!r}")
|
|
if ok:
|
|
int_passed += 1
|
|
|
|
total = len(WRAPPER_CASES) + int_total
|
|
print()
|
|
print(f"{passed + int_passed}/{total} wrapper-script cases pass")
|
|
return 0 if (passed == len(WRAPPER_CASES) and int_passed == int_total) else 1
|
|
|
|
|
|
def main() -> int:
|
|
head_pkg = json.loads(HEAD_PKG.read_text())
|
|
print(f"Running {len(CASES)} edge cases against {SCRIPT.relative_to(REPO)}")
|
|
print()
|
|
results: list[tuple[Case, bool, str]] = []
|
|
for c in CASES:
|
|
ok, detail = run_case(c, head_pkg)
|
|
results.append((c, ok, detail))
|
|
mark = "PASS" if ok else "FAIL"
|
|
print(f" [{mark}] {c.id}: {c.desc}")
|
|
if not ok:
|
|
for line in detail.splitlines():
|
|
print(f" {line}")
|
|
print()
|
|
passed = sum(1 for _, ok, _ in results if ok)
|
|
total = len(results)
|
|
print(f"{passed}/{total} edge cases pass")
|
|
|
|
print()
|
|
print(f"Running {len(CLASSIFY_CASES)} classify() unit cases")
|
|
print()
|
|
cls_rc = run_classify_unit_tests()
|
|
|
|
print()
|
|
print(f"Running {len(ADV_CASES)} adversarial end-to-end cases")
|
|
print()
|
|
adv_rc = run_adversarial_cases()
|
|
|
|
print()
|
|
print(f"Running {len(PKG_FIELD_CASES)} package.json-field cases")
|
|
print()
|
|
pkg_rc = run_pkg_field_cases()
|
|
|
|
print()
|
|
print(f"Running {len(ENUM_CASES)} dead-dep enumeration cases")
|
|
print()
|
|
enum_rc = run_enum_cases()
|
|
|
|
print()
|
|
print(
|
|
f"Running {len(WRAPPER_CASES)} script-wrapper cases "
|
|
"(_next_real_bin + scripts_bin_refs end-to-end)"
|
|
)
|
|
print()
|
|
wrap_rc = run_wrapper_cases()
|
|
|
|
if (
|
|
passed == total
|
|
and cls_rc == 0
|
|
and adv_rc == 0
|
|
and pkg_rc == 0
|
|
and enum_rc == 0
|
|
and wrap_rc == 0
|
|
):
|
|
return 0
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|