Skip to content

Commit 8d5c2e4

Browse files
committed
补充 Erlang 显式函数终止符结构初检
1 parent 23d8d70 commit 8d5c2e4

23 files changed

Lines changed: 1677 additions & 25 deletions

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,3 +826,6 @@ These additions describe the current source candidate; the public npm release is
826826
Standalone `lint <registered-language> FILE` now accepts every canonical registry ID. Existing native language adapters keep their original paths; other languages return `syntax_lint_feedback` with an explicit native-adapter gap and unknown configuration. WASM-enabled source builds additionally run matching bounded grammar candidates and reuse `.codeguard/` confirmation tasks with `--workspace ABS_ROOT`. This is partial syntax feedback, not native lint coverage or a new published npm capability.
827827

828828
C/C++ source builds also accept explicit standalone native context: `codeguard lint c main.c --clang-tool /ABS/PATH/clang --standard c11 --format=json` (C++ uses `cpp`/`c++17`). This measured Apple Clang 21 profile reports original native rules and byte positions plus a reusable verification argv. It does not yet synchronize Clang tasks or replace project lint; unresolved preprocessor context remains incomplete.
829+
830+
831+
Explicit `grammar probe erlang FILE --format=json` now adds bounded direct-function-form termination candidates, separately from parser recovery. Literal punctuation and valid clause continuations are preserved; project/Hook/task integration and native precision acceptance remain open. See [acceptance](tests/acceptance/erlang-form-candidates.md).

‎README.zh-CN.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,3 +809,6 @@ JavaScript 回退候选已接入项目检查与确认编辑 Hook:重复顶层
809809
独立 `lint <注册表规范语言> FILE` 已接受全部规范语言ID:已有专用原生适配器保留原路径,其余输出 `syntax_lint_feedback`,明确原生适配缺口及配置未知。启用WASM的源码构建追加匹配的有界grammar候选,提供 `--workspace ABS_ROOT` 时复用 `.codeguard/` 原生确认任务。这是局部语法反馈,不代表原生lint全覆盖,也未更新公开npm能力。
810810

811811
C/C++源码构建还支持明确的独立原生上下文:`codeguard lint c main.c --clang-tool /ABS/PATH/clang --standard c11 --format=json`(C++使用 `cpp`/`c++17`)。已实测Apple Clang21档案返回原生规则、字节位置及可复用复检argv;尚未同步Clang原生任务,不替代项目lint,预处理上下文未解析时保持未完成。
812+
813+
814+
显式 `grammar probe erlang FILE --format=json` 已补充直接函数form终止符候选,与原始解析恢复分开;字面量标点和合法子句续接不误判。项目/Hook/任务接线及原生精度验收仍开放,见[验收](tests/acceptance/erlang-form-candidates.md)。
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use sha2::{Digest, Sha256};
2+
const RULE: &[u8] = include_bytes!("../../../rulepacks/erlang/form_terminator.json");
3+
4+
/// 固定Erlang直接函数form终止符规则摘要;不赋予原生或语言资格权威。
5+
#[must_use]
6+
pub fn erlang_form_rule_sha256() -> String {
7+
format!("{:x}", Sha256::digest(RULE))
8+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,3 +434,6 @@ pub use cfquery_projection_rule::{cfquery_projection_rule_sha256, cfquery_projec
434434

435435
mod clang_sarif;
436436
pub use clang_sarif::parse_clang_stdin_sarif;
437+
438+
mod erlang_form_rule;
439+
pub use erlang_form_rule::erlang_form_rule_sha256;

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! 显式单文件候选语法观察;不将未经验收的 grammar 结果变为 lint 结论。
22
3-
use crate::syntax_worker_runner::{run_syntax_worker_candidate, run_syntax_worker_binding_candidate};
3+
use crate::syntax_worker_runner::{
4+
run_syntax_worker_candidate, run_syntax_worker_binding_candidate,
5+
run_syntax_worker_form_candidate,
6+
};
47
use codeguard_adapters::bundled_grammar_candidate;
58
use serde_json::json;
69
use std::path::PathBuf;
@@ -42,6 +45,8 @@ pub fn run(args: &[String]) -> ExitCode {
4245
};
4346
let runner = if language == "javascript" {
4447
run_syntax_worker_binding_candidate
48+
} else if language == "erlang" {
49+
run_syntax_worker_form_candidate
4550
} else {
4651
run_syntax_worker_candidate
4752
};
@@ -75,7 +80,9 @@ pub fn run(args: &[String]) -> ExitCode {
7580
}
7681
});
7782
if !observation.structural_observations.is_empty() {
78-
report["schema_version"] = json!(if language == "javascript" {
83+
report["schema_version"] = json!(if language == "erlang" {
84+
"0.7.0"
85+
} else if language == "javascript" {
7986
"0.6.0"
8087
} else if language == "cfquery" {
8188
"0.4.0"
@@ -90,6 +97,9 @@ pub fn run(args: &[String]) -> ExitCode {
9097
if language == "javascript" {
9198
report["structural_rule_scope"] = json!("direct_simple_lexical_names_only");
9299
}
100+
if language == "erlang" {
101+
report["source_scope"] = json!("direct_function_forms");
102+
}
93103
if language == "go" {
94104
report["source_scope"] = json!("whole_file");
95105
}
@@ -99,7 +109,7 @@ pub fn run(args: &[String]) -> ExitCode {
99109
report["structural_observations"] = json!(observation.structural_observations);
100110
}
101111
if observation.parser_error_location_unavailable {
102-
if report["schema_version"] != "0.6.0" {
112+
if report["schema_version"] != "0.6.0" && report["schema_version"] != "0.7.0" {
103113
report["schema_version"] = json!("0.5.0");
104114
}
105115
report["parser_error_location_unavailable"] = json!(true);

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

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ pub fn run(args: &[String]) -> ExitCode {
2121
[language, option] if language == "javascript" && option == "--direct-bindings" => {
2222
(language, true)
2323
}
24+
[language, option] if language == "erlang" && option == "--form-terminators" => {
25+
(language, true)
26+
}
2427
_ => {
2528
eprintln!("语法工作进程参数无效");
2629
return ExitCode::from(2);
@@ -76,7 +79,7 @@ fn observe(language: &str, source: &[u8], bindings: bool) -> Result<SyntaxWorker
7679
let scanned = scan_wasm_recoveries(&tree, MAX_RECOVERIES)?;
7780
let mut structural_observations = Vec::new();
7881
let mut structural_truncated = false;
79-
if bindings {
82+
if bindings && language == "javascript" {
8083
let kinds = codeguard_adapters::javascript_binding_node_kinds()?;
8184
let scanned_bindings = codeguard_runtime::scan_wasm_sibling_bindings(
8285
&tree,
@@ -194,6 +197,34 @@ fn observe(language: &str, source: &[u8], bindings: bool) -> Result<SyntaxWorker
194197
});
195198
}
196199
}
200+
if bindings && language == "erlang" && tree.root_node().kind() == "source_file" {
201+
let (facts, truncated) = codeguard_runtime::scan_wasm_form_terminators(
202+
&tree,
203+
"fun_decl",
204+
MAX_RECOVERIES,
205+
200_000,
206+
)?;
207+
structural_truncated |= truncated;
208+
for fact in facts {
209+
if scanned.recoveries.len() + structural_observations.len() == MAX_RECOVERIES {
210+
structural_truncated = true;
211+
break;
212+
}
213+
structural_observations.push(crate::syntax_worker_structure::SyntaxWorkerStructure {
214+
basis: "codeguard_structure_rule".into(),
215+
rule_id: "codeguard.erlang.form_terminator".into(),
216+
rule_version: "1.0.0".into(),
217+
rule_sha256: codeguard_adapters::erlang_form_rule_sha256(),
218+
parent_syntax_kind: fact.parent_syntax_kind,
219+
start_byte: fact.start_byte,
220+
end_byte: fact.end_byte,
221+
start_row: fact.start_row,
222+
start_column_byte: fact.start_column_byte,
223+
end_row: fact.end_row,
224+
end_column_byte: fact.end_column_byte,
225+
});
226+
}
227+
}
197228
let recoveries = scanned
198229
.recoveries
199230
.into_iter()
@@ -210,20 +241,23 @@ fn observe(language: &str, source: &[u8], bindings: bool) -> Result<SyntaxWorker
210241
})
211242
.collect();
212243
Ok(SyntaxWorkerEnvelope {
213-
schema_version: if bindings && !structural_observations.is_empty() {
214-
"1.5.0"
215-
} else if scanned.parser_error_location_unavailable {
216-
"1.4.0"
217-
} else if structural_observations.is_empty() {
218-
"1.0.0"
219-
} else if language == "cfquery" {
220-
"1.3.0"
221-
} else if language == "go" {
222-
"1.2.0"
223-
} else {
224-
"1.1.0"
225-
}
226-
.into(),
244+
schema_version:
245+
if bindings && language == "erlang" && !structural_observations.is_empty() {
246+
"1.6.0"
247+
} else if bindings && !structural_observations.is_empty() {
248+
"1.5.0"
249+
} else if scanned.parser_error_location_unavailable {
250+
"1.4.0"
251+
} else if structural_observations.is_empty() {
252+
"1.0.0"
253+
} else if language == "cfquery" {
254+
"1.3.0"
255+
} else if language == "go" {
256+
"1.2.0"
257+
} else {
258+
"1.1.0"
259+
}
260+
.into(),
227261
report_type: "syntax_worker_candidate".into(),
228262
language: language.into(),
229263
grammar_sha256: asset.sha256.clone(),

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ pub struct SyntaxWorkerEnvelope {
2020
pub source_sha256: String,
2121
/// 恢复节点或遍历预算是否截断。
2222
pub truncated: bool,
23-
/// 1.4/1.5明确记录不可定位的解析树错误;更旧版本不得携带该字段。
23+
/// 1.4/1.5/1.6明确记录不可定位的解析树错误;更旧版本不得携带该字段。
2424
#[serde(default, skip_serializing_if = "Option::is_none")]
2525
pub parser_error_location_unavailable: Option<bool>,
2626
/// ERROR/MISSING 恢复锚点。
2727
pub recoveries: Vec<SyntaxWorkerRecovery>,
28-
/// Python1.1、Go1.2、CFQuery1.3及显式JavaScript1.5结构观察;1.0中不存在。
28+
/// Python1.1、Go1.2、CFQuery1.3及显式JavaScript1.5/Erlang1.6结构观察;1.0中不存在。
2929
#[serde(default, skip_serializing_if = "Vec::is_empty")]
3030
pub structural_observations: Vec<crate::syntax_worker_structure::SyntaxWorkerStructure>,
3131
}

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

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,30 @@ pub fn run_syntax_worker_binding_candidate(
6262
)
6363
}
6464

65+
/// 显式观察Erlang直接form终止符;输入为冻结源码、固定语言和共享预算。
66+
/// 返回仅需原生确认的结构候选,项目级接线须使用相容报告协议。
67+
pub fn run_syntax_worker_form_candidate(
68+
executable: &Path,
69+
language: &str,
70+
relative_path: &str,
71+
source: &[u8],
72+
deadline: Instant,
73+
cancelled: &AtomicBool,
74+
) -> Result<SyntaxWorkerCandidateObservation, String> {
75+
if language != "erlang" {
76+
return Err("syntax_form_language_invalid".into());
77+
}
78+
run_candidate(
79+
executable,
80+
language,
81+
relative_path,
82+
source,
83+
deadline,
84+
cancelled,
85+
true,
86+
)
87+
}
88+
6589
fn run_candidate(
6690
executable: &Path,
6791
language: &str,
@@ -81,7 +105,11 @@ fn run_candidate(
81105
let expected_sha = format!("{:x}", Sha256::digest(source));
82106
let mut args = vec![OsString::from("__syntax-worker"), OsString::from(language)];
83107
if bindings {
84-
args.push(OsString::from("--direct-bindings"));
108+
args.push(OsString::from(if language == "erlang" {
109+
"--form-terminators"
110+
} else {
111+
"--direct-bindings"
112+
}));
85113
}
86114
let spec = ProcessSpec {
87115
executable: executable.to_path_buf(),
@@ -107,16 +135,18 @@ fn run_candidate(
107135
if value["schema_version"] == "1.0.0" && value.get("structural_observations").is_some() {
108136
return Err("syntax_worker_version_fields_mismatch".into());
109137
}
110-
if !matches!(value["schema_version"].as_str(), Some("1.4.0" | "1.5.0"))
111-
&& value.get("parser_error_location_unavailable").is_some()
138+
if !matches!(
139+
value["schema_version"].as_str(),
140+
Some("1.4.0" | "1.5.0" | "1.6.0")
141+
) && value.get("parser_error_location_unavailable").is_some()
112142
{
113143
return Err("syntax_worker_version_fields_mismatch".into());
114144
}
115145
let report: SyntaxWorkerEnvelope =
116146
serde_json::from_value(value).map_err(|_| "syntax_worker_report_invalid")?;
117147
if !matches!(
118148
report.schema_version.as_str(),
119-
"1.0.0" | "1.1.0" | "1.2.0" | "1.3.0" | "1.4.0" | "1.5.0"
149+
"1.0.0" | "1.1.0" | "1.2.0" | "1.3.0" | "1.4.0" | "1.5.0" | "1.6.0"
120150
) || (report.schema_version == "1.0.0" && !report.structural_observations.is_empty())
121151
|| (report.schema_version == "1.1.0"
122152
&& (language != "python" || report.structural_observations.is_empty()))
@@ -128,10 +158,15 @@ fn run_candidate(
128158
&& (report.parser_error_location_unavailable != Some(true) || !report.truncated))
129159
|| (report.schema_version == "1.5.0"
130160
&& (!bindings || language != "javascript" || report.structural_observations.is_empty()))
161+
|| (report.schema_version == "1.6.0"
162+
&& (!bindings || language != "erlang" || report.structural_observations.is_empty()))
163+
|| (language == "erlang"
164+
&& !report.structural_observations.is_empty()
165+
&& report.schema_version != "1.6.0")
131166
|| (language == "javascript"
132167
&& !report.structural_observations.is_empty()
133168
&& report.schema_version != "1.5.0")
134-
|| (report.schema_version == "1.5.0"
169+
|| (matches!(report.schema_version.as_str(), "1.5.0" | "1.6.0")
135170
&& report
136171
.parser_error_location_unavailable
137172
.is_some_and(|flag| !flag || !report.truncated))

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ impl SyntaxWorkerStructure {
3838
&& self.parent_syntax_kind == "program"
3939
&& self.start_byte < self.end_byte
4040
}
41+
"erlang" => {
42+
self.rule_id == "codeguard.erlang.form_terminator"
43+
&& self.rule_sha256 == codeguard_adapters::erlang_form_rule_sha256()
44+
&& self.parent_syntax_kind == "fun_decl"
45+
&& source
46+
.get(self.start_byte..self.end_byte)
47+
.is_some_and(|span| span.is_empty() || span == b";")
48+
}
4149
"python" => {
4250
self.rule_id == "codeguard.python.required_suite"
4351
&& self.rule_sha256 == codeguard_adapters::python_suite_rule_sha256()
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#![cfg(all(feature = "wasm-precheck", unix))]
2+
use serde_json::Value;
3+
use std::{fs, process::Command};
4+
5+
#[test]
6+
fn original_erlang_forms_preserve_legal_continuations_and_detect_missing_terminators() {
7+
let cases: Value = serde_json::from_str(include_str!(
8+
"../../../tests/fixtures/erlang_source_forms.json"
9+
))
10+
.unwrap();
11+
let root = std::env::temp_dir()
12+
.canonicalize()
13+
.unwrap()
14+
.join(format!("cg-erlang-form-candidates-{}", std::process::id()));
15+
fs::create_dir(&root).unwrap();
16+
let mut reports = Vec::new();
17+
for case in cases.as_array().unwrap() {
18+
let path = root.join(format!("{}.erl", case["name"].as_str().unwrap()));
19+
fs::write(&path, case["source"].as_str().unwrap()).unwrap();
20+
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
21+
.args(["grammar", "probe", "erlang"])
22+
.arg(&path)
23+
.arg("--format=json")
24+
.output()
25+
.unwrap();
26+
assert_eq!(output.status.code(), Some(3));
27+
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
28+
let candidates = report["structural_observations"]
29+
.as_array()
30+
.map_or(0, Vec::len);
31+
if case["valid"] == true {
32+
assert_eq!(candidates, 0, "legal form {:?}: {report}", case["name"]);
33+
} else if case["name"] != "bare_expression" && case["name"] != "bare_call" {
34+
assert!(
35+
candidates > 0,
36+
"missing terminator {:?}: {report}",
37+
case["name"]
38+
);
39+
assert_eq!(report["schema_version"], "0.7.0");
40+
assert_eq!(
41+
report["structural_observations"][0]["rule_id"],
42+
"codeguard.erlang.form_terminator"
43+
);
44+
}
45+
if case["name"] == "bare_expression" || case["name"] == "bare_call" {
46+
assert!(
47+
report["recoveries"]
48+
.as_array()
49+
.is_some_and(|rows| !rows.is_empty()),
50+
"bare form lost parser recovery: {report}"
51+
);
52+
}
53+
assert_eq!(
54+
report["grammar_qualified"], false,
55+
"case {:?}: {report}",
56+
case["name"]
57+
);
58+
assert_eq!(report["native"]["status"], "not_run");
59+
assert_eq!(report["delivery_decision"], "not_evaluated");
60+
assert_eq!(
61+
fs::read_to_string(&path).unwrap(),
62+
case["source"].as_str().unwrap()
63+
);
64+
reports
65+
.push(serde_json::json!({"name":case["name"],"valid":case["valid"],"report":report}));
66+
}
67+
if let Ok(path) = std::env::var("CODEGUARD_ERLANG_FORM_REPORTS") {
68+
fs::write(path, serde_json::to_vec_pretty(&reports).unwrap()).unwrap();
69+
}
70+
fs::remove_dir_all(root).unwrap();
71+
}

0 commit comments

Comments
 (0)