Skip to content

Commit

Permalink
feat(linter): implement useThrowOnlyError (#2915)
Browse files Browse the repository at this point in the history
  • Loading branch information
minht11 authored May 22, 2024
1 parent f30f766 commit b2fe7b4
Show file tree
Hide file tree
Showing 13 changed files with 758 additions and 6 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b

#### New features

- Add [nursery/useImportExtensions](https://biomejs.dev/linter/rules/use-import-extensions/).
Contributed by @minht11
- Add [nursery/useThrowOnlyError](https://biomejs.dev/linter/rules/use_throw_only_error/). Contributed by @minht11
- Add [nursery/useImportExtensions](https://biomejs.dev/linter/rules/use-import-extensions/). Contributed by @minht11

- [useNamingConvention](https://biomejs.dev/linter/rules/use-naming-convention/) now supports an option to enforce custom conventions ([#1900](https://github.com/biomejs/biome/issues/1900)).

Expand Down
40 changes: 38 additions & 2 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.

28 changes: 26 additions & 2 deletions crates/biome_configuration/src/linter/rules.rs

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

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 @@ -150,6 +150,7 @@ define_categories! {
"lint/nursery/useSemanticElements": "https://biomejs.dev/linter/rules/use-semantic-elements",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
"lint/nursery/useThrowNewError": "https://biomejs.dev/linter/rules/use-throw-new-error",
"lint/nursery/useThrowOnlyError": "https://biomejs.dev/linter/rules/use-throw-only-error",
"lint/nursery/useTopLevelRegex": "https://biomejs.dev/linter/rules/use-top-level-regex",
"lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread",
"lint/performance/noBarrelFile": "https://biomejs.dev/linter/rules/no-barrel-file",
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.

140 changes: 140 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/use_throw_only_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use biome_analyze::{
context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource, RuleSourceKind,
};
use biome_console::markup;
use biome_js_syntax::{
AnyJsExpression, AnyJsLiteralExpression, JsLogicalExpression, JsObjectExpression, JsSyntaxKind,
JsThrowStatement,
};
use biome_rowan::AstNode;

declare_rule! {
/// Disallow throwing non-`Error` values.
///
/// It is considered good practice only to throw the `Error` object itself or an object using the `Error` object
/// as base objects for user-defined exceptions. The fundamental benefit of `Error` objects is that they automatically
/// keep track of where they were built and originated.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// throw undefined;
/// ```
/// ```js,expect_diagnostic
/// throw false;
/// ```
/// ```js,expect_diagnostic
/// throw "a" + "b";
/// ```
///
/// ### Valid
///
/// ```js
/// throw new Error();
/// ```
/// ```js
/// throw new TypeError('biome');
/// ```
/// ```js
/// class CustomError extends Error {}
///
/// throw new CustomError();
/// ```
///
/// ## Caveats
///
/// This rule only covers cases where throwing the value can be known statically.
/// Complex cases such as object and function access aren't checked.
/// This will be improved in the future once Biome supports type inference.
///
pub UseThrowOnlyError {
version: "next",
name: "useThrowOnlyError",
language: "js",
sources: &[RuleSource::Eslint("no-throw-literal"), RuleSource::EslintTypeScript("no-throw-literal"), RuleSource::Eslint("only-throw-error")],
source_kind: RuleSourceKind::Inspired,
recommended: false,
}
}

impl Rule for UseThrowOnlyError {
type Query = Ast<JsThrowStatement>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let expr = node.argument().ok()?.omit_parentheses();

is_invalid_throw_value(&expr).and(Some(()))
}

fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();

Some(RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"Throwing non-"<Emphasis>"Error"</Emphasis>" values is not allowed."
},
).note(markup! {
"While Javascript supports throwing any value, handling non-"<Emphasis>"Error"</Emphasis>" values is confusing."
}))
}
}

fn is_invalid_throw_value(any_expr: &AnyJsExpression) -> Option<bool> {
let kind = any_expr.syntax().kind();

if AnyJsLiteralExpression::can_cast(kind)
|| JsObjectExpression::can_cast(kind)
|| matches!(
kind,
JsSyntaxKind::JS_BINARY_EXPRESSION | JsSyntaxKind::JS_TEMPLATE_EXPRESSION
)
{
return Some(true);
}

if let Some(logical_expr) = JsLogicalExpression::cast_ref(any_expr.syntax()) {
let left = &logical_expr.left().ok()?;

// This will produce some false positives, but having a logical expression
// as a throw value is not a good practice anyway.
return is_invalid_throw_value(left).or_else(|| {
let right = logical_expr.right().ok()?;

is_invalid_throw_value(&right)
});
}

if let Some(assignment_expr) = any_expr.as_js_assignment_expression() {
return is_invalid_throw_value(&assignment_expr.right().ok()?.omit_parentheses());
}

if let Some(expr) = any_expr.as_js_sequence_expression() {
return is_invalid_throw_value(&expr.right().ok()?);
}

if let Some(expr) = any_expr.as_js_conditional_expression() {
let consequent = expr.consequent().ok()?;

return is_invalid_throw_value(&consequent).or_else(|| {
let alternate = expr.alternate().ok()?;

is_invalid_throw_value(&alternate)
});
}

if let Some(identifier) = any_expr.as_js_reference_identifier() {
if identifier.is_undefined() {
return Some(true);
}
}

None
}
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,24 @@
throw undefined;
throw "error";
throw 0;
throw false;
throw null;
throw {};
throw "a" + "b";
const a = "";
throw a + "b";
let foo;
throw (foo = "error");
throw (new Error(), 1, 2, 3);
throw "literal" && "not an Error";
throw "literal" || new Error();
throw new Error() && "literal";
throw "literal" ?? new Error();
throw foo ? "not an Error" : "literal";
throw foo ? new Error() : "literal";
throw foo ? "literal" : new Error();
throw `${foo}`;

// False positives while valid, not a good practice.
throw "literal" && new Error();
throw new Error() || "literal";
Loading

0 comments on commit b2fe7b4

Please sign in to comment.