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 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 let content_false1 = r#"add R1, R2, R3"#; let content_false2 = r#"add R1, R2, R3"#; 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 let content_false1 = r#"aND R1, R2, R3"#; let content_false2 = r#"AnD R1, R2, R3"#; 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 let content_false1 = r#"ADD R1, R2, R3"#; let content_false2 = r#"add_r1, r2, r3"#; 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"#; test_true(style, content_true1);
433 test_true(style, content_true2);
434 test_true(style, content_true3);
435
436 let content_false1 = r#"ADD R1, R2, R3"#; 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 let content_false1 = r#"Loop: ADD R1, R2, R3"#; let content_false2 = r#"start_label: AND R4, R5, R6"#; let content_false3 = r#"START_LABEL: ADD R1, R2, R3"#; 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 let content_false1 = r#"loopStart: ADD R1, R2, R3"#; let content_false2 = r#"loop_start: AND R4, R5, R6"#; let content_false3 = r#"LOOP_START: ADD R1, R2, R3"#; 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 let content_false1 = r#"loopStart: ADD R1, R2, R3"#; let content_false2 = r#"loop_start: AND R4, R5, R6"#; let content_false3 = r#"LoopStart: ADD R1, R2, R3"#; 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 let content_false1 = r#"LoopStart: ADD R1, R2, R3"#; let content_false2 = r#"loopStart: AND R4, R5, R6"#; 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 let content_false1 = r#"LoopStart: ADD R1, R2, R3"#; let content_false2 = r#"loopStart: AND R4, R5, R6"#; 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 let content_false1 = r#"
574 ;dasdas
575 LoopStart: ADD R1, R2, R3"#; let content_false2 = r#"loopStart: AND R4, R5, R6"#; 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}