Skip to content

Commit

Permalink
feat(linter): implement no-unsafe-declaration-merging (#748)
Browse files Browse the repository at this point in the history
  • Loading branch information
makotot authored Aug 23, 2023
1 parent 2b1e535 commit 9c50bc0
Show file tree
Hide file tree
Showing 3 changed files with 227 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 @@ -86,6 +86,7 @@ mod typescript {
pub mod no_non_null_asserted_optional_chain;
pub mod no_this_alias;
pub mod no_unnecessary_type_constraint;
pub mod no_unsafe_declaration_merging;
pub mod no_var_requires;
pub mod prefer_as_const;
}
Expand Down Expand Up @@ -174,6 +175,7 @@ oxc_macros::declare_all_lint_rules! {
typescript::no_extra_non_null_assertion,
typescript::no_non_null_asserted_optional_chain,
typescript::no_unnecessary_type_constraint,
typescript::no_unsafe_declaration_merging,
typescript::no_misused_new,
typescript::no_this_alias,
typescript::no_namespace,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use oxc_ast::{ast::BindingIdentifier, AstKind};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_semantic::SymbolId;
use oxc_span::Span;

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

#[derive(Debug, Error, Diagnostic)]
#[error("typescript-eslint(no-unsafe-declaration-merging): Unsafe declaration merging between classes and interfaces.")]
#[diagnostic(severity(warning), help("The TypeScript compiler doesn't check whether properties are initialized, which can cause lead to TypeScript not detecting code that will cause runtime errors."))]
struct NoUnsafeDeclarationMergingDiagnostic(#[label] Span, #[label] Span);

#[derive(Debug, Default, Clone)]
pub struct NoUnsafeDeclarationMerging;

declare_oxc_lint!(
/// ### What it does
///
/// Disallow unsafe declaration merging.
///
/// ### Why is this bad?
///
/// Declaration merging between classes and interfaces is unsafe.
/// The TypeScript compiler doesn't check whether properties are initialized, which can cause lead to TypeScript not detecting code that will cause runtime errors.
///
/// ### Example
/// ```javascript
/// interface Foo {}
/// class Foo {}
/// ```
NoUnsafeDeclarationMerging,
correctness
);

impl Rule for NoUnsafeDeclarationMerging {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if !ctx.source_type().is_typescript() {
return;
}

match node.kind() {
AstKind::Class(decl) => {
if let Some(ident) = decl.id.as_ref() {
for (_, symbol_id) in ctx.semantic().scopes().get_bindings(node.scope_id()) {
if let AstKind::TSInterfaceDeclaration(scope_interface) =
get_symbol_kind(*symbol_id, ctx)
{
check_and_diagnostic(ident, &scope_interface.id, ctx);
}
}
}
}
AstKind::TSInterfaceDeclaration(decl) => {
for (_, symbol_id) in ctx.semantic().scopes().get_bindings(node.scope_id()) {
if let AstKind::Class(scope_class) = get_symbol_kind(*symbol_id, ctx) {
if let Some(scope_class_ident) = scope_class.id.as_ref() {
check_and_diagnostic(&decl.id, scope_class_ident, ctx);
}
}
}
}
_ => {}
}
}
}

fn check_and_diagnostic(
ident: &BindingIdentifier,
scope_ident: &BindingIdentifier,
ctx: &LintContext<'_>,
) {
if scope_ident.name.as_str() == ident.name.as_str() {
ctx.diagnostic(NoUnsafeDeclarationMergingDiagnostic(ident.span, scope_ident.span));
}
}

fn get_symbol_kind<'a>(symbol_id: SymbolId, ctx: &LintContext<'a>) -> AstKind<'a> {
return ctx.nodes().get_node(ctx.symbols().get_declaration(symbol_id)).kind();
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
(
"
interface Foo {}
class Bar implements Foo {}
",
None,
),
(
"
namespace Foo {}
namespace Foo {}
",
None,
),
(
"
enum Foo {}
namespace Foo {}
",
None,
),
(
"
namespace Fooo {}
function Foo() {}
",
None,
),
(
"
const Foo = class {};
",
None,
),
(
"
interface Foo {
props: string;
}
function bar() {
return class Foo {};
}
",
None,
),
(
"
interface Foo {
props: string;
}
(function bar() {
class Foo {}
})();
",
None,
),
(
"
declare global {
interface Foo {}
}
class Foo {}
",
None,
),
];

let fail = vec![
(
"
interface Foo {}
class Foo {}
",
None,
),
(
"
class Foo {}
interface Foo {}
",
None,
),
(
"
declare global {
interface Foo {}
class Foo {}
}
",
None,
),
];

Tester::new(NoUnsafeDeclarationMerging::NAME, pass, fail).test_and_snapshot();
}
38 changes: 38 additions & 0 deletions crates/oxc_linter/src/snapshots/no_unsafe_declaration_merging.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
source: crates/oxc_linter/src/tester.rs
expression: no_unsafe_declaration_merging
---
typescript-eslint(no-unsafe-declaration-merging): Unsafe declaration merging between classes and interfaces.
╭─[no_unsafe_declaration_merging.tsx:1:1]
1
2interface Foo {}
· ───
3class Foo {}
· ───
4
╰────
help: The TypeScript compiler doesn't check whether properties are initialized, which can cause lead to TypeScript not detecting code that will cause runtime errors.

typescript-eslint(no-unsafe-declaration-merging): Unsafe declaration merging between classes and interfaces.
╭─[no_unsafe_declaration_merging.tsx:1:1]
1
2class Foo {}
· ───
3interface Foo {}
· ───
4
╰────
help: The TypeScript compiler doesn't check whether properties are initialized, which can cause lead to TypeScript not detecting code that will cause runtime errors.

typescript-eslint(no-unsafe-declaration-merging): Unsafe declaration merging between classes and interfaces.
╭─[no_unsafe_declaration_merging.tsx:2:1]
2declare global {
3interface Foo {}
· ───
4class Foo {}
· ───
5 │ }
╰────
help: The TypeScript compiler doesn't check whether properties are initialized, which can cause lead to TypeScript not detecting code that will cause runtime errors.


0 comments on commit 9c50bc0

Please sign in to comment.