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 Nov 26, 2023
1 parent 70d4377 commit a3b1f33
Show file tree
Hide file tree
Showing 23 changed files with 810 additions and 69 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom

- Add [noDefaultExport](https://biomejs.dev/linter/rules/no-default-export) which disallows `export default`. Contributed by @Conaclos

- Add [`useExportType`](https://biomejs.dev/linter/rules/use-export-type) which requires the usage of `export type` when the exported element is only a type. Contributed by @Conaclos

#### Bug fixes

- Fix [#639](https://github.com/biomejs/biome/issues/639) by ignoring unused TypeScript's mapped key. Contributed by @Conaclos
Expand Down
1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ define_categories! {
"lint/nursery/useAsConstAssertion": "https://biomejs.dev/linter/rules/use-as-const-assertion",
"lint/nursery/useAwait": "https://biomejs.dev/linter/rules/use-await",
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
"lint/nursery/useExportType": "https://biomejs.dev/linter/rules/use-export-type",
"lint/nursery/useGroupedTypeImport": "https://biomejs.dev/linter/rules/use-grouped-type-import",
"lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions",
"lint/nursery/useRegexLiterals": "https://biomejs.dev/linter/rules/use-regex-literals",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ impl From<JsImport> for ImportNode {

for element in named_import_specifiers.specifiers().elements() {
let node = element.node.ok()?;
let key = node.local_name()?.token_text_trimmed();
let key = node.imported_name()?.token_text_trimmed();

let trailing_separator = element.trailing_separator.ok()?;
separator_count += usize::from(trailing_separator.is_some());
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_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,131 @@
use crate::{semantic_services::Semantic, JsRuleAction};
use biome_analyze::{
context::RuleContext, declare_rule, ActionCategory, FixKind, Rule, RuleDiagnostic,
};
use biome_console::markup;
use biome_diagnostics::Applicability;
use biome_js_factory::make;
use biome_js_syntax::{
AnyJsExportNamedSpecifier, JsExportNamedClause, JsExportNamedSpecifierList, JsFileSource, T,
};
use biome_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 allows transpilers to safely drop exports of types without looking for their 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,
fix_kind: FixKind::Safe,
}
}

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.exports_only_types() {
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."
}).note(markup! {
"Using "<Emphasis>"export type"</Emphasis>" allows transpilers to safely drop exports of types without looking for their definition."
})
)
}

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.len() == 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 };
Loading

0 comments on commit a3b1f33

Please sign in to comment.