Skip to content

Commit ac8cfce

Browse files
committed
fix: 为 TSX 初检使用独立 grammar
1 parent 1e8fba1 commit ac8cfce

27 files changed

Lines changed: 299 additions & 39 deletions

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ cargo build --locked -p codeguard-cli
9494

9595
Version output includes `cli_version`, `target`, and protocol information. Discovery and init preview return observations and unknowns. Dry-run creates neither `AGENTS.md` nor a project data directory. Add Cargo `--offline` only when dependencies are cached.
9696

97-
The workspace requires `std`. The opt-in `wasm-precheck` build feature uses a bounded Rust worker with pinned Java/TypeScript grammar candidates. It currently reaches only single-file `lint java` and `lint typescript` requests without explicit native context; it is absent from the published npm `0.1.2` package. No `no_std`, completed project-wide WASM fallback, zero-unsafe, or fastest-runtime claim is made. Runtime OS calls require safety review; a comprehensive security audit is not claimed.
97+
The workspace requires `std`. The opt-in `wasm-precheck` build feature uses a bounded Rust worker with pinned Java, TypeScript, and TSX grammar candidates. It currently reaches only single-file `lint java` and `lint typescript` requests without explicit native context; `.tsx` uses its own grammar. It is absent from the published npm `0.1.2` package. No `no_std`, completed project-wide WASM fallback, zero-unsafe, or fastest-runtime claim is made. Runtime OS calls require safety review; a comprehensive security audit is not claimed.
9898

99-
The current source-built CLI's `codeguard grammar status --format=json` reports the coverage gap without loading grammars or running lint: the pinned CodeGraph source has 30 vendored WASM files plus two distinct grammars resolved through its dependency; CodeGuard has two unqualified candidates and zero released syntax capabilities. The separate [coverage inventory](grammars/codegraph-coverage.json) is not an approved asset manifest. COBOL's 16.4 MB source asset also exceeds the current Rust loader's 8 MiB limit.
99+
The current source-built CLI's `codeguard grammar status --format=json` reports the coverage gap without loading grammars or running lint: the pinned CodeGraph source has 30 vendored WASM files plus two distinct grammars resolved through its dependency; CodeGuard has three unqualified candidates and zero released syntax capabilities. The separate [coverage inventory](grammars/codegraph-coverage.json) is not an approved asset manifest. COBOL's 16.4 MB source asset also exceeds the current Rust loader's 8 MiB limit.
100100

101101
### npm installation and one-off use
102102

‎README.zh-CN.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ cargo build --locked -p codeguard-cli
9494

9595
版本输出包含 `cli_version`、`target` 和协议信息。发现与 init 预览返回观察及未知项。Dry-run 不创建 `AGENTS.md` 或项目数据目录。仅在依赖已缓存时为 Cargo 添加 `--offline`。
9696

97-
工作区要求 `std`。可选 `wasm-precheck` 构建特性已用固定的 Java/TypeScript grammar 候选资产与有界 Rust worker,为缺少显式原生上下文的 `lint java`、`lint typescript` 单文件请求提供疑似语法观察;公开 npm `0.1.2` 不含该路径。不声称 `no_std`、已完成项目级 WASM 兜底、零 unsafe 或性能最快。运行层系统调用需要安全审查,当前不宣称已完成整体安全审计。
97+
工作区要求 `std`。可选 `wasm-precheck` 构建特性已用固定的 Java、TypeScript、TSX grammar 候选资产与有界 Rust worker,为缺少显式原生上下文的 `lint java`、`lint typescript` 单文件请求提供疑似语法观察;`.tsx` 使用独立 grammar。公开 npm `0.1.2` 不含该路径。不声称 `no_std`、已完成项目级 WASM 兜底、零 unsafe 或性能最快。运行层系统调用需要安全审查,当前不宣称已完成整体安全审计。
9898

99-
当前源码构建的 CLI 可用 `codeguard grammar status --format=json` 只读展示来源覆盖缺口,不加载 grammar 或运行 lint:固定 CodeGraph 来源含 30 份随仓 WASM,另有两种独立 grammar 从其依赖取得;CodeGuard 当前只有两份未验收候选,已发行语法能力为零。[覆盖库存](grammars/codegraph-coverage.json)不是已批准资产清单。COBOL 来源资产约 16.4 MB,还超过当前 Rust 加载器的 8 MiB 限额。
99+
当前源码构建的 CLI 可用 `codeguard grammar status --format=json` 只读展示来源覆盖缺口,不加载 grammar 或运行 lint:固定 CodeGraph 来源含 30 份随仓 WASM,另有两种独立 grammar 从其依赖取得;CodeGuard 当前有三份未验收候选,已发行语法能力为零。[覆盖库存](grammars/codegraph-coverage.json)不是已批准资产清单。COBOL 来源资产约 16.4 MB,还超过当前 Rust 加载器的 8 MiB 限额。
100100

