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(linter): add oxc/no-optional-chaining rule #3700

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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]
1 │ var x = a?.b
· ────
╰────

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

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

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

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

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

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

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

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

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