Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ Generic `dependencies`, generic `security`, arbitrary category/language combinat

**The published `0.1.2` does not yet implement bundled WASM fallback or automatic conversation delivery.** The existing commands remain the unified entry points; native adapter-specific prerequisites still apply today:

An opt-in source build with `--features codeguard-cli/wasm-precheck` has narrow TypeScript and Java single-file candidate paths. For TypeScript, a visible local ESLint 10 entry and one flat config take priority: when an executable Node is resolved from `PATH`, the CLI runs its existing bounded native probe; without Node it reports the setup gap. If no local entry is visible, the bundled TypeScript/TSX candidate can provide suspected syntax positions. The Java candidate likewise remains incomplete; no bundled grammar has an accepted language/dialect range. Native observations and prechecks never approve delivery. Default and published binaries keep their previous native-context behavior. [TypeScript acceptance](tests/acceptance/typescript-syntax-fallback-candidate.md) · [native-first acceptance](tests/acceptance/native-first-eslint-candidate.md) · [Java acceptance](tests/acceptance/java-syntax-fallback-candidate.md).
An opt-in source build with `--features codeguard-cli/wasm-precheck` has narrow TypeScript and Java single-file candidate paths. For TypeScript, a visible local ESLint 10 entry and one flat config take priority: when an executable Node is resolved from `PATH`, the CLI runs its existing bounded native probe; without Node it reports the setup gap. Only when the project-local ESLint package is not observed can the bundled TypeScript/TSX candidate provide suspected syntax positions; an untrusted entry, package identity or config returns a specific setup blocker. The Java candidate likewise remains incomplete; no bundled grammar has an accepted language/dialect range. Native observations and prechecks never approve delivery. Default and published binaries keep their previous native-context behavior. [TypeScript acceptance](tests/acceptance/typescript-syntax-fallback-candidate.md) · [native-first acceptance](tests/acceptance/native-first-eslint-candidate.md) · [Java acceptance](tests/acceptance/java-syntax-fallback-candidate.md).

```bash
codeguard lint java .
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ Python 编辑快反馈可执行 `codeguard lint python . --file src/changed.py -

**公开 `0.1.2` 尚未实现内置 WASM 兜底或自动对话交付。** 保留已有命令作为统一入口;当前仍须满足各原生适配器的参数和前置条件:

可选源码构建已有 Java/TypeScript 单文件候选路径。TypeScript 项目若有可见的本地 ESLint 10 入口和唯一 flat config,会先尝试从 `PATH` 解析 Node 并运行既有有界原生探针;缺 Node 时报告准备缺口,无本地入口时才提供 TypeScript/TSX 疑似语法初检。Java 候选仍须原生确认,所有内置 grammar 的版本/方言尚未验收,原生局部观察和初检都不能批准交付。默认及公开二进制沿用既有原生上下文路径,见 [Java 局部验收](tests/acceptance/java-syntax-fallback-candidate.md)、[TypeScript 局部验收](tests/acceptance/typescript-syntax-fallback-candidate.md)与[原生优先局部验收](tests/acceptance/native-first-eslint-candidate.md)。
可选源码构建已有 Java/TypeScript 单文件候选路径。TypeScript 项目若有可见的本地 ESLint 10 入口和唯一 flat config,会先尝试从 `PATH` 解析 Node 并运行既有有界原生探针;缺 Node 时报告准备缺口;仅本地 ESLint 包未观察到时才提供 TypeScript/TSX 疑似语法初检。入口、包身份或配置不可信时保留具体环境阻塞。Java 候选仍须原生确认,所有内置 grammar 的版本/方言尚未验收,原生局部观察和初检都不能批准交付。默认及公开二进制沿用既有原生上下文路径,见 [Java 局部验收](tests/acceptance/java-syntax-fallback-candidate.md)、[TypeScript 局部验收](tests/acceptance/typescript-syntax-fallback-candidate.md)与[原生优先局部验收](tests/acceptance/native-first-eslint-candidate.md)。

```bash
codeguard lint java .
Expand Down
72 changes: 37 additions & 35 deletions crates/codeguard-cli/src/eslint_lint_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,42 +43,37 @@ pub fn run(args: &[String]) -> ExitCode {
}
#[cfg(feature = "wasm-precheck")]
if crate::typescript_syntax_precheck::eligible(&args) {
if let Some(candidate) =
crate::eslint_native_first_candidate::observed_candidate(&args.source)
{
let mut native_args = args.clone();
native_args.entry = Some(candidate.entry);
native_args.config = Some(candidate.config);
native_args.cwd = Some(candidate.root);
native_args.version = Some(candidate.version);
let report = if let Some(node) = crate::eslint_native_first_candidate::node_on_path() {
native_args.node = Some(node);
observe_with_preparation(&native_args, deadline)
} else {
observe_with_preparation_reason(
&args,
deadline,
Some("eslint_node_runtime_unresolved"),
)
};
if args.json {
println!("{report}");
} else {
print_feedback(&report);
let report = match crate::eslint_native_first_candidate::observed_candidate(&args.source) {
Ok(Some(candidate)) => {
let mut native_args = args.clone();
native_args.entry = Some(candidate.entry);
native_args.config = Some(candidate.config);
native_args.cwd = Some(candidate.root);
native_args.version = Some(candidate.version);
if let Some(node) = crate::eslint_native_first_candidate::node_on_path() {
native_args.node = Some(node);
observe_with_preparation(&native_args, deadline)
} else {
observe_with_preparation_reason(
&args,
deadline,
Some("eslint_node_runtime_unresolved"),
)
}
}
return ExitCode::from(if report["reason"] == "request_cancelled" {
130
} else {
3
});
}
let report = crate::typescript_syntax_precheck::observe(&args, deadline);
Ok(None) => crate::typescript_syntax_precheck::observe(&args, deadline),
Err(reason) => observe_with_preparation_reason(&args, deadline, Some(reason)),
};
if args.json {
println!("{report}");
} else {
print_feedback(&report);
}
return ExitCode::from(3);
return ExitCode::from(if report["reason"] == "request_cancelled" {
130
} else {
3
});
}
let report = observe_with_preparation(&args, deadline);
if args.json {
Expand All @@ -102,12 +97,11 @@ fn observe_with_preparation_reason(
) -> Value {
let mut report = observe(args, deadline);
if report["reason"] == "eslint_execution_context_missing"
&& missing_context_reason == Some("eslint_node_runtime_unresolved")
&& missing_context_reason.is_some_and(crate::eslint_preparation::valid_reason)
{
report["reason"] = json!("eslint_node_runtime_unresolved");
report["next_action"] = json!(
"项目本地 ESLint 候选与原配置已发现;当前未解析到 Node。核对项目运行时并提供受控 Node 路径后原生复检,不重复安装 ESLint 或修改无依据源码"
);
let reason = missing_context_reason.unwrap_or("eslint_execution_context_missing");
report["reason"] = json!(reason);
report["next_action"] = feedback(reason)["next_action"].clone();
}
// 早期上下文失败也进入环境待办;取消、外部目标或不支持范围不能制造源码任务。
if let (Some(workspace), Some(reason)) = (
Expand Down Expand Up @@ -278,6 +272,14 @@ fn feedback(reason: &str) -> Value {
json!({"schema_version":"0.2.0","report_type":"eslint_local_feedback","status":"incomplete","local_coherent":false,"coverage_proven":false,"delivery_decision":"not_evaluated","reason":reason,"findings":[],"suppressed_count":0,"workbench_status":"not_connected","workbench":null,"next_action":match reason {
"eslint_execution_context_missing"=>"提供显式 Node、原 ESLint JS 入口、具体版本、原工作目录与项目原 flat config 后复检;不安装或替换规则",
"eslint_node_runtime_unresolved"=>"项目本地 ESLint 候选已发现;提供受控 Node 路径后原生复检,不重复安装 ESLint 或修改无依据源码",
"eslint_project_manifest_untrusted"=>"核对项目 package.json 的文件类型、内容及 ESLint 声明后再选择原生检查;不要先改源码或重复安装",
"eslint_local_dependency_path_untrusted"=>"核对 node_modules 与本地 ESLint 包目录的链接、类型及来源;不能据此判定工具缺失",
"eslint_local_package_identity_invalid"=>"核对本地 ESLint package.json 的名称、版本和入口声明,恢复可信包身份后原生复检",
"eslint_local_entry_untrusted"=>"核对本地 ESLint JS 入口的真实类型与来源,不跟随链接执行;恢复后原生复检",
"eslint_local_version_unresolved"=>"核对项目 ESLint 声明与本地包版本,按原项目方案恢复一致后原生复检",
"eslint_adapter_version_unsupported"=>"项目 ESLint 版本不受当前适配器支持;保留原项目版本与检查义务,补齐适配器或提出具体决策,不修改无依据源码",
"eslint_config_selection_unresolved"=>"项目本地 ESLint 已发现,但 flat config 缺失或存在多份;确认原项目选中的配置后原生复检",
"eslint_config_untrusted"=>"项目本地 ESLint 已发现,但配置是链接、特殊文件或不可读;恢复原配置后原生复检",
"eslint_scope_requires_explicit_file"=>"当前入口需显式单文件;项目目录与完整源集调度尚未接通,不将目录标为已检查",
"request_cancelled"|"request_deadline_exceeded"=>"重新安排原工具检查预算;中断不要求修改源码",
_=>"核对原配置、parser/插件与本轮规则;按原生发现修复后用同一入口和配置复检,抑制及未知覆盖需核查,任务不能自行关闭",
Expand Down
72 changes: 49 additions & 23 deletions crates/codeguard-cli/src/eslint_native_first_candidate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use codeguard_adapters::{EslintConfigState, inspect_eslint_local_candidate};
use codeguard_core::ObservedPathKind;
use codeguard_runtime::read_bounded_regular_file;
use std::{
io::ErrorKind,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
};
Expand All @@ -21,38 +22,52 @@ pub(crate) struct LocalEslintCandidate {
}

/// 识别源码最近项目根的本地 ESLint 包、入口和单一 flat config。
/// 参数为普通源码路径;返回候选仅表示可以尝试原生版本探测,不证明规则有效。
pub(crate) fn observed_candidate(source: &Path) -> Option<LocalEslintCandidate> {
let source = source.canonicalize().ok()?;
let parent = source.parent()?;
/// 参数为普通源码路径;返回 Ok(None) 仅表示本地入口未观察到,错误表示必须保留的环境阻塞。
pub(crate) fn observed_candidate(
source: &Path,
) -> Result<Option<LocalEslintCandidate>, &'static str> {
let source = source
.canonicalize()
.map_err(|_| "eslint_source_unavailable")?;
let parent = source.parent().ok_or("eslint_source_unavailable")?;
for root in parent.ancestors() {
let manifest = root.join("package.json");
if !regular_file(&manifest) {
continue;
match std::fs::symlink_metadata(&manifest) {
Err(error) if error.kind() == ErrorKind::NotFound => continue,
Ok(metadata) if metadata.is_file() => {}
_ => return Err("eslint_project_manifest_untrusted"),
}
let project_manifest = read_bounded_regular_file(&manifest, 256 * 1024).ok()?;
let project_manifest = read_bounded_regular_file(&manifest, 256 * 1024)
.map_err(|_| "eslint_project_manifest_untrusted")?;
let local_root = root.join("node_modules");
let package_root = local_root.join("eslint");
if !regular_directory(&local_root) || !regular_directory(&package_root) {
return None;
if !directory_or_absent(&local_root)? || !directory_or_absent(&package_root)? {
return Ok(None);
}
let local_manifest = package_root.join("package.json");
let bin_root = package_root.join("bin");
let entry = package_root.join("bin/eslint.js");
if !regular_file(&local_manifest) || !regular_file(&entry) {
return None;
if !regular_file(&local_manifest) {
return Err("eslint_local_package_identity_invalid");
}
let local_manifest = read_bounded_regular_file(&local_manifest, 256 * 1024)
.map_err(|_| "eslint_local_package_identity_invalid")?;
if !std::fs::symlink_metadata(&bin_root).is_ok_and(|metadata| metadata.is_dir())
|| !regular_file(&entry)
{
return Err("eslint_local_entry_untrusted");
}
let local_manifest = read_bounded_regular_file(&local_manifest, 256 * 1024).ok()?;
// 旧配置、TS loader 与多配置选择尚未受控,不能构造自动原生命令。
let configs: Vec<_> = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"]
.iter()
.map(|name| root.join(name))
.filter(|path| std::fs::symlink_metadata(path).is_ok())
.collect();
let [config] = configs.as_slice() else {
return None;
return Err("eslint_config_selection_unresolved");
Comment on lines 66 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify unsupported ESLint versions before requiring flat config

When a project uses an unsupported ESLint version without one of these flat-config files—as is typical for an ESLint 8 project using .eslintrc—this early return reports eslint_config_selection_unresolved before inspect_eslint_local_candidate can return local_version_not_supported_by_adapter. The resulting stable task incorrectly tells the user to select a flat config rather than emitting the promised eslint_adapter_version_unsupported guidance to extend the adapter; version/identity classification should therefore occur before config selection.

Useful? React with 👍 / 👎.

};
if !regular_file(config) || read_bounded_regular_file(config, 1024 * 1024).is_err() {
return None;
return Err("eslint_config_untrusted");
}
let candidate = inspect_eslint_local_candidate(
Some(&project_manifest),
Expand All @@ -61,16 +76,31 @@ pub(crate) fn observed_candidate(source: &Path) -> Option<LocalEslintCandidate>
EslintConfigState::Observed,
);
if candidate.state != "local_candidate_requires_native_probe" {
return None;
return Err(match candidate.state {
"project_manifest_invalid" => "eslint_project_manifest_untrusted",
"local_package_identity_invalid" => "eslint_local_package_identity_invalid",
"local_version_not_supported_by_adapter" => "eslint_adapter_version_unsupported",
_ => "eslint_local_version_unresolved",
});
}
return Some(LocalEslintCandidate {
return Ok(Some(LocalEslintCandidate {
root: root.to_path_buf(),
entry,
config: config.clone(),
version: candidate.observed_version?,
});
version: candidate
.observed_version
.ok_or("eslint_local_version_unresolved")?,
}));
}
Ok(None)
}

fn directory_or_absent(path: &Path) -> Result<bool, &'static str> {
match std::fs::symlink_metadata(path) {
Ok(metadata) if metadata.is_dir() => Ok(true),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
_ => Err("eslint_local_dependency_path_untrusted"),
}
None
}

/// 从当前进程 PATH 选择普通可执行 Node 文件;缺失只表示当前调用未解析到运行时。
Expand Down Expand Up @@ -100,7 +130,3 @@ pub(crate) fn node_on_path() -> Option<PathBuf> {
fn regular_file(path: &Path) -> bool {
std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.is_file())
}

fn regular_directory(path: &Path) -> bool {
std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.is_dir())
}
35 changes: 31 additions & 4 deletions crates/codeguard-cli/src/eslint_preparation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ pub(crate) fn valid_reason(reason: &str) -> bool {
reason,
"eslint_execution_context_missing"
| "eslint_node_runtime_unresolved"
| "eslint_project_manifest_untrusted"
| "eslint_local_dependency_path_untrusted"
| "eslint_local_package_identity_invalid"
| "eslint_local_entry_untrusted"
| "eslint_local_version_unresolved"
| "eslint_adapter_version_unsupported"
| "eslint_config_selection_unresolved"
| "eslint_config_untrusted"
| "eslint_input_unavailable"
| "eslint_private_workspace_unavailable"
| "request_deadline_exceeded"
Expand Down Expand Up @@ -293,10 +301,29 @@ pub(crate) fn guidance(root: &Path, fact: &Value) -> Value {
{
return fallback;
}
let step = if report["diagnostic_reason"] == "eslint_node_runtime_unresolved" {
"项目本地 ESLint 候选与原配置已发现;核对并提供受控 Node 路径后执行原生复检,不重复安装 ESLint 或修改无依据源码"
} else {
"核对最新ESLint前置诊断、原配置、parser/插件和运行条件;解析或抑制先复现根因,恢复原检查后复扫,不修改无关源码"
let step = match report["diagnostic_reason"].as_str() {
Some("eslint_node_runtime_unresolved") => {
"项目本地 ESLint 候选与原配置已发现;核对并提供受控 Node 路径后执行原生复检,不重复安装 ESLint 或修改无依据源码"
}
Some("eslint_config_selection_unresolved" | "eslint_config_untrusted") => {
"项目本地 ESLint 已发现;核对原项目实际选中的 flat config、文件类型与来源,恢复配置后原生复检,不修改无依据源码"
}
Some("eslint_local_entry_untrusted" | "eslint_local_dependency_path_untrusted") => {
"项目本地 ESLint 路径不可信;核对链接、文件类型和来源后原生复检,不重复安装或修改无依据源码"
}
Some("eslint_adapter_version_unsupported") => {
"项目 ESLint 版本不受当前适配器支持;保留原项目版本与检查义务,补齐适配器或提出具体决策,不修改无依据源码"
}
Some(
"eslint_local_package_identity_invalid"
| "eslint_local_version_unresolved"
| "eslint_project_manifest_untrusted",
) => {
"项目本地 ESLint 身份或版本声明无法核对;检查 package.json 与原依赖方案,恢复一致后原生复检,不修改无依据源码"
}
_ => {
"核对最新ESLint前置诊断、原配置、parser/插件和运行条件;解析或抑制先复现根因,恢复原检查后复扫,不修改无关源码"
}
};
json!({"disposition":"actionable","step":step,"diagnostic_reason":report["diagnostic_reason"],"run_id":run,"report_sha256":sha})
}
Loading
Loading