Skip to content

Commit 1a295e9

Browse files
committed
修复 Clippy 新增源码与嵌套清单变化后的陈旧诊断和覆盖
1 parent bdae3fd commit 1a295e9

13 files changed

Lines changed: 242 additions & 1 deletion

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
- name: Verify Linux WASM worker resource boundary
5050
run: |
5151
# 帮助目录须保留当前构建特性;默认 suite 无法证明 WASM 入口状态。
52-
cargo test --locked -p codeguard-cli --features wasm-precheck --test help_command_contract --test rules_list_cli --test version_cli --test rust_lint_cli --test ruby_lint_cli --test check_all_ruby --test ruby_native_hook --test ruby_project_version --test shell_lint_cli --test shell_workbench --test check_all_shell --test shell_native_hook --test hook_failed_write_options --test go_native_hook --test cfquery_postgres_dialect --test cfquery_structure
52+
cargo test --locked -p codeguard-cli --features wasm-precheck --test help_command_contract --test rules_list_cli --test version_cli --test rust_lint_cli --test rust_lint_input_stability --test check_all_rust_native --test ruby_lint_cli --test check_all_ruby --test ruby_native_hook --test ruby_project_version --test shell_lint_cli --test shell_workbench --test check_all_shell --test shell_native_hook --test hook_failed_write_options --test go_native_hook --test cfquery_postgres_dialect --test cfquery_structure
5353
cargo test --locked -p codeguard-cli --features wasm-precheck --test workspace_msrv --test crate_boundaries
5454
cargo test -p codeguard-runtime --features wasm-precheck --test process_contract
5555
cargo test -p codeguard-runtime --features wasm-precheck --test wasm_grammar_load --test wasm_recovery_scan --test wasm_empty_block_scan --test wasm_root_child_scan --test dart_upstream_corpus

‎crates/codeguard-cli/src/lib.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ pub(crate) mod rust_cve_task_recheck;
219219
#[cfg(unix)]
220220
mod rust_lint_inputs;
221221
#[cfg(unix)]
222+
mod rust_input_inventory;
223+
#[cfg(unix)]
222224
pub mod rust_lint_scan;
223225
#[cfg(unix)]
224226
mod rust_native_syntax_coverage;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use std::{
2+
collections::BTreeSet,
3+
io,
4+
path::{Path, PathBuf},
5+
};
6+
7+
/// Rust 输入范围的静态清单;来源:OpenSpec execution-kernel 输入前后复核。
8+
/// 沿用普通发现的排除和条目预算,不推断 Cargo 动态目标或外部依赖闭包。
9+
#[derive(Eq, PartialEq)]
10+
pub(crate) struct RustInputInventory {
11+
pub(crate) source_files: BTreeSet<String>,
12+
pub(crate) configuration_files: BTreeSet<PathBuf>,
13+
}
14+
15+
impl RustInputInventory {
16+
/// 读取普通发现范围内的 Rust 源码、Cargo 清单与锁文件名集合。
17+
/// 参数为受检根;返回静态清单,读取失败或截断不能作为空清单消费。
18+
pub(crate) fn capture(root: &Path) -> io::Result<Self> {
19+
let registry = codeguard_adapters::legacy_registry()
20+
.map_err(|_| io::Error::other("Rust 输入注册表不可用"))?;
21+
let observation =
22+
crate::discovery::discover(root, &registry, &codeguard_runtime::NativeObservation);
23+
if !observation.observation_complete {
24+
return Err(io::Error::other("Rust 输入范围发现未完成"));
25+
}
26+
let source_files = observation
27+
.languages
28+
.get("rust")
29+
.map(|language| language.source_files.clone())
30+
.unwrap_or_default();
31+
let mut configuration_files = observation
32+
.languages
33+
.get("rust")
34+
.map(|language| language.manifests.iter().map(PathBuf::from).collect())
35+
.unwrap_or_else(BTreeSet::new);
36+
configuration_files.extend(
37+
observation
38+
.lockfiles
39+
.iter()
40+
.filter(|path| {
41+
Path::new(path)
42+
.file_name()
43+
.is_some_and(|name| name == "Cargo.lock")
44+
})
45+
.map(PathBuf::from),
46+
);
47+
Ok(Self {
48+
source_files,
49+
configuration_files,
50+
})
51+
}
52+
}

