Skip to content

Commit

Permalink
feat(lint/useExportType): add rule
Browse files Browse the repository at this point in the history
  • Loading branch information
Conaclos committed Aug 21, 2023
1 parent b569e25 commit d987fd4
Show file tree
Hide file tree
Showing 20 changed files with 862 additions and 36 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@ New entries must be placed in a section entitled `Unreleased`.

This rule disallows useless `export {}`.

- Add [`useExportType`](https://docs.rome.tools/lint/rules/useExportType/)

This rule requires the usage of `export type` when the exported element is only a type.

- Add [useIsArray](https://biomejs.dev/lint/rules/useIsArray/)

This rule proposes using `Array.isArray()` instead of `instanceof Array`.
Expand Down
1 change: 1 addition & 0 deletions crates/rome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ define_categories! {
"lint/nursery/useAriaPropTypes": "https://biomejs.dev/lint/rules/useAriaPropTypes",
"lint/nursery/useArrowFunction": "https://biomejs.dev/lint/rules/useArrowFunction",
"lint/nursery/useExhaustiveDependencies": "https://biomejs.dev/lint/rules/useExhaustiveDependencies",
"lint/nursery/useExportType": "https://biomejs.dev/lint/rules/useExportType",
"lint/nursery/useGetterReturn": "https://biomejs.dev/lint/rules/useGetterReturn",
"lint/nursery/useGroupedTypeImport": "https://biomejs.dev/lint/rules/useGroupedTypeImport",
"lint/nursery/useHookAtTopLevel": "https://biomejs.dev/lint/rules/useHookAtTopLevel",
Expand Down
2 changes: 2 additions & 0 deletions crates/rome_js_analyze/src/semantic_analyzers/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use crate::{semantic_services::Semantic, JsRuleAction};
use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Rule, RuleDiagnostic};
use rome_console::markup;
use rome_diagnostics::Applicability;
use rome_js_factory::make;
use rome_js_syntax::{
AnyJsExportNamedSpecifier, JsExportNamedClause, JsExportNamedSpecifierList, JsFileSource, T,
};
use rome_rowan::{AstNode, AstSeparatedList, BatchMutationExt, TextRange, TriviaPieceKind};

declare_rule! {
/// Promotes the use of `export type` for type-only types.
///
/// _TypeScript_ allows specifying a `type` keyword on an `export` to indicate that
/// the `export` doesn't exist at runtime.
/// This will enable transpilers to safely drop exports of types without looking for its definition.
///
/// ## Examples
///
/// ### Invalid
///
/// ```ts,expect_diagnostic
/// interface I {}
/// export { I };
/// ```
///
/// ```ts,expect_diagnostic
/// type T = number;
/// export { T };
/// ```
///
/// ```ts,expect_diagnostic
/// import type { T } from "./mod.js";
/// export { T };
/// ```
///
/// ## Valid
///
/// ```js
/// class C {}
/// function f() {}
/// export { C, f };
/// ```
///
/// This rules applies only to identifiers locally defined.
/// It doesn't warn against a type exported as a value in re-export clause such as:
///
/// ```ts
/// export { TypeA } from "./mod.ts"
/// ```
pub(crate) UseExportType {
version: "next",
name: "useExportType",
recommended: true,
}
}

impl Rule for UseExportType {
type Query = Semantic<AnyJsExportNamedSpecifier>;
type State = TextRange;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let source_type = ctx.source_type::<JsFileSource>();
if !source_type.language().is_typescript() {
return None;
}
let specifier = ctx.query();
let model = ctx.model();
if specifier.is_type_only() {
return None;
}
let reference = specifier.local_name().ok()?;
let binding = model.binding(&reference)?;
let binding = binding.tree();
if binding.is_type_only() {
return Some(binding.range());
}
None
}

fn diagnostic(ctx: &RuleContext<Self>, declaration: &Self::State) -> Option<RuleDiagnostic> {
Some(
RuleDiagnostic::new(
rule_category!(),
ctx.query().range(),
markup! {
"This export is only a type and should thus use "<Emphasis>"export type"</Emphasis>"."
},
).detail(declaration, markup! {
"The type is defined here."
})
)
}

fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
let specifier = ctx.query();
let mut mutation = ctx.root().begin();
let type_token =
Some(make::token(T![type]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]));
if let Some(specifier_list) = specifier.parent::<JsExportNamedSpecifierList>() {
if specifier_list.iter().count() == 1 {
if let Some(export) = specifier_list.parent::<JsExportNamedClause>() {
let new_export = export.clone().with_type_token(type_token);
mutation.replace_node(export, new_export);
return Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::Always,
message: markup! { "Use "<Emphasis>"export type"</Emphasis>"." }.to_owned(),
mutation,
});
}
}
}
mutation.replace_node_discard_trivia(
specifier.clone(),
specifier.clone().with_type_token(type_token),
);
Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::Always,
message: markup! { "Use an inline type export." }.to_owned(),
mutation,
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type T1, V1 } from "./mod.ts";
export { T1, V1 };

import type { T2, T3 } from "./mod.ts";
export { T2, T3 };

import type T4 from "./mod.ts";
export { T4 };

import type * as ns from "./mod.ts";
export { ns };

interface Interface {}
type TypeAlias = {}
enum Enum {}
function func() {}
class Class {}
export { Interface, TypeAlias, Enum, func as f, Class };
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
---
source: crates/rome_js_analyze/tests/spec_tests.rs
expression: invalid.ts
---
# Input
```js
import { type T1, V1 } from "./mod.ts";
export { T1, V1 };

import type { T2, T3 } from "./mod.ts";
export { T2, T3 };

import type T4 from "./mod.ts";
export { T4 };

import type * as ns from "./mod.ts";
export { ns };

interface Interface {}
type TypeAlias = {}
enum Enum {}
function func() {}
class Class {}
export { Interface, TypeAlias, Enum, func as f, Class };

```

# Diagnostics
```
invalid.ts:2:10 lint/nursery/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! This export is only a type and should thus use export type.
1 │ import { type T1, V1 } from "./mod.ts";
> 2 │ export { T1, V1 };
│ ^^
3 │
4 │ import type { T2, T3 } from "./mod.ts";
i The type is defined here.
> 1 │ import { type T1, V1 } from "./mod.ts";
│ ^^
2 │ export { T1, V1 };
3 │
i Safe fix: Use an inline type export.
2 │ export·{·type·T1,·V1·};
│ +++++
```

```
invalid.ts:5:10 lint/nursery/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! This export is only a type and should thus use export type.
4 │ import type { T2, T3 } from "./mod.ts";
> 5 │ export { T2, T3 };
│ ^^
6 │
7 │ import type T4 from "./mod.ts";
i The type is defined here.
2 │ export { T1, V1 };
3 │
> 4 │ import type { T2, T3 } from "./mod.ts";
│ ^^
5 │ export { T2, T3 };
6 │
i Safe fix: Use an inline type export.
5 │ export·{·type·T2,·T3·};
│ +++++
```

```
invalid.ts:5:14 lint/nursery/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! This export is only a type and should thus use export type.
4 │ import type { T2, T3 } from "./mod.ts";
> 5 │ export { T2, T3 };
│ ^^
6 │
7 │ import type T4 from "./mod.ts";
i The type is defined here.
2 │ export { T1, V1 };
3 │
> 4 │ import type { T2, T3 } from "./mod.ts";
│ ^^
5 │ export { T2, T3 };
6 │
i Safe fix: Use an inline type export.
5 │ export·{·T2,·type·T3·};
│ +++++
```

```
invalid.ts:8:10 lint/nursery/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! This export is only a type and should thus use export type.
7 │ import type T4 from "./mod.ts";
> 8 │ export { T4 };
│ ^^
9 │
10 │ import type * as ns from "./mod.ts";
i The type is defined here.
5 │ export { T2, T3 };
6 │
> 7 │ import type T4 from "./mod.ts";
│ ^^
8 │ export { T4 };
9 │
i Safe fix: Use export type.
8 │ export·type·{·T4·};
│ +++++
```

```
invalid.ts:11:10 lint/nursery/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! This export is only a type and should thus use export type.
10 │ import type * as ns from "./mod.ts";
> 11 │ export { ns };
│ ^^
12 │
13 │ interface Interface {}
i The type is defined here.
8 │ export { T4 };
9 │
> 10 │ import type * as ns from "./mod.ts";
│ ^^
11 │ export { ns };
12 │
i Safe fix: Use export type.
11 │ export·type·{·ns·};
│ +++++
```

```
invalid.ts:18:10 lint/nursery/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! This export is only a type and should thus use export type.
16 │ function func() {}
17 │ class Class {}
> 18 │ export { Interface, TypeAlias, Enum, func as f, Class };
│ ^^^^^^^^^
19 │
i The type is defined here.
11 │ export { ns };
12 │
> 13 │ interface Interface {}
│ ^^^^^^^^^
14 │ type TypeAlias = {}
15 │ enum Enum {}
i Safe fix: Use an inline type export.
18 │ export·{·type·Interface,·TypeAlias,·Enum,·func·as·f,·Class·};
│ +++++
```

```
invalid.ts:18:21 lint/nursery/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! This export is only a type and should thus use export type.
16 │ function func() {}
17 │ class Class {}
> 18 │ export { Interface, TypeAlias, Enum, func as f, Class };
│ ^^^^^^^^^
19 │
i The type is defined here.
13 │ interface Interface {}
> 14 │ type TypeAlias = {}
│ ^^^^^^^^^
15 │ enum Enum {}
16 │ function func() {}
i Safe fix: Use an inline type export.
18 │ export·{·Interface,·type·TypeAlias,·Enum,·func·as·f,·Class·};
│ +++++
```


Loading

0 comments on commit d987fd4

Please sign in to comment.