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 crates/wright-analyzer/src/canonical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod traversal;
pub use analysis::{Finding, LintFix, RepeatedValue, RepeatedValueSpan, analyze};
pub use cfg::BLOCK_KINDS;
pub(crate) use cfg::matching_end;
pub use service::{SemanticService, span_json};
pub use service::{FindingMeta, SemanticService, span_json};
pub use symbols::*;
pub(crate) use traversal::{dead_actions, unwrapped};
pub use traversal::{live_actions, subroutine_callee};
44 changes: 41 additions & 3 deletions crates/wright-analyzer/src/canonical/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use super::analysis::Finding;
use super::cfg::cfg_response;
use super::facts::persistent_objects;
use super::symbols::{Reference, RuleId, SemanticIndex, Symbol, SymbolId};
use crate::analysis::Boundedness;
use crate::analysis::{Boundedness, Severity};
use crate::registry::{LintConfig, SkippedRule};
use crate::service::{ErrorInfo, Origin, Request, Response};

Expand Down Expand Up @@ -48,10 +48,20 @@ pub struct SemanticService<'a> {
registry: Arc<crate::registry::LintRegistry>,
symbols_json: OnceLock<Vec<JsonValue>>,
references_json: OnceLock<Vec<JsonValue>>,
findings_json: OnceLock<JsonValue>,
findings_json: OnceLock<Vec<JsonValue>>,
lint_rules_json: OnceLock<JsonValue>,
}

/// One finding's typed identity, parallel to its serialized row: the
/// severity and code consumers must not re-parse out of JSON (#1001).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FindingMeta {
pub severity: Severity,
pub code: String,
/// The finding's span file index — the key `span.file` serializes.
pub span_file: Option<usize>,
}

