diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 2d0f0ac4f..440312168 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -43,6 +43,7 @@ mod eslint { pub mod eqeqeq; pub mod for_direction; pub mod getter_return; + pub mod max_lines; pub mod no_array_constructor; pub mod no_async_promise_executor; pub mod no_bitwise; @@ -356,6 +357,7 @@ oxc_macros::declare_all_lint_rules! { eslint::eqeqeq, eslint::for_direction, eslint::getter_return, + eslint::max_lines, eslint::no_this_before_super, eslint::no_array_constructor, eslint::no_async_promise_executor, diff --git a/crates/oxc_linter/src/rules/eslint/max_lines.rs b/crates/oxc_linter/src/rules/eslint/max_lines.rs new file mode 100644 index 000000000..a438e4bbf --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/max_lines.rs @@ -0,0 +1,413 @@ +use serde_json::Value; + +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, Span}; + +use crate::{context::LintContext, rule::Rule}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(max-lines): {0:?}")] +#[diagnostic(severity(warning), help("Reduce the number of lines in this file"))] +struct MaxLinesDiagnostic(CompactStr, #[label] Span); + +#[derive(Debug, Default, Clone)] +pub struct MaxLines(Box); + +#[derive(Debug, Clone)] +pub struct MaxLinesConfig { + max: usize, + skip_blank_lines: bool, + skip_comments: bool, +} + +impl std::ops::Deref for MaxLines { + type Target = MaxLinesConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Default for MaxLinesConfig { + fn default() -> Self { + Self { max: 300, skip_blank_lines: false, skip_comments: false } + } +} + +declare_oxc_lint!( + /// ### What it does + /// Enforce a maximum number of lines per file + /// + /// ### Why is this bad? + /// + /// Some people consider large files a code smell. Large files tend to do a lot of things and can make it hard following what’s going. + /// While there is not an objective maximum number of lines considered acceptable in a file, most people would agree it should not be in the thousands. Recommendations usually range from 100 to 500 lines. + /// + /// ### Example + /// ```javascript + /// ``` + MaxLines, + pedantic +); + +impl Rule for MaxLines { + fn from_configuration(value: Value) -> Self { + let config = value.get(0); + if let Some(max) = config + .and_then(Value::as_number) + .and_then(serde_json::Number::as_u64) + .and_then(|v| usize::try_from(v).ok()) + { + Self(Box::new(MaxLinesConfig { max, skip_comments: false, skip_blank_lines: false })) + } else { + let max = config + .and_then(|config| config.get("max")) + .and_then(Value::as_number) + .and_then(serde_json::Number::as_u64) + .map_or(300, |v| usize::try_from(v).unwrap_or(300)); + let skip_comments = config + .and_then(|config| config.get("skipComments")) + .and_then(Value::as_bool) + .unwrap_or(false); + let skip_blank_lines = config + .and_then(|config| config.get("skipBlankLines")) + .and_then(Value::as_bool) + .unwrap_or(false); + + Self(Box::new(MaxLinesConfig { max, skip_blank_lines, skip_comments })) + } + } + fn run_once(&self, ctx: &LintContext) { + let comment_lines = if self.skip_comments { + let mut comment_lines: usize = 0; + for (kind, span) in ctx.semantic().trivias().comments() { + if kind.is_single_line() { + let comment_line = + ctx.source_text()[..span.start as usize].lines().next_back().unwrap_or(""); + if line_has_just_comment(comment_line, "//") { + comment_lines += 1; + } + } else { + let mut start_line = ctx.source_text()[..span.start as usize].lines().count(); + let comment_start_line = + ctx.source_text()[..span.start as usize].lines().next_back().unwrap_or(""); + if !line_has_just_comment(comment_start_line, "/*") { + start_line += 1; + } + let mut end_line = ctx.source_text()[..=span.end as usize].lines().count(); + let comment_end_line = + ctx.source_text()[span.end as usize..].lines().next().unwrap_or(""); + if line_has_just_comment(comment_end_line, "*/") { + end_line += 1; + } + comment_lines += end_line - start_line; + } + } + comment_lines + } else { + 0 + }; + + let lines_in_file = + if ctx.source_text().is_empty() { 1 } else { ctx.source_text().lines().count() }; + + let blank_lines = if self.skip_blank_lines { + ctx.source_text().lines().filter(|&line| line.trim().is_empty()).count() + } else { + 0 + }; + + if lines_in_file.saturating_sub(blank_lines).saturating_sub(comment_lines) > self.max { + let error = CompactStr::from(format!( + "File has too many lines ({}). Maximum allowed is {}.", + lines_in_file, self.max, + )); + ctx.diagnostic(MaxLinesDiagnostic( + error, + Span::new(0, u32::try_from(ctx.source_text().len()).unwrap_or(u32::MAX)), + )); + } + } +} + +fn line_has_just_comment(line: &str, comment_chars: &str) -> bool { + if let Some(line) = line.trim().strip_prefix(comment_chars) { + line.is_empty() + } else { + false + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("var x;", None), + ("var xy;\nvar xy;", None), + ("A", Some(serde_json::json!([1]))), + ("A\n", Some(serde_json::json!([1]))), + ("A\r", Some(serde_json::json!([1]))), + ("A\r\n", Some(serde_json::json!([1]))), + ("var xy;\nvar xy;", Some(serde_json::json!([2]))), + ("var xy;\nvar xy;\n", Some(serde_json::json!([2]))), + ("var xy;\nvar xy;", Some(serde_json::json!([{ "max": 2 }]))), + ("// comment\n", Some(serde_json::json!([{ "max": 0, "skipComments": true }]))), + ("foo;\n /* comment */\n", Some(serde_json::json!([{ "max": 1, "skipComments": true }]))), + ( + "//a single line comment + var xy; + var xy; + /* a multiline + really really + long comment*/ ", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "var x; /* inline comment\nspanning multiple lines */ var z;", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "var x; /* inline comment + spanning multiple lines */ + var z;", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "var x; + + + + var y;", + Some(serde_json::json!([{ "max": 2, "skipBlankLines": true }])), + ), + ( + "//a single line comment + var xy; + + var xy; + + /* a multiline + really really + long comment*/", + Some(serde_json::json!([{ "max": 2, "skipComments": true, "skipBlankLines": true }])), + ), + ]; + + let fail = vec![ + ("var xyz;\nvar xyz;\nvar xyz;", Some(serde_json::json!([2]))), + ( + "/* a multiline comment\n that goes to many lines*/\nvar xy;\nvar xy;", + Some(serde_json::json!([2])), + ), + ("//a single line comment\nvar xy;\nvar xy;", Some(serde_json::json!([2]))), + ( + "var x; + + + + var y;", + Some(serde_json::json!([{ "max": 2 }])), + ), + ( + "//a single line comment + var xy; + + var xy; + + /* a multiline + really really + long comment*/", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "var x; // inline comment + var y; + var z;", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "var x; /* inline comment + spanning multiple lines */ + var y; + var z;", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "//a single line comment + var xy; + + var xy; + + /* a multiline + really really + long comment*/", + Some(serde_json::json!([{ "max": 2, "skipBlankLines": true }])), + ), + ("", Some(serde_json::json!([{ "max": 0 }]))), + (" ", Some(serde_json::json!([{ "max": 0 }]))), + ( + " + ", + Some(serde_json::json!([{ "max": 0 }])), + ), + ("A", Some(serde_json::json!([{ "max": 0 }]))), + ( + "A + ", + Some(serde_json::json!([{ "max": 0 }])), + ), + ( + "A + ", + Some(serde_json::json!([{ "max": 0 }])), + ), + ( + "A + ", + Some(serde_json::json!([{ "max": 1 }])), + ), + ( + "A + + ", + Some(serde_json::json!([{ "max": 1 }])), + ), + ( + "var a = 'a'; + var x + var c; + console.log", + Some(serde_json::json!([{ "max": 2 }])), + ), + ( + "var a = 'a', + c, + x; +", + Some(serde_json::json!([{ "max": 2 }])), + ), + ( + "var a = 'a', + c, + x; + ", + Some(serde_json::json!([{ "max": 2 }])), + ), + ( + " + + var a = 'a', + c, + x; + ", + Some(serde_json::json!([{ "max": 2, "skipBlankLines": true }])), + ), + ( + "var a = 'a'; + var x + var c; + console.log + // some block + // comments", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "var a = 'a'; + var x + var c; + console.log + /* block comments */", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "var a = 'a'; + var x + var c; + console.log + /* block comments */ + ", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "var a = 'a'; + var x + var c; + console.log + /** block + + comments */", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "var a = 'a'; + + + // comment", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "var a = 'a'; + var x + + + var c; + console.log + + ", + Some(serde_json::json!([{ "max": 2, "skipBlankLines": true }])), + ), + ( + "var a = 'a'; + + + var x + var c; + console.log + + ", + Some(serde_json::json!([{ "max": 2, "skipBlankLines": true }])), + ), + ( + "var a = 'a'; + // + var x + var c; + console.log + //", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + "// hello world + /*hello + world 2 */ + var a, + b + // hh + c, + e, + f;", + Some(serde_json::json!([{ "max": 2, "skipComments": true }])), + ), + ( + " + var x = ''; + + // comment + + var b = '', + c, + d, + e + + // comment", + Some(serde_json::json!([{ "max": 2, "skipComments": true, "skipBlankLines": true }])), + ), + ]; + + Tester::new(MaxLines::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/max_lines.snap b/crates/oxc_linter/src/snapshots/max_lines.snap new file mode 100644 index 000000000..6c4f5cc59 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/max_lines.snap @@ -0,0 +1,293 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: max_lines +--- + ⚠ eslint(max-lines): "File has too many lines (3). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var xyz; + 2 │ │ var xyz; + 3 │ ╰─▶ var xyz; + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (4). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ /* a multiline comment + 2 │ │ that goes to many lines*/ + 3 │ │ var xy; + 4 │ ╰─▶ var xy; + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (3). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ //a single line comment + 2 │ │ var xy; + 3 │ ╰─▶ var xy; + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (5). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var x; + 2 │ │ + 3 │ │ + 4 │ │ + 5 │ ╰─▶ var y; + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (8). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ //a single line comment + 2 │ │ var xy; + 3 │ │ + 4 │ │ var xy; + 5 │ │ + 6 │ │ /* a multiline + 7 │ │ really really + 8 │ ╰─▶ long comment*/ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (3). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var x; // inline comment + 2 │ │ var y; + 3 │ ╰─▶ var z; + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (4). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var x; /* inline comment + 2 │ │ spanning multiple lines */ + 3 │ │ var y; + 4 │ ╰─▶ var z; + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (8). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ //a single line comment + 2 │ │ var xy; + 3 │ │ + 4 │ │ var xy; + 5 │ │ + 6 │ │ /* a multiline + 7 │ │ really really + 8 │ ╰─▶ long comment*/ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (1). Maximum allowed is 0." + ╭─[max_lines.tsx:1:1] + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (1). Maximum allowed is 0." + ╭─[max_lines.tsx:1:1] + 1 │ + · ─ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (2). Maximum allowed is 0." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ + 2 │ ╰─▶ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (1). Maximum allowed is 0." + ╭─[max_lines.tsx:1:1] + 1 │ A + · ─ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (2). Maximum allowed is 0." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ A + 2 │ ╰─▶ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (2). Maximum allowed is 0." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ A + 2 │ ╰─▶ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (2). Maximum allowed is 1." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ A + 2 │ ╰─▶ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (3). Maximum allowed is 1." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ A + 2 │ │ + 3 │ ╰─▶ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (4). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a'; + 2 │ │ var x + 3 │ │ var c; + 4 │ ╰─▶ console.log + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (3). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a', + 2 │ │ c, + 3 │ ╰─▶ x; + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (4). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a', + 2 │ │ c, + 3 │ │ x; + 4 │ ╰─▶ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (6). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ + 2 │ │ + 3 │ │ var a = 'a', + 4 │ │ c, + 5 │ │ x; + 6 │ ╰─▶ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (6). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a'; + 2 │ │ var x + 3 │ │ var c; + 4 │ │ console.log + 5 │ │ // some block + 6 │ ╰─▶ // comments + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (5). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a'; + 2 │ │ var x + 3 │ │ var c; + 4 │ │ console.log + 5 │ ╰─▶ /* block comments */ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (6). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a'; + 2 │ │ var x + 3 │ │ var c; + 4 │ │ console.log + 5 │ │ /* block comments */ + 6 │ ╰─▶ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (7). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a'; + 2 │ │ var x + 3 │ │ var c; + 4 │ │ console.log + 5 │ │ /** block + 6 │ │ + 7 │ ╰─▶ comments */ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (4). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a'; + 2 │ │ + 3 │ │ + 4 │ ╰─▶ // comment + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (8). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a'; + 2 │ │ var x + 3 │ │ + 4 │ │ + 5 │ │ var c; + 6 │ │ console.log + 7 │ │ + 8 │ ╰─▶ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (8). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a'; + 2 │ │ + 3 │ │ + 4 │ │ var x + 5 │ │ var c; + 6 │ │ console.log + 7 │ │ + 8 │ ╰─▶ + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (6). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ var a = 'a'; + 2 │ │ // + 3 │ │ var x + 4 │ │ var c; + 5 │ │ console.log + 6 │ ╰─▶ // + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (9). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ // hello world + 2 │ │ /*hello + 3 │ │ world 2 */ + 4 │ │ var a, + 5 │ │ b + 6 │ │ // hh + 7 │ │ c, + 8 │ │ e, + 9 │ ╰─▶ f; + ╰──── + help: Reduce the number of lines in this file + + ⚠ eslint(max-lines): "File has too many lines (11). Maximum allowed is 2." + ╭─[max_lines.tsx:1:1] + 1 │ ╭─▶ + 2 │ │ var x = ''; + 3 │ │ + 4 │ │ // comment + 5 │ │ + 6 │ │ var b = '', + 7 │ │ c, + 8 │ │ d, + 9 │ │ e + 10 │ │ + 11 │ ╰─▶ // comment + ╰──── + help: Reduce the number of lines in this file