101101
### npm 安装与一次性调用
102102

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ pub struct GrammarAsset {
6868
pub release_status: String,
6969
}
7070

71-
/// 读取仓内固定清单,并检查 CodeGraph 许可证及两份资产的原始字节。
71+
/// 读取仓内固定清单,并检查 CodeGraph 许可证及三份资产的原始字节。
7272
pub fn bundled_grammar_candidates() -> Result<GrammarAssetManifest, String> {
7373
let manifest = parse_grammar_asset_manifest(include_bytes!("../../../grammars/manifest.json"))?;
7474
let codegraph_license = include_bytes!("../../../grammars/LICENSE.codegraph");
@@ -85,6 +85,10 @@ pub fn bundled_grammar_candidates() -> Result<GrammarAssetManifest, String> {
8585
include_bytes!("../../../grammars/typescript/parser.wasm").as_slice(),
8686
include_bytes!("../../../grammars/typescript/LICENSE").as_slice(),
8787
),
88+
"tsx" => (
89+
include_bytes!("../../../grammars/tsx/parser.wasm").as_slice(),
90+
include_bytes!("../../../grammars/typescript/LICENSE").as_slice(),
91+
),
8892
_ => return Err("候选语言未知".into()),
8993
};
9094
verify_grammar_asset(asset, wasm, license)?;
@@ -106,7 +110,7 @@ pub fn parse_grammar_asset_manifest(raw: &[u8]) -> Result<GrammarAssetManifest,
106110
|| manifest.codegraph_license != "LICENSE.codegraph"
107111
|| manifest.codegraph_license_sha256 != CODEGRAPH_LICENSE_SHA256
108112
|| manifest.build_tool != "tree-sitter-cli 0.25.10 build --wasm"
109-
|| manifest.assets.len() != 2
113+
|| manifest.assets.len() != 3
110114
{
111115
return Err("grammar 清单版本、来源或资产数量不符".into());
112116
}
@@ -115,7 +119,7 @@ pub fn parse_grammar_asset_manifest(raw: &[u8]) -> Result<GrammarAssetManifest,
115119
.iter()
116120
.map(|asset| asset.language.as_str())
117121
.collect();
118-
if languages != BTreeSet::from(["java", "typescript"]) {
122+
if languages != BTreeSet::from(["java", "typescript", "tsx"]) {
119123
return Err("grammar 语言资产缺失或重复".into());
120124
}
121125
for asset in &manifest.assets {
@@ -141,6 +145,16 @@ pub fn parse_grammar_asset_manifest(raw: &[u8]) -> Result<GrammarAssetManifest,
141145
"typescript/LICENSE",
142146
"49bf33cf78ef5897e4e161ce1517df7de1ae5042a65b6bcfd44401e0fc606559",
143147
),
148+
"tsx" => (
149+
"https://github.com/tree-sitter/tree-sitter-typescript",
150+
TYPESCRIPT_COMMIT,
151+
"v0.23.2",
152+
"tsx/parser.wasm",
153+
"8f647a1b2cafe9ab00fb2056d79021d2a144ba17a72f45511072311c1b05d08e",
154+
1445641,
155+
"typescript/LICENSE",
156+
"49bf33cf78ef5897e4e161ce1517df7de1ae5042a65b6bcfd44401e0fc606559",
157+
),
144158
_ => return Err("grammar 语言未知".into()),
145159
};
146160
if asset.dialect != asset.language

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ pub fn parse_syntax_precheck_candidate_report(
100100
}
101101
let mut observations = Vec::with_capacity(report.files.len());
102102
for file in report.files {
103+
if !path_matches_language(&file.path, &report.backend.language) {
104+
return Err("syntax_precheck_source_dialect_mismatch".into());
105+
}
103106
let source = source_bytes
104107
.get(&file.path)
105108
.ok_or("syntax_precheck_source_missing")?;
@@ -132,3 +135,14 @@ pub fn parse_syntax_precheck_candidate_report(
132135
}
133136
Ok(actual)
134137
}
138+
139+
fn path_matches_language(path: &str, language: &str) -> bool {
140+
match language {
141+
"java" => path.ends_with(".java"),
142+
"typescript" => [".ts", ".mts", ".cts"]
143+
.iter()
144+
.any(|suffix| path.ends_with(suffix)),
145+
"tsx" => path.ends_with(".tsx"),
146+
_ => false,
147+
}
148+
}

