Skip to content
Merged
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
16 changes: 16 additions & 0 deletions crates/codeguard-cli/src/eslint_lint_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ pub fn run(args: &[String]) -> ExitCode {
}
#[cfg(feature = "wasm-precheck")]
if crate::typescript_syntax_precheck::eligible(&args) {
if crate::eslint_native_first_candidate::observed_candidate(&args.source) {
let mut report = observe_with_preparation(&args, deadline);
if report["reason"] == "eslint_execution_context_missing"
&& report["workbench"]["next"]["repair_brief"]["step"].is_null()
{
report["next_action"] = json!(
"项目本地 ESLint 候选和配置已发现;先核对 Node 路径、ESLint 版本与原配置并执行原生检查。候选尚未通过探测,不能认定 lint 已完成"
);
}
if args.json {
println!("{report}");
} else {
print_feedback(&report);
}
return ExitCode::from(3);
}
let report = crate::typescript_syntax_precheck::observe(&args, deadline);
if args.json {
println!("{report}");
Expand Down
65 changes: 65 additions & 0 deletions crates/codeguard-cli/src/eslint_native_first_candidate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! 单文件检查前的项目本地 ESLint 候选识别;只读且不执行配置。

use codeguard_adapters::{EslintConfigState, inspect_eslint_local_candidate};
use codeguard_core::ObservedPathKind;
use codeguard_runtime::read_bounded_regular_file;
use std::path::Path;

/// 识别源码最近的项目根是否具备可进一步原生探测的本地 ESLint 与配置。
/// 参数为普通源码路径;返回真仅表示应先确认原生调用,不证明工具可执行或规则有效。
pub(crate) fn observed_candidate(source: &Path) -> bool {
let Ok(source) = source.canonicalize() else {
return false;
};
let Some(parent) = source.parent() else {
return false;
};
for root in parent.ancestors() {
let manifest = root.join("package.json");
if !regular_file(&manifest) {
continue;
}
let Ok(project_manifest) = read_bounded_regular_file(&manifest, 256 * 1024) else {
return false;
};
let local_root = root.join("node_modules");
let package_root = local_root.join("eslint");
if !regular_directory(&local_root) || !regular_directory(&package_root) {
return false;
}
let local_manifest = package_root.join("package.json");
let entry = package_root.join("bin/eslint.js");
if !regular_file(&local_manifest) || !regular_file(&entry) {
return false;
}
let Ok(local_manifest) = read_bounded_regular_file(&local_manifest, 256 * 1024) else {
return false;
};
// 仅把 ESLint 10 可直接选取的普通 flat config 作为候选;旧配置和 TS loader 须另行确认。
let config_count = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"]
.iter()
.filter(|name| {
let path = root.join(name);
regular_file(&path) && read_bounded_regular_file(&path, 1024 * 1024).is_ok()
});
if config_count.count() != 1 {
return false;
}
let candidate = inspect_eslint_local_candidate(
Some(&project_manifest),
Some(&local_manifest),
Some(ObservedPathKind::File),
EslintConfigState::Observed,
);
return candidate.state == "local_candidate_requires_native_probe";
}
false
}

fn regular_file(path: &Path) -> bool {
std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.is_file())
}

fn regular_directory(path: &Path) -> bool {
std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.is_dir())
}
2 changes: 2 additions & 0 deletions crates/codeguard-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ mod eslint_effective_settings;
mod eslint_lint_arguments;
#[cfg(unix)]
pub mod eslint_lint_command;
#[cfg(feature = "wasm-precheck")]
mod eslint_native_first_candidate;
mod eslint_preparation;
pub mod eslint_probe;
pub mod eslint_probe_request;
Expand Down
53 changes: 53 additions & 0 deletions crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,59 @@ fn no_recovery_remains_incomplete_and_javascript_does_not_use_typescript_grammar
assert!(js_report.get("syntax_precheck").is_none());
}

