Skip to content

Commit 0614a44

Browse files
committed
约束 Ruby 项目版本以避免旧解析器误报
1 parent 0dc24af commit 0614a44

19 files changed

Lines changed: 990 additions & 6 deletions

‎.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
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
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

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,3 +677,5 @@ Source builds add `codeguard lint ruby FILE.rb --ruby-tool /absolute/path/to/rub
677677

678678

679679
Ruby edit feedback: source builds of `hook execute` / `hook claude post-tool-use` accept `--ruby-tool ABS_PATH` or select Ruby from the caller's absolute PATH entries. Only confirmed edited files are parsed under the event deadline. Selected failures do not fall back; absence retains WASM candidates. Bounded dialogue carries current line positions and real stable tasks without source/tool-message echoes or invented columns, and asks agents to verify project Ruby compatibility first. `repair_ready` rechecks using the selected tool and retains real evidence references; zero diagnostics do not close tasks. New closed protocols are edit feedback 0.19 (inner 0.10) and recheck feedback 0.20 (inner 0.6); old schemas are preserved. Actual installed-host triggering, complete RuboCop coverage, and public npm release remain unverified.
680+
681+
Ruby version declaration guard: source builds read the nearest `.ruby-version` between the source and project root, bounded to 64 ancestor levels and 4096 bytes. Only `2.6.10`, `2.6.10p210` and their `ruby-` forms match the fixed parser. Other explicit versions yield `ruby_project_version_mismatch`; aliases/ambiguity yield `ruby_project_version_unresolved`; unsafe links/read failures yield `ruby_project_version_unreadable`. None starts the old Ruby or falls back to WASM. Declaration bytes and nearer missing declarations are rechecked after parsing; changes withdraw diagnostics. Saved positions are also withdrawn when a new declaration is incompatible. Standalone lint locates a `.codeguard` workspace before a Git root or Gemfile, so nested Gemfiles cannot hide the workspace pin; the nearest module version declaration remains authoritative. Absence retains an unapproved preliminary observation. Gemfile constraints, JRuby/RVM, complete project profiling and release remain pending. See [version guard acceptance](tests/acceptance/ruby-project-version.md).

‎README.zh-CN.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,3 +660,5 @@ codeguard task verify --help
660660

661661

662662
Ruby 编辑快检接线:源码构建的 `hook execute` / `hook claude post-tool-use` 可选择 `--ruby-tool ABS_PATH` 或调用方绝对 PATH 的 Ruby。只检查确认编辑的文件,共用事件截止时间;选定工具失败不回退,缺工具保留 WASM 候选。原生行号与稳定任务进入有界对话,不回显源码/工具消息或猜列号;先核对项目 Ruby 版本适用性。`repair_ready` 用同一工具复检并保存真实证据引用,零诊断仍不关闭任务。编辑反馈 0.19(内层 0.10)、复检反馈 0.20(内层 0.6)新增封闭 schema,旧协议不改。实际宿主自动触发、完整 RuboCop 与公开 npm 版本尚未验收。
663+
664+
Ruby 版本声明约束:源码构建在原生解析前有界读取源码祖先至项目根最近的 `.ruby-version`(最多 64 层、4096 字节),只接受 `2.6.10`、`2.6.10p210` 及对应 `ruby-` 前缀。其它明确版本返回 `ruby_project_version_mismatch`,别名/歧义返回 `ruby_project_version_unresolved`,链接或读取失败返回 `ruby_project_version_unreadable`;均不启动旧 Ruby,也不回退 WASM。解析前后复核声明字节及更近目录的缺项,变化撤回诊断;已保存诊断遇到不适用的新声明也会撤回。单文件入口优先定位 `.codeguard` 工作区、再定位 Git 根和 Gemfile,模块 Gemfile 不能遮蔽工作区版本,模块自身最近版本声明仍优先。无声明继续提供未批准的初步观察;Gemfile 运行时约束、JRuby/RVM、完整项目版本识别及发行仍待实现。见[版本约束验收](tests/acceptance/ruby-project-version.md)。

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,14 @@ pub(crate) fn observe(
4444
file["source_sha256"] = json!(digest(&source));
4545
file["current"] = json!(true);
4646
if let Some(tool) = selection.tool() {
47-
file["native"] = crate::ruby_syntax_probe::observe(tool, &source, deadline, cancelled);
47+
file["native"] = crate::ruby_project_version::observe(
48+
root,
49+
&root.join(relative),
50+
tool,
51+
&source,
52+
deadline,
53+
cancelled,
54+
);
4855
let has_syntax = file["native"]["diagnostics"]
4956
.as_array()
5057
.is_some_and(|r| !r.is_empty());
@@ -89,6 +96,14 @@ pub(crate) fn refresh(root: &Path, report: &mut Value, deadline: Instant) {
8996
}
9097
let reason = if Instant::now() >= deadline {
9198
Some("request_deadline_exceeded")
99+
} else if matches!(
100+
file["native"]["status"].as_str(),
101+
Some("completed" | "diagnostics_observed")
102+
) && file["path"]
103+
.as_str()
104+
.is_none_or(|p| !crate::ruby_project_version::compatible(root, &root.join(p)))
105+
{
106+
Some("ruby_project_version_changed_after_check")
92107
} else if file["tool_path"]
93108
.as_str()
94109
.is_some_and(|p| resolved.as_deref() != Some(Path::new(p)))

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,8 @@ mod javascript_syntax_probe;
386386
#[cfg(unix)]
387387
mod ruby_syntax_probe;
388388
#[cfg(unix)]
389+
mod ruby_project_version;
390+
#[cfg(unix)]
389391
pub mod ruby_lint_command;
390392
#[cfg(unix)]
391393
mod ruby_tool_selection;

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,22 @@ pub fn run(args: &[String]) -> ExitCode {
8080
let source = read_bounded_regular_file(&file, 1024 * 1024).ok();
8181
let valid_source = source.as_ref().filter(|b| std::str::from_utf8(b).is_ok());
8282
let mut native = match (valid_source, selected) {
83-
(Some(bytes), Some(tool)) => {
84-
crate::ruby_syntax_probe::observe(tool, bytes, deadline, &AtomicBool::new(false))
85-
}
83+
(Some(bytes), Some(tool)) => crate::ruby_project_version::source_root(&file)
84+
.map(|root| {
85+
crate::ruby_project_version::observe(
86+
&root,
87+
&if file.is_absolute() {
88+
file.clone()
89+
} else {
90+
std::env::current_dir().unwrap_or_default().join(&file)
91+
},
92+
tool,
93+
bytes,
94+
deadline,
95+
&AtomicBool::new(false),
96+
)
97+
})
98+
.unwrap_or_else(|| unavailable("ruby_project_version_unresolved")),
8699
(Some(_), None) => unavailable("ruby_tool_not_found"),
87100
_ => unavailable("ruby_source_unavailable_or_invalid"),
88101
};
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//! Ruby 固定解析器的有界项目版本约束;声明不适用时不制造源码诊断。
2+
use codeguard_runtime::read_bounded_regular_file;
3+
use serde_json::Value;
4+
use std::{
5+
io::ErrorKind,
6+
path::{Component, Path, PathBuf},
7+
sync::atomic::{AtomicBool, Ordering},
8+
time::Instant,
9+
};
10+
11+
/// 获取单文件入口的受限项目根;按工作台、Git、Gemfile优先级寻找最近标记,不执行配置。
12+
pub(crate) fn source_root(source: &Path) -> Option<PathBuf> {
13+
let absolute = if source.is_absolute() {
14+
source.to_path_buf()
15+
} else {
16+
std::env::current_dir().ok()?.join(source)
17+
};
18+
let parent = absolute.parent()?;
19+
Some(
20+
[".codeguard", ".git", "Gemfile"]
21+
.iter()
22+
.find_map(|name| {
23+
parent
24+
.ancestors()
25+
.take(64)
26+
.find(|p| std::fs::symlink_metadata(p.join(name)).is_ok())
27+
})
28+
.unwrap_or(parent)
29+
.to_path_buf(),
30+
)
31+
}
32+
33+
/// 版本观察输入,保留已有声明及更近目录中尚不存在的声明路径。
34+
type VersionInputs = Vec<(PathBuf, Option<Vec<u8>>)>;
35+
36+
/// 读取源码祖先内最近版本声明及所有更近缺项;返回原始字节快照或环境原因。
37+
fn capture(root: &Path, source: &Path) -> Result<VersionInputs, &'static str> {
38+
if !root.is_absolute()
39+
|| source
40+
.components()
41+
.any(|c| matches!(c, Component::ParentDir))
42+
|| !source.starts_with(root)
43+
{
44+
return Err("ruby_project_version_unresolved");
45+
}
46+
let mut inputs = Vec::new();
47+
for parent in source
48+
.parent()
49+
.ok_or("ruby_project_version_unresolved")?
50+
.ancestors()
51+
.take(64)
52+
{
53+
if !parent.starts_with(root) {
54+
break;
55+
}
56+
let mut prefix = PathBuf::new();
57+
for component in parent.components() {
58+
prefix.push(component);
59+
if std::fs::symlink_metadata(&prefix)
60+
.map_err(|_| "ruby_project_version_unreadable")?
61+
.file_type()
62+
.is_symlink()
63+
{
64+
return Err("ruby_project_version_unreadable");
65+
}
66+
}
67+
let path = parent.join(".ruby-version");
68+
match std::fs::symlink_metadata(&path) {
69+
Err(e) if e.kind() == ErrorKind::NotFound => inputs.push((path, None)),
70+
Err(_) => return Err("ruby_project_version_unreadable"),
71+
Ok(_) => {
72+
let bytes = read_bounded_regular_file(&path, 4096)
73+
.map_err(|_| "ruby_project_version_unreadable")?;
74+
let text = std::str::from_utf8(&bytes)
75+
.map_err(|_| "ruby_project_version_unresolved")?
76+
.trim();
77+
let version = text.strip_prefix("ruby-").unwrap_or(text);
78+
if !matches!(version, "2.6.10" | "2.6.10p210") {
79+
let numeric = version.split('.').collect::<Vec<_>>();
80+
return Err(
81+
if numeric.len() == 3
82+
&& numeric
83+
.iter()
84+
.all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
85+
{
86+
"ruby_project_version_mismatch"
87+
} else {
88+
"ruby_project_version_unresolved"
89+
},
90+
);
91+
}
92+
inputs.push((path, Some(bytes)));
93+
return Ok(inputs);
94+
}
95+
}
96+
if parent == root {
97+
return Ok(inputs);
98+
}
99+
}
100+
Err("ruby_project_version_unresolved")
101+
}
102+
103+
/// 仅核对当前明确声明对固定解析器是否适用;无声明仍是未批准的初步观察。
104+
pub(crate) fn compatible(root: &Path, source: &Path) -> bool {
105+
capture(root, source).is_ok()
106+
}
107+
108+
/// 调用固定Ruby解析器并复核版本输入;参数绑定项目根、源码路径、字节和共同预算。
109+
pub(crate) fn observe(
110+
root: &Path,
111+
source: &Path,
112+
tool: &Path,
113+
bytes: &[u8],
114+
deadline: Instant,
115+
cancelled: &AtomicBool,
116+
) -> Value {
117+
if cancelled.load(Ordering::Relaxed) || codeguard_runtime::sigint_cancellation_requested() {
118+
return crate::ruby_lint_command::unavailable("request_cancelled");
119+
}
120+
if Instant::now() >= deadline {
121+
return crate::ruby_lint_command::unavailable("request_deadline_exceeded");
122+
}
123+
let original = match capture(root, source) {
124+
Ok(inputs) => inputs,
125+
Err(reason) => return crate::ruby_lint_command::unavailable(reason),
126+
};
127+
let report = crate::ruby_syntax_probe::observe(tool, bytes, deadline, cancelled);
128+
if capture(root, source).ok().as_ref() != Some(&original) {
129+
crate::ruby_lint_command::unavailable("ruby_project_version_changed_during_check")
130+
} else {
131+
report
132+
}
133+
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ fn run_with_tools(
136136
} else if language == "ruby" {
137137
selected_ruby
138138
.map(|tool| {
139-
crate::ruby_syntax_probe::observe(
139+
crate::ruby_project_version::observe(
140+
root,
141+
&root.join(path),
140142
tool,
141143
bytes,
142144
deadline,
@@ -320,6 +322,11 @@ pub(crate) fn inputs_current(root: &Path, report: &Value) -> bool {
320322
.and_then(|p| source_bytes(root, p))
321323
.is_some_and(|b| report["target"]["source_sha256"] == digest(&b))
322324
&& (report["native"]["tool_sha256"].is_null() || tool_current(report))
325+
&& (report["target"]["language"] != "ruby"
326+
|| report["native"]["status"] == "incomplete"
327+
|| report["target"]["path"]
328+
.as_str()
329+
.is_some_and(|p| crate::ruby_project_version::compatible(root, &root.join(p))))
323330
&& (report["target"]["language"] != "go"
324331
|| report["native"]["companion_binding_sha256"].is_null()
325332
|| report["tool_path"].as_str().is_some_and(|tool| {

0 commit comments

Comments
 (0)