feat(linter): eslint/max-lines (#2739)

Relates to #479 

Rule detail: https://eslint.org/docs/latest/rules/max-lines
This commit is contained in:
Andi Pabst 2024-03-17 11:59:47 +01:00 committed by GitHub
parent 0623a5335f
commit 81752b2790
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 708 additions and 0 deletions

View file

@ -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,

View file

@ -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<MaxLinesConfig>);
#[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 whats 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();
}

View file

@ -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