#[test]
fn observed_project_local_eslint_candidate_is_not_treated_as_missing_native_lint() {
let project = Project::new();
fs::write(project.0.join("app.ts"), "const value: number = ;\n").unwrap();
fs::write(
project.0.join("package.json"),
r#"{"devDependencies":{"eslint":"10.0.0"}}"#,
)
.unwrap();
fs::write(project.0.join("eslint.config.mjs"), "export default [];\n").unwrap();
fs::create_dir_all(project.0.join("node_modules/eslint/bin")).unwrap();
fs::write(
project.0.join("node_modules/eslint/package.json"),
r#"{"name":"eslint","version":"10.0.0","bin":{"eslint":"bin/eslint.js"}}"#,
)
.unwrap();
fs::write(
project.0.join("node_modules/eslint/bin/eslint.js"),
"process.exit(99);\n",
)
.unwrap();

let (status, report) = project.lint("app.ts");
assert_eq!(status, 3);
assert_eq!(report["reason"], "eslint_execution_context_missing");
assert!(report.get("syntax_precheck").is_none());
assert!(
report["next_action"]
.as_str()
.unwrap()
.contains("项目本地 ESLint 候选")
);
assert_eq!(report["delivery_decision"], "not_evaluated");
}

#[test]
fn configured_but_uninstalled_eslint_still_allows_candidate_precheck() {
let project = Project::new();
fs::write(project.0.join("app.ts"), "const value: number = ;\n").unwrap();
fs::write(
project.0.join("package.json"),
r#"{"devDependencies":{"eslint":"10.0.0"}}"#,
)
.unwrap();
fs::write(project.0.join("eslint.config.mjs"), "export default [];\n").unwrap();

let (status, report) = project.lint("app.ts");
assert_eq!(status, 3);
assert_eq!(report["native"]["status"], "not_run");
assert!(report.get("syntax_precheck").is_some());
assert_eq!(report["delivery_decision"], "not_evaluated");
}

