nginx_lint_parser/
parser.rs1use crate::syntax_kind::SyntaxKind;
7use rowan::GreenNode;
8use rowan::GreenNodeBuilder;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct SyntaxError {
13 pub message: String,
14 pub offset: usize,
15}
16
17pub fn parse(tokens: Vec<(SyntaxKind, &str)>) -> (GreenNode, Vec<SyntaxError>) {
21 let mut parser = Parser::new(tokens);
22 parser.parse_root();
23 (parser.builder.finish(), parser.errors)
24}
25
26struct Parser<'a> {
29 tokens: Vec<(SyntaxKind, &'a str)>,
30 pos: usize,
31 builder: GreenNodeBuilder<'static>,
32 errors: Vec<SyntaxError>,
33 offset: usize,
35}
36
37impl<'a> Parser<'a> {
38 fn new(tokens: Vec<(SyntaxKind, &'a str)>) -> Self {
39 Self {
40 tokens,
41 pos: 0,
42 builder: GreenNodeBuilder::new(),
43 errors: Vec::new(),
44 offset: 0,
45 }
46 }
47
48 fn current(&self) -> Option<SyntaxKind> {
52 self.tokens.get(self.pos).map(|(k, _)| *k)
53 }
54
55 fn current_text(&self) -> &'a str {
57 self.tokens.get(self.pos).map(|(_, t)| *t).unwrap_or("")
58 }
59
60 fn at(&self, kind: SyntaxKind) -> bool {
62 self.current() == Some(kind)
63 }
64
65 fn at_end(&self) -> bool {
67 self.pos >= self.tokens.len()
68 }
69
70 fn bump(&mut self) {
72 if let Some(&(kind, text)) = self.tokens.get(self.pos) {
73 self.builder.token(kind.into(), text);
74 self.offset += text.len();
75 self.pos += 1;
76 }
77 }
78
79 fn eat_trivia(&mut self) {
82 while let Some(kind) = self.current() {
83 if kind == SyntaxKind::WHITESPACE
84 || kind == SyntaxKind::NEWLINE
85 || kind == SyntaxKind::COMMENT
86 {
87 self.bump();
88 } else {
89 break;
90 }
91 }
92 }
93
94 fn peek_non_trivia(&self) -> Option<SyntaxKind> {
96 let mut i = self.pos;
97 while i < self.tokens.len() {
98 let kind = self.tokens[i].0;
99 if kind != SyntaxKind::WHITESPACE
100 && kind != SyntaxKind::NEWLINE
101 && kind != SyntaxKind::COMMENT
102 {
103 return Some(kind);
104 }
105 i += 1;
106 }
107 None
108 }
109
110 fn error(&mut self, message: impl Into<String>) {
111 self.errors.push(SyntaxError {
112 message: message.into(),
113 offset: self.offset,
114 });
115 }
116
117 fn parse_root(&mut self) {
121 self.builder.start_node(SyntaxKind::ROOT.into());
122 self.parse_items(false);
123 self.builder.finish_node();
124 }
125
126 fn parse_items(&mut self, in_block: bool) {
128 loop {
129 match self.current() {
130 None => break,
131 Some(SyntaxKind::R_BRACE) if in_block => break,
132 Some(SyntaxKind::R_BRACE) => {
133 self.error("unexpected '}'");
135 self.builder.start_node(SyntaxKind::ERROR.into());
136 self.bump();
137 self.builder.finish_node();
138 }
139 Some(SyntaxKind::WHITESPACE) => {
140 if self.is_blank_line_start() {
143 self.parse_blank_line();
144 } else {
145 self.bump(); }
147 }
148 Some(SyntaxKind::NEWLINE) => {
149 self.bump();
152 }
153 Some(SyntaxKind::COMMENT) => {
154 self.bump();
155 }
156 Some(kind) if is_directive_start(kind) => {
157 self.parse_directive();
158 }
159 Some(SyntaxKind::ERROR) => {
160 self.error("unexpected token");
161 self.bump();
162 }
163 Some(_) => {
164 self.error(format!("unexpected token: {:?}", self.current().unwrap()));
166 self.builder.start_node(SyntaxKind::ERROR.into());
167 self.bump();
168 self.builder.finish_node();
169 }
170 }
171 }
172 }
173
174 fn is_blank_line_start(&self) -> bool {
178 if !self.at(SyntaxKind::WHITESPACE) {
179 return false;
180 }
181 let next = self.tokens.get(self.pos + 1).map(|(k, _)| *k);
183 if next != Some(SyntaxKind::NEWLINE) {
184 return false;
185 }
186 if self.pos == 0 {
188 return true;
189 }
190 let prev = self.tokens[self.pos - 1].0;
191 prev == SyntaxKind::NEWLINE
192 }
193
194 fn parse_blank_line(&mut self) {
196 self.builder.start_node(SyntaxKind::BLANK_LINE.into());
197 self.bump(); self.bump(); self.builder.finish_node();
200 }
201
202 fn parse_directive(&mut self) {
207 self.builder.start_node(SyntaxKind::DIRECTIVE.into());
208
209 let name = self.current_text().to_string();
211 self.bump(); self.parse_arguments();
215
216 let is_lua_block = name.ends_with("_by_lua_block");
218
219 match self.peek_non_trivia() {
221 Some(SyntaxKind::SEMICOLON) => {
222 self.eat_trivia();
223 self.bump(); self.eat_trailing_comment();
226 }
227 Some(SyntaxKind::L_BRACE) => {
228 self.eat_trivia();
229 if is_lua_block {
230 self.parse_raw_block();
231 } else {
232 self.parse_block();
233 }
234 }
235 _ => {
236 self.error("expected ';' or '{'");
238 }
239 }
240
241 self.builder.finish_node(); }
243
244 fn parse_arguments(&mut self) {
250 loop {
251 let mut lookahead = self.pos;
254 while lookahead < self.tokens.len() {
255 let kind = self.tokens[lookahead].0;
256 if kind == SyntaxKind::WHITESPACE
257 || kind == SyntaxKind::NEWLINE
258 || kind == SyntaxKind::COMMENT
259 {
260 lookahead += 1;
261 } else {
262 break;
263 }
264 }
265 if lookahead >= self.tokens.len() {
266 break;
267 }
268 let next_kind = self.tokens[lookahead].0;
269
270 if is_argument_kind(next_kind) {
271 self.eat_trivia();
274 self.bump(); } else {
276 break;
277 }
278 }
279 }
280
281 fn eat_trailing_comment(&mut self) {
287 if self.at(SyntaxKind::WHITESPACE) {
288 let next = self.tokens.get(self.pos + 1).map(|(k, _)| *k);
289 if next == Some(SyntaxKind::COMMENT) {
290 self.bump(); self.bump(); }
293 }
294 }
295
296 fn parse_block(&mut self) {
298 self.builder.start_node(SyntaxKind::BLOCK.into());
299 self.bump(); self.parse_items(true);
302
303 if self.at(SyntaxKind::R_BRACE) {
304 self.bump(); } else {
306 self.error("expected '}'");
307 }
308 self.builder.finish_node();
309 }
310
311 fn parse_raw_block(&mut self) {
315 self.builder.start_node(SyntaxKind::BLOCK.into());
316 self.bump(); let mut depth: u32 = 1;
319 while !self.at_end() && depth > 0 {
320 match self.current() {
321 Some(SyntaxKind::L_BRACE) => {
322 depth += 1;
323 self.bump();
324 }
325 Some(SyntaxKind::R_BRACE) => {
326 depth -= 1;
327 if depth == 0 {
328 self.bump(); } else {
330 self.bump(); }
332 }
333 Some(_) => {
334 self.bump();
335 }
336 None => break,
337 }
338 }
339
340 if depth > 0 {
341 self.error("expected '}' for lua block");
342 }
343
344 self.builder.finish_node();
345 }
346}
347
348fn is_argument_kind(kind: SyntaxKind) -> bool {
350 matches!(
351 kind,
352 SyntaxKind::ARGUMENT
353 | SyntaxKind::IDENT
354 | SyntaxKind::VARIABLE
355 | SyntaxKind::DOUBLE_QUOTED_STRING
356 | SyntaxKind::SINGLE_QUOTED_STRING
357 )
358}
359
360fn is_directive_start(kind: SyntaxKind) -> bool {
365 matches!(
366 kind,
367 SyntaxKind::IDENT
368 | SyntaxKind::ARGUMENT
369 | SyntaxKind::VARIABLE
370 | SyntaxKind::DOUBLE_QUOTED_STRING
371 | SyntaxKind::SINGLE_QUOTED_STRING
372 )
373}
374
375#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::lexer_rowan::tokenize;
381 use crate::syntax_kind::SyntaxNode;
382
383 fn parse_source(source: &str) -> (SyntaxNode, Vec<SyntaxError>) {
384 let tokens = tokenize(source);
385 let (green, errors) = parse(tokens);
386 (SyntaxNode::new_root(green), errors)
387 }
388
389 fn assert_lossless(source: &str) {
391 let (root, _) = parse_source(source);
392 assert_eq!(
393 root.text().to_string(),
394 source,
395 "lossless round-trip failed"
396 );
397 }
398
399 fn assert_no_errors(source: &str) -> SyntaxNode {
401 let (root, errors) = parse_source(source);
402 assert!(errors.is_empty(), "unexpected errors: {:?}", errors);
403 root
404 }
405
406 fn first_directive(root: &SyntaxNode) -> SyntaxNode {
408 root.children()
409 .find(|n| n.kind() == SyntaxKind::DIRECTIVE)
410 .expect("no DIRECTIVE node found")
411 }
412
413 fn child_kinds(node: &SyntaxNode) -> Vec<SyntaxKind> {
415 node.children_with_tokens()
416 .map(|child| child.kind())
417 .collect()
418 }
419
420 #[test]
423 fn simple_directive() {
424 let source = "listen 80;";
425 let root = assert_no_errors(source);
426 assert_lossless(source);
427
428 let dir = first_directive(&root);
429 let kinds = child_kinds(&dir);
430 assert_eq!(
431 kinds,
432 vec![
433 SyntaxKind::IDENT,
434 SyntaxKind::WHITESPACE,
435 SyntaxKind::ARGUMENT,
436 SyntaxKind::SEMICOLON
437 ]
438 );
439 }
440
441 #[test]
442 fn directive_no_args() {
443 let source = "accept_mutex on;";
444 let root = assert_no_errors(source);
445 assert_lossless(source);
446
447 let dir = first_directive(&root);
448 let kinds = child_kinds(&dir);
449 assert_eq!(
450 kinds,
451 vec![
452 SyntaxKind::IDENT,
453 SyntaxKind::WHITESPACE,
454 SyntaxKind::IDENT,
455 SyntaxKind::SEMICOLON
456 ]
457 );
458 }
459
460 #[test]
463 fn block_directive() {
464 let source = "server { listen 80; }";
465 let root = assert_no_errors(source);
466 assert_lossless(source);
467
468 let dir = first_directive(&root);
469 let kinds = child_kinds(&dir);
470 assert!(kinds.contains(&SyntaxKind::IDENT));
472 assert!(kinds.contains(&SyntaxKind::BLOCK));
473 }
474
475 #[test]
476 fn nested_blocks() {
477 let source = "http { server { listen 80; } }";
478 assert_no_errors(source);
479 assert_lossless(source);
480 }
481
482 #[test]
485 fn multiline_config() {
486 let source = "http {\n server {\n listen 80;\n }\n}";
487 assert_no_errors(source);
488 assert_lossless(source);
489 }
490
491 #[test]
494 fn comment_standalone() {
495 let source = "# this is a comment\nlisten 80;";
496 assert_no_errors(source);
497 assert_lossless(source);
498 }
499
500 #[test]
501 fn comment_after_directive() {
502 let source = "listen 80; # port";
503 let root = assert_no_errors(source);
504 assert_lossless(source);
505
506 let dir = first_directive(&root);
508 let kinds = child_kinds(&dir);
509 assert!(kinds.contains(&SyntaxKind::COMMENT));
510 }
511
512 #[test]
515 fn double_quoted_string_arg() {
516 let source = r#"return 200 "hello world";"#;
517 let root = assert_no_errors(source);
518 assert_lossless(source);
519
520 let dir = first_directive(&root);
521 let kinds = child_kinds(&dir);
522 assert!(kinds.contains(&SyntaxKind::DOUBLE_QUOTED_STRING));
523 }
524
525 #[test]
526 fn single_quoted_string_arg() {
527 let source = "return 200 'hello world';";
528 let root = assert_no_errors(source);
529 assert_lossless(source);
530
531 let dir = first_directive(&root);
532 let kinds = child_kinds(&dir);
533 assert!(kinds.contains(&SyntaxKind::SINGLE_QUOTED_STRING));
534 }
535
536 #[test]
537 fn variable_arg() {
538 let source = "set $var value;";
539 let root = assert_no_errors(source);
540 assert_lossless(source);
541
542 let dir = first_directive(&root);
543 let kinds = child_kinds(&dir);
544 assert!(kinds.contains(&SyntaxKind::VARIABLE));
545 }
546
547 #[test]
550 fn lua_block() {
551 let source = "content_by_lua_block {\n ngx.say(\"hello\")\n}";
552 let root = assert_no_errors(source);
553 assert_lossless(source);
554
555 let dir = first_directive(&root);
556 let kinds = child_kinds(&dir);
557 assert!(kinds.contains(&SyntaxKind::BLOCK));
558 }
559
560 #[test]
561 fn lua_block_nested_braces() {
562 let source =
563 "content_by_lua_block {\n if true then\n local t = {1, 2}\n end\n}";
564 assert_no_errors(source);
565 assert_lossless(source);
566 }
567
568 #[test]
571 fn missing_semicolon() {
572 let source = "listen 80";
576 let (_root, errors) = parse_source(source);
577 assert_lossless(source);
578 assert!(!errors.is_empty(), "should report missing semicolon");
579 }
580
581 #[test]
582 fn missing_closing_brace() {
583 let source = "server { listen 80;";
584 let (_root, errors) = parse_source(source);
585 assert_lossless(source);
586 assert!(!errors.is_empty(), "should report missing '}}'");
587 }
588
589 #[test]
590 fn unexpected_closing_brace() {
591 let source = "} listen 80;";
592 let (_root, errors) = parse_source(source);
593 assert_lossless(source);
594 assert!(!errors.is_empty(), "should report unexpected '}}'");
595 }
596
597 #[test]
602 fn multiline_directive_with_comment_between_args() {
603 let source = "set_by_lua_file $var\narg1\n# Comment\narg2;\n";
604 let root = assert_no_errors(source);
605 assert_lossless(source);
606
607 let dir = first_directive(&root);
610 let kinds = child_kinds(&dir);
611 assert!(kinds.contains(&SyntaxKind::COMMENT));
612 assert!(kinds.contains(&SyntaxKind::SEMICOLON));
613 }
614
615 #[test]
616 fn comment_before_semicolon() {
617 let source = "listen 80\n# comment\n;";
618 assert_no_errors(source);
619 assert_lossless(source);
620 }
621
622 #[test]
623 fn comment_before_block() {
624 let source = "server # comment\n{\n listen 80;\n}";
625 let root = assert_no_errors(source);
626 assert_lossless(source);
627
628 let dir = first_directive(&root);
629 assert!(child_kinds(&dir).contains(&SyntaxKind::BLOCK));
630 }
631
632 #[test]
633 fn missing_semicolon_before_standalone_comment_still_error() {
634 let source = "listen 80\n# comment\n";
635 let (root, errors) = parse_source(source);
636 assert_lossless(source);
637 assert!(!errors.is_empty(), "should report missing semicolon");
638
639 let dir = first_directive(&root);
641 assert!(!child_kinds(&dir).contains(&SyntaxKind::COMMENT));
642 }
643
644 #[test]
645 fn lossless_empty() {
646 assert_lossless("");
647 }
648
649 #[test]
650 fn lossless_whitespace_only() {
651 assert_lossless(" \n \n");
652 }
653
654 #[test]
655 fn lossless_complex_config() {
656 let source = r#"http {
657 # Main server
658 server {
659 listen 80;
660 server_name example.com;
661 location / {
662 proxy_pass http://backend;
663 }
664 }
665}
666"#;
667 assert_lossless(source);
668 assert_no_errors(source);
669 }
670
671 #[test]
672 fn lossless_blank_lines() {
673 let source = "listen 80;\n\nlisten 443;\n";
674 assert_lossless(source);
675 assert_no_errors(source);
676 }
677
678 #[test]
679 fn lossless_utf8() {
680 let source = "# これは日本語コメント\nlisten 80;\n";
681 assert_lossless(source);
682 assert_no_errors(source);
683 }
684
685 #[test]
686 fn location_with_regex() {
687 let source = "location ~ ^/api/(.*) {\n proxy_pass http://backend;\n}";
688 assert_no_errors(source);
689 assert_lossless(source);
690 }
691
692 #[test]
693 fn multiple_directives() {
694 let source = "worker_processes auto;\nevents {\n worker_connections 1024;\n}\n";
695 assert_no_errors(source);
696 assert_lossless(source);
697 }
698}