‎crates/codeguard-adapters/tests/grammar_asset_manifest.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use codeguard_adapters::{
55
#[test]
66
fn bundled_candidates_pin_source_bytes_license_and_abi_without_claiming_support() {
77
let manifest = bundled_grammar_candidates().expect("bundled manifest");
8-
assert_eq!(manifest.assets.len(), 2);
8+
assert_eq!(manifest.assets.len(), 3);
99
for asset in &manifest.assets {
1010
let (wasm, license) = match asset.language.as_str() {
1111
"java" => (
@@ -16,6 +16,10 @@ fn bundled_candidates_pin_source_bytes_license_and_abi_without_claiming_support(
1616
include_bytes!("../../../grammars/typescript/parser.wasm").as_slice(),
1717
include_bytes!("../../../grammars/typescript/LICENSE").as_slice(),
1818
),
19+
"tsx" => (
20+
include_bytes!("../../../grammars/tsx/parser.wasm").as_slice(),
21+
include_bytes!("../../../grammars/typescript/LICENSE").as_slice(),
22+
),
1923
other => panic!("unexpected language {other}"),
2024
};
2125
verify_grammar_asset(asset, wasm, license).expect("pinned candidate bytes");

‎crates/codeguard-adapters/tests/syntax_precheck_report_contract.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,20 @@ fn candidate_schema_declares_closed_versioned_contract() {
105105
assert_eq!(schema["additionalProperties"], false);
106106
assert_eq!(schema["properties"]["precheck"]["$ref"], "#/$defs/precheck");
107107
}
108+
109+
#[test]
110+
fn tsx_candidate_requires_tsx_source_and_cannot_relabel_java() {
111+
let mut report = candidate();
112+
report["backend"]["language"] = json!("tsx");
113+
report["backend"]["dialect"] = json!("tsx");
114+
report["backend"]["grammar_sha256"] =
115+
json!("8f647a1b2cafe9ab00fb2056d79021d2a144ba17a72f45511072311c1b05d08e");
116+
assert!(read(&report).is_err());
117+
118+
report["files"][0]["path"] = json!("src/Component.tsx");
119+
let sources = BTreeMap::from([("src/Component.tsx".into(), b"hello".to_vec())]);
120+
let status =
121+
parse_syntax_precheck_candidate_report(&serde_json::to_vec(&report).unwrap(), &sources)
122+
.unwrap();
123+
assert_eq!(status.status, SyntaxPrecheckStatus::Incomplete);
124+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ fn observe(language: &str, source: &[u8]) -> Result<SyntaxWorkerEnvelope, String
6363
let wasm = match language {
6464
"java" => include_bytes!("../../../grammars/java/parser.wasm").as_slice(),
6565
"typescript" => include_bytes!("../../../grammars/typescript/parser.wasm").as_slice(),
66+
"tsx" => include_bytes!("../../../grammars/tsx/parser.wasm").as_slice(),
6667
_ => return Err("不支持的 grammar 语种".into()),
6768
};
6869
let mut grammar = WasmGrammar::load(language, wasm, &asset.sha256, asset.abi_version as usize)?;

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,24 @@ pub(crate) fn eligible(args: &EslintLintArguments) -> bool {
2828
}
2929

3030
/// 使用本轮源码快照与私有 Rust worker 补充疑似语法观察;原生仍未运行。
31-
/// 参数为受支持的单文件请求及共同截止时间;返回无交付权威的 0.3.0 反馈。
31+
/// 参数为受支持的单文件请求及共同截止时间;返回无交付权威的 TypeScript 0.3.0 或 TSX 0.4.0 反馈。
3232
pub(crate) fn observe(args: &EslintLintArguments, deadline: Instant) -> Value {
33+
let language = if args
34+
.source
35+
.extension()
36+
.is_some_and(|extension| extension == "tsx")
37+
{
38+
"tsx"
39+
} else {
40+
"typescript"
41+
};
3342
let source_name = args
3443
.source
3544
.file_name()
3645
.and_then(|name| name.to_str())
3746
.unwrap_or("source.ts");
3847
let mut report = json!({
39-
"schema_version": "0.3.0",
48+
"schema_version": if language == "tsx" { "0.4.0" } else { "0.3.0" },
4049
"report_type": "eslint_local_feedback",
4150
"status": "incomplete",
4251
"local_coherent": false,
@@ -51,7 +60,7 @@ pub(crate) fn observe(args: &EslintLintArguments, deadline: Instant) -> Value {
5160
"setup": {"requirement": "required", "reason": "native_confirmation_needed", "task_id": null},
5261
"syntax_precheck": {
5362
"backend": "bundled_tree_sitter_wasm_candidate",
54-
"language": "typescript",
63+
"language": language,
5564
"status": "incomplete",
5665
"reason": "not_run",
5766
"source_path": source_name,
@@ -82,7 +91,7 @@ pub(crate) fn observe(args: &EslintLintArguments, deadline: Instant) -> Value {
8291
};
8392
let observation = match run_syntax_worker_candidate(
8493
&executable,
85-
"typescript",
94+
language,
8695
source_name,
8796
&source,
8897
deadline,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ fn codegraph_coverage_is_explicit_and_never_claims_parser_or_gate_completion() {
1212
assert_eq!(report["report_type"], "grammar_coverage_inventory");
1313
assert_eq!(report["codegraph_grammar_count"], 32);
1414
assert_eq!(report["codegraph_vendored_count"], 30);
15-
assert_eq!(report["candidate_count"], 2);
15+
assert_eq!(report["candidate_count"], 3);
1616
assert_eq!(report["released_count"], 0);
1717
assert_eq!(report["authority"], "source_inventory_only");
1818
assert_eq!(report["parser_capability"], "unverified");
@@ -25,7 +25,7 @@ fn codegraph_coverage_is_explicit_and_never_claims_parser_or_gate_completion() {
2525
assert_eq!(asset["released"], false);
2626
assert!(!asset["gap"].as_str().unwrap().is_empty());
2727
}
28-
for language in ["java", "typescript"] {
28+
for language in ["java", "typescript", "tsx"] {
2929
let asset = assets
3030
.iter()
3131
.find(|row| row["language"] == language)

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,72 @@ fn no_recovery_remains_incomplete_and_javascript_does_not_use_typescript_grammar
9090
assert!(js_report.get("syntax_precheck").is_none());
9191
}
9292

93+
#[test]
94+
fn tsx_uses_its_own_grammar_instead_of_reporting_valid_jsx_as_suspect() {
95+
let project = Project::new();
96+
fs::write(
97+
project.0.join("component.tsx"),
98+
"const node = <div title=\"ok\">hello</div>;\n",
99+
)
100+
.unwrap();
101+
let (status, report) = project.lint("component.tsx");
102+
assert_eq!(status, 3);
103+
assert_eq!(report["schema_version"], "0.4.0");
104+
assert_eq!(report["native"]["status"], "not_run");
105+
assert_eq!(report["syntax_precheck"]["language"], "tsx");
106+
assert_eq!(report["syntax_precheck"]["checked_files"], 1);
107+
assert_eq!(
108+
report["syntax_precheck"]["grammar_sha256"],
109+
"8f647a1b2cafe9ab00fb2056d79021d2a144ba17a72f45511072311c1b05d08e"
110+
);
111+
assert!(
112+
report["syntax_precheck"]["observations"]
113+
.as_array()
114+
.unwrap()
115+
.is_empty()
116+
);
117+
assert_eq!(report["delivery_decision"], "not_evaluated");
118+
119+
fs::write(
120+
project.0.join("broken.tsx"),
121+
"const node = <div title=></div>;\n",
122+
)
123+
.unwrap();
124+
let (_, broken) = project.lint("broken.tsx");
125+
assert_eq!(broken["syntax_precheck"]["language"], "tsx");
126+
assert!(
127+
!broken["syntax_precheck"]["observations"]
128+
.as_array()
129+
.unwrap()
130+
.is_empty()
131+
);
132+
assert_eq!(broken["syntax_precheck"]["status"], "incomplete");
133+
assert!(broken["findings"].as_array().unwrap().is_empty());
134+
135+
let schema: Value = serde_json::from_str(include_str!(
136+
"../../../schemas/eslint-local-feedback-v0.4.schema.json"
137+
))
138+
.unwrap();
139+
assert_eq!(schema["properties"]["schema_version"]["const"], "0.4.0");
140+
assert_eq!(
141+
schema["properties"]["syntax_precheck"]["properties"]["language"]["const"],
142+
"tsx"
143+
);
144+
let actual: std::collections::BTreeSet<_> = report
145+
.as_object()
146+
.unwrap()
147+
.keys()
148+
.map(String::as_str)
149+
.collect();
150+
let required: std::collections::BTreeSet<_> = schema["required"]
151+
.as_array()
152+
.unwrap()
153+
.iter()
154+
.map(|item| item.as_str().unwrap())
155+
.collect();
156+
assert_eq!(actual, required);
157+
}
158+
93159
#[test]
94160
fn candidate_feedback_schema_is_versioned_and_closed() {
95161
let schema: Value = serde_json::from_str(include_str!(

0 commit comments

Comments
 (0)