Skip to main content

perspt_coding/diag/
pyright.rs

1//! Pyright diagnostics — the declared fallback Python sensor
2//! (PSP-10 system 26).
3//!
4//! Pyright is used only when `ty` is unavailable, under its own distinct
5//! fingerprint; measurements from `ty` and Pyright are never pooled as one
6//! calibration stratum.
7
8use perspt_sdk::ResidualClass;
9
10use super::types::StructuredDiagnostic;
11
12/// Parser identity; enters the sensor fingerprint.
13pub const PARSER_ID: &str = "pyright-json-v1";
14
15fn classify_rule(rule: &str) -> ResidualClass {
16    match rule {
17        "reportMissingImports" | "reportMissingModuleSource" => ResidualClass::ImportGraph,
18        "reportUndefinedVariable" | "reportAttributeAccessIssue" => ResidualClass::SymbolMismatch,
19        "reportAssignmentType"
20        | "reportArgumentType"
21        | "reportReturnType"
22        | "reportGeneralTypeIssues"
23        | "reportOperatorIssue" => ResidualClass::Type,
24        _ => ResidualClass::Type,
25    }
26}
27
28/// Parse `pyright --outputjson` output.
29pub fn parse(raw: &str) -> Vec<StructuredDiagnostic> {
30    let Ok(value) = serde_json::from_str::<serde_json::Value>(raw) else {
31        return Vec::new();
32    };
33    let Some(items) = value.get("generalDiagnostics").and_then(|d| d.as_array()) else {
34        return Vec::new();
35    };
36    items
37        .iter()
38        .filter(|item| item.get("severity").and_then(|s| s.as_str()) == Some("error"))
39        .map(|item| {
40            let rule = item
41                .get("rule")
42                .and_then(|rule| rule.as_str())
43                .map(str::to_string);
44            StructuredDiagnostic {
45                class: rule
46                    .as_deref()
47                    .map(classify_rule)
48                    .unwrap_or(ResidualClass::Type),
49                code: rule,
50                message: item
51                    .get("message")
52                    .and_then(|m| m.as_str())
53                    .unwrap_or_default()
54                    .to_string(),
55                path: item
56                    .get("file")
57                    .and_then(|f| f.as_str())
58                    .map(str::to_string),
59                line: item
60                    .pointer("/range/start/line")
61                    .and_then(|l| l.as_u64())
62                    .and_then(|l| u32::try_from(l + 1).ok()),
63                column: item
64                    .pointer("/range/start/character")
65                    .and_then(|c| c.as_u64())
66                    .and_then(|c| u32::try_from(c + 1).ok()),
67                primary: true,
68                suggestion: None,
69            }
70        })
71        .collect()
72}