#[test]
fn tsx_uses_its_own_grammar_instead_of_reporting_valid_jsx_as_suspect() {
let project = Project::new();
Expand Down
2 changes: 1 addition & 1 deletion openspec/changes/introduce-rust-codeguard-cli/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@ TDD缺自动同步先RED。相关普通:build9、work sync15、next6,共30
- [ ] 14.3 进程资源隔离。责任:runtime。规格:SP05。依赖:14.2。验收:验证超时、内存、输出、取消与崩溃;损坏 grammar 不阻塞其他模块、不遗留进程。进行中:Unix 候选 worker 复用受控进程组内核,输入 1 MiB、输出 64 KiB,父进程有共同截止时间和取消;崩溃后代清理、输出洪泛、运行中取消及随后正常解析均增加真实子进程反例。Linux 2 GiB `RLIMIT_AS`、3 GiB 分配负例与 worker 故障隔离在 [Ubuntu CI](https://github.com/full-stack-plugins/codeguard/actions/runs/36478461118) 真实通过;Wasmtime 虚拟预留缩小。macOS 本机硬限制实测不支持,候选仍不具内存隔离权威;见[局部验收](../../../tests/acceptance/syntax-worker-candidate.md)。其它目标平台、损坏 grammar 隔离及正式调度仍缺,不勾选。
- [ ] 14.4 语言覆盖与源码映射。责任:adapters。规格:SP06。依赖:14.2。验收:识别 ERROR/MISSING,合并恢复节点;测试 Unicode、换行、嵌入语言、新语法和未知版本,不将不支持当源码违规。进行中:Rust 已提取两类原始节点,adapters 核对 UTF-8/CRLF 字节锚点并转换 Unicode 标量列;结构祖先归组保留并列错误,预算截断保持不完整;[局部证据](../../../tests/acceptance/syntax-recovery-candidate.md)。级联归并语料、版本/嵌入语言及原生对照仍缺。
- [ ] 14.5 原生工具准备探测。责任:adapters。规格:SP01,SP03。依赖:现有配置发现。验收:识别项目本地工具、wrapper、版本和无效配置;PATH 缺失不等于未安装,不对坏配置重复建议安装。进行中:ESLint 本地包/入口及 Maven Wrapper 脚本/发行配置已有只读候选分类,`detect` 与 `check` 已输出固定路径观察的候选;`node_modules` 普通源码遍历被排除,链接和损坏配置保留阻塞;发现协议升级为 0.4.0,聚合协议升级为 0.31.0 并保留旧 schema;[局部证据](../../../tests/acceptance/native-tool-readiness-candidates.md)。显式工具、受控原生版本探针、原生优先调度、更多构建器和真实多平台验收尚缺,不勾选。
- [ ] 14.6 逐模块原生优先调度。责任:CLI/core。规格:SP01。依赖:14.4,14.5。验收:原生可用先执行;原生违规不得被 WASM 洗白;多根分别选择并保留运行失败与兄弟结果。进行中:可选 CLI 特性在 `lint typescript`、`lint java` 显式单文件且完全缺少原生上下文时附加候选语法观察,部分原生上下文、符号链接、其它扩展名及 Javadoc/Checkstyle 选择保留原路径,见 [TypeScript 局部验收](../../../tests/acceptance/typescript-syntax-fallback-candidate.md)、[Java 局部验收](../../../tests/acceptance/java-syntax-fallback-candidate.md)。新增 `.tsx` 独立 grammar 的有效/无效 JSX 反例与候选报告方言绑定,见 [TSX 纠错验收](../../../tests/acceptance/tsx-grammar-candidate.md)。这只证明未给显式上下文,不证明原生工具缺失;本地工具自动选择、多模块调度、真实原生/初检混合保留仍缺,不勾选。
- [ ] 14.6 逐模块原生优先调度。责任:CLI/core。规格:SP01。依赖:14.4,14.5。验收:原生可用先执行;原生违规不得被 WASM 洗白;多根分别选择并保留运行失败与兄弟结果。进行中:可选 CLI 特性在 `lint typescript`、`lint java` 显式单文件且完全缺少原生上下文时附加候选语法观察,部分原生上下文、符号链接、其它扩展名及 Javadoc/Checkstyle 选择保留原路径,见 [TypeScript 局部验收](../../../tests/acceptance/typescript-syntax-fallback-candidate.md)、[Java 局部验收](../../../tests/acceptance/java-syntax-fallback-candidate.md)。新增 `.tsx` 独立 grammar 的有效/无效 JSX 反例与候选报告方言绑定,见 [TSX 纠错验收](../../../tests/acceptance/tsx-grammar-candidate.md)。本地 ESLint 10 候选与配置可见时,单文件 TypeScript 路径现在先返回待原生探测而不抢先启动 WASM;只声明/配置但无本地入口仍可初检,见 [本地原生优先局部验收](../../../tests/acceptance/native-first-eslint-candidate.md)。这不证明本地原生工具已执行;可信 Node/参数自动选择、多模块调度、真实原生/初检混合保留仍缺,不勾选。
- [ ] 14.7 初检协议与 schema。责任:core/CLI。规格:SP02,SP11。依赖:14.4。验收:固定版本和状态枚举、输入/资产身份、范围及缺口;空文件集、部分解析和未知版本不能 clean;未知 major 拒绝。进行中:core 已有状态聚合及[局部证据](../../../tests/acceptance/syntax-precheck-status-candidate.md);adapters 候选报告 1.0.0 的封闭 schema 与严格读者拒绝重复键、未知版本、身份漂移与伪造 clean,见[候选报告证据](../../../tests/acceptance/syntax-precheck-report-candidate.md);可选 CLI `lint typescript` 与 `lint java` 单文件入口分别产生 TypeScript 0.3.0、TSX 0.4.0、Java 0.1.0 局部候选反馈及位置化疑似观察,见 [TypeScript](../../../tests/acceptance/typescript-syntax-fallback-candidate.md) 与 [Java](../../../tests/acceptance/java-syntax-fallback-candidate.md) 验收。正式全范围/嵌入区域/排除原因、持久化任务引用、所有消费者未知 major 拒绝与发布兼容仍缺,不勾选。
- [ ] 14.8 安装建议与必须确认策略。责任:core。规格:SP03。依赖:14.6,14.7。验收:可选原生缺失且完整 clean 仅推荐;疑似或已有必需义务要求恢复原生确认;无适配器给具体决策。进行中:core 已有纯决策函数,区分缺工具、坏配置、执行失败和无适配器;仅非空、完整、已验收且无恢复节点的 clean 才允许可选建议,伪造 clean 状态不能降级。见[局部验收](../../../tests/acceptance/syntax-setup-guidance-candidate.md)。真实工具准备来源、CLI/工作台任务投影及宿主反馈尚未接线,不勾选。
- [ ] 14.9 四类可读与 JSON 报告。责任:CLI。规格:SP07,SP11。依赖:14.7,14.8。验收:覆盖原生成功、缺原生且 clean、疑似语法、初检不可用;结果范围/行动/交付语义一致;不伪造任务 ID。进行中:启用可选 WASM 特性的 TypeScript 与 Java 单文件在无显式原生上下文时提供 JSON/human 候选疑似观察;无恢复节点仍为 incomplete,任务引用为空;Java 超大输入保留 worker 未完成,见 [TypeScript](../../../tests/acceptance/typescript-syntax-fallback-candidate.md) 与 [Java](../../../tests/acceptance/java-syntax-fallback-candidate.md) 局部验收。正式四类报告、多模块范围与宿主渲染仍缺,不勾选。
Expand Down
9 changes: 9 additions & 0 deletions tests/acceptance/native-first-eslint-candidate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# TypeScript 本地 ESLint 候选优先:局部验收(2026-09-29)

对应 OpenSpec 14.6。启用 `wasm-precheck` 的单文件 `lint typescript` 在缺少显式原生参数时,先只读核对源码最近的 `package.json`、普通 `node_modules/eslint/package.json` 与 `bin/eslint.js`,以及唯一的普通 `eslint.config.js/mjs/cjs`。只有适配器将其分类为 `local_candidate_requires_native_probe` 时,命令不启动 WASM worker,返回未完成的原生准备反馈,并提示先核对 Node 路径、版本与原配置。旧 `.eslintrc`、TypeScript 配置及多个 flat config 不作为可直接选取的候选。候选的存在不等于原生可运行;本增量不自动执行本地入口,也不授予交付通过。

先新增真实 CLI 反例:项目本地 ESLint 10 包、入口和配置可见时,旧命令仍启动 WASM,断言因出现 `syntax_precheck` 而失败。接线后该用例通过。另一个反例保留相同声明和配置但不安装本地包,仍允许候选初检,且 `native=not_run`、`delivery_decision=not_evaluated`。`eslint_lint_cli` 现有显式原生路径与 `grammar_status_cli` 一并回归。

本次定向测试:`CARGO_NET_OFFLINE=true cargo test --locked -p codeguard-cli --features wasm-precheck --test typescript_syntax_fallback_candidate --test eslint_lint_cli --test grammar_status_cli`,共 16 通过、4 项需要显式真实 Node/ESLint 的用例忽略。候选探测不执行项目 JavaScript、不修改 `node_modules`,不把“未给显式参数”当作“原生工具不存在”。

仍缺:可信 Node 路径与原调用参数解析、受控版本探针、动态配置有效性确认、项目本地原生自动执行、多模块原生优先调度及真实宿主反馈。因此 14.5/14.6 均不勾选。
Loading