impl<'a> SemanticService<'a> {
pub fn new(program: &'a Program) -> Self {
Self::with_origin_and_config(
Expand Down Expand Up @@ -145,6 +155,34 @@ impl<'a> SemanticService<'a> {
&self.findings
}

/// The typed finding identities in [`Request::GetFindings`] order
/// (#1001), parallel to [`findings_json`](Self::findings_json): index
/// `i` describes serialized finding `i`.
pub fn findings_meta(&self) -> Vec<FindingMeta> {
self.findings
.iter()
.map(|finding| FindingMeta {
severity: finding.severity,
code: finding.code.clone(),
span_file: finding.span.map(|span| span.file.index()),
})
.collect()
}

/// The serialized findings in [`Request::GetFindings`] order, one JSON
/// row per finding — the payload rows [`findings_meta`](Self::findings_meta)
/// annotates (#1001).
pub fn findings_json(&self) -> &[JsonValue] {
self.findings_json
.get_or_init(|| {
self.findings
.iter()
.map(|finding| finding_json(&self.index, finding))
.collect()
})
.as_slice()
}

pub fn symbol_count(&self) -> usize {
self.index.symbols().count()
}
Expand Down Expand Up @@ -228,7 +266,7 @@ impl<'a> SemanticService<'a> {
Request::FindReferences { symbol } => { let id = SymbolId::from_index(*symbol as usize); if self.index.symbol(id).is_none() { self.error("invalid-id", format!("unknown symbol {symbol}")) } else { Response::Ok { result: self.references_json()[id.index()].clone() } } }
Request::GetUsage { symbol } => { let id = SymbolId::from_index(*symbol as usize); self.index.symbol(id).map_or_else(|| self.error("invalid-id", format!("unknown symbol {symbol}")), |data| { let usage = self.index.usage(id); Response::Ok { result: json!({"id": id.index(), "kind": data.kind.as_str(), "symbol": data.name, "reads": usage.reads, "writes": usage.writes, "calls": usage.calls, "rules": usage.rules}) } }) }
Request::GetCfg { rule } => cfg_response(self.program.as_ref(), *rule as usize),
Request::GetFindings => Response::Ok { result: self.findings_json.get_or_init(|| json!(self.findings.iter().map(|finding| finding_json(&self.index, finding)).collect::<Vec<_>>())).clone() },
Request::GetFindings => Response::Ok { result: JsonValue::Array(self.findings_json().to_vec()) },
Request::GetPersistentObjects => Response::Ok { result: json!(persistent_objects(self.program.as_ref())) },
Request::LintRules => Response::Ok {
result: self.lint_rules_json.get_or_init(|| lint_rules(&self.index, &self.registry, &self.config, &self.skipped)).clone(),
Expand Down
73 changes: 73 additions & 0 deletions crates/wright-cli/src/essential.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! The CLI-side essential-projection plumbing: wrap a workflow result's
//! serialized payload in an [`EssentialResult`] that also declares which
//! command produced it and carries the findings' typed identities, so no
//! downstream renderer re-parses the projection (#1001).

use wright_driver::Severity;
use wright_driver::result::{AnalyzeResult, Envelope, InspectResult, LintResult};
use wright_driver::result::{EssentialCommand, EssentialResult};
use wright_driver::select::SelectionOutcome;

/// Project `lint` to its essential form: the payload rows keep their
/// serialized shape while `findings_meta` carries the typed severities
/// renderers consume (#1001); `max_severity` covers the full set so the
/// verdict reflects every finding, not the projected remainder.
pub(crate) fn lint(envelope: Envelope<LintResult>, all: bool) -> Envelope<EssentialResult> {
envelope.map_result(|result| {
let serialized = serde_json::to_value(&result).expect("result serializes");
let max_severity = result.findings_meta.iter().map(|meta| meta.severity).max();
let (payload, findings_meta) =
wright_driver::essential::lint(&serialized, &result.findings_meta, all);
essential(
result.selection.clone(),
payload,
findings_meta,
max_severity,
EssentialCommand::Lint,
)
})
}

/// Project `inspect` to its essential form — inspect emits no findings,
/// so the meta channel is empty (#1001).
pub(crate) fn inspect(envelope: Envelope<InspectResult>, all: bool) -> Envelope<EssentialResult> {
envelope.map_result(|result| {
let serialized = serde_json::to_value(&result).expect("result serializes");
let payload = wright_driver::essential::inspect(&serialized, all);
essential(None, payload, Vec::new(), None, EssentialCommand::Inspect)
})
}

/// Project `analyze` to its essential form: `risks_meta` supplies the
/// typed risk severities and the full-set max (#1001).
pub(crate) fn analyze(envelope: Envelope<AnalyzeResult>, all: bool) -> Envelope<EssentialResult> {
envelope.map_result(|result| {
let serialized = serde_json::to_value(&result).expect("result serializes");
let max_severity = result.risks_meta.iter().map(|meta| meta.severity).max();
let (payload, findings_meta) =
wright_driver::essential::analyze(&serialized, &result.risks_meta, all);
essential(
result.selection.clone(),
payload,
findings_meta,
max_severity,
EssentialCommand::Analyze,
)
})
}

fn essential(
selection: Option<SelectionOutcome>,
payload: serde_json::Value,
findings_meta: Vec<wright_driver::FindingMeta>,
max_severity: Option<Severity>,
command: EssentialCommand,
) -> EssentialResult {
EssentialResult {
result: payload,
selection,
command,
findings_meta,
max_severity,
}
}
10 changes: 2 additions & 8 deletions crates/wright-cli/src/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::process::ExitCode;
use wright_driver::result::exit;

use crate::present;
use crate::{cli, essential_envelope, fail_on, run_configured, selection_from_args};
use crate::{cli, fail_on, run_configured, selection_from_args};
use crate::{config_from_common, present::terminal};

pub(crate) fn run(args: cli::LintArgs) -> ExitCode {
Expand Down Expand Up @@ -36,12 +36,6 @@ pub(crate) fn run(args: cli::LintArgs) -> ExitCode {
presentation.with_all(args.all),
fail_on(&args.common),
None,
|session| {
essential_envelope(
session.lint(),
|value| wright_driver::essential::lint(value, args.all),
|result| result.selection.clone(),
)
},
|session| crate::essential::lint(session.lint(), args.all),
)
}
28 changes: 3 additions & 25 deletions crates/wright-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod agent;
mod agenttools;
mod cli;
mod completion;
mod essential;
mod lint;
mod mcp;
mod plain;
Expand Down Expand Up @@ -195,13 +196,7 @@ fn run_workflow(command: Command) -> ExitCode {
presentation.with_all(args.all),
fail_on(&args.common),
None,
|session| {
essential_envelope(
session.analyze(),
|value| wright_driver::essential::analyze(value, args.all),
|result| result.selection.clone(),
)
},
|session| crate::essential::analyze(session.analyze(), args.all),
)
}
Command::Lint(args) => lint::run(args),
Expand All @@ -212,13 +207,7 @@ fn run_workflow(command: Command) -> ExitCode {
present::Presentation::from_common(&args.common).with_all(args.all),
fail_on(&args.common),
None,
|session| {
essential_envelope(
session.inspect(),
|value| wright_driver::essential::inspect(value, args.all),
|_| None,
)
},
|session| crate::essential::inspect(session.inspect(), args.all),
),
Some(cli::InspectQuery::Rules(query)) => run_configured(
"rules",
Expand Down Expand Up @@ -367,17 +356,6 @@ fn run_workflow(command: Command) -> ExitCode {
}
}

pub(crate) fn essential_envelope<T: serde::Serialize>(
envelope: wright_driver::Envelope<T>,
form: impl FnOnce(&serde_json::Value) -> serde_json::Value,
outcome: impl FnOnce(&T) -> Option<wright_driver::SelectionOutcome>,
) -> wright_driver::Envelope<wright_driver::EssentialResult> {
envelope.map_result(|result| wright_driver::EssentialResult {
result: form(&serde_json::to_value(&result).expect("result serializes")),
selection: outcome(&result),
})
}

pub(crate) fn run_configured<
T: serde::Serialize
+ present::ResultPresentation
Expand Down
Loading
Loading