Skip to main content

presolve_compiler/
summarize.rs

1use std::path::Path;
2
3use crate::model::{
4    ClassSummary, DecoratorSummary, Diagnostic, RenderMethodSummary, Severity, SourceSummary, Span,
5};
6
7/// Produce a first-pass source summary.
8///
9/// This function is intentionally lightweight. It is not a parser. Its job is to
10/// establish the compiler habit of preserving source positions and explaining
11/// what the tool thinks it found.
12pub fn summarize_source(path: impl AsRef<Path>, source: &str) -> SourceSummary {
13    let path = path.as_ref().to_path_buf();
14    let mut diagnostics = Vec::new();
15
16    if source.trim().is_empty() {
17        diagnostics.push(Diagnostic {
18            severity: Severity::Error,
19            code: "PS0001".to_string(),
20            message: "source file is empty".to_string(),
21            span: None,
22        });
23    }
24
25    let component_decorators = find_string_decorators(source, "component");
26    let route_decorators = find_string_decorators(source, "route");
27    let class_declarations = find_class_declarations(source);
28    let render_methods = find_render_methods(source);
29    let has_tsx_like_syntax =
30        source.contains('<') && source.contains('>') && source.contains("render");
31
32    if component_decorators.is_empty() {
33        diagnostics.push(Diagnostic {
34            severity: Severity::Warning,
35            code: "PS0100".to_string(),
36            message: "no @component(...) decorator found".to_string(),
37            span: None,
38        });
39    }
40
41    if class_declarations.is_empty() {
42        diagnostics.push(Diagnostic {
43            severity: Severity::Warning,
44            code: "PS0101".to_string(),
45            message: "no class declaration found".to_string(),
46            span: None,
47        });
48    }
49
50    if render_methods.is_empty() {
51        diagnostics.push(Diagnostic {
52            severity: Severity::Warning,
53            code: "PS0102".to_string(),
54            message: "no render() method found".to_string(),
55            span: None,
56        });
57    }
58
59    SourceSummary {
60        path,
61        byte_len: source.len(),
62        line_count: source.lines().count(),
63        char_count: source.chars().count(),
64        has_tsx_like_syntax,
65        component_decorators,
66        route_decorators,
67        class_declarations,
68        render_methods,
69        diagnostics,
70    }
71}
72
73fn find_string_decorators(source: &str, name: &str) -> Vec<DecoratorSummary> {
74    let marker = format!("@{name}(");
75    let mut items = Vec::new();
76    let mut search_start = 0;
77
78    while let Some(relative_index) = source[search_start..].find(&marker) {
79        let start = search_start + relative_index;
80        let argument_start = start + marker.len();
81        let argument = extract_first_string_argument(&source[argument_start..]);
82        let end = source[argument_start..]
83            .find(')')
84            .map_or(argument_start, |relative_end| {
85                argument_start + relative_end + 1
86            });
87
88        items.push(DecoratorSummary {
89            name: name.to_string(),
90            argument,
91            span: span_at(source, start, end),
92        });
93
94        search_start = end.max(start + marker.len());
95    }
96
97    items
98}
99
100fn extract_first_string_argument(fragment: &str) -> Option<String> {
101    let mut chars = fragment.char_indices();
102    let (_, quote) = chars.find(|(_, ch)| *ch == '"' || *ch == '\'')?;
103    let content_start = fragment.find(quote)? + quote.len_utf8();
104    let rest = &fragment[content_start..];
105    let content_end = rest.find(quote)?;
106    Some(rest[..content_end].to_string())
107}
108
109fn find_class_declarations(source: &str) -> Vec<ClassSummary> {
110    let mut classes = Vec::new();
111    let mut search_start = 0;
112
113    while let Some(relative_index) = source[search_start..].find("class ") {
114        let start = search_start + relative_index;
115        let name_start = start + "class ".len();
116        let name_end = source[name_start..]
117            .char_indices()
118            .find(|(_, ch)| !is_identifier_char(*ch))
119            .map_or(source.len(), |(offset, _)| name_start + offset);
120
121        if name_end > name_start {
122            classes.push(ClassSummary {
123                name: source[name_start..name_end].to_string(),
124                span: span_at(source, start, name_end),
125            });
126        }
127
128        search_start = name_end.max(start + "class ".len());
129    }
130
131    classes
132}
133
134fn find_render_methods(source: &str) -> Vec<RenderMethodSummary> {
135    let mut methods = Vec::new();
136    let mut search_start = 0;
137
138    while let Some(relative_index) = source[search_start..].find("render()") {
139        let start = search_start + relative_index;
140        let end = start + "render()".len();
141        methods.push(RenderMethodSummary {
142            span: span_at(source, start, end),
143        });
144        search_start = end;
145    }
146
147    methods
148}
149
150fn is_identifier_char(ch: char) -> bool {
151    ch == '_' || ch == '$' || ch.is_ascii_alphanumeric()
152}
153
154fn span_at(source: &str, start: usize, end: usize) -> Span {
155    let prefix = &source[..start.min(source.len())];
156    let line = prefix.bytes().filter(|b| *b == b'\n').count() + 1;
157    let column = prefix
158        .rsplit_once('\n')
159        .map_or(prefix.len() + 1, |(_, tail)| tail.len() + 1);
160
161    Span {
162        start,
163        end,
164        line,
165        column,
166    }
167}