Skip to content
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
1 change: 1 addition & 0 deletions apps/oxlint/fixtures/tsgolint/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"typescript/only-throw-error": "error",
"typescript/prefer-includes": "error",
"typescript/prefer-nullish-coalescing": "error",
"typescript/prefer-optional-chain": "error",
"typescript/prefer-promise-reject-errors": "error",
"typescript/prefer-reduce-type-parameter": "error",
"typescript/prefer-return-this-type": "error",
Expand Down
3 changes: 3 additions & 0 deletions apps/oxlint/fixtures/tsgolint/prefer-optional-chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// prefer-optional-chain: The pattern `foo && foo.bar` should use optional chaining `foo?.bar`
declare const fooOptC: { bar: number } | null | undefined;
fooOptC && fooOptC.bar;
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ arguments: --type-aware --silent
working directory: fixtures/tsgolint
----------

Found 0 warnings and 51 errors.
Finished in <variable>ms on 45 files with 44 rules using 1 threads.
Found 0 warnings and 52 errors.
Finished in <variable>ms on 46 files with 45 rules using 1 threads.
----------
CLI result: LintFoundErrors
----------
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ working directory: fixtures/tsgolint
help: Remove the debugger statement

Found 2 warnings and 2 errors.
Finished in <variable>ms on 45 files with 1 rules using 1 threads.
Finished in <variable>ms on 46 files with 1 rules using 1 threads.
----------
CLI result: LintFoundErrors
----------
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ working directory: fixtures/tsgolint
help: Remove the debugger statement

Found 0 warnings and 1 error.
Finished in <variable>ms on 1 file with 44 rules using 1 threads.
Finished in <variable>ms on 1 file with 45 rules using 1 threads.
----------
CLI result: LintFoundErrors
----------
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ working directory: fixtures/tsgolint
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`----

x typescript-eslint(prefer-optional-chain): Prefer using an optional chain expression instead, as it's more concise and easier to read.
,-[prefer-optional-chain.ts:3:1]
2 | declare const fooOptC: { bar: number } | null | undefined;
3 | fooOptC && fooOptC.bar;
: ^^^^^^^^^^^^^^^^^^^^^^
`----

x typescript-eslint(prefer-promise-reject-errors): Expected the Promise rejection reason to be an Error.
,-[prefer-promise-reject-errors.ts:1:1]
1 | Promise.reject('error');
Expand Down Expand Up @@ -396,8 +403,8 @@ working directory: fixtures/tsgolint
: ^^^^^^^^
`----

Found 0 warnings and 51 errors.
Finished in <variable>ms on 45 files with 44 rules using 1 threads.
Found 0 warnings and 52 errors.
Finished in <variable>ms on 46 files with 45 rules using 1 threads.
----------
CLI result: LintFoundErrors
----------
5 changes: 5 additions & 0 deletions crates/oxc_linter/src/generated/rule_runner_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3098,6 +3098,11 @@ impl RuleRunner for crate::rules::typescript::prefer_nullish_coalescing::PreferN
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown;
}

impl RuleRunner for crate::rules::typescript::prefer_optional_chain::PreferOptionalChain {
const NODE_TYPES: Option<&AstTypesBitset> = None;
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown;
}

impl RuleRunner
for crate::rules::typescript::prefer_promise_reject_errors::PreferPromiseRejectErrors
{
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ pub(crate) mod typescript {
pub mod prefer_literal_enum_member;
pub mod prefer_namespace_keyword;
pub mod prefer_nullish_coalescing;
pub mod prefer_optional_chain;
pub mod prefer_promise_reject_errors;
pub mod prefer_reduce_type_parameter;
pub mod prefer_return_this_type;
Expand Down Expand Up @@ -1202,6 +1203,7 @@ oxc_macros::declare_all_lint_rules! {
typescript::prefer_literal_enum_member,
typescript::prefer_namespace_keyword,
typescript::prefer_nullish_coalescing,
typescript::prefer_optional_chain,
typescript::prefer_promise_reject_errors,
typescript::prefer_reduce_type_parameter,
typescript::prefer_return_this_type,
Expand Down
107 changes: 107 additions & 0 deletions crates/oxc_linter/src/rules/typescript/prefer_optional_chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use oxc_macros::declare_oxc_lint;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::rule::{DefaultRuleConfig, Rule};

#[derive(Debug, Default, Clone, Deserialize)]
pub struct PreferOptionalChain(Box<PreferOptionalChainConfig>);

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct PreferOptionalChainConfig {
/// Allow autofixers that will change the return type of the expression.
/// This option is considered unsafe as it may break the build.
pub allow_potentially_unsafe_fixes_that_modify_the_return_type_i_know_what_im_doing: bool,

/// Check operands that are typed as `any` when inspecting "loose boolean" operands.
pub check_any: bool,

/// Check operands that are typed as `bigint` when inspecting "loose boolean" operands.
pub check_big_int: bool,

/// Check operands that are typed as `boolean` when inspecting "loose boolean" operands.
pub check_boolean: bool,

/// Check operands that are typed as `number` when inspecting "loose boolean" operands.
pub check_number: bool,

/// Check operands that are typed as `string` when inspecting "loose boolean" operands.
pub check_string: bool,

/// Check operands that are typed as `unknown` when inspecting "loose boolean" operands.
pub check_unknown: bool,

/// Skip operands that are not typed with `null` and/or `undefined` when inspecting
/// "loose boolean" operands.
pub require_nullish: bool,
}

impl Default for PreferOptionalChainConfig {
fn default() -> Self {
Self {
allow_potentially_unsafe_fixes_that_modify_the_return_type_i_know_what_im_doing: false,
check_any: true,
check_big_int: true,
check_boolean: true,
check_number: true,
check_string: true,
check_unknown: true,
require_nullish: false,
}
}
}

declare_oxc_lint!(
/// ### What it does
///
/// Enforce using concise optional chain expressions instead of chained logical AND
/// operators, negated logical OR operators, or empty objects.
///
/// ### Why is this bad?
///
/// TypeScript 3.7 introduced optional chaining (`?.`) which provides a more concise
/// and readable way to access properties on potentially nullish values. Using optional
/// chaining instead of logical AND chains (`&&`) or other patterns improves code clarity.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```ts
/// foo && foo.bar;
/// foo && foo.bar && foo.bar.baz;
/// foo && foo['bar'];
/// foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz;
/// foo && foo.bar && foo.bar.baz.buzz;
/// foo && foo.bar.baz && foo.bar.baz.buzz;
/// (foo || {}).bar;
/// ```
///
/// Examples of **correct** code for this rule:
/// ```ts
/// foo?.bar;
/// foo?.bar?.baz;
/// foo?.['bar'];
/// foo?.bar?.baz?.buzz;
/// foo?.bar?.baz.buzz;
/// foo?.bar.baz?.buzz;
/// foo?.bar;
/// ```
PreferOptionalChain(tsgolint),
typescript,
style,
fix,
config = PreferOptionalChainConfig,
);

impl Rule for PreferOptionalChain {
fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
.unwrap_or_default()
.into_inner())
}

fn to_configuration(&self) -> Option<Result<serde_json::Value, serde_json::Error>> {
Some(serde_json::to_value(&*self.0))
}
}
Loading