presolve_compiler/
summarize.rs1use std::path::Path;
2
3use crate::model::{
4 ClassSummary, DecoratorSummary, Diagnostic, RenderMethodSummary, Severity, SourceSummary, Span,
5};
6
7pub 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}