lc3_toolchain/lint/
lint.rs

1use crate::ast::processed_ast::{LineColumn, Program, ProgramItem};
2use crate::ast::raw_ast::{Comment, Directive, Instruction, Label, Span};
3use getset::Getters;
4use once_cell::sync::Lazy;
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7
8#[derive(PartialOrd, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
9pub enum CaseStyle {
10    LowerCamelCase,
11    UpperCamelCase,
12    SnakeCase,
13    ScreamingSnakeCase,
14}
15
16#[derive(Copy, Clone, Serialize, Deserialize)]
17#[serde(rename_all = "kebab-case")]
18pub struct LintStyle {
19    pub colon_after_label: bool,
20    pub label_style: CaseStyle,
21    pub instruction_style: CaseStyle,
22    pub directive_style: CaseStyle,
23}
24
25#[derive(Debug, Getters)]
26pub struct Error {
27    #[get = "pub"]
28    case_style_error: Result<(), (CaseStyle, Option<CaseStyle>)>,
29    #[get = "pub"]
30    colon_style_error: Result<(), ()>,
31    #[get = "pub"]
32    span: Span,
33}
34
35pub struct Linter {
36    program: Program,
37    visitor: Box<dyn ProgramItemVisitor>,
38}
39
40impl Linter {
41    pub fn new(style: LintStyle, program: Program) -> Self {
42        Self {
43            program,
44            visitor: Box::new(StyleCheckerVisitor { style }),
45        }
46    }
47
48    pub fn check(&mut self) -> Result<(), Vec<Error>> {
49        self.accept()
50    }
51
52    fn accept(&mut self) -> Result<(), Vec<Error>> {
53        let mut errors = vec![];
54        for line in self.program.items() {
55            let mut res = match line {
56                ProgramItem::Comment(comment, lc) => self.visitor.visit_comment(comment, lc),
57                ProgramItem::Instruction(labels, instruction, comment, lc) => self
58                    .visitor
59                    .visit_instruction(labels, instruction, comment, lc),
60                ProgramItem::Directive(labels, directive, comment, lc) => {
61                    self.visitor.visit_directive(labels, directive, comment, lc)
62                }
63                ProgramItem::EOL(labels) => self.visitor.visit_eol(labels),
64            };
65            errors.append(&mut res);
66        }
67        if errors.is_empty() {
68            Ok(())
69        } else {
70            Err(errors)
71        }
72    }
73}
74
75trait ProgramItemVisitor {
76    fn visit_comment(&mut self, comment: &Comment, location: &LineColumn) -> Vec<Error>;
77    fn visit_instruction(
78        &mut self,
79        labels: &[Label],
80        instruction: &Instruction,
81        comment: &Option<Comment>,
82        location: &LineColumn,
83    ) -> Vec<Error>;
84    fn visit_directive(
85        &mut self,
86        labels: &[Label],
87        directive: &Directive,
88        comment: &Option<Comment>,
89        location: &LineColumn,
90    ) -> Vec<Error>;
91    fn visit_eol(&mut self, labels: &[Label]) -> Vec<Error>;
92}
93
94struct StyleCheckerVisitor {
95    style: LintStyle,
96}
97
98static LOWER_CAMEL: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z]+(?:[A-Z][a-z0-9]*)*$").unwrap());
99static UPPER_CAMEL: Lazy<Regex> =
100    Lazy::new(|| Regex::new(r"^[A-Z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$").unwrap());
101static SNAKE_CASE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z]+(?:_[a-z0-9]+)*$").unwrap());
102static SCREAMING_SNAKE: Lazy<Regex> =
103    Lazy::new(|| Regex::new(r"^[A-Z0-9]+(?:_[A-Z0-9]+)*$").unwrap());
104
105impl StyleCheckerVisitor {
106    fn check_label(&self, label: &str) -> (Result<(), Option<CaseStyle>>, Result<(), ()>) {
107        let case_error = Self::check_keyword_style(
108            label.strip_suffix(":").unwrap_or_else(|| label),
109            &self.style.label_style,
110        );
111        let colon_error = match label.ends_with(":") {
112            true => {
113                if self.style.colon_after_label {
114                    Ok(())
115                } else {
116                    Err(())
117                }
118            }
119            false => {
120                if self.style.colon_after_label {
121                    Err(())
122                } else {
123                    Ok(())
124                }
125            }
126        };
127        (case_error, colon_error)
128    }
129
130    fn check_instruction(&self, instruction: &str) -> Result<(), Option<CaseStyle>> {
131        Self::check_keyword_style(instruction, &self.style.instruction_style)
132    }
133
134    fn check_directive_style(&self, directive: &str) -> Result<(), Option<CaseStyle>> {
135        Self::check_keyword_style(directive, &self.style.directive_style)
136    }
137
138    fn check_keyword_style(keyword: &str, case_style: &CaseStyle) -> Result<(), Option<CaseStyle>> {
139        let found_style = match Self::get_identifier_style(keyword) {
140            None => {
141                return Err(None);
142            }
143            Some(st) => st,
144        };
145        if &found_style == case_style {
146            Ok(())
147        }
148        // snakecase is a subset of lower camelcase without _
149        else if found_style == CaseStyle::SnakeCase
150            && (!keyword.contains("_"))
151            && *case_style == CaseStyle::LowerCamelCase
152        {
153            Ok(())
154        } else {
155            Err(Some(found_style))
156        }
157    }
158    fn get_identifier_style(identifier: &str) -> Option<CaseStyle> {
159        if SNAKE_CASE.is_match(identifier) {
160            Some(CaseStyle::SnakeCase)
161        } else if SCREAMING_SNAKE.is_match(identifier) {
162            Some(CaseStyle::ScreamingSnakeCase)
163        } else if LOWER_CAMEL.is_match(identifier) {
164            Some(CaseStyle::LowerCamelCase)
165        } else if UPPER_CAMEL.is_match(identifier) {
166            Some(CaseStyle::UpperCamelCase)
167        } else {
168            None
169        }
170    }
171
172    fn label_error_to_error(
173        label: &Label,
174        expected_case: &CaseStyle,
175        case_error: Result<(), Option<CaseStyle>>,
176        colon_error: Result<(), ()>,
177    ) -> Error {
178        Error {
179            case_style_error: case_error.map_err(|e| (expected_case.clone(), e)),
180            colon_style_error: colon_error,
181            span: label.span().clone(),
182        }
183    }
184
185    fn check_label_style(&self, labels: &[Label]) -> Vec<Error> {
186        let mut errors = vec![];
187        for label in labels {
188            let (case_error, colon_error) = self.check_label(label.content());
189            if case_error.is_err() || colon_error.is_err() {
190                errors.push(Self::label_error_to_error(
191                    label,
192                    &self.style.label_style,
193                    case_error,
194                    colon_error,
195                ))
196            }
197        }
198        errors
199    }
200}
201
202impl ProgramItemVisitor for StyleCheckerVisitor {
203    fn visit_comment(&mut self, _: &Comment, _: &LineColumn) -> Vec<Error> {
204        vec![]
205    }
206
207    fn visit_instruction(
208        &mut self,
209        labels: &[Label],
210        instruction: &Instruction,
211        comment: &Option<Comment>,
212        lc: &LineColumn,
213    ) -> Vec<Error> {
214        let mut errors = vec![];
215        errors.append(&mut self.check_label_style(labels));
216        match self.check_instruction(instruction.content()) {
217            Ok(_) => {}
218            Err(err) => errors.push(Error {
219                case_style_error: Err((self.style.instruction_style.clone(), err)),
220                colon_style_error: Ok(()),
221                span: instruction.span().clone(),
222            }),
223        }
224        match comment {
225            None => {}
226            Some(comment) => {
227                errors.append(&mut self.visit_comment(comment, lc));
228            }
229        }
230        errors
231    }
232
233    fn visit_directive(
234        &mut self,
235        labels: &[Label],
236        directive: &Directive,
237        comment: &Option<Comment>,
238        lc: &LineColumn,
239    ) -> Vec<Error> {
240        let mut errors = vec![];
241        errors.append(&mut self.check_label_style(labels));
242        assert!(directive.content().starts_with("."));
243        match self.check_directive_style(directive.content().strip_prefix(".").unwrap()) {
244            Ok(_) => {}
245            Err(error) => {
246                errors.push(Error {
247                    case_style_error: Err((self.style.directive_style.clone(), error)),
248                    colon_style_error: Ok(()),
249                    span: directive.span().clone(),
250                });
251            }
252        }
253        match comment {
254            None => {}
255            Some(comment) => {
256                errors.append(&mut self.visit_comment(comment, lc));
257            }
258        }
259        errors
260    }
261
262    fn visit_eol(&mut self, labels: &[Label]) -> Vec<Error> {
263        let mut errors = vec![];
264        errors.append(&mut self.check_label_style(labels));
265        errors
266    }
267}
268
269#[cfg(test)]
270mod test {
271    use super::*;
272    use crate::ast::get_ast;
273
274    fn test_true(style: LintStyle, content: &str) {
275        let ast = get_ast(content);
276        assert!(ast.is_ok());
277        match ast {
278            Ok(program) => {
279                let c = Linter::new(style, program).check();
280                if c.is_err() {
281                    println!("{:?}", c.as_ref().err().unwrap());
282                }
283                assert!(c.is_ok());
284            }
285            Err(_) => {}
286        }
287    }
288
289    fn test_false(style: LintStyle, content: &str) {
290        let ast = get_ast(content);
291        assert!(ast.is_ok());
292        match ast {
293            Ok(program) => {
294                let c = Linter::new(style, program).check();
295                assert!(c.is_err());
296            }
297            Err(_) => {}
298        }
299    }
300
301    #[test]
302    fn test_empty() {
303        let style = LintStyle {
304            colon_after_label: true,
305            label_style: CaseStyle::LowerCamelCase,
306            instruction_style: CaseStyle::LowerCamelCase,
307            directive_style: CaseStyle::LowerCamelCase,
308        };
309        let content = r#""#;
310        test_true(style, content);
311    }
312
313    #[test]
314    fn test_directive_uppercase() {
315        let style = LintStyle {
316            colon_after_label: true,
317            label_style: CaseStyle::LowerCamelCase,
318            instruction_style: CaseStyle::UpperCamelCase,
319            directive_style: CaseStyle::ScreamingSnakeCase,
320        };
321        let content_true = r#".ORIG x3000 .END"#;
322        let content_false1 = r#".OrIG x3000 .EnD"#;
323        let content_false2 = r#".orig x3000 .end"#;
324        let content_false3 = r#".Orig x3000 .End"#;
325        test_true(style.clone(), content_true);
326        test_false(style.clone(), content_false1);
327        test_false(style.clone(), content_false2);
328        test_false(style.clone(), content_false3);
329    }
330
331    #[test]
332    fn test_directive_lowercamelcase() {
333        let style = LintStyle {
334            colon_after_label: true,
335            label_style: CaseStyle::LowerCamelCase,
336            instruction_style: CaseStyle::UpperCamelCase,
337            directive_style: CaseStyle::LowerCamelCase,
338        };
339        let content_true1 = r#".oRIG x3000 .eND"#;
340        let content_true2 = r#".orig x3000 .eND"#;
341        test_true(style, content_true1);
342        test_true(style, content_true2);
343    }
344
345    #[test]
346    fn test_directive_snakecase() {
347        let style = LintStyle {
348            colon_after_label: true,
349            label_style: CaseStyle::LowerCamelCase,
350            instruction_style: CaseStyle::UpperCamelCase,
351            directive_style: CaseStyle::SnakeCase,
352        };
353        let content_true1 = r#".orig x3000 .end"#;
354        test_true(style, content_true1);
355    }
356
357    #[test]
358    fn test_instruction_uppercamelcase() {
359        let style = LintStyle {
360            colon_after_label: true,
361            label_style: CaseStyle::LowerCamelCase,
362            instruction_style: CaseStyle::UpperCamelCase,
363            directive_style: CaseStyle::LowerCamelCase,
364        };
365
366        let content_true1 = r#"And R1, R2, R3"#;
367        let content_true2 = r#"And R4, R5, R6"#;
368        test_true(style, content_true1);
369        test_true(style, content_true2);
370
371        // Negation assertions: should fail for incorrect styles
372        let content_false1 = r#"add R1, R2, R3"#; // LowerCamelCase
373        let content_false2 = r#"add R1, R2, R3"#; // SnakeCase
374        test_false(style, content_false1);
375        test_false(style, content_false2);
376    }
377
378    #[test]
379    fn test_instruction_screaming_camelcase() {
380        let style = LintStyle {
381            colon_after_label: true,
382            label_style: CaseStyle::LowerCamelCase,
383            instruction_style: CaseStyle::ScreamingSnakeCase,
384            directive_style: CaseStyle::LowerCamelCase,
385        };
386
387        let content_true1 = r#"AND R1, R2, R3"#;
388        let content_true2 = r#"AND R4, R5, R6"#;
389        test_true(style, content_true1);
390        test_true(style, content_true2);
391
392        // Negation assertions: should fail for incorrect styles
393        let content_false1 = r#"aND R1, R2, R3"#; // LowerCamelCase
394        let content_false2 = r#"AnD R1, R2, R3"#; // SnakeCase
395        test_false(style, content_false1);
396        test_false(style, content_false2);
397    }
398
399    #[test]
400    fn test_instruction_lowercamelcase() {
401        let style = LintStyle {
402            colon_after_label: true,
403            label_style: CaseStyle::LowerCamelCase,
404            instruction_style: CaseStyle::LowerCamelCase,
405            directive_style: CaseStyle::LowerCamelCase,
406        };
407
408        let content_true1 = r#"add R1, R2, R3"#;
409        let content_true2 = r#"and R4, R5, R6"#;
410        test_true(style, content_true1);
411        test_true(style, content_true2);
412
413        // Negation assertions: should fail for incorrect styles
414        let content_false1 = r#"ADD R1, R2, R3"#; // UpperCamelCase
415        let content_false2 = r#"add_r1, r2, r3"#; // SnakeCase
416        test_false(style, content_false1);
417        test_false(style, content_false2);
418    }
419
420    #[test]
421    fn test_instruction_snakecase() {
422        let style = LintStyle {
423            colon_after_label: true,
424            label_style: CaseStyle::LowerCamelCase,
425            instruction_style: CaseStyle::SnakeCase,
426            directive_style: CaseStyle::LowerCamelCase,
427        };
428
429        let content_true1 = r#"add R1, R2, R3"#;
430        let content_true2 = r#"and R4, R5, R6"#;
431        let content_true3 = r#"add R1, R2, R3"#; // LowerCamelCase
432        test_true(style, content_true1);
433        test_true(style, content_true2);
434        test_true(style, content_true3);
435
436        // Negation assertions: should fail for incorrect styles
437        let content_false1 = r#"ADD R1, R2, R3"#; // UpperCamelCase
438        test_false(style, content_false1);
439    }
440
441    #[test]
442    fn test_label_lowercamelcase() {
443        let style = LintStyle {
444            colon_after_label: true,
445            label_style: CaseStyle::LowerCamelCase,
446            instruction_style: CaseStyle::ScreamingSnakeCase,
447            directive_style: CaseStyle::ScreamingSnakeCase,
448        };
449
450        let content_true1 = r#"loop: ADD R1, R2, R3"#;
451        let content_true2 = r#"startLabel: AND R4, R5, R6"#;
452        test_true(style, content_true1);
453        test_true(style, content_true2);
454
455        // Negation assertions: should fail for incorrect styles
456        let content_false1 = r#"Loop: ADD R1, R2, R3"#; // UpperCamelCase
457        let content_false2 = r#"start_label: AND R4, R5, R6"#; // SnakeCase
458        let content_false3 = r#"START_LABEL: ADD R1, R2, R3"#; // ScreamingSnakeCase
459        test_false(style, content_false1);
460        test_false(style, content_false2);
461        test_false(style, content_false3);
462    }
463
464    #[test]
465    fn test_label_uppercamelcase() {
466        let style = LintStyle {
467            colon_after_label: true,
468            label_style: CaseStyle::UpperCamelCase,
469            instruction_style: CaseStyle::ScreamingSnakeCase,
470            directive_style: CaseStyle::ScreamingSnakeCase,
471        };
472
473        let content_true1 = r#"LoopStart: ADD R1, R2, R3"#;
474        let content_true2 = r#"MainFunction: AND R4, R5, R6"#;
475        test_true(style, content_true1);
476        test_true(style, content_true2);
477
478        // Negation assertions: should fail for incorrect styles
479        let content_false1 = r#"loopStart: ADD R1, R2, R3"#; // LowerCamelCase
480        let content_false2 = r#"loop_start: AND R4, R5, R6"#; // SnakeCase
481        let content_false3 = r#"LOOP_START: ADD R1, R2, R3"#; // ScreamingSnakeCase
482        test_false(style, content_false1);
483        test_false(style, content_false2);
484        test_false(style, content_false3);
485    }
486
487    #[test]
488    fn test_label_scream_snake_case() {
489        let style = LintStyle {
490            colon_after_label: true,
491            label_style: CaseStyle::ScreamingSnakeCase,
492            instruction_style: CaseStyle::ScreamingSnakeCase,
493            directive_style: CaseStyle::ScreamingSnakeCase,
494        };
495
496        let content_true1 = r#"LOOP2: ADD R1, R2, R3"#;
497        let content_true2 = r#"MAIN_FUNCTION0: AND R4, R5, R6"#;
498        test_true(style, content_true1);
499        test_true(style, content_true2);
500
501        // Negation assertions: should fail for incorrect styles
502        let content_false1 = r#"loopStart: ADD R1, R2, R3"#; // LowerCamelCase
503        let content_false2 = r#"loop_start: AND R4, R5, R6"#; // SnakeCase
504        let content_false3 = r#"LoopStart: ADD R1, R2, R3"#; // ScreamingSnakeCase
505        test_false(style, content_false1);
506        test_false(style, content_false2);
507        test_false(style, content_false3);
508    }
509
510    #[test]
511    fn test_label_snakecase() {
512        let style = LintStyle {
513            colon_after_label: true,
514            label_style: CaseStyle::SnakeCase,
515            instruction_style: CaseStyle::ScreamingSnakeCase,
516            directive_style: CaseStyle::ScreamingSnakeCase,
517        };
518
519        let content_true1 = r#"loop_start: ADD R1, R2, R3"#;
520        let content_true2 = r#"main_function: AND R4, R5, R6"#;
521        test_true(style, content_true1);
522        test_true(style, content_true2);
523
524        // Negation assertions: should fail for incorrect styles
525        let content_false1 = r#"LoopStart: ADD R1, R2, R3"#; // UpperCamelCase
526        let content_false2 = r#"loopStart: AND R4, R5, R6"#; // LowerCamelCase
527        let content_false3 = r#"LOOP_START: ADD R4, R5, R6"#;
528
529        test_false(style, content_false1);
530        test_false(style, content_false2);
531        test_false(style, content_false3);
532    }
533
534    #[test]
535    fn test_label_colon() {
536        let style = LintStyle {
537            colon_after_label: false,
538            label_style: CaseStyle::SnakeCase,
539            instruction_style: CaseStyle::ScreamingSnakeCase,
540            directive_style: CaseStyle::ScreamingSnakeCase,
541        };
542
543        let content_true1 = r#"loop_start ADD R1, R2, R3"#;
544        let content_true2 = r#"main_function AND R4, R5, R6"#;
545        test_true(style, content_true1);
546        test_true(style, content_true2);
547
548        // Negation assertions: should fail for incorrect styles
549        let content_false1 = r#"LoopStart: ADD R1, R2, R3"#; // UpperCamelCase
550        let content_false2 = r#"loopStart: AND R4, R5, R6"#; // LowerCamelCase
551        let content_false3 = r#"LOOP_START: ADD R4, R5, R6"#;
552
553        test_false(style, content_false1);
554        test_false(style, content_false2);
555        test_false(style, content_false3);
556    }
557
558    #[test]
559    fn test_comments() {
560        let style = LintStyle {
561            colon_after_label: false,
562            label_style: CaseStyle::SnakeCase,
563            instruction_style: CaseStyle::ScreamingSnakeCase,
564            directive_style: CaseStyle::ScreamingSnakeCase,
565        };
566
567        let content_true1 = r#"loop_start ADD R1, R2, R3 ; sdasd"#;
568        let content_true2 = r#"main_function AND R4, R5, R6 ; asdsa"#;
569        test_true(style, content_true1);
570        test_true(style, content_true2);
571
572        // Negation assertions: should fail for incorrect styles
573        let content_false1 = r#"
574        ;dasdas
575        LoopStart: ADD R1, R2, R3"#; // UpperCamelCase
576        let content_false2 = r#"loopStart: AND R4, R5, R6"#; // LowerCamelCase
577        let content_false3 = r#"LOOP_START: ADD R4, R5, R6"#;
578
579        test_false(style, content_false1);
580        test_false(style, content_false2);
581        test_false(style, content_false3);
582    }
583}