Skip to main content

flowscope_core/linter/rules/
cv_001.rs

1//! LINT_CV_001: Not-equal style.
2//!
3//! SQLFluff CV01 parity (current scope): flag statements that mix `<>` and
4//! `!=` not-equal operators.
5
6use crate::linter::config::LintConfig;
7use crate::linter::rule::{LintContext, LintRule};
8use crate::linter::visit::visit_expressions;
9use crate::types::{issue_codes, Issue, IssueAutofixApplicability, IssuePatchEdit};
10use sqlparser::ast::{BinaryOperator, Expr, Spanned, Statement};
11use sqlparser::tokenizer::{Token, TokenWithSpan, Tokenizer, Whitespace};
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14enum PreferredNotEqualStyle {
15    Consistent,
16    CStyle,
17    Ansi,
18}
19
20impl PreferredNotEqualStyle {
21    fn from_config(config: &LintConfig) -> Self {
22        match config
23            .rule_option_str(issue_codes::LINT_CV_001, "preferred_not_equal_style")
24            .unwrap_or("consistent")
25            .to_ascii_lowercase()
26            .as_str()
27        {
28            "c_style" => Self::CStyle,
29            "ansi" => Self::Ansi,
30            _ => Self::Consistent,
31        }
32    }
33
34    fn violation(self, usage: &NotEqualUsage) -> bool {
35        match self {
36            Self::Consistent => usage.saw_angle_style && usage.saw_bang_style,
37            Self::CStyle => usage.saw_angle_style,
38            Self::Ansi => usage.saw_bang_style,
39        }
40    }
41
42    fn message(self) -> &'static str {
43        match self {
44            Self::Consistent => "Use consistent not-equal style.",
45            Self::CStyle => "Use `!=` for not-equal comparisons.",
46            Self::Ansi => "Use `<>` for not-equal comparisons.",
47        }
48    }
49
50    fn target_style(self, occurrences: &[NotEqualOccurrence]) -> Option<NotEqualStyle> {
51        match self {
52            // In consistent mode, normalize to whichever style appears first
53            // in the statement (SQLFluff parity).
54            Self::Consistent => occurrences.first().map(|o| o.style),
55            Self::CStyle => Some(NotEqualStyle::Bang),
56            Self::Ansi => Some(NotEqualStyle::Angle),
57        }
58    }
59
60    fn violating_occurrences(self, occurrences: &[NotEqualOccurrence]) -> Vec<NotEqualOccurrence> {
61        let Some(target_style) = self.target_style(occurrences) else {
62            return Vec::new();
63        };
64
65        occurrences
66            .iter()
67            .copied()
68            .filter(|occurrence| occurrence.style != target_style)
69            .collect()
70    }
71}
72
73pub struct ConventionNotEqual {
74    preferred_style: PreferredNotEqualStyle,
75}
76
77impl ConventionNotEqual {
78    pub fn from_config(config: &LintConfig) -> Self {
79        Self {
80            preferred_style: PreferredNotEqualStyle::from_config(config),
81        }
82    }
83}
84
85impl Default for ConventionNotEqual {
86    fn default() -> Self {
87        Self {
88            preferred_style: PreferredNotEqualStyle::Consistent,
89        }
90    }
91}
92
93impl LintRule for ConventionNotEqual {
94    fn code(&self) -> &'static str {
95        issue_codes::LINT_CV_001
96    }
97
98    fn name(&self) -> &'static str {
99        "Not-equal style"
100    }
101
102    fn description(&self) -> &'static str {
103        "Consistent usage of '!=' or '<>' for \"not equal to\" operator."
104    }
105
106    fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
107        let tokens =
108            tokenized_for_context(ctx).or_else(|| tokenized(ctx.statement_sql(), ctx.dialect()));
109        let mut occurrences = statement_not_equal_occurrences_with_tokens(
110            statement,
111            ctx.statement_sql(),
112            tokens.as_deref(),
113        );
114        occurrences.sort_by_key(|occurrence| (occurrence.start, occurrence.end));
115        let usage = usage_from_occurrences(&occurrences);
116
117        if self.preferred_style.violation(&usage) {
118            let violating_occurrences = self.preferred_style.violating_occurrences(&occurrences);
119            let mut issue = Issue::info(issue_codes::LINT_CV_001, self.preferred_style.message())
120                .with_statement(ctx.statement_index);
121
122            if let (Some(target_style), Some(first_occurrence)) = (
123                self.preferred_style.target_style(&occurrences),
124                violating_occurrences.first().copied(),
125            ) {
126                let issue_span =
127                    ctx.span_from_statement_offset(first_occurrence.start, first_occurrence.end);
128                let mut edits = Vec::new();
129                for occurrence in violating_occurrences {
130                    if let Some((first_pos, second_pos)) = occurrence.split_positions {
131                        let (first_replacement, second_replacement) =
132                            target_style.split_replacements();
133                        edits.push(IssuePatchEdit::new(
134                            ctx.span_from_statement_offset(first_pos, first_pos + 1),
135                            first_replacement,
136                        ));
137                        edits.push(IssuePatchEdit::new(
138                            ctx.span_from_statement_offset(second_pos, second_pos + 1),
139                            second_replacement,
140                        ));
141                    } else {
142                        edits.push(IssuePatchEdit::new(
143                            ctx.span_from_statement_offset(occurrence.start, occurrence.end),
144                            target_style.replacement(),
145                        ));
146                    }
147                }
148                issue = issue
149                    .with_span(issue_span)
150                    .with_autofix_edits(IssueAutofixApplicability::Safe, edits);
151            }
152
153            vec![issue]
154        } else {
155            Vec::new()
156        }
157    }
158}
159
160#[derive(Default)]
161struct NotEqualUsage {
162    saw_angle_style: bool,
163    saw_bang_style: bool,
164}
165
166#[derive(Clone, Copy, Debug, Eq, PartialEq)]
167enum NotEqualStyle {
168    Angle,
169    Bang,
170}
171
172impl NotEqualStyle {
173    fn replacement(self) -> &'static str {
174        match self {
175            Self::Angle => "<>",
176            Self::Bang => "!=",
177        }
178    }
179
180    fn split_replacements(self) -> (&'static str, &'static str) {
181        match self {
182            Self::Angle => ("<", ">"),
183            Self::Bang => ("!", "="),
184        }
185    }
186}
187
188#[derive(Clone, Copy, Debug, Eq, PartialEq)]
189struct NotEqualOccurrence {
190    style: NotEqualStyle,
191    start: usize,
192    end: usize,
193    split_positions: Option<(usize, usize)>,
194}
195
196fn statement_not_equal_occurrences_with_tokens(
197    statement: &Statement,
198    sql: &str,
199    tokens: Option<&[LocatedToken]>,
200) -> Vec<NotEqualOccurrence> {
201    let mut occurrences = Vec::new();
202    visit_expressions(statement, &mut |expr| {
203        let occurrence = match expr {
204            Expr::BinaryOp { left, op, right } if *op == BinaryOperator::NotEq => {
205                not_equal_occurrence_between(sql, left.as_ref(), right.as_ref(), tokens)
206            }
207            Expr::AnyOp {
208                left,
209                compare_op,
210                right,
211                ..
212            } if *compare_op == BinaryOperator::NotEq => {
213                not_equal_occurrence_between(sql, left.as_ref(), right.as_ref(), tokens)
214            }
215            Expr::AllOp {
216                left,
217                compare_op,
218                right,
219            } if *compare_op == BinaryOperator::NotEq => {
220                not_equal_occurrence_between(sql, left.as_ref(), right.as_ref(), tokens)
221            }
222            _ => None,
223        };
224
225        if let Some(occurrence) = occurrence {
226            occurrences.push(occurrence);
227        }
228    });
229
230    occurrences.extend(scan_split_not_equal_occurrences(sql));
231    occurrences.sort_by_key(|occurrence| (occurrence.start, occurrence.end));
232    occurrences.dedup_by(|left, right| {
233        left.style == right.style
234            && left.start == right.start
235            && left.end == right.end
236            && left.split_positions == right.split_positions
237    });
238
239    occurrences
240}
241
242fn usage_from_occurrences(occurrences: &[NotEqualOccurrence]) -> NotEqualUsage {
243    let mut usage = NotEqualUsage::default();
244    for occurrence in occurrences {
245        match occurrence.style {
246            NotEqualStyle::Angle => usage.saw_angle_style = true,
247            NotEqualStyle::Bang => usage.saw_bang_style = true,
248        }
249    }
250    usage
251}
252
253fn not_equal_occurrence_between(
254    sql: &str,
255    left: &Expr,
256    right: &Expr,
257    tokens: Option<&[LocatedToken]>,
258) -> Option<NotEqualOccurrence> {
259    let left_end = left.span().end;
260    let right_start = right.span().start;
261    if left_end.line == 0
262        || left_end.column == 0
263        || right_start.line == 0
264        || right_start.column == 0
265    {
266        return None;
267    }
268
269    let start = line_col_to_offset(sql, left_end.line as usize, left_end.column as usize)?;
270    let end = line_col_to_offset(sql, right_start.line as usize, right_start.column as usize)?;
271    if end < start {
272        return None;
273    }
274
275    if let Some(tokens) = tokens {
276        return not_equal_occurrence_in_tokens(sql, tokens, start, end);
277    }
278
279    None
280}
281
282fn not_equal_occurrence_in_tokens(
283    sql: &str,
284    tokens: &[LocatedToken],
285    start: usize,
286    end: usize,
287) -> Option<NotEqualOccurrence> {
288    for token in tokens {
289        if token.end <= start || token.start >= end {
290            continue;
291        }
292        if is_trivia_token(&token.token) {
293            continue;
294        }
295
296        if !matches!(token.token, Token::Neq) {
297            return None;
298        }
299        if token.end > sql.len() {
300            return None;
301        }
302
303        let raw = &sql[token.start..token.end];
304        let style = match raw {
305            "<>" => Some(NotEqualStyle::Angle),
306            "!=" => Some(NotEqualStyle::Bang),
307            _ => None,
308        }?;
309
310        return Some(NotEqualOccurrence {
311            style,
312            start: token.start,
313            end: token.end,
314            split_positions: None,
315        });
316    }
317
318    None
319}
320
321fn scan_split_not_equal_occurrences(sql: &str) -> Vec<NotEqualOccurrence> {
322    let mut occurrences = Vec::new();
323    let bytes = sql.as_bytes();
324    let mut index = 0usize;
325
326    while index < bytes.len() {
327        let (style, expected_second) = match bytes[index] {
328            b'<' => (NotEqualStyle::Angle, b'>'),
329            b'!' => (NotEqualStyle::Bang, b'='),
330            _ => {
331                index += 1;
332                continue;
333            }
334        };
335
336        let mut probe = index + 1;
337        let mut saw_separator = false;
338
339        while probe < bytes.len() {
340            match bytes[probe] {
341                b' ' | b'\t' | b'\n' | b'\r' => {
342                    saw_separator = true;
343                    probe += 1;
344                }
345                b'-' if probe + 1 < bytes.len() && bytes[probe + 1] == b'-' => {
346                    saw_separator = true;
347                    probe += 2;
348                    while probe < bytes.len() && bytes[probe] != b'\n' {
349                        probe += 1;
350                    }
351                }
352                b'/' if probe + 1 < bytes.len() && bytes[probe + 1] == b'*' => {
353                    saw_separator = true;
354                    probe += 2;
355                    while probe + 1 < bytes.len() {
356                        if bytes[probe] == b'*' && bytes[probe + 1] == b'/' {
357                            probe += 2;
358                            break;
359                        }
360                        probe += 1;
361                    }
362                }
363                _ => break,
364            }
365        }
366
367        if saw_separator && probe < bytes.len() && bytes[probe] == expected_second {
368            occurrences.push(NotEqualOccurrence {
369                style,
370                start: index,
371                end: probe + 1,
372                split_positions: Some((index, probe)),
373            });
374            index = probe + 1;
375            continue;
376        }
377
378        index += 1;
379    }
380
381    occurrences
382}
383
384#[derive(Clone)]
385struct LocatedToken {
386    token: Token,
387    start: usize,
388    end: usize,
389}
390
391fn tokenized(sql: &str, dialect: crate::types::Dialect) -> Option<Vec<LocatedToken>> {
392    let dialect = dialect.to_sqlparser_dialect();
393    let mut tokenizer = Tokenizer::new(dialect.as_ref(), sql);
394    let tokens = tokenizer.tokenize_with_location().ok()?;
395
396    let mut out = Vec::with_capacity(tokens.len());
397    for token in tokens {
398        let Some((start, end)) = token_with_span_offsets(sql, &token) else {
399            continue;
400        };
401        out.push(LocatedToken {
402            token: token.token,
403            start,
404            end,
405        });
406    }
407    Some(out)
408}
409
410fn tokenized_for_context(ctx: &LintContext) -> Option<Vec<LocatedToken>> {
411    let statement_start = ctx.statement_range.start;
412    let from_document = ctx.with_document_tokens(|tokens| {
413        if tokens.is_empty() {
414            return None;
415        }
416
417        Some(
418            tokens
419                .iter()
420                .filter_map(|token| {
421                    let (start, end) = token_with_span_offsets(ctx.sql, token)?;
422                    if start < ctx.statement_range.start || end > ctx.statement_range.end {
423                        return None;
424                    }
425
426                    Some(LocatedToken {
427                        token: token.token.clone(),
428                        start: start - statement_start,
429                        end: end - statement_start,
430                    })
431                })
432                .collect::<Vec<_>>(),
433        )
434    });
435
436    if let Some(tokens) = from_document {
437        return Some(tokens);
438    }
439
440    tokenized(ctx.statement_sql(), ctx.dialect())
441}
442
443fn token_with_span_offsets(sql: &str, token: &TokenWithSpan) -> Option<(usize, usize)> {
444    let start = line_col_to_offset(
445        sql,
446        token.span.start.line as usize,
447        token.span.start.column as usize,
448    )?;
449    let end = line_col_to_offset(
450        sql,
451        token.span.end.line as usize,
452        token.span.end.column as usize,
453    )?;
454    Some((start, end))
455}
456
457fn is_trivia_token(token: &Token) -> bool {
458    matches!(
459        token,
460        Token::Whitespace(Whitespace::Space | Whitespace::Tab | Whitespace::Newline)
461            | Token::Whitespace(Whitespace::SingleLineComment { .. })
462            | Token::Whitespace(Whitespace::MultiLineComment(_))
463    )
464}
465
466fn line_col_to_offset(sql: &str, line: usize, column: usize) -> Option<usize> {
467    if line == 0 || column == 0 {
468        return None;
469    }
470
471    let mut current_line = 1usize;
472    let mut current_col = 1usize;
473    for (offset, ch) in sql.char_indices() {
474        if current_line == line && current_col == column {
475            return Some(offset);
476        }
477        if ch == '\n' {
478            current_line += 1;
479            current_col = 1;
480        } else {
481            current_col += 1;
482        }
483    }
484
485    if current_line == line && current_col == column {
486        Some(sql.len())
487    } else {
488        None
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495    use crate::parser::parse_sql;
496    use crate::types::IssueAutofixApplicability;
497
498    fn run(sql: &str) -> Vec<Issue> {
499        let statements = parse_sql(sql).expect("parse");
500        let rule = ConventionNotEqual::default();
501        statements
502            .iter()
503            .enumerate()
504            .flat_map(|(index, statement)| {
505                rule.check(
506                    statement,
507                    &LintContext {
508                        sql,
509                        statement_range: 0..sql.len(),
510                        statement_index: index,
511                    },
512                )
513            })
514            .collect()
515    }
516
517    fn apply_issue_autofix(sql: &str, issue: &Issue) -> Option<String> {
518        let autofix = issue.autofix.as_ref()?;
519        let mut output = sql.to_string();
520        let mut edits = autofix.edits.clone();
521        edits.sort_by_key(|edit| (edit.span.start, edit.span.end));
522        for edit in edits.into_iter().rev() {
523            output.replace_range(edit.span.start..edit.span.end, &edit.replacement);
524        }
525        Some(output)
526    }
527
528    #[test]
529    fn flags_mixed_not_equal_styles() {
530        // Consistent mode: first occurrence (`<>`) determines the target style.
531        // The violating occurrence is `!=`.
532        let sql = "SELECT * FROM t WHERE a <> b AND c != d";
533        let issues = run(sql);
534        assert_eq!(issues.len(), 1);
535        assert_eq!(issues[0].code, issue_codes::LINT_CV_001);
536
537        let bang_start = sql.find("!=").expect("bang operator");
538        let issue_span = issues[0].span.expect("issue span");
539        assert_eq!(issue_span.start, bang_start);
540        assert_eq!(issue_span.end, bang_start + 2);
541
542        let autofix = issues[0].autofix.as_ref().expect("autofix metadata");
543        assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
544        assert_eq!(autofix.edits.len(), 1);
545        assert_eq!(autofix.edits[0].span.start, bang_start);
546        assert_eq!(autofix.edits[0].span.end, bang_start + 2);
547        assert_eq!(autofix.edits[0].replacement, "<>");
548    }
549
550    #[test]
551    fn does_not_flag_single_not_equal_style() {
552        assert!(run("SELECT * FROM t WHERE a <> b").is_empty());
553        assert!(run("SELECT * FROM t WHERE a != b").is_empty());
554    }
555
556    #[test]
557    fn does_not_flag_not_equal_tokens_inside_string_literal() {
558        assert!(run("SELECT 'a <> b and c != d' AS txt FROM t").is_empty());
559    }
560
561    #[test]
562    fn does_not_flag_not_equal_tokens_inside_comments() {
563        assert!(run("SELECT * FROM t -- a <> b and c != d").is_empty());
564    }
565
566    #[test]
567    fn c_style_preference_flags_angle_bracket_operator() {
568        let config = LintConfig {
569            enabled: true,
570            disabled_rules: vec![],
571            rule_configs: std::collections::BTreeMap::from([(
572                "convention.not_equal".to_string(),
573                serde_json::json!({"preferred_not_equal_style": "c_style"}),
574            )]),
575        };
576        let rule = ConventionNotEqual::from_config(&config);
577        let sql = "SELECT * FROM t WHERE a <> b";
578        let statements = parse_sql(sql).expect("parse");
579        let issues = rule.check(
580            &statements[0],
581            &LintContext {
582                sql,
583                statement_range: 0..sql.len(),
584                statement_index: 0,
585            },
586        );
587        assert_eq!(issues.len(), 1);
588        let angle_start = sql.find("<>").expect("angle operator");
589        let issue_span = issues[0].span.expect("issue span");
590        assert_eq!(issue_span.start, angle_start);
591        assert_eq!(issue_span.end, angle_start + 2);
592        let autofix = issues[0].autofix.as_ref().expect("autofix metadata");
593        assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
594        assert_eq!(autofix.edits.len(), 1);
595        assert_eq!(autofix.edits[0].span.start, angle_start);
596        assert_eq!(autofix.edits[0].span.end, angle_start + 2);
597        assert_eq!(autofix.edits[0].replacement, "!=");
598    }
599
600    #[test]
601    fn c_style_preference_includes_all_angle_operator_edits() {
602        let config = LintConfig {
603            enabled: true,
604            disabled_rules: vec![],
605            rule_configs: std::collections::BTreeMap::from([(
606                "convention.not_equal".to_string(),
607                serde_json::json!({"preferred_not_equal_style": "c_style"}),
608            )]),
609        };
610        let rule = ConventionNotEqual::from_config(&config);
611        let sql = "SELECT * FROM t WHERE a <> b AND c <> d";
612        let statements = parse_sql(sql).expect("parse");
613        let issues = rule.check(
614            &statements[0],
615            &LintContext {
616                sql,
617                statement_range: 0..sql.len(),
618                statement_index: 0,
619            },
620        );
621        assert_eq!(issues.len(), 1);
622
623        let first_start = sql.find("<>").expect("first angle operator");
624        let second_start = sql[first_start + 2..]
625            .find("<>")
626            .map(|offset| first_start + 2 + offset)
627            .expect("second angle operator");
628
629        let issue_span = issues[0].span.expect("issue span");
630        assert_eq!(issue_span.start, first_start);
631        assert_eq!(issue_span.end, first_start + 2);
632
633        let autofix = issues[0].autofix.as_ref().expect("autofix metadata");
634        assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
635        assert_eq!(autofix.edits.len(), 2);
636        assert_eq!(autofix.edits[0].span.start, first_start);
637        assert_eq!(autofix.edits[0].span.end, first_start + 2);
638        assert_eq!(autofix.edits[0].replacement, "!=");
639        assert_eq!(autofix.edits[1].span.start, second_start);
640        assert_eq!(autofix.edits[1].span.end, second_start + 2);
641        assert_eq!(autofix.edits[1].replacement, "!=");
642    }
643
644    #[test]
645    fn ansi_preference_flags_bang_operator() {
646        let config = LintConfig {
647            enabled: true,
648            disabled_rules: vec![],
649            rule_configs: std::collections::BTreeMap::from([(
650                "LINT_CV_001".to_string(),
651                serde_json::json!({"preferred_not_equal_style": "ansi"}),
652            )]),
653        };
654        let rule = ConventionNotEqual::from_config(&config);
655        let sql = "SELECT * FROM t WHERE a != b";
656        let statements = parse_sql(sql).expect("parse");
657        let issues = rule.check(
658            &statements[0],
659            &LintContext {
660                sql,
661                statement_range: 0..sql.len(),
662                statement_index: 0,
663            },
664        );
665        assert_eq!(issues.len(), 1);
666        let bang_start = sql.find("!=").expect("bang operator");
667        let issue_span = issues[0].span.expect("issue span");
668        assert_eq!(issue_span.start, bang_start);
669        assert_eq!(issue_span.end, bang_start + 2);
670        let autofix = issues[0].autofix.as_ref().expect("autofix metadata");
671        assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
672        assert_eq!(autofix.edits.len(), 1);
673        assert_eq!(autofix.edits[0].span.start, bang_start);
674        assert_eq!(autofix.edits[0].span.end, bang_start + 2);
675        assert_eq!(autofix.edits[0].replacement, "<>");
676    }
677
678    #[test]
679    fn c_style_preference_fixes_split_angle_operator_around_comment() {
680        let config = LintConfig {
681            enabled: true,
682            disabled_rules: vec![],
683            rule_configs: std::collections::BTreeMap::from([(
684                "convention.not_equal".to_string(),
685                serde_json::json!({"preferred_not_equal_style": "c_style"}),
686            )]),
687        };
688        let rule = ConventionNotEqual::from_config(&config);
689        let sql = "SELECT * FROM X WHERE 1  <\n  -- some comment\n> 2\n";
690        let statements = parse_sql("SELECT 1").expect("synthetic parse");
691        let issues = rule.check(
692            &statements[0],
693            &LintContext {
694                sql,
695                statement_range: 0..sql.len(),
696                statement_index: 0,
697            },
698        );
699        assert_eq!(issues.len(), 1);
700        let autofix = issues[0].autofix.as_ref().expect("autofix metadata");
701        assert_eq!(autofix.edits.len(), 2);
702        let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
703        assert_eq!(
704            fixed,
705            "SELECT * FROM X WHERE 1  !\n  -- some comment\n= 2\n"
706        );
707    }
708
709    #[test]
710    fn ansi_preference_fixes_split_bang_operator_around_comment() {
711        let config = LintConfig {
712            enabled: true,
713            disabled_rules: vec![],
714            rule_configs: std::collections::BTreeMap::from([(
715                "convention.not_equal".to_string(),
716                serde_json::json!({"preferred_not_equal_style": "ansi"}),
717            )]),
718        };
719        let rule = ConventionNotEqual::from_config(&config);
720        let sql = "SELECT * FROM X WHERE 1  !\n  -- some comment\n= 2\n";
721        let statements = parse_sql("SELECT 1").expect("synthetic parse");
722        let issues = rule.check(
723            &statements[0],
724            &LintContext {
725                sql,
726                statement_range: 0..sql.len(),
727                statement_index: 0,
728            },
729        );
730        assert_eq!(issues.len(), 1);
731        let autofix = issues[0].autofix.as_ref().expect("autofix metadata");
732        assert_eq!(autofix.edits.len(), 2);
733        let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
734        assert_eq!(
735            fixed,
736            "SELECT * FROM X WHERE 1  <\n  -- some comment\n> 2\n"
737        );
738    }
739}