Skip to main content

perspt_coding/diag/
ty.rs

1//! `ty` diagnostics (PSP-10 system 26): the primary Python sensor.
2//!
3//! The LSP observation is primary; when it is empty the runtime falls back
4//! to `ty check --output-format concise`, whose versioned text format is
5//! `path:line:col: error[rule] message` (sampled against ty 0.0.x). The
6//! PSP claims no JSON format for `ty`.
7
8use perspt_sdk::ResidualClass;
9
10use super::types::StructuredDiagnostic;
11
12/// Text-parser identity; enters the sensor fingerprint.
13pub const PARSER_ID: &str = "ty-check-text-v1";
14
15fn classify_rule(rule: &str) -> ResidualClass {
16    match rule {
17        "unresolved-import" | "unresolved-module" => ResidualClass::ImportGraph,
18        "unresolved-reference" | "unresolved-attribute" | "possibly-unresolved-reference" => {
19            ResidualClass::SymbolMismatch
20        }
21        "invalid-assignment"
22        | "invalid-argument-type"
23        | "invalid-return-type"
24        | "unsupported-operator"
25        | "invalid-type-form" => ResidualClass::Type,
26        _ => ResidualClass::Type,
27    }
28}
29
30/// Parse `ty check --output-format concise` lines.
31pub fn parse_check_text(raw: &str) -> Vec<StructuredDiagnostic> {
32    raw.lines().filter_map(parse_line).collect()
33}
34
35fn parse_line(line: &str) -> Option<StructuredDiagnostic> {
36    // `bad.py:1:8: error[unresolved-import] Cannot resolve ...`
37    let (location, rest) = line.split_once(": error[")?;
38    let (rule, message) = rest.split_once("] ")?;
39    let mut parts = location.rsplitn(3, ':');
40    let column: u32 = parts.next()?.parse().ok()?;
41    let line_no: u32 = parts.next()?.parse().ok()?;
42    let path = parts.next()?.to_string();
43    Some(StructuredDiagnostic {
44        class: classify_rule(rule),
45        code: Some(rule.to_string()),
46        message: message.to_string(),
47        path: Some(path),
48        line: Some(line_no),
49        column: Some(column),
50        primary: true,
51        suggestion: None,
52    })
53}
54
55/// Preserve LSP `textDocument/publishDiagnostics` items as structured
56/// diagnostics. `items` is the LSP `diagnostics` array; `path` the
57/// document they belong to.
58pub fn from_lsp_diagnostics(path: &str, items: &[serde_json::Value]) -> Vec<StructuredDiagnostic> {
59    items
60        .iter()
61        .filter(|item| item.get("severity").and_then(|s| s.as_u64()).unwrap_or(1) == 1)
62        .map(|item| {
63            let rule = item
64                .get("code")
65                .and_then(|code| code.as_str())
66                .map(str::to_string);
67            StructuredDiagnostic {
68                class: rule
69                    .as_deref()
70                    .map(classify_rule)
71                    .unwrap_or(ResidualClass::Type),
72                code: rule,
73                message: item
74                    .get("message")
75                    .and_then(|m| m.as_str())
76                    .unwrap_or_default()
77                    .to_string(),
78                path: Some(path.to_string()),
79                line: item
80                    .pointer("/range/start/line")
81                    .and_then(|l| l.as_u64())
82                    .and_then(|l| u32::try_from(l + 1).ok()),
83                column: item
84                    .pointer("/range/start/character")
85                    .and_then(|c| c.as_u64())
86                    .and_then(|c| u32::try_from(c + 1).ok()),
87                primary: true,
88                suggestion: None,
89            }
90        })
91        .collect()
92}