‎crates/codeguard-cli/src/rust_lint_inputs.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub(crate) struct RustLintInputs {
1313
snapshot: SourceSnapshot,
1414
absent: Vec<PathBuf>,
1515
root_identity: (u64, u64),
16+
inventory: crate::rust_input_inventory::RustInputInventory,
1617
}
1718
impl RustLintInputs {
1819
/// 在原生启动前捕获指定源码和根配置;返回有界快照或不完整原因。
@@ -31,7 +32,10 @@ impl RustLintInputs {
3132
));
3233
}
3334
let root_identity = (metadata.dev(), metadata.ino());
35+
let inventory = crate::rust_input_inventory::RustInputInventory::capture(root)?;
3436
let mut paths: BTreeSet<PathBuf> = sources.iter().map(PathBuf::from).collect();
37+
paths.extend(inventory.source_files.iter().map(PathBuf::from));
38+
paths.extend(inventory.configuration_files.iter().cloned());
3539
paths.insert(PathBuf::from("Cargo.toml"));
3640
let mut absent = Vec::new();
3741
// 同时固定两种配置名称;不存在也必须固定,避免扫描中新增配置改变规则。
@@ -66,6 +70,7 @@ impl RustLintInputs {
6670
snapshot,
6771
absent,
6872
root_identity,
73+
inventory,
6974
})
7075
}
7176
/// 返回扫描前指定文件的原始字节,供诊断指纹和源码摘要绑定。
@@ -83,6 +88,8 @@ impl RustLintInputs {
8388
.snapshot
8489
.verify_source_unchanged()
8590
.is_ok_and(|same| same)
91+
&& crate::rust_input_inventory::RustInputInventory::capture(root)
92+
.is_ok_and(|current| current == self.inventory)
8693
&& self.absent.iter().all(|relative| {
8794
fs::symlink_metadata(root.join(relative))
8895
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)

‎crates/codeguard-cli/tests/rust_lint_input_stability.rs‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,55 @@ fn stable_native_diagnostic_keeps_original_source_binding() {
120120
assert_eq!(report["delivery_decision"], "not_evaluated");
121121
}
122122

123+
#[test]
124+
fn new_rust_source_with_no_native_diagnostics_withdraws_completion() {
125+
let p = Project::new();
126+
let report = p.scan("printf '%s\\n' 'pub fn added() {}' > src/added.rs", false);
127+
assert_eq!(report["local_scan_complete"], false, "{report}");
128+
assert_eq!(report["reason"], "rust_inputs_changed_during_scan");
129+
}
130+
131+
#[test]
132+
fn changed_nested_cargo_manifest_withdraws_current_diagnostic() {
133+
let p = Project::new();
134+
fs::create_dir(p.0.join("member")).unwrap();
135+
fs::write(
136+
p.0.join("member/Cargo.toml"),
137+
"[package]\nname='member'\nversion='0.1.0'\nedition='2021'\n",
138+
)
139+
.unwrap();
140+
let report = p.scan("printf '%s\\n' '[package]' \"name='member'\" \"version='0.1.0'\" \"edition='2024'\" > member/Cargo.toml", true);
141+
assert_eq!(report["local_scan_complete"], false, "{report}");
142+
assert_eq!(report["reason"], "rust_inputs_changed_during_scan");
143+
assert_eq!(report["findings"], serde_json::json!([]));
144+
}
145+
146+
#[test]
147+
fn changed_other_observed_source_or_renamed_source_withdraws_completion() {
148+
for mutation in [
149+
"printf '%s\\n' 'pub fn changed() {}' > src/other.rs",
150+
"mv src/other.rs src/renamed.rs",
151+
"rm src/other.rs",
152+
] {
153+
let p = Project::new();
154+
fs::write(p.0.join("src/other.rs"), "pub fn other() {}\n").unwrap();
155+
let report = p.scan(mutation, false);
156+
assert_eq!(report["local_scan_complete"], false, "{mutation}: {report}");
157+
assert_eq!(report["reason"], "rust_inputs_changed_during_scan");
158+
}
159+
}
160+
161+
#[test]
162+
fn excluded_workbench_records_do_not_invalidate_rust_inputs() {
163+
let p = Project::new();
164+
let report = p.scan(
165+
"mkdir -p .codeguard/reports; printf '{}' > .codeguard/reports/current.json",
166+
false,
167+
);
168+
assert_eq!(report["local_scan_complete"], true, "{report}");
169+
assert_eq!(report["delivery_decision"], "not_evaluated");
170+
}
171+
123172
#[test]
124173
fn stable_partial_native_report_keeps_valid_diagnostic() {
125174
let p = Project::new();
@@ -167,6 +216,47 @@ fn real_clippy_output_is_withdrawn_when_source_changes_after_native_execution()
167216
}
168217
}
169218

219+
#[test]
220+
#[ignore = "requires installed native Cargo Clippy; set CODEGUARD_CARGO_BIN"]
221+
fn real_clippy_output_is_withdrawn_when_new_source_appears_after_execution() {
222+
let tool = std::env::var("CODEGUARD_CARGO_BIN").expect("原生 Cargo 路径");
223+
assert!(std::path::Path::new(&tool).is_absolute());
224+
let quoted = format!("'{}'", tool.replace('\'', "'\\''"));
225+
let p = Project::new();
226+
let wrapper = p.0.join("cargo-original");
227+
fs::write(&wrapper,format!("#!/bin/sh\n{quoted} \"$@\" > native-output.jsonl\nresult=$?\ncat native-output.jsonl\nif [ \"$result\" = 0 ]; then printf '%s\\n' 'pub fn added() {{}}' > src/added.rs; fi\nexit \"$result\"\n")).unwrap();
228+
fs::set_permissions(&wrapper, fs::Permissions::from_mode(0o700)).unwrap();
229+
let report = observe_cargo_clippy(
230+
&p.0,
231+
&BTreeSet::from(["src/lib.rs".into()]),
232+
Some(&wrapper),
233+
Instant::now() + Duration::from_secs(60),
234+
&AtomicBool::new(false),
235+
);
236+
let native = codeguard_adapters::parse_cargo_clippy_json(
237+
&fs::read(p.0.join("native-output.jsonl")).unwrap(),
238+
);
239+
assert!(native.issue.is_none());
240+
assert!(
241+
native
242+
.findings
243+
.iter()
244+
.any(|f| f.rule_id == "clippy::needless_return")
245+
);
246+
assert!(p.0.join("src/added.rs").is_file());
247+
assert_eq!(report["reason"], "rust_inputs_changed_during_scan");
248+
assert_eq!(report["local_scan_complete"], false);
249+
assert_eq!(report["findings"], serde_json::json!([]));
250+
if let Ok(dir) = std::env::var("CODEGUARD_CLIPPY_REPORT_DIR") {
251+
fs::create_dir_all(&dir).unwrap();
252+
fs::write(
253+
PathBuf::from(dir).join("real-clippy-new-source.json"),
254+
serde_json::to_vec_pretty(&report).unwrap(),
255+
)
256+
.unwrap();
257+
}
258+
}
259+
170260
#[test]
171261
fn unavailable_source_is_rejected_before_native_startup() {
172262
let p = Project::new();

‎docs/Codeguard-Architecture.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,8 @@ For Cargo proxies such as rustup that dispatch by entry-point name, Clippy, rust
669669
After native aggregation, current source uses non-cached Clippy `compiler-artifact` records, the original root-manifest identity and pre-run source hashes to avoid duplicate WASM for byte-matching Rust target entry points. Coverage stays in the current process and cannot be restored from editable reports. Failures, changed inputs/tools, duplicate JSON keys or events after the finish record grant no coverage. `all-targets` does not establish that the entire directory was parsed: unproven modules and conditionally excluded files retain prechecks. Native warnings, stable tasks and complete delivery obligations remain. Diagnostics repeated across library/test targets at the same file/rule/line/column project one finding, preferring error over warning; distinct positions remain separate and historical duplicate tasks are not automatically closed. See [Rust native-first acceptance](../tests/acceptance/check-all-native-preferred-rust.md). Public npm 0.1.4 does not include this batch.
670670

671671

672+
Clippy input protection also compares Rust source, Cargo manifest and lock-file membership under the same bounded static discovery policy. The snapshot binds all observed Rust files and nested manifests/locks. Added, removed, renamed or unreadable inputs invalidate the observation, current findings and native coverage. Managed workbench records follow existing discovery exclusions. External dependencies, dynamic targets and the complete configuration closure remain unproven.
673+
672674
### Existing Cargo discovery (current source)
673675

674676
`check all`, `comments rust`, `build rust` and their Cargo task rechecks accept an optional `--cargo-tool`. Without it, they select the first regular executable `cargo` in an absolute invoking-PATH directory. Empty/relative directories and non-executable entries are skipped. An explicit invalid tool or selected native failure remains a concrete blocker; it never triggers a search for another Cargo. Aggregate Rust checks share the selected entry.

‎docs/Codeguard-Architecture.zh_CN.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,8 @@ Cargo 为 rustup 等按入口名称分派的代理时,Clippy、rustdoc 与构
669669
聚合原生阶段完成后,当前源码会用 Clippy 的非缓存 `compiler-artifact`、原根清单身份和启动前源码摘要,免除同字节 Rust 目标入口的重复 WASM。覆盖只在本次进程内传递,不从可编辑报告恢复;失败、输入/工具变化、重复 JSON 键或结束记录后的事件不授予覆盖。`all-targets` 不能证明整个目录已解析,未证明的模块和条件排除文件继续初检;原生告警、稳定任务及完整交付义务均保留。同一文件/规则/行/列在库和测试目标中重复报告时只投影一项 finding,级别以 error 优先;不同位置分别保留,历史重复任务不自动关闭。见 [Rust 原生优先验收](../tests/acceptance/check-all-native-preferred-rust.md)。公开 npm 0.1.4 尚不包含本批修改。
670670

671671

672+
Clippy 输入保护还会沿同一有界静态发现策略复核 Rust 源码、Cargo 清单和锁文件的集合,快照固定全部已观察 Rust 文件及嵌套清单/锁的字节。新增、删除、重命名或读取失败使本轮观察未完成,撤回当前 finding 与原生覆盖;自有工作台记录沿既有排除策略处理,不制造源码变化。外部依赖、动态目标及完整配置闭包仍未证明。
673+
672674
### 已安装 Cargo 自动发现(当前源码)
673675

674676
`check all`、`comments rust`、`build rust` 及其 Cargo 任务复检接受可选 `--cargo-tool`。未指定时,从调用方 PATH 的绝对目录中选择首个普通可执行 `cargo`;空目录、相对目录和不可执行入口不作为候选。显式无效工具或已选工具执行失败保留具体阻塞,不换用下一个 Cargo。聚合 Rust 检查共用本轮已选入口。

‎docs/Codeguard-Technical-Design.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,8 @@ For Cargo proxies such as rustup that dispatch by entry-point name, Clippy, rust
11321132
After native aggregation, current source uses non-cached Clippy `compiler-artifact` records, the original root-manifest identity and pre-run source hashes to avoid duplicate WASM for byte-matching Rust target entry points. Coverage stays in the current process and cannot be restored from editable reports. Failures, changed inputs/tools, duplicate JSON keys or events after the finish record grant no coverage. `all-targets` does not establish that the entire directory was parsed: unproven modules and conditionally excluded files retain prechecks. Native warnings, stable tasks and complete delivery obligations remain. Diagnostics repeated across library/test targets at the same file/rule/line/column project one finding, preferring error over warning; distinct positions remain separate and historical duplicate tasks are not automatically closed. See [Rust native-first acceptance](../tests/acceptance/check-all-native-preferred-rust.md). Public npm 0.1.4 does not include this batch.
11331133

11341134

1135+
Clippy input protection also compares Rust source, Cargo manifest and lock-file membership under the same bounded static discovery policy. The snapshot binds all observed Rust files and nested manifests/locks. Added, removed, renamed or unreadable inputs invalidate the observation, current findings and native coverage. Managed workbench records follow existing discovery exclusions. External dependencies, dynamic targets and the complete configuration closure remain unproven.
1136+
11351137
### Existing Cargo discovery (current source)
11361138

11371139
`check all`, `comments rust`, `build rust` and their Cargo task rechecks accept an optional `--cargo-tool`. Without it, they select the first regular executable `cargo` in an absolute invoking-PATH directory. Empty/relative directories and non-executable entries are skipped. An explicit invalid tool or selected native failure remains a concrete blocker; it never triggers a search for another Cargo. Aggregate Rust checks share the selected entry.

‎docs/Codeguard-Technical-Design.zh_CN.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,8 @@ Cargo 为 rustup 等按入口名称分派的代理时,Clippy、rustdoc 与构
11331133
聚合原生阶段完成后,当前源码会用 Clippy 的非缓存 `compiler-artifact`、原根清单身份和启动前源码摘要,免除同字节 Rust 目标入口的重复 WASM。覆盖只在本次进程内传递,不从可编辑报告恢复;失败、输入/工具变化、重复 JSON 键或结束记录后的事件不授予覆盖。`all-targets` 不能证明整个目录已解析,未证明的模块和条件排除文件继续初检;原生告警、稳定任务及完整交付义务均保留。同一文件/规则/行/列在库和测试目标中重复报告时只投影一项 finding,级别以 error 优先;不同位置分别保留,历史重复任务不自动关闭。见 [Rust 原生优先验收](../tests/acceptance/check-all-native-preferred-rust.md)。公开 npm 0.1.4 尚不包含本批修改。
11341134

11351135

1136+
Clippy 输入保护还会沿同一有界静态发现策略复核 Rust 源码、Cargo 清单和锁文件的集合,快照固定全部已观察 Rust 文件及嵌套清单/锁的字节。新增、删除、重命名或读取失败使本轮观察未完成,撤回当前 finding 与原生覆盖;自有工作台记录沿既有排除策略处理,不制造源码变化。外部依赖、动态目标及完整配置闭包仍未证明。
1137+
11361138
### 已安装 Cargo 自动发现(当前源码)
11371139

11381140
`check all`、`comments rust`、`build rust` 及其 Cargo 任务复检接受可选 `--cargo-tool`。未指定时,从调用方 PATH 的绝对目录中选择首个普通可执行 `cargo`;空目录、相对目录和不可执行入口不作为候选。显式无效工具或已选工具执行失败保留具体阻塞,不换用下一个 Cargo。聚合 Rust 检查共用本轮已选入口。

‎openspec/changes/introduce-rust-codeguard-cli/specs/execution-kernel/spec.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
- **WHEN** Git hook 环境含 GIT_INDEX_FILE
5252
- **THEN** 检查该 index,保留真实工作树及 index 内容
5353

54+
#### Scenario: Rust source membership changes during Clippy
55+
- **WHEN** 原生 Clippy 运行期间普通发现范围内新增、删除或重命名 Rust 源码,或已观察的其它 Rust 文件、嵌套 Cargo 清单及锁文件内容变化
56+
- **THEN** MUST 撤回局部完成、当前源码诊断与本轮原生覆盖,反馈 rust_inputs_changed_during_scan;源码范围按同一静态发现策略前后复核,发现失败或截断不能当作空集合。根配置的存在性保护继续有效;不据此宣称动态 Cargo 模型、外部依赖和完整配置闭包均已固定
57+
5458
### Requirement: Managed artifact publication SHALL preserve identity and ownership
5559

5660
受管工具安装的缓存发布 MUST 与质量结果缓存分开。安装字节须有上限并核对预期 SHA-256,缓存根须归当前用户且不可被其它用户写入;使用固定目录描述符和不覆盖的原子发布,拒绝缓存根/目标链接、损坏或权限不符的已有制品。取消、到期或校验失败不得发布未完成文件;正常退出清理暂存,重复相同字节只能在再次核验后复用。缓存写入收据只说明字节发布,不证明锁/发行来源批准、平台兼容、可启动或质量通过;CLI 安装仍须独立核验批准来源。

0 commit comments

Comments
 (0)