Skip to content

Commit

Permalink
feat(linter): add oxc/no-optional-chaining rule (#3700)
Browse files Browse the repository at this point in the history
To support: vuejs/core#10919
  • Loading branch information
mysteryven committed Jun 17, 2024
1 parent 139adfe commit 9493fbe
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ mod oxc {
pub mod no_async_await;
pub mod no_barrel_file;
pub mod no_const_enum;
pub mod no_optional_chaining;
pub mod no_rest_spread_properties;
pub mod number_arg_out_of_range;
pub mod only_used_in_recursion;
Expand Down Expand Up @@ -739,6 +740,7 @@ oxc_macros::declare_all_lint_rules! {
oxc::const_comparisons,
oxc::double_comparisons,
oxc::erasing_op,
oxc::no_optional_chaining,
oxc::no_rest_spread_properties,
oxc::misrefactored_assign_op,
oxc::missing_throw,
Expand Down
111 changes: 111 additions & 0 deletions crates/oxc_linter/src/rules/oxc/no_optional_chaining.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::{context::LintContext, rule::Rule, AstNode};

fn no_optional_chaining_diagnostic(span0: Span, x1: &str) -> OxcDiagnostic {
if x1.is_empty() {
OxcDiagnostic::warn("oxc(no-optional-chaining): Optional chaining is not allowed.")
.with_labels([span0.into()])
} else {
OxcDiagnostic::warn("oxc(no-optional-chaining): Optional chaining is not allowed.")
.with_help(x1)
.with_labels([span0.into()])
}
}

#[derive(Debug, Default, Clone)]
pub struct NoOptionalChaining(Box<NoOptionalChainingConfig>);

#[derive(Debug, Default, Clone)]
pub struct NoOptionalChainingConfig {
message: String,
}

impl std::ops::Deref for NoOptionalChaining {
type Target = NoOptionalChainingConfig;

fn deref(&self) -> &Self::Target {
&self.0
}
}

declare_oxc_lint!(
/// ### What it does
///
/// Disallow [optional chaining](https://github.com/tc39/proposal-optional-chaining).
///
/// ### Example
///
/// ```javascript
/// const foo = obj?.foo;
/// obj.fn?.();
/// ```
///
/// ### Options
///
/// ```json
/// {
/// "rules": {
/// "no-optional-chaining": [
/// "error",
/// {
/// "message": "Our output target is ES2016, and optional chaining results in verbose
/// helpers and should be avoided.",
/// }
/// ]
/// }
/// }
/// ```
///
/// - `message`: A custom help message to display when optional chaining is found.
///
NoOptionalChaining,
restriction,
);

impl Rule for NoOptionalChaining {
fn from_configuration(value: serde_json::Value) -> Self {
let config = value.get(0);
let message = config
.and_then(|v| v.get("message"))
.and_then(serde_json::Value::as_str)
.unwrap_or_default();

Self(Box::new(NoOptionalChainingConfig { message: message.to_string() }))
}

fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::ChainExpression(expr) = node.kind() {
ctx.diagnostic(no_optional_chaining_diagnostic(expr.span, &self.message));
}
}
}

// Test cases port from: https://github.com/mysticatea/eslint-plugin-es/blob/v4.1.0/tests/lib/rules/no-optional-chaining.js
#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![("var x = a.b", None), ("var x = a[b]", None), ("foo()", None)];

let fail = vec![
("var x = a?.b", None),
("var x = a?.[b]", None),
("foo?.()", None),
("var x = ((a?.b)?.c)?.()", None),
("var x = a/*?.*/?.b", None),
("var x = '?.'?.['?.']", None),
("var x = '?.'?.['?.']", None),
(
"var x = a?.b",
Some(serde_json::json!([{
"message": "Our output target is ES2016, and optional chaining results in verbose helpers and should be avoided."
}])),
),
];

Tester::new(NoOptionalChaining::NAME, pass, fail).test_and_snapshot();
}
64 changes: 64 additions & 0 deletions crates/oxc_linter/src/snapshots/no_optional_chaining.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
source: crates/oxc_linter/src/tester.rs
expression: no_optional_chaining
---
oxc(no-optional-chaining): Optional chaining is not allowed.
╭─[no_optional_chaining.tsx:1:9]
1var x = a?.b
· ────
╰────

oxc(no-optional-chaining): Optional chaining is not allowed.
╭─[no_optional_chaining.tsx:1:9]
1var x = a?.[b]
· ──────
╰────

oxc(no-optional-chaining): Optional chaining is not allowed.
╭─[no_optional_chaining.tsx:1:1]
1foo?.()
· ───────
╰────

oxc(no-optional-chaining): Optional chaining is not allowed.
╭─[no_optional_chaining.tsx:1:9]
1var x = ((a?.b)?.c)?.()
· ───────────────
╰────

oxc(no-optional-chaining): Optional chaining is not allowed.
╭─[no_optional_chaining.tsx:1:10]
1var x = ((a?.b)?.c)?.()
· ─────────
╰────

oxc(no-optional-chaining): Optional chaining is not allowed.
╭─[no_optional_chaining.tsx:1:11]
1var x = ((a?.b)?.c)?.()
· ────
╰────

oxc(no-optional-chaining): Optional chaining is not allowed.
╭─[no_optional_chaining.tsx:1:9]
1var x = a/*?.*/?.b
· ──────────
╰────

oxc(no-optional-chaining): Optional chaining is not allowed.
╭─[no_optional_chaining.tsx:1:9]
1var x = '?.'?.['?.']
· ────────────
╰────

oxc(no-optional-chaining): Optional chaining is not allowed.
╭─[no_optional_chaining.tsx:1:9]
1var x = '?.'?.['?.']
· ────────────
╰────

oxc(no-optional-chaining): Optional chaining is not allowed.
╭─[no_optional_chaining.tsx:1:9]
1var x = a?.b
· ────
╰────
help: Our output target is ES2016, and optional chaining results in verbose helpers and should be avoided.

0 comments on commit 9493fbe

Please sign in to comment.