Skip to content

Commit

Permalink
refactor: treat named clause at once
Browse files Browse the repository at this point in the history
  • Loading branch information
Conaclos committed Dec 5, 2023
1 parent 27018be commit 1c41acb
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 220 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom

#### New features

- Add [useExportType](https://biomejs.dev/linter/rules/use-export-type) that enforces the use of type-only exports for names that are only types. Contributed by @Conaclos

```diff
interface A {}
interface B {}
class C {}

- export type { A, C }
+ export { type A, C }

- export { type B }
+ export type { B }
```

#### Enhancements

#### Bug fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ use biome_analyze::{
use biome_console::markup;
use biome_diagnostics::Applicability;
use biome_js_factory::make;
use biome_js_syntax::{
AnyJsExportNamedSpecifier, JsExportNamedClause, JsExportNamedSpecifierList, JsFileSource, T,
use biome_js_syntax::{AnyJsExportNamedSpecifier, JsExportNamedClause, JsFileSource, T};
use biome_rowan::{
trim_leading_trivia_pieces, AstNode, AstSeparatedList, BatchMutationExt, TriviaPieceKind,
};
use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt, TextRange, TriviaPieceKind};

declare_rule! {
/// Promotes the use of `export type` for type-only types.
/// Promotes the use of `export type` and `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.
/// _TypeScript_ allows specifying a `type` marker 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.
///
/// The rule ensures that type-only bindings are exported using a type-only export.
/// It also normalizes an `export` declaration that contains only type-only exports into a grouped `export type`.
///
/// ## Examples
///
/// ### Invalid
Expand All @@ -35,6 +38,10 @@ declare_rule! {
/// export { T };
/// ```
///
/// ```ts,expect_diagnostic
/// export { type X, type Y };
/// ```
///
/// ## Valid
///
/// ```js
Expand All @@ -58,8 +65,8 @@ declare_rule! {
}

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

Expand All @@ -68,64 +75,140 @@ impl Rule for UseExportType {
if !source_type.language().is_typescript() {
return None;
}
let specifier = ctx.query();
let model = ctx.model();
if specifier.exports_only_types() {
let export_named_clause = ctx.query();
if export_named_clause.type_token().is_some() {
// `export type {}`
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());
let mut exports_only_types = true;
let mut specifiers_requiring_type_marker = Vec::new();
for specifier in export_named_clause.specifiers() {
let Ok((ref_name, specifier)) = specifier.and_then(|x| Ok((x.local_name()?, x))) else {
exports_only_types = false;
continue;
};
if specifier.type_token().is_some() {
// `export { type <specifier> }`
continue;
}
let model = ctx.model();
let binding = model.binding(&ref_name)?;
let binding = binding.tree();
if binding.is_type_only() {
specifiers_requiring_type_marker.push(specifier);
} else {
exports_only_types = false;
}
}
if exports_only_types {
Some(ExportTypeFix::Factorize)
} else if specifiers_requiring_type_marker.is_empty() {
None
} else {
Some(ExportTypeFix::AddInlineType(
specifiers_requiring_type_marker,
))
}
None
}

fn diagnostic(ctx: &RuleContext<Self>, declaration: &Self::State) -> Option<RuleDiagnostic> {
Some(
RuleDiagnostic::new(
rule_category!(),
ctx.query().range(),
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let range = ctx.query().range();
let (diagnostic_range, diagnostic_message) = match state {
ExportTypeFix::Factorize => (
range,
markup! {
"This export is only a type and should thus use "<Emphasis>"export type"</Emphasis>"."
"All exports are only types 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."
})
)
),
ExportTypeFix::AddInlineType(specifiers) => {
let range = specifiers
.iter()
.map(|x| x.range())
.reduce(|acc, x| acc.cover(x))
.unwrap_or(range);
(
range,
markup! {
"Several exports are only types and should thus use "<Emphasis>"export type"</Emphasis>"."
},
)
}
};
Some(RuleDiagnostic::new(
rule_category!(),
diagnostic_range,
diagnostic_message,
).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();
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
let export_named_clause = 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,
});
let diagnostic = match state {
ExportTypeFix::Factorize => {
let specifier_list = export_named_clause.specifiers();
let mut new_specifiers = Vec::new();
for specifier in specifier_list.iter().filter_map(|x| x.ok()) {
if let Some(type_token) = specifier.type_token() {
let mut new_specifier = specifier.with_type_token(None);
if type_token.has_trailing_comments() {
new_specifier = new_specifier.prepend_trivia_pieces(
trim_leading_trivia_pieces(type_token.trailing_trivia().pieces()),
)?;
}
new_specifiers.push(new_specifier);
} else {
new_specifiers.push(specifier)
}
}
let new_specifier_list = make::js_export_named_specifier_list(
new_specifiers,
specifier_list
.separators()
.filter_map(|sep| sep.ok())
.collect::<Vec<_>>(),
);
mutation.replace_node(
export_named_clause.clone(),
export_named_clause
.clone()
.with_type_token(type_token)
.with_specifiers(new_specifier_list),
);
JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::Always,
message: markup! { "Use a grouped "<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,
})
ExportTypeFix::AddInlineType(specifiers) => {
for specifier in specifiers {
mutation.replace_node(
specifier.clone(),
specifier.clone().with_type_token(type_token.clone()),
);
}
JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::Always,
message: markup! { "Use inline "<Emphasis>"type"</Emphasis>" exports." }
.to_owned(),
mutation,
}
}
};
Some(diagnostic)
}
}

#[derive(Debug)]
pub(crate) enum ExportTypeFix {
Factorize,
AddInlineType(Vec<AnyJsExportNamedSpecifier>),
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ type TypeAlias = {}
enum Enum {}
function func() {}
class Class {}
export { Interface, TypeAlias, Enum, func as f, Class };
export { Interface, TypeAlias, Enum, func as f, Class };

export /*0*/ { /*1*/ type /*2*/ func /*3*/, /*4*/ type Class as C /*5*/ } /*6*/;
Loading

0 comments on commit 1c41acb

Please sign in to comment.