Skip to content

Commit ebc5538

Browse files
committed
让静态发现保留共享后缀的语言歧义
1 parent 207e958 commit ebc5538

13 files changed

Lines changed: 153 additions & 28 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1474,7 +1474,7 @@ pub fn run(args: &[String]) -> ExitCode {
14741474
let syntax_candidates = json!({
14751475
"status":"not_run","reason":"binary_without_wasm_precheck","execution_phase":"after_native",
14761476
"authority":"candidate_unqualified","delivery_decision":"incomplete",
1477-
"source_file_count":discovery.languages.values().map(|item| item.source_files.len()).sum::<usize>(),
1477+
"source_file_count":discovery.languages.values().map(|item| item.source_files.len()).sum::<usize>() + discovery.ambiguous_source_files.len(),
14781478
"skipped_count":0,"unrouted_count":0,"native_preferred_count":0,"observations":[],
14791479
"next_action":"使用包含固定语法资产的发行包运行候选初检,并完成适用原生检查"
14801480
});

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ pub fn observe(
3939
paths.extend(evidence.source_files.iter());
4040
}
4141
}
42+
if !java_only {
43+
paths.extend(discovery.ambiguous_source_files.iter());
44+
}
4245
let source_file_count = paths.len();
4346
let mut observations = Vec::new();
4447
let mut skipped_count = 0;

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub struct DiscoveryReport {
4444
pub root: String,
4545
pub observation_complete: bool,
4646
pub languages: BTreeMap<String, LanguageEvidence>,
47+
/// 后缀不足以判定语言的源码;仍须进入候选范围的未路由计数。
48+
pub(crate) ambiguous_source_files: BTreeSet<String>,
4749
pub build_roots: BTreeMap<String, BTreeSet<String>>,
4850
pub declared_versions: BTreeMap<String, String>,
4951
/// 与同次清单字节摘要绑定的 Maven 直接模块声明。
@@ -147,6 +149,7 @@ pub fn discover<P: ObservationPort>(
147149
root: root.to_string_lossy().into_owned(),
148150
observation_complete: true,
149151
languages: BTreeMap::new(),
152+
ambiguous_source_files: BTreeSet::new(),
150153
build_roots: BTreeMap::new(),
151154
declared_versions: BTreeMap::new(),
152155
maven_module_models: BTreeMap::new(),
@@ -406,6 +409,31 @@ fn observe_file<P: ObservationPort>(
406409
}
407410
}
408411
}
412+
if name.ends_with(".sc") {
413+
report.ambiguous_source_files.insert(relative.into());
414+
report
415+
.unknown_conditions
416+
.push(format!("ambiguous_language_suffix:{relative}"));
417+
return;
418+
}
419+
if name.ends_with(".m") {
420+
match observation.read_bounded(path, 1024 * 1024) {
421+
Ok(source) if crate::source_language_hint::has_objc_marker(&source) => {}
422+
Ok(_) => {
423+
report.ambiguous_source_files.insert(relative.into());
424+
report
425+
.unknown_conditions
426+
.push(format!("ambiguous_language_suffix:{relative}"));
427+
return;
428+
}
429+
Err(_) => {
430+
report.observation_complete = false;
431+
report.blocked_paths.push(relative.into());
432+
report.ambiguous_source_files.insert(relative.into());
433+
return;
434+
}
435+
}
436+
}
409437
for language in &registry.languages {
410438
if language
411439
.extensions

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

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! 按源码后缀和明确的嵌入边界选择候选 grammar;不推断项目语言版本。
22
3+
use crate::source_language_hint::has_objc_marker;
4+
35
/// 一段可独立解析的候选源码;嵌入片段的位置相对于原文件的字节起点。
46
#[derive(Clone, Debug)]
57
pub struct GrammarRoute<'a> {
@@ -95,30 +97,6 @@ pub fn route_source<'a>(relative_path: &str, source: &'a [u8]) -> Vec<GrammarRou
9597
routes
9698
}
9799

98-
// `.m` 也用于 MATLAB;只在源码具有 Objective-C 专属词法标记时自动解析。
99-
// `.sc` 同时用于 Scala 脚本与 SuperCollider,缺少项目上下文时不猜测。
100-
fn has_objc_marker(source: &[u8]) -> bool {
101-
let markers: &[&[u8]] = &[
102-
b"@interface".as_slice(),
103-
b"@implementation",
104-
b"@protocol",
105-
b"@class",
106-
b"@property",
107-
b"@synthesize",
108-
b"@autoreleasepool",
109-
b"#import",
110-
];
111-
source.split(|byte| *byte == b'\n').any(|line| {
112-
let line = line.trim_ascii_start();
113-
markers.iter().any(|marker| {
114-
line.starts_with(marker)
115-
&& line.get(marker.len()).is_some_and(|next| {
116-
next.is_ascii_whitespace() || matches!(next, b'(' | b'{' | b'<' | b'"')
117-
})
118-
})
119-
})
120-
}
121-
122100
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
123101
haystack
124102
.windows(needle.len())

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ pub mod rustdoc_repair_brief;
178178
#[cfg(unix)]
179179
pub(crate) mod rustdoc_task_recheck;
180180
pub mod sarif_feedback;
181+
mod source_language_hint;
181182
#[cfg(feature = "wasm-precheck")]
182183
mod syntax_worker_candidate_observation;
183184
#[cfg(feature = "wasm-precheck")]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//! 只读语言候选证据;供默认发现与可选 WASM 路由共用。
2+
3+
/// 识别 `.m` 源码行首的 Objective-C 专属词法标记。
4+
/// 参数为有界原始源码;返回值仅是语言候选证据,不证明语法或项目配置。
5+
pub(crate) fn has_objc_marker(source: &[u8]) -> bool {
6+
let markers: &[&[u8]] = &[
7+
b"@interface".as_slice(),
8+
b"@implementation",
9+
b"@protocol",
10+
b"@class",
11+
b"@property",
12+
b"@synthesize",
13+
b"@autoreleasepool",
14+
b"#import",
15+
];
16+
source.split(|byte| *byte == b'\n').any(|line| {
17+
let line = line.trim_ascii_start();
18+
markers.iter().any(|marker| {
19+
line.starts_with(marker)
20+
&& line.get(marker.len()).is_some_and(|next| {
21+
next.is_ascii_whitespace() || matches!(next, b'(' | b'{' | b'<' | b'"')
22+
})
23+
})
24+
})
25+
}

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ fn check_all_routes_distinct_dialects_after_native_without_claiming_clean() {
112112
assert!(!observations.iter().any(|item| item["path"] == "plot.m"));
113113
assert!(!observations.iter().any(|item| item["path"] == "synth.sc"));
114114
assert_eq!(report["syntax_candidates"]["unrouted_count"], 2);
115+
let unknown = report["discovery"]["unknown_conditions"]
116+
.as_array()
117+
.unwrap();
118+
for path in ["plot.m", "synth.sc"] {
119+
assert!(
120+
unknown
121+
.iter()
122+
.any(|item| item == &format!("ambiguous_language_suffix:{path}")),
123+
"{path}: {unknown:?}"
124+
);
125+
}
115126
assert_eq!(
116127
observations
117128
.iter()
@@ -132,6 +143,37 @@ fn check_all_routes_distinct_dialects_after_native_without_claiming_clean() {
132143
);
133144
}
134145

146+
#[test]
147+
fn ambiguous_only_project_remains_an_explicit_unrouted_scope() {
148+
let root = std::env::temp_dir()
149+
.canonicalize()
150+
.unwrap()
151+
.join(format!("codeguard-ambiguous-only-{}", std::process::id()));
152+
fs::create_dir_all(&root).unwrap();
153+
fs::write(root.join("plot.m"), "plot(1:3);\n").unwrap();
154+
fs::write(root.join("synth.sc"), "{ SinOsc.ar(440) }.play;\n").unwrap();
155+
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
156+
.args(["check", "all"])
157+
.arg(&root)
158+
.args(["--format=json", "--timeout", "30s"])
159+
.output()
160+
.unwrap();
161+
fs::remove_dir_all(&root).unwrap();
162+
assert_eq!(output.status.code(), Some(3));
163+
let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
164+
assert_eq!(report["syntax_candidates"]["source_file_count"], 2);
165+
assert_eq!(report["syntax_candidates"]["unrouted_count"], 2);
166+
assert_eq!(report["syntax_candidates"]["status"], "not_run");
167+
assert_eq!(report["syntax_candidates"]["reason"], "unrouted_source");
168+
assert_eq!(report["delivery_decision"], "incomplete");
169+
assert!(
170+
report["syntax_candidates"]["observations"]
171+
.as_array()
172+
.unwrap()
173+
.is_empty()
174+
);
175+
}
176+
135177
#[test]
136178
fn check_all_invokes_all_32_pinned_candidates_across_bounded_projects() {
137179
let root = std::env::temp_dir().canonicalize().unwrap().join(format!(

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,44 @@ use std::sync::atomic::{AtomicU64, Ordering};
77

88
static NEXT: AtomicU64 = AtomicU64::new(0);
99

10+
#[test]
11+
fn detect_keeps_shared_m_and_sc_suffixes_ambiguous_without_false_languages() {
12+
let project = TempProject::new();
13+
fs::write(
14+
project.0.join("plot.m"),
15+
"title('@interface Foo');\nplot(1:3);\n",
16+
)
17+
.unwrap();
18+
fs::write(project.0.join("synth.sc"), "{ SinOsc.ar(440) }.play;\n").unwrap();
19+
fs::write(
20+
project.0.join("model.m"),
21+
"@interface Model : NSObject\n@end\n",
22+
)
23+
.unwrap();
24+
fs::write(project.0.join("main.scala"), "object Main {}\n").unwrap();
25+
let report = detect_json(&project);
26+
let languages = report["languages"].as_array().unwrap();
27+
let objc = languages.iter().find(|item| item["id"] == "objc").unwrap();
28+
let scala = languages.iter().find(|item| item["id"] == "scala").unwrap();
29+
assert_eq!(objc["source_files"], serde_json::json!(["model.m"]));
30+
assert_eq!(scala["source_files"], serde_json::json!(["main.scala"]));
31+
assert!(report["observation_complete"].as_bool().unwrap());
32+
assert!(
33+
report["unknown_conditions"]
34+
.as_array()
35+
.unwrap()
36+
.iter()
37+
.any(|item| item == "ambiguous_language_suffix:plot.m")
38+
);
39+
assert!(
40+
report["unknown_conditions"]
41+
.as_array()
42+
.unwrap()
43+
.iter()
44+
.any(|item| item == "ambiguous_language_suffix:synth.sc")
45+
);
46+
}
47+
1048
struct TempProject(PathBuf);
1149

1250
impl TempProject {

‎docs/Codeguard-Technical-Design.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ The source-built `check all` now adds a narrower actual project report: native a
212212
}
213213
```
214214

215-
The actual report also includes native results, source/grammar digests, fragment offsets, bounded file-relative recovery anchors, skipped counts and unresolved obligations. A complete `<cfquery>` tag body is an embedded candidate; ordinary SQL or ambiguous C/C++ headers are not guessed. Automatic `.m` routing requires an Objective-C line marker, while `.sc` is left unresolved because SuperCollider shares that suffix; the static language inventory and per-file unresolved diagnostics still need improvement. Four bounded 32-grammar CLI fixtures took about 54 seconds on this Mac; a single 31-file Linux run completed 26/32 within 90 seconds; language-version corpora, native differential checks, task synchronization, host delivery and package release are still open.
215+
The actual report also includes native results, source/grammar digests, fragment offsets, bounded file-relative recovery anchors, skipped counts and unresolved obligations. A complete `<cfquery>` tag body is an embedded candidate; ordinary SQL or ambiguous C/C++ headers are not guessed. Automatic `.m` routing requires an Objective-C line marker, while `.sc` is left unresolved because SuperCollider shares that suffix. Read-only discovery now uses the same bounded `.m` evidence and records unresolved shared-suffix paths in `unknown_conditions`; `check all` retains them in its unrouted count. Project-level dialect evidence and structured per-file ambiguity reporting still need work. Four bounded 32-grammar CLI fixtures took about 54 seconds on this Mac; a single 31-file Linux run completed 26/32 within 90 seconds; language-version corpora, native differential checks, task synchronization, host delivery and package release are still open.
216216

217217
The opt-in CLI build first checks for a local ESLint 10 package and a single ordinary flat config on single-file TypeScript requests without explicit native context. With an executable Node resolved from `PATH`, it invokes the existing bounded native version/report probe; without Node it reports the preparation gap rather than running WASM first. Only when no project-local ESLint package is observed does the CLI emit [ESLint feedback 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json) for `.ts/.mts/.cts` or [0.4.0](../schemas/eslint-local-feedback-v0.4.schema.json) for `.tsx`. TSX uses its own pinned grammar. Candidate prechecks preserve `native=not_run` and `delivery=not_evaluated`, and remain incomplete even with zero recoveries. An ambiguous config, untrusted local path or invalid package identity remains a setup blocker and does not trigger WASM; a partial explicit context also retains its existing path. This is a single-file slice, not project script equivalence, per-module orchestration or automatic host feedback; see the [native-first acceptance](../tests/acceptance/native-first-eslint-candidate.md).
218218

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ flowchart LR
212212
}
213213
```
214214

215-
实际报告还包含原生结果、源码与 grammar 摘要、片段偏移、有界原文件恢复坐标、未执行数量和未解决义务。完整 `<cfquery>` 标签体可作嵌入候选;普通 SQL 和有歧义的 C/C++ 头文件不猜测。自动 `.m` 路由要求 Objective-C 行首标记,`.sc` 与 SuperCollider 共用后缀,缺少项目证据时保持未解析;静态语言库存及逐文件未解析诊断仍待完善。本机四批 32 份 grammar 的 CLI 样例约 54 秒;Linux 单次 31 文件检查在 90 秒内完成 26/32;语言版本语料、原生差分对照、任务同步、宿主交付和发行包验收仍待完成。
215+
实际报告还包含原生结果、源码与 grammar 摘要、片段偏移、有界原文件恢复坐标、未执行数量和未解决义务。完整 `<cfquery>` 标签体可作嵌入候选;普通 SQL 和有歧义的 C/C++ 头文件不猜测。自动 `.m` 路由要求 Objective-C 行首标记,`.sc` 与 SuperCollider 共用后缀,缺少项目证据时保持未解析。只读发现现在使用相同的有界 `.m` 证据,把共享后缀的未判定路径记入 `unknown_conditions`;`check all` 仍将其计入未路由数量。项目级方言依据及逐文件结构化歧义报告仍待完善。本机四批 32 份 grammar 的 CLI 样例约 54 秒;Linux 单次 31 文件检查在 90 秒内完成 26/32;语言版本语料、原生差分对照、任务同步、宿主交付和发行包验收仍待完成。
216216

217217
可选特性构建的 CLI 在无显式原生上下文的 TypeScript 单文件请求中,先核对本地 ESLint 10 包及唯一普通 flat config。若从 `PATH` 解析到可执行 Node,就调用既有有界原生版本与报告探针;缺 Node 时报告准备缺口,不让 WASM 抢跑。仅未观察到本地 ESLint 包时,`.ts/.mts/.cts` 输出 [ESLint 反馈 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json),`.tsx` 用独立 grammar 输出 [0.4.0](../schemas/eslint-local-feedback-v0.4.schema.json)。候选初检保留 `native=not_run`、`delivery=not_evaluated`,即使零恢复节点也保持未完成。配置选择歧义、本地路径不可信或包身份损坏时保留环境阻塞而不启动 WASM;部分显式上下文也沿用原路径。该增量不证明项目脚本参数等价、逐模块调度或宿主对话交付,见[原生优先局部验收](../tests/acceptance/native-first-eslint-candidate.md)。
218218

0 commit comments

Comments
 (0)