Skip to content

Commit 6cf2253

Browse files
committed
建立JavaScript模块return的有界AST事实与模式对照验收
1 parent 40cc624 commit 6cf2253

10 files changed

Lines changed: 442 additions & 0 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,12 @@ pub use wasm_form_terminator::WasmFormTerminator;
238238
mod wasm_form_terminator_scan;
239239
#[cfg(feature = "wasm-precheck")]
240240
pub use wasm_form_terminator_scan::scan_wasm_form_terminators;
241+
242+
#[cfg(feature = "wasm-precheck")]
243+
mod wasm_outer_return;
244+
#[cfg(feature = "wasm-precheck")]
245+
pub use wasm_outer_return::WasmOuterReturn;
246+
#[cfg(feature = "wasm-precheck")]
247+
mod wasm_outer_return_scan;
248+
#[cfg(feature = "wasm-precheck")]
249+
pub use wasm_outer_return_scan::{WasmOuterReturnScan, scan_wasm_outer_returns};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// 函数边界之外的return节点字节事实;不自行认定模块模式或源码违规。
2+
#[derive(Clone, Debug, Eq, PartialEq)]
3+
pub struct WasmOuterReturn {
4+
/// 当前AST节点类型。
5+
pub syntax_kind: String,
6+
/// 原始字节起点。
7+
pub start_byte: usize,
8+
/// 原始字节终点。
9+
pub end_byte: usize,
10+
/// 零基起始行。
11+
pub start_row: usize,
12+
/// 零基起始字节列。
13+
pub start_column_byte: usize,
14+
/// 零基结束行。
15+
pub end_row: usize,
16+
/// 零基结束字节列。
17+
pub end_column_byte: usize,
18+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use crate::WasmOuterReturn;
2+
use tree_sitter::Tree;
3+
4+
/// 有界AST函数外return事实集合;是否适用模块规则由调用者证明。
5+
#[derive(Clone, Debug, Eq, PartialEq)]
6+
pub struct WasmOuterReturnScan {
7+
/// 已观察到的原始节点位置,不输出源码文本。
8+
pub returns: Vec<WasmOuterReturn>,
9+
/// 记录或访问预算耗尽,不得解释为完整清洁结果。
10+
pub truncated: bool,
11+
}
12+
13+
/// 在固定根节点内线性遍历,跳过调用者列出的完整函数子树。
14+
/// 参数为同字节树/源码、根/return类型、函数边界及记录/访问预算;
15+
/// 返回仅AST事实或参数错误,不决定module/CommonJS/未知模式的语言语义。
16+
pub fn scan_wasm_outer_returns(
17+
tree: &Tree,
18+
source: &[u8],
19+
kinds: [&str; 2],
20+
function_kinds: &[&str],
21+
max_records: usize,
22+
max_visits: usize,
23+
) -> Result<WasmOuterReturnScan, String> {
24+
let root = tree.root_node();
25+
let [root_kind, return_kind] = kinds;
26+
let valid_kind =
27+
|kind: &&str| !kind.is_empty() && kind.len() <= 128 && !kind.chars().any(char::is_control);
28+
if !(1..=128).contains(&max_records)
29+
|| !(1..=200_000).contains(&max_visits)
30+
|| !(1..=32).contains(&function_kinds.len())
31+
|| !kinds.iter().all(valid_kind)
32+
|| !function_kinds.iter().all(valid_kind)
33+
|| function_kinds.contains(&root_kind)
34+
|| function_kinds.contains(&return_kind)
35+
|| root_kind == return_kind
36+
|| source.len() > 1024 * 1024
37+
|| std::str::from_utf8(source).is_err()
38+
|| root.kind() != root_kind
39+
|| root.end_byte() > source.len()
40+
{
41+
return Err("函数外return事实参数或源码边界无效".into());
42+
}
43+
let mut cursor = root.walk();
44+
let mut returns = Vec::new();
45+
let mut visited = 0usize;
46+
let mut truncated = false;
47+
loop {
48+
// 每个游标节点只处理一次;函数子树整体跳过,不做递归或反复祖先查找。
49+
if visited == max_visits {
50+
truncated = true;
51+
break;
52+
}
53+
visited += 1;
54+
let node = cursor.node();
55+
if node.kind() == return_kind {
56+
if returns.len() == max_records {
57+
truncated = true;
58+
break;
59+
}
60+
let start = node.start_position();
61+
let end = node.end_position();
62+
returns.push(WasmOuterReturn {
63+
syntax_kind: node.kind().to_owned(),
64+
start_byte: node.start_byte(),
65+
end_byte: node.end_byte(),
66+
start_row: start.row,
67+
start_column_byte: start.column,
68+
end_row: end.row,
69+
end_column_byte: end.column,
70+
});
71+
}
72+
if !function_kinds.contains(&node.kind()) && cursor.goto_first_child() {
73+
continue;
74+
}
75+
loop {
76+
if cursor.goto_next_sibling() {
77+
break;
78+
}
79+
if !cursor.goto_parent() {
80+
return Ok(WasmOuterReturnScan { returns, truncated });
81+
}
82+
}
83+
}
84+
Ok(WasmOuterReturnScan { returns, truncated })
85+
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#![cfg(feature = "wasm-precheck")]
2+
use codeguard_runtime::{WasmGrammar, scan_wasm_outer_returns};
3+
4+
fn javascript() -> WasmGrammar {
5+
WasmGrammar::load(
6+
"javascript",
7+
include_bytes!("../../../grammars/javascript/parser.wasm"),
8+
"7978e62bcc851ab1d1f6dcd4678f9eda79df2b3b3490e75f81dd819d9bccccfa",
9+
15,
10+
)
11+
.unwrap()
12+
}
13+
fn scan(
14+
tree: &tree_sitter::Tree,
15+
source: &[u8],
16+
records: usize,
17+
visits: usize,
18+
) -> codeguard_runtime::WasmOuterReturnScan {
19+
scan_wasm_outer_returns(
20+
tree,
21+
source,
22+
["program", "return_statement"],
23+
&[
24+
"function_declaration",
25+
"function_expression",
26+
"generator_function_declaration",
27+
"generator_function",
28+
"arrow_function",
29+
"method_definition",
30+
],
31+
records,
32+
visits,
33+
)
34+
.unwrap()
35+
}
36+
#[test]
37+
fn outside_function_returns_include_control_flow_and_byte_positions() {
38+
let mut grammar = javascript();
39+
for source in [
40+
"return 1;",
41+
"if (ready) { return 1; }",
42+
"while (ready) { return 2; }",
43+
"try { return 1; } catch (e) { return 2; }",
44+
"const 名字=1;\r\nif (名字) { return 名字; }",
45+
] {
46+
let tree = grammar.parse(source.as_bytes()).unwrap();
47+
assert!(!tree.root_node().has_error(), "{source}");
48+
let facts = scan(&tree, source.as_bytes(), 128, 200_000);
49+
assert!(!facts.returns.is_empty(), "{source}");
50+
assert!(!facts.truncated);
51+
for fact in &facts.returns {
52+
assert!(source[fact.start_byte..fact.end_byte].starts_with("return"));
53+
assert_eq!(fact.syntax_kind, "return_statement");
54+
let prefix = &source.as_bytes()[..fact.start_byte];
55+
assert_eq!(
56+
fact.start_row,
57+
prefix.iter().filter(|b| **b == b'\n').count()
58+
);
59+
assert_eq!(
60+
fact.start_column_byte,
61+
prefix.rsplit(|b| *b == b'\n').next().unwrap().len()
62+
);
63+
}
64+
}
65+
}
66+
#[test]
67+
fn every_function_boundary_and_comment_text_is_skipped() {
68+
let mut grammar = javascript();
69+
for source in [
70+
"function f(){if(true){return 1;}}",
71+
"const f = function(){return 1;};",
72+
"function* f(){return 1;}",
73+
"const f = function*(){return 1;};",
74+
"const f = () => {return 1;};",
75+
"class C { f(){return 1;} get x(){return 2;} }",
76+
"const o = {f(){return 1;}};",
77+
"async function f(){return 1;}",
78+
"const f = async () => {return 1;};",
79+
"const x = 'return 1;'; // return 2;\n",
80+
] {
81+
let tree = grammar.parse(source.as_bytes()).unwrap();
82+
assert!(!tree.root_node().has_error(), "{source}");
83+
let facts = scan(&tree, source.as_bytes(), 128, 200_000);
84+
assert!(facts.returns.is_empty(), "{source}");
85+
assert!(!facts.truncated);
86+
}
87+
let source = b"function f(){return 1;} if(true){return 2;}";
88+
let tree = grammar.parse(source).unwrap();
89+
let facts = scan(&tree, source, 128, 200_000);
90+
assert_eq!(facts.returns.len(), 1);
91+
assert_eq!(
92+
&source[facts.returns[0].start_byte..facts.returns[0].end_byte],
93+
b"return 2;"
94+
);
95+
}
96+
#[test]
97+
fn exhausted_scan_retains_facts_and_invalid_bounds_are_rejected() {
98+
let mut grammar = javascript();
99+
let source = b"return 1; return 2;";
100+
let tree = grammar.parse(source).unwrap();
101+
let facts = scan(&tree, source, 1, 200_000);
102+
assert_eq!(facts.returns.len(), 1);
103+
assert!(facts.truncated);
104+
let facts = scan(&tree, source, 128, 1);
105+
assert!(facts.returns.is_empty());
106+
assert!(facts.truncated);
107+
for (root, ret, scopes, records, visits, bytes) in [
108+
(
109+
"program",
110+
"return_statement",
111+
&["function_declaration"][..],
112+
0,
113+
10,
114+
&source[..],
115+
),
116+
(
117+
"program",
118+
"return_statement",
119+
&["function_declaration"][..],
120+
1,
121+
0,
122+
&source[..],
123+
),
124+
(
125+
"wrong",
126+
"return_statement",
127+
&["function_declaration"][..],
128+
1,
129+
10,
130+
&source[..],
131+
),
132+
(
133+
"program",
134+
"",
135+
&["function_declaration"][..],
136+
1,
137+
10,
138+
&source[..],
139+
),
140+
("program", "return_statement", &[][..], 1, 10, &source[..]),
141+
(
142+
"program",
143+
"return_statement",
144+
&["function_declaration"][..],
145+
1,
146+
10,
147+
&b""[..],
148+
),
149+
] {
150+
assert!(
151+
scan_wasm_outer_returns(&tree, bytes, [root, ret], scopes, records, visits).is_err()
152+
);
153+
}
154+
}
155+
156+
#[test]
157+
#[ignore = "需显式已有Node24.18.0,验证module/CommonJS模式边界,不批准语言资格"]
158+
fn actual_node_modes_confirm_outer_return_facts_without_commonjs_findings() {
159+
use codeguard_runtime::{ProcessSpec, Termination, run_process};
160+
use std::{
161+
collections::BTreeMap,
162+
ffi::OsString,
163+
path::{Path, PathBuf},
164+
sync::atomic::AtomicBool,
165+
time::{Duration, Instant},
166+
};
167+
let input = PathBuf::from(std::env::var_os("CODEGUARD_NODE_BIN").expect("显式指定Node"));
168+
assert!(input.is_absolute());
169+
let executable = input.canonicalize().unwrap();
170+
let before = std::fs::read(&executable).unwrap();
171+
let deadline = Instant::now() + Duration::from_secs(30);
172+
let cancelled = AtomicBool::new(false);
173+
let invoke = |args: &[&str], stdin| {
174+
run_process(
175+
&ProcessSpec {
176+
executable: executable.clone(),
177+
args: args.iter().map(OsString::from).collect(),
178+
cwd: Path::new("/").to_path_buf(),
179+
env: BTreeMap::new(),
180+
stdin,
181+
deadline,
182+
output_limit_bytes: 65536,
183+
},
184+
&cancelled,
185+
)
186+
};
187+
let version = invoke(&["--version"], None);
188+
assert_eq!(version.termination, Termination::Exited(0));
189+
assert_eq!(version.stdout, b"v24.18.0\n");
190+
assert!(version.stderr.is_empty());
191+
let mut grammar = javascript();
192+
for source in [
193+
"return 1;",
194+
"if(true){return 1;}",
195+
"while(false){return 1;}",
196+
"function f(){return 1;}",
197+
"const f=function(){return 1;};",
198+
"function* f(){return 1;}",
199+
"const f=function*(){return 1;};",
200+
"const f=()=>{return 1;};",
201+
"class C {f(){return 1;}}",
202+
"const o={f(){return 1;}};",
203+
"const s='return 1;';",
204+
] {
205+
let tree = grammar.parse(source.as_bytes()).unwrap();
206+
assert!(!tree.root_node().has_error());
207+
let facts = scan(&tree, source.as_bytes(), 128, 200_000);
208+
assert!(!facts.truncated);
209+
for (mode, module) in [
210+
("--input-type=module", true),
211+
("--input-type=commonjs", false),
212+
] {
213+
let outcome = invoke(&["--check", mode], Some(source.as_bytes().to_vec()));
214+
let invalid = module && !facts.returns.is_empty();
215+
assert_eq!(
216+
outcome.termination,
217+
Termination::Exited(i32::from(invalid)),
218+
"{mode} {source}"
219+
);
220+
assert!(outcome.stdout.is_empty());
221+
if invalid {
222+
assert!(String::from_utf8_lossy(&outcome.stderr).contains("SyntaxError"));
223+
} else {
224+
assert!(outcome.stderr.is_empty());
225+
}
226+
}
227+
}
228+
assert_eq!(input.canonicalize().unwrap(), executable);
229+
assert_eq!(std::fs::read(executable).unwrap(), before);
230+
}

‎docs/Codeguard-Grammar-Evaluation.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,7 @@ Java differential acceptance now checks truncated_files before treating empty re
165165
## JavaScript native differential uses project structure candidates
166166

167167
The development differential now shares the project's direct duplicate-binding worker. Report0.9 separates raw parser recovery from combined structure metrics. Actual Node24.18.0 over18 additional module cases gives raw5TP/11TN/0FP/2FN and combined6TP/11TN/0FP/1FN: duplicate binding has rule evidence, while module-level return remains missed. The schema constrains rule digests and language scope; existing protocols remain unchanged and0.4 consumers reject the new version. This measurement correction does not change the fixed358-case metrics or grant grammar qualification. CommonJS or unknown modes must not receive unconditional module-return findings. Independent holdout and formal qualification remain open. See [limited acceptance](../tests/acceptance/javascript-native-project-binding.md).
168+
169+
## AST facts for a module-only return candidate
170+
171+
The runtime now traverses bounded AST nodes outside function/generator/arrow/method subtrees, including outer control flow. It retains UTF-8 byte locations and explicit budget truncation. Actual Node24.18.0 compared11 source samples in module and CommonJS modes (22 syntax checks), confirming the need for explicit module context. This is only the fact layer; public worker/probe, project mode and differential wiring remain open. The existing module_return false negative and0/32 qualification are unchanged. See [limited acceptance](../tests/acceptance/javascript-module-return-ast.md).

‎docs/Codeguard-Grammar-Evaluation.zh_CN.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,7 @@ Java差分验收先核对truncated_files,隐藏恢复保留unknown,避免空
177177
## JavaScript 原生差分对齐项目结构候选
178178

179179
开发差分现在复用项目检查的重复直接绑定 worker,报告0.9分别保留原始恢复与组合结构指标。真实 Node24.18.0 的18个额外 module 样例:原始5TP/11TN/0FP/2FN,组合6TP/11TN/0FP/1FN;重复绑定得到结构规则证据,顶层 module return 仍漏检。规则摘要和语言范围受 schema 约束;旧协议保持不变,旧0.4消费者拒绝新版本。此修正不新增发行资格、不改变固定358例指标,也不能对 CommonJS/未知模式无条件报顶层 return。独立holdout及正式资格仍未完成。见[局部验收](../tests/acceptance/javascript-native-project-binding.md)。
180+
181+
## 模块 return 候选的 AST 事实基础
182+
183+
runtime新增有界函数外return扫描:覆盖顶层控制流、跳过函数/生成器/箭头/方法子树、保留UTF-8字节位置和预算截断。真实Node24.18.0对11份源码在module/CommonJS下共22次对照,证明规则必须带模块模式条件。当前仅有事实层,公开worker/probe/项目模式/差分尚未接线,因此module_return仍漏检、资格0/32。见[局部验收](../tests/acceptance/javascript-module-return-ast.md)。

‎openspec/changes/introduce-rust-codeguard-cli/specs/syntax-precheck/spec.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,3 +858,13 @@ The native differential SHALL use the same frozen JavaScript binding worker as p
858858
#### Scenario: Native-invalid duplicate declaration has a combined candidate
859859
- **WHEN** a frozen direct lexical duplicate is rejected by the selected Node syntax observer but has no parser recovery
860860
- **THEN** the raw comparison remains false_negative, the combined comparison is true_positive with the project rule evidence, and no grammar qualification or delivery permission is issued
861+
862+
### Requirement: Module-only return candidates SHALL use bounded AST facts and explicit mode
863+
864+
The implementation SHALL derive return-outside-function facts from bounded AST traversal, including returns nested in control-flow blocks, and SHALL skip function declarations, expressions, generators, arrows and methods. Facts SHALL retain byte locations and incomplete traversal. The presence of these facts alone SHALL NOT classify CommonJS or unknown-mode input as invalid. Activation of a module-only candidate SHALL require an explicit applicable module context.
865+
866+
#### Scenario: Returns in functions remain legal candidates
867+
- **WHEN** a JavaScript AST contains function, generator, arrow or method returns and an outer control-flow return
868+
- **THEN** only the return outside function boundaries is retained as an AST fact
869+
- **AND** record or traversal exhaustion remains visibly truncated
870+
- **AND** no grammar qualification, native completion or project delivery permission is granted

‎openspec/changes/introduce-rust-codeguard-cli/tasks.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,3 +1936,7 @@ next/task show在已核验当前原生零诊断、原grammar引用及同一源
19361936
## 2026-10-06 JavaScript 原生差分复用项目绑定候选
19371937

19381938
延续12.11/14.17/14.19,父任务不勾选。JavaScript原生差分复用项目直接重复绑定worker,raw与combined分开;报告0.9按语言/规则摘要约束,旧协议不变。受控RED→GREEN,相关四目标15通过/6条件忽略,真实Node24.18.0显式1通过:18例raw5TP/11TN/0FP/2FN,combined6TP/11TN/0FP/1FN。module_return仍漏检,不扩大到CommonJS/未知模式;固定358指标不变。实际schema与三种篡改/旧消费者拒绝、严格Clippy修复后通过。见tests/acceptance/javascript-native-project-binding.md。独立holdout/正式资格0/32及完整发布验收仍缺。
1939+
1940+
## 2026-10-06 JavaScript 模块 return 的 AST 事实基础
1941+
1942+
延续12.11/14.17/14.19,父任务不勾选。runtime新增有界函数外return事实,覆盖顶层控制流并跳过函数/生成器/箭头/方法子树;预算截断和字节位置真实保留。新API缺失RED后实现,四目标16通过/2条件忽略,实际Node24.18.0显式1通过(11源码、22次module/CommonJS对照),严格WASM runtime/all-targets Clippy通过。公共worker/probe/项目模式/差分尚未接线,module_return漏检统计不变;禁止未知模式自启。见tests/acceptance/javascript-module-return-ast.md。40cc624 CI37431391905仍失败于固定插件证据源码checkout,后续门禁未运行。

0 commit comments

Comments
 (0)