1#![forbid(unsafe_code)]
2pub mod command_norm;
12pub mod command_schema;
14mod flow;
15mod resolve;
16pub(crate) mod scope;
17
18#[cfg(test)]
19mod tests;
20
21pub use command_norm::NormalizedCommandInvoke;
22use command_schema::{CommandRegistry, EmptyCommandRegistry};
23
24use flow::FlowLintAnalyzer;
25use resolve::Analyzer;
26use scope::ScopeCollector;
27use std::sync::Arc;
28
29use mel_ast::{SourceFile, Stmt};
30use mel_syntax::{SourceView, TextRange};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum DiagnosticSeverity {
35 Error,
36 Warning,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum DiagnosticFilter {
42 All,
43 ErrorsOnly,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Diagnostic {
52 pub severity: DiagnosticSeverity,
53 pub message: Arc<str>,
54 pub range: TextRange,
55 pub labels: Vec<DiagnosticLabel>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct DiagnosticLabel {
61 pub range: TextRange,
62 pub message: Arc<str>,
63 pub is_primary: bool,
64}
65
66impl Diagnostic {
67 fn error(message: impl Into<Arc<str>>, range: TextRange) -> Self {
68 let message = message.into();
69 Self {
70 severity: DiagnosticSeverity::Error,
71 message: message.clone(),
72 range,
73 labels: vec![DiagnosticLabel {
74 range,
75 message,
76 is_primary: true,
77 }],
78 }
79 }
80
81 fn warning(message: impl Into<Arc<str>>, range: TextRange) -> Self {
82 let message = message.into();
83 Self {
84 severity: DiagnosticSeverity::Warning,
85 message: message.clone(),
86 range,
87 labels: vec![DiagnosticLabel {
88 range,
89 message,
90 is_primary: true,
91 }],
92 }
93 }
94
95 fn with_secondary_label(mut self, message: impl Into<Arc<str>>, range: TextRange) -> Self {
96 self.labels.push(DiagnosticLabel {
97 range,
98 message: message.into(),
99 is_primary: false,
100 });
101 self
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub struct ScopeId(pub(crate) usize);
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub struct ProcSymbolId(pub(crate) usize);
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub struct VariableSymbolId(pub(crate) usize);
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct ProcSymbol {
119 pub id: ProcSymbolId,
120 pub name_range: TextRange,
121 pub is_global: bool,
122 pub return_type: Option<mel_ast::ProcReturnType>,
123 pub owner_scope: ScopeId,
124 pub decl_order: usize,
125 pub range: TextRange,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum VariableKind {
130 Parameter,
131 Local,
132 Global,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct VariableSymbol {
137 pub id: VariableSymbolId,
138 pub name_range: TextRange,
139 pub kind: VariableKind,
140 pub ty: mel_ast::TypeName,
141 pub is_array: bool,
142 pub owner_scope: ScopeId,
143 pub decl_order: usize,
144 pub range: TextRange,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct InvokeResolution {
149 pub range: TextRange,
150 pub scope: ScopeId,
151 pub resolution: ResolvedCallee,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum ResolvedCallee {
156 Unresolved,
157 Proc(ProcSymbolId),
158 BuiltinCommand(Arc<str>),
159 PluginCommand(Arc<str>),
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum IdentTarget {
164 Unresolved,
165 Variable(VariableSymbolId),
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct IdentResolution {
170 pub range: TextRange,
171 pub scope: ScopeId,
172 pub name_range: TextRange,
173 pub resolution: IdentTarget,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Default)]
177pub struct Analysis {
182 pub diagnostics: Vec<Diagnostic>,
183 pub proc_symbols: Vec<ProcSymbol>,
184 pub variable_symbols: Vec<VariableSymbol>,
185 pub invoke_resolutions: Vec<InvokeResolution>,
186 pub ident_resolutions: Vec<IdentResolution>,
187 pub normalized_invokes: Vec<NormalizedCommandInvoke>,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191enum AnalysisMode {
192 Full,
193 DiagnosticsOnly,
194}
195
196#[must_use]
197pub fn analyze(syntax: &SourceFile, source: SourceView<'_>) -> Analysis {
209 analyze_with_registry(syntax, source, &EmptyCommandRegistry)
210}
211
212#[must_use]
213pub fn analyze_with_registry<R>(
225 syntax: &SourceFile,
226 source: SourceView<'_>,
227 registry: &R,
228) -> Analysis
229where
230 R: CommandRegistry + ?Sized,
231{
232 analyze_with_registry_mode(
233 syntax,
234 source,
235 registry,
236 AnalysisMode::Full,
237 DiagnosticFilter::All,
238 )
239}
240
241#[must_use]
242pub fn analyze_diagnostics_with_registry<R>(
244 syntax: &SourceFile,
245 source: SourceView<'_>,
246 registry: &R,
247) -> Vec<Diagnostic>
248where
249 R: CommandRegistry + ?Sized,
250{
251 analyze_diagnostics_with_registry_filtered(syntax, source, registry, DiagnosticFilter::All)
252}
253
254#[must_use]
255pub fn analyze_diagnostics_with_registry_filtered<R>(
257 syntax: &SourceFile,
258 source: SourceView<'_>,
259 registry: &R,
260 filter: DiagnosticFilter,
261) -> Vec<Diagnostic>
262where
263 R: CommandRegistry + ?Sized,
264{
265 analyze_with_registry_mode(
266 syntax,
267 source,
268 registry,
269 AnalysisMode::DiagnosticsOnly,
270 filter,
271 )
272 .diagnostics
273}
274
275fn analyze_with_registry_mode<R>(
276 syntax: &SourceFile,
277 source: SourceView<'_>,
278 registry: &R,
279 mode: AnalysisMode,
280 filter: DiagnosticFilter,
281) -> Analysis
282where
283 R: CommandRegistry + ?Sized,
284{
285 let collected = ScopeCollector::collect(syntax);
286 let mut analyzer = Analyzer::new(
287 &collected,
288 source,
289 registry,
290 matches!(mode, AnalysisMode::Full),
291 filter,
292 );
293
294 for item in &syntax.items {
295 analyzer.walk_item(item, collected.root_scope);
296 }
297
298 let mut diagnostics = analyzer.diagnostics;
299 if matches!(filter, DiagnosticFilter::All) {
300 let mut flow_lint = FlowLintAnalyzer::new(&collected, source);
301 flow_lint.walk_source(syntax);
302 diagnostics.extend(flow_lint.diagnostics);
303 }
304
305 Analysis {
306 diagnostics,
307 proc_symbols: if matches!(mode, AnalysisMode::Full) {
308 collected.proc_symbols.clone()
309 } else {
310 Vec::new()
311 },
312 variable_symbols: if matches!(mode, AnalysisMode::Full) {
313 collected.variable_symbols.clone()
314 } else {
315 Vec::new()
316 },
317 invoke_resolutions: if matches!(mode, AnalysisMode::Full) {
318 analyzer.invoke_resolutions
319 } else {
320 Vec::new()
321 },
322 ident_resolutions: if matches!(mode, AnalysisMode::Full) {
323 analyzer.ident_resolutions
324 } else {
325 Vec::new()
326 },
327 normalized_invokes: if matches!(mode, AnalysisMode::Full) {
328 analyzer.normalized_invokes
329 } else {
330 Vec::new()
331 },
332 }
333}
334
335pub(crate) fn stmt_range(stmt: &Stmt) -> TextRange {
336 match stmt {
337 Stmt::Empty { range }
338 | Stmt::Proc { range, .. }
339 | Stmt::Block { range, .. }
340 | Stmt::Expr { range, .. }
341 | Stmt::VarDecl { range, .. }
342 | Stmt::If { range, .. }
343 | Stmt::While { range, .. }
344 | Stmt::DoWhile { range, .. }
345 | Stmt::Switch { range, .. }
346 | Stmt::For { range, .. }
347 | Stmt::ForIn { range, .. }
348 | Stmt::Return { range, .. }
349 | Stmt::Break { range }
350 | Stmt::Continue { range } => *range,
351 }
352}