Skip to content

Commit

Permalink
feat(linter): @typescript-eslint/no-namespace (#703)
Browse files Browse the repository at this point in the history
  • Loading branch information
metreniuk authored Aug 9, 2023
1 parent 17acbc4 commit f8358a1
Show file tree
Hide file tree
Showing 6 changed files with 715 additions and 1 deletion.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ mod typescript {
pub mod no_empty_interface;
pub mod no_extra_non_null_assertion;
pub mod no_misused_new;
pub mod no_namespace;
pub mod no_non_null_asserted_optional_chain;
pub mod no_this_alias;
pub mod no_unnecessary_type_constraint;
Expand Down Expand Up @@ -164,6 +165,7 @@ oxc_macros::declare_all_lint_rules! {
typescript::no_unnecessary_type_constraint,
typescript::no_misused_new,
typescript::no_this_alias,
typescript::no_namespace,
typescript::no_var_requires,
jest::no_disabled_tests,
jest::no_test_prefixes,
Expand Down
337 changes: 337 additions & 0 deletions crates/oxc_linter/src/rules/typescript/no_namespace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
use oxc_ast::{
ast::{ModifierKind, TSModuleDeclarationName},
AstKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

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

#[derive(Debug, Error, Diagnostic)]
#[error("ES2015 module syntax is preferred over namespaces.")]
#[diagnostic(severity(warning), help("Replace the namespace with an ES2015 module"))]
struct NoNamespaceDiagnostic(#[label] pub Span);

#[derive(Debug, Default, Clone)]
pub struct NoNamespace {
allow_declarations: bool,
allow_definition_files: bool,
}

declare_oxc_lint!(
/// ### What it does
/// Disallow TypeScript namespaces.
///
/// ### Why is this bad?
/// TypeScript historically allowed a form of code organization called "custom modules" (module Example {}),
/// later renamed to "namespaces" (namespace Example). Namespaces are an outdated way to organize TypeScript code.
/// ES2015 module syntax is now preferred (import/export).
///
/// ### Example
/// ```typescript
/// module foo {}
/// namespace foo {}
/// declare module foo {}
/// declare namespace foo {}
/// ```
NoNamespace,
correctness
);

impl Rule for NoNamespace {
fn from_configuration(value: serde_json::Value) -> Self {
Self {
allow_declarations: value
.get(0)
.and_then(|x| x.get("allowDeclarations"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
allow_definition_files: value
.get(0)
.and_then(|x| x.get("allowDefinitionFiles"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
}
}

fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::TSModuleDeclaration(declaration) = node.kind() else { return };
let TSModuleDeclarationName::Identifier(ident) = &declaration.id else { return };

if ident.name == "global" {
return;
}

if let Some(parent) = ctx.nodes().parent_node(node.id()) {
if let AstKind::TSModuleDeclaration(_) = parent.kind() {
return;
}
}

if self.allow_declarations && is_declaration(node, ctx) {
return;
}

if self.allow_definition_files && ctx.source_type().is_typescript_definition() {
return;
}

ctx.diagnostic(NoNamespaceDiagnostic(declaration.span));
}
}

fn is_declaration(node: &AstNode, ctx: &LintContext) -> bool {
ctx.nodes().iter_parents(node.id()).any(|node| {
let AstKind::TSModuleDeclaration(declaration) = node.kind() else { return false };
declaration.modifiers.contains(ModifierKind::Declare)
})
}

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

let pass = vec![
("declare global {}", None),
("declare module 'foo' {}", None),
("declare module foo {}", Some(serde_json::json!([{ "allowDeclarations": true }]))),
("declare namespace foo {}", Some(serde_json::json!([{ "allowDeclarations": true }]))),
(
"
declare global {
namespace foo {}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
declare module foo {
namespace bar {}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
declare global {
namespace foo {
namespace bar {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
declare namespace foo {
namespace bar {
namespace baz {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
export declare namespace foo {
export namespace bar {
namespace baz {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
];

let fail = vec![
("module foo {}", None),
("namespace foo {}", None),
("module foo {}", Some(serde_json::json!([{ "allowDeclarations": false }]))),
("namespace foo {}", Some(serde_json::json!([{ "allowDeclarations": false }]))),
("module foo {}", Some(serde_json::json!([{ "allowDeclarations": true }]))),
("namespace foo {}", Some(serde_json::json!([{ "allowDeclarations": true }]))),
("declare module foo {}", None),
("declare namespace foo {}", None),
("declare module foo {}", Some(serde_json::json!([{ "allowDeclarations": false }]))),
("declare namespace foo {}", Some(serde_json::json!([{ "allowDeclarations": false }]))),
("namespace Foo.Bar {}", Some(serde_json::json!([{ "allowDeclarations": false }]))),
(
"
namespace Foo.Bar {
namespace Baz.Bas {
interface X {}
}
}
",
None,
),
(
"
namespace A {
namespace B {
declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
namespace A {
namespace B {
export declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
namespace A {
declare namespace B {
namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
namespace A {
export declare namespace B {
namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
namespace A {
export declare namespace B {
declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
namespace A {
export declare namespace B {
export declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
namespace A {
declare namespace B {
export declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
namespace A {
export namespace B {
export declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
export namespace A {
namespace B {
declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
export namespace A {
namespace B {
export declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
export namespace A {
declare namespace B {
namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
export namespace A {
export declare namespace B {
namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
export namespace A {
export declare namespace B {
declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
export namespace A {
export declare namespace B {
export declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
export namespace A {
declare namespace B {
export declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
(
"
export namespace A {
export namespace B {
export declare namespace C {}
}
}
",
Some(serde_json::json!([{ "allowDeclarations": true }])),
),
];

Tester::new(NoNamespace::NAME, pass, fail).test_and_snapshot();
}
Loading

0 comments on commit f8358a1

Please sign in to comment.