Skip to content

Commit

Permalink
feat: add noIrregularWhitespace rule (#3333)
Browse files Browse the repository at this point in the history
  • Loading branch information
michellocana authored Jul 11, 2024
1 parent 6163e58 commit 4284b19
Show file tree
Hide file tree
Showing 16 changed files with 1,456 additions and 117 deletions.
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>>()
}
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} */ constfoo='thing';
/* \u{202f} */ const foo ='thing';
/* \u{205f} */ const foo ='thing';
/* \u{3000} */ const foo = 'thing';
Loading

0 comments on commit 4284b19

Please sign in to comment.