Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add noIrregularWhitespace rule #3333

Merged
merged 19 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
80de83c
feat(lint/noIrregularWhitespace): add basic rule implementation
michellocana Jun 17, 2024
9087a5b
feat(lint/noIrregularWhitespace): update snapshots with correct error…
michellocana Jun 17, 2024
542353b
feat(lint/noIrregularWhitespace): improve diagnostics of all validate…
michellocana Jun 20, 2024
888be03
feat(lint/noIrregularWhitespace): update biome_json_parser test snapshot
michellocana Jun 20, 2024
7c7da0c
feat(lint/noIrregularWhitespace): improve error message
michellocana Jun 24, 2024
a3f6ecf
feat(lint/noIrregularWhitespace): add valid cases
michellocana Jul 1, 2024
1165db6
feat(lint/noIrregularWhitespace): improve documentation
michellocana Jul 1, 2024
2771b1d
feat(lint/noIrregularWhitespace): improve code formatting
michellocana Jul 1, 2024
4e7b184
feat(lint/noIrregularWhitespace): fix errors from cargo lint
michellocana Jul 1, 2024
5891e60
feat(lint/noIrregularWhitespace): improve whitespace validation
michellocana Jul 3, 2024
b57ba9f
feat(lint/noIrregularWhitespace): fix declare_lint_rule macro usage
michellocana Jul 4, 2024
c5ce6d1
feat(lint/noIrregularWhitespace): remove useless explicit type on trivia
michellocana Jul 4, 2024
d6787af
feat(lint/noIrregularWhitespace): improve whitespace range display
michellocana Jul 5, 2024
cceae35
feat(lint/noIrregularWhitespace): remove dbg call
michellocana Jul 5, 2024
d13fba1
feat(lint/noIrregularWhitespace): add rule source
michellocana Jul 5, 2024
3f77863
feat(lint/noIrregularWhitespace): run gen-lint
michellocana Jul 5, 2024
b709461
feat(lint/noIrregularWhitespace): improve rule performance
michellocana Jul 7, 2024
72395e6
feat(lint/noIrregularWhitespace): replace then_some with then
michellocana Jul 7, 2024
1229ea2
feat(lint/noIrregularWhitespace): replace chain with a normal for loop
michellocana Jul 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

187 changes: 103 additions & 84 deletions crates/biome_configuration/src/linter/rules.rs

Large diffs are not rendered by default.

72 changes: 51 additions & 21 deletions crates/biome_diagnostics/src/display/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,8 @@ pub(super) fn print_frame(fmt: &mut fmt::Formatter<'_>, location: Location<'_>)
match c {
'\t' => fmt.write_str("\t")?,
_ => {
if let Some(width) = c.width() {
for _ in 0..width {
fmt.write_str(" ")?;
}
for _ in 0..char_width(c) {
fmt.write_str(" ")?;
}
}
}
Expand Down Expand Up @@ -338,20 +336,35 @@ pub(super) fn calculate_print_width(mut value: OneIndexed) -> NonZeroUsize {
width
}

/// Compute the unicode display width of a string, with the width of tab
/// characters set to [TAB_WIDTH] and the width of control characters set to 0
pub(super) fn text_width(text: &str) -> usize {
text.chars().map(char_width).sum()
}

/// We need to set a value here since we have no way of knowing what the user's
/// preferred tab display width is, so this is set to `2` to match how tab
/// characters are printed by [print_invisibles]
const TAB_WIDTH: usize = 2;

/// Compute the unicode display width of a string, with the width of tab
/// characters set to [TAB_WIDTH] and the width of control characters set to 0
pub(super) fn text_width(text: &str) -> usize {
text.chars()
.map(|char| match char {
'\t' => TAB_WIDTH,
_ => char.width().unwrap_or(0),
})
.sum()
/// Some esoteric space characters don't return a width using `char.width()`, so
/// we need to assume a fixed length for them
const ESOTERIC_SPACE_WIDTH: usize = 1;

/// Return the width of characters, treating whitespace characters in the way
/// we need to properly display it
pub(super) fn char_width(char: char) -> usize {
match char {
'\t' => TAB_WIDTH,
'\u{c}' => ESOTERIC_SPACE_WIDTH,
'\u{b}' => ESOTERIC_SPACE_WIDTH,
'\u{85}' => ESOTERIC_SPACE_WIDTH,
'\u{feff}' => ESOTERIC_SPACE_WIDTH,
'\u{180e}' => ESOTERIC_SPACE_WIDTH,
'\u{200b}' => ESOTERIC_SPACE_WIDTH,
'\u{3000}' => ESOTERIC_SPACE_WIDTH,
_ => char.width().unwrap_or(0),
}
}

pub(super) struct PrintInvisiblesOptions {
Expand Down Expand Up @@ -462,14 +475,31 @@ pub(super) fn print_invisibles(

fn show_invisible_char(char: char) -> Option<&'static str> {
match char {
' ' => Some("\u{b7}"), // Middle Dot
'\r' => Some("\u{240d}"), // Carriage Return Symbol
'\n' => Some("\u{23ce}"), // Return Symbol
'\t' => Some("\u{2192} "), // Rightwards Arrow
'\0' => Some("\u{2400}"), // Null Symbol
'\x0b' => Some("\u{240b}"), // Vertical Tabulation Symbol
'\x08' => Some("\u{232b}"), // Backspace Symbol
'\x0c' => Some("\u{21a1}"), // Downards Two Headed Arrow
' ' => Some("\u{b7}"), // Middle Dot
'\r' => Some("\u{240d}"), // Carriage Return Symbol
'\n' => Some("\u{23ce}"), // Return Symbol
'\t' => Some("\u{2192} "), // Rightwards Arrow
'\0' => Some("\u{2400}"), // Null Symbol
'\x0b' => Some("\u{240b}"), // Vertical Tabulation Symbol
'\x08' => Some("\u{232b}"), // Backspace Symbol
'\x0c' => Some("\u{21a1}"), // Downwards Two Headed Arrow
'\u{85}' => Some("\u{2420}"), // Space Symbol
'\u{a0}' => Some("\u{2420}"), // Space Symbol
'\u{1680}' => Some("\u{2420}"), // Space Symbol
'\u{2000}' => Some("\u{2420}"), // Space Symbol
'\u{2001}' => Some("\u{2420}"), // Space Symbol
'\u{2002}' => Some("\u{2420}"), // Space Symbol
'\u{2003}' => Some("\u{2420}"), // Space Symbol
'\u{2004}' => Some("\u{2420}"), // Space Symbol
'\u{2005}' => Some("\u{2420}"), // Space Symbol
'\u{2006}' => Some("\u{2420}"), // Space Symbol
'\u{2007}' => Some("\u{2420}"), // Space Symbol
'\u{2008}' => Some("\u{2420}"), // Space Symbol
'\u{2009}' => Some("\u{2420}"), // Space Symbol
'\u{200a}' => Some("\u{2420}"), // Space Symbol
'\u{202f}' => Some("\u{2420}"), // Space Symbol
'\u{205f}' => Some("\u{2420}"), // Space Symbol
'\u{3000}' => Some("\u{2420}"), // Space Symbol
_ => None,
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ define_categories! {
"lint/nursery/noImportantInKeyframe": "https://biomejs.dev/linter/rules/no-important-in-keyframe",
"lint/nursery/noInvalidDirectionInLinearGradient": "https://biomejs.dev/linter/rules/no-invalid-direction-in-linear-gradient",
"lint/nursery/noInvalidPositionAtImportRule": "https://biomejs.dev/linter/rules/no-invalid-position-at-import-rule",
"lint/nursery/noIrregularWhitespace": "https://biomejs.dev/linter/rules/no-irregular-whitespace",
"lint/nursery/noLabelWithoutControl": "https://biomejs.dev/linter/rules/no-label-without-control",
"lint/nursery/noMisplacedAssertion": "https://biomejs.dev/linter/rules/no-misplaced-assertion",
"lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 112 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_irregular_whitespace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use biome_analyze::{context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic};
use biome_analyze::{RuleSource, RuleSourceKind};
use biome_console::markup;
use biome_js_syntax::{JsLanguage, JsModule};
use biome_rowan::{AstNode, Direction, SyntaxTriviaPiece, TextRange};

const IRREGULAR_WHITESPACES: &[char; 22] = &[
'\u{c}', '\u{b}', '\u{85}', '\u{feff}', '\u{a0}', '\u{1680}', '\u{180e}', '\u{2000}',
'\u{2001}', '\u{2002}', '\u{2003}', '\u{2004}', '\u{2005}', '\u{2006}', '\u{2007}', '\u{2008}',
'\u{2009}', '\u{200a}', '\u{200b}', '\u{202f}', '\u{205f}', '\u{3000}',
];

declare_lint_rule! {
/// Disallows the use of irregular whitespace characters.
///
/// Invalid or irregular whitespace causes issues with ECMAScript 5 parsers and also makes code harder to debug.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// const count = 1;
/// ```
///
/// ```js,expect_diagnostic
/// const foo = 'thing';
/// ```
///
/// ### Valid
///
/// ```js
/// const count = 1;
/// ```
///
/// ```js
/// const foo = ' ';
/// ```
///
pub NoIrregularWhitespace {
version: "next",
name: "noIrregularWhitespace",
language: "js",
recommended: false,
sources: &[RuleSource::Eslint("no-irregular-whitespace")],
source_kind: RuleSourceKind::SameLogic,
}
}

impl Rule for NoIrregularWhitespace {
type Query = Ast<JsModule>;
type State = TextRange;
type Signals = Vec<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
get_irregular_whitespace(node)
}

fn diagnostic(_ctx: &RuleContext<Self>, range: &Self::State) -> Option<RuleDiagnostic> {
Some(
RuleDiagnostic::new(
rule_category!(),
range,
markup! {
"Irregular whitespaces found."
},
)
.note(markup! {
"Replace the irregular whitespaces with normal whitespaces or tabs."
}),
)
}
}

fn get_irregular_whitespace(node: &JsModule) -> Vec<TextRange> {
let syntax = node.syntax();
let mut all_whitespaces_trivia: Vec<SyntaxTriviaPiece<JsLanguage>> = vec![];
let is_whitespace = |trivia: &SyntaxTriviaPiece<JsLanguage>| {
trivia.is_whitespace() && !trivia.text().replace(' ', "").is_empty()
};

for token in syntax.descendants_tokens(Direction::Next) {
let leading_trivia_pieces = token.leading_trivia().pieces();
let trailing_trivia_pieces = token.trailing_trivia().pieces();

for trivia in leading_trivia_pieces {
if is_whitespace(&trivia) {
all_whitespaces_trivia.push(trivia);
}
}

for trivia in trailing_trivia_pieces {
if is_whitespace(&trivia) {
all_whitespaces_trivia.push(trivia);
}
}
}

all_whitespaces_trivia
.iter()
.filter_map(|trivia| {
let has_irregular_whitespace = trivia.text().chars().any(|char| {
IRREGULAR_WHITESPACES
.iter()
.any(|irregular_whitespace| &char == irregular_whitespace)
});
has_irregular_whitespace.then(|| trivia.text_range())
})
.collect::<Vec<TextRange>>()
ematipico marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/options.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* \u{b} */ const foo = 'thing';
/* \u{c} */ const foo = 'thing';
/* \u{feff} */ constfoo='thing';
/* \u{a0} */ const foo = 'thing';
/* \u{1680} */ const foo = 'thing';
/* \u{2000} */ const foo = 'thing';
/* \u{2001} */ const foo = 'thing';
/* \u{2002} */ const foo = 'thing';
/* \u{2003} */ const foo = 'thing';
/* \u{2004} */ const foo = 'thing';
/* \u{2005} */ const foo = 'thing';
/* \u{2006} */ const foo = 'thing';
/* \u{2007} */ const foo = 'thing';
/* \u{2008} */ const foo = 'thing';
/* \u{2009} */ const foo = 'thing';
/* \u{200a} */ const foo = 'thing';
/* \u{200b} */ const​foo​=​'thing';
/* \u{202f} */ const foo = 'thing';
/* \u{205f} */ const foo = 'thing';
/* \u{3000} */ const foo = 'thing';
Loading