|
| 1 | +use plc_ast::ast::AstId; |
| 2 | +use plc_ast::provider::IdProvider; |
| 3 | +use plc_ast::visitor::{AstVisitor, Walker}; |
| 4 | +use plc_source::SourceCode; |
| 5 | +use std::collections::HashSet; |
| 6 | +use std::ops::Range; |
| 7 | +use tabled::derive::display; |
| 8 | +use tabled::settings::object::Columns; |
| 9 | +use tabled::settings::{Alignment, Style}; |
| 10 | +use tabled::Tabled; |
| 11 | + |
| 12 | +use crate::resolver::{AnnotationMap, AnnotationMapImpl, StatementAnnotation}; |
| 13 | +use crate::{resolver::TypeAnnotator, test_utils::tests::index_with_ids}; |
| 14 | + |
| 15 | +/// Extracts marked ranges from the source code, returning the cleaned source and marker ranges. |
| 16 | +/// Markers are denoted by `{...}` in the source. |
| 17 | +fn extract_markers(src: &str) -> Result<(String, Vec<Range<usize>>), String> { |
| 18 | + let mut clean_src = String::new(); |
| 19 | + let mut marker_ranges = Vec::new(); |
| 20 | + let mut open_marker_positions = Vec::new(); |
| 21 | + |
| 22 | + for (i, c) in src.chars().enumerate() { |
| 23 | + match c { |
| 24 | + '{' => { |
| 25 | + open_marker_positions.push(clean_src.len()); |
| 26 | + } |
| 27 | + '}' => { |
| 28 | + if let Some(start) = open_marker_positions.pop() { |
| 29 | + let end = clean_src.len(); |
| 30 | + marker_ranges.push(start..end); |
| 31 | + } else { |
| 32 | + return Err(format!("Unmatched closing marker '}}' at offset {i}.")); |
| 33 | + } |
| 34 | + } |
| 35 | + _ => { |
| 36 | + clean_src.push(c); |
| 37 | + } |
| 38 | + } |
| 39 | + } |
| 40 | + |
| 41 | + if !open_marker_positions.is_empty() { |
| 42 | + return Err(format!("Unmatched opening marker '{{' at offset {open_marker_positions:?}.")); |
| 43 | + } |
| 44 | + |
| 45 | + Ok((clean_src, marker_ranges)) |
| 46 | +} |
| 47 | + |
| 48 | +/// Collects AST nodes whose spans match the provided marker ranges. |
| 49 | +struct ExpressionCollector { |
| 50 | + // the location of the marked expression and its id |
| 51 | + expressions: Vec<(Range<usize>, AstId)>, |
| 52 | + // the location of un-processed markers |
| 53 | + // once an expression was visisted for a marker, it is removed from this set |
| 54 | + ordered_markers: HashSet<Range<usize>>, |
| 55 | +} |
| 56 | + |
| 57 | +impl ExpressionCollector { |
| 58 | + /// Creates a new ExpressionCollector with the given marker ranges. |
| 59 | + /// |
| 60 | + /// see also `fn extract_markers(...)` |
| 61 | + pub fn new(markers: &[Range<usize>]) -> Self { |
| 62 | + Self { expressions: Vec::new(), ordered_markers: markers.iter().cloned().collect() } |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +impl AstVisitor for ExpressionCollector { |
| 67 | + fn visit(&mut self, node: &plc_ast::ast::AstNode) { |
| 68 | + let current_range = node.get_location().to_range().unwrap_or_else(|| 0..0); |
| 69 | + |
| 70 | + // lets see if we have a marker for this range |
| 71 | + if self.ordered_markers.remove(¤t_range) { |
| 72 | + // record the id and the range |
| 73 | + self.expressions.push((current_range, node.get_id())) |
| 74 | + } |
| 75 | + |
| 76 | + node.walk(self); |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +/// Represents the result of resolving an expression, including its type and hint. |
| 81 | +#[derive(Tabled)] |
| 82 | +struct ResolveResult { |
| 83 | + #[tabled(rename = "EXPR")] |
| 84 | + expression: String, |
| 85 | + #[tabled(rename = "TYPE", display("display::option", "-"))] |
| 86 | + ty: Option<String>, |
| 87 | + #[tabled(rename = "HINT", display("display::option", "-"))] |
| 88 | + hint: Option<String>, |
| 89 | +} |
| 90 | + |
| 91 | +/// Helper function to display the type of a StatementAnnotation. |
| 92 | +fn display_annotation( |
| 93 | + annotation: &StatementAnnotation, |
| 94 | + annotation_map: &AnnotationMapImpl, |
| 95 | +) -> Option<String> { |
| 96 | + annotation_map.get_type_name_for_annotation(annotation).map(|it| it.to_string()) |
| 97 | +} |
| 98 | + |
| 99 | +/// Runs type resolution on the provided source code and returns a formatted table of results. |
| 100 | +/// Expressions to be resolved should be surrounded by `{}` in the source. |
| 101 | +pub fn test_resolve<T: Into<SourceCode>>(src: T) -> Result<String, String> { |
| 102 | + let id_provider = IdProvider::default(); |
| 103 | + |
| 104 | + let (clean_src, marker_ranges) = extract_markers(src.into().source.as_str())?; |
| 105 | + |
| 106 | + let (unit, index) = index_with_ids(clean_src.as_str(), id_provider.clone()); |
| 107 | + let (annotation_map, ..) = TypeAnnotator::visit_unit(&index, &unit, id_provider); |
| 108 | + |
| 109 | + let mut collector = ExpressionCollector::new(&marker_ranges); |
| 110 | + collector.visit_compilation_unit(&unit); |
| 111 | + |
| 112 | + let results = collector |
| 113 | + .expressions |
| 114 | + .iter() |
| 115 | + .map(|(marker, ast_id)| { |
| 116 | + let ty = annotation_map |
| 117 | + .get_with_id(*ast_id) |
| 118 | + .and_then(|annotation| display_annotation(annotation, &annotation_map)); |
| 119 | + let hint = annotation_map |
| 120 | + .get_hint_with_id(*ast_id) |
| 121 | + .and_then(|annotation| display_annotation(annotation, &annotation_map)); |
| 122 | + |
| 123 | + ResolveResult { expression: clean_src[marker.start..marker.end].to_string(), ty, hint } |
| 124 | + }) |
| 125 | + .collect::<Vec<_>>(); |
| 126 | + |
| 127 | + let table = tabled::Table::new(results) |
| 128 | + .with(Style::psql()) |
| 129 | + .modify(Columns::first(), Alignment::right()) |
| 130 | + .to_string(); |
| 131 | + |
| 132 | + if collector.ordered_markers.is_empty() { |
| 133 | + Ok(table) |
| 134 | + } else { |
| 135 | + Err(format!( |
| 136 | + "Cannot find Expression for markers at {:?}.", |
| 137 | + collector.ordered_markers.iter().collect::<Vec<_>>() |
| 138 | + )) |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +#[cfg(test)] |
| 143 | +mod tests { |
| 144 | + use insta::assert_snapshot; |
| 145 | + |
| 146 | + use super::*; |
| 147 | + |
| 148 | + #[test] |
| 149 | + fn print_marked_expressions() { |
| 150 | + let src = " |
| 151 | + FUNCTION foo |
| 152 | + VAR |
| 153 | + x : INT; |
| 154 | + y : DINT; |
| 155 | + END_VAR |
| 156 | + {{x} + {y}} // surrounded expressions to be evaluated |
| 157 | + END_FUNCTION |
| 158 | + "; |
| 159 | + |
| 160 | + let resolves = test_resolve(src).unwrap(); |
| 161 | + assert_snapshot!(resolves, @r" |
| 162 | + EXPR | TYPE | HINT |
| 163 | + -------+------+------ |
| 164 | + x + y | DINT | - |
| 165 | + x | INT | DINT |
| 166 | + y | DINT | - |
| 167 | + "); |
| 168 | + } |
| 169 | + |
| 170 | + #[test] |
| 171 | + fn test_unclosed_marker() { |
| 172 | + let src_unmatched_open = " |
| 173 | + FUNCTION foo |
| 174 | + VAR |
| 175 | + x : INT; |
| 176 | + y : DINT; |
| 177 | + END_VAR |
| 178 | + {x + {y // Missing closing markers |
| 179 | + END_FUNCTION |
| 180 | + "; |
| 181 | + |
| 182 | + assert_eq!( |
| 183 | + test_resolve(src_unmatched_open), |
| 184 | + Err("Unmatched opening marker '{' at offset [120, 124].".to_string()) |
| 185 | + ); |
| 186 | + } |
| 187 | + |
| 188 | + #[test] |
| 189 | + fn test_invalid_closing_marker() { |
| 190 | + let src_unmatched_close = " |
| 191 | + FUNCTION foo |
| 192 | + VAR |
| 193 | + x : INT; |
| 194 | + y : DINT; |
| 195 | + END_VAR |
| 196 | + {x + y}} // Extra closing marker |
| 197 | + END_FUNCTION |
| 198 | + "; |
| 199 | + |
| 200 | + assert_eq!( |
| 201 | + test_resolve(src_unmatched_close), |
| 202 | + Err("Unmatched closing marker '}' at offset 127.".to_string()) |
| 203 | + ); |
| 204 | + } |
| 205 | + |
| 206 | + #[test] |
| 207 | + fn test_no_markers() { |
| 208 | + let src_no_markers = " |
| 209 | + FUNCTION foo |
| 210 | + VAR |
| 211 | + x : INT; |
| 212 | + y : DINT; |
| 213 | + END_VAR |
| 214 | + x + y // No markers present |
| 215 | + END_FUNCTION |
| 216 | + "; |
| 217 | + |
| 218 | + let resolves = test_resolve(src_no_markers).unwrap(); |
| 219 | + assert_snapshot!(resolves, @r" |
| 220 | + EXPR | TYPE | HINT |
| 221 | + ------+------+------ |
| 222 | + "); |
| 223 | + } |
| 224 | + |
| 225 | + #[test] |
| 226 | + fn test_invalid_marker_position() { |
| 227 | + let src_invalid_marker = " |
| 228 | + FUNCTION foo |
| 229 | + VAR |
| 230 | + x : INT; |
| 231 | + y : DINT; |
| 232 | + END_VAR |
| 233 | + {x +} y // marker does not match any expression |
| 234 | + END_FUNCTION |
| 235 | + "; |
| 236 | + |
| 237 | + assert_eq!( |
| 238 | + test_resolve(src_invalid_marker), |
| 239 | + Err("Cannot find Expression for markers at [120..123].".to_string()) |
| 240 | + ); |
| 241 | + } |
| 242 | +} |
0 commit comments