Skip to content

Commit

Permalink
fix(useFilenamingConvention): take export renaming into account (#4260)
Browse files Browse the repository at this point in the history
  • Loading branch information
Conaclos committed Oct 18, 2024
1 parent 1a0ebdb commit c5d8d54
Show file tree
Hide file tree
Showing 12 changed files with 120 additions and 10 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,27 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b

Contributed by @Conaclos

- [useFilenamingConvention](https://biomejs.dev/linter/rules/use-filenaming-convention) now correctly handles renamed exports ([#4254](https://github.com/biomejs/biome/issues/4254)).

The rule allows the filename to be named as one of the exports of the module.
For instance, the file containing the following export can be named `Button`.

```js
class Button {}
export { Button }
```

The rule now correctly handles the renaming of an export.
For example, the file containing the following export can only be named `Button`.
Previously the rule expected the file to be named `A`.

```js
class A {}
export { A as Button }
```

Contributed by @Conaclos

### Parser

#### Bug Fixes
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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
Expand Up @@ -4,7 +4,10 @@ use biome_analyze::{
};
use biome_console::markup;
use biome_deserialize_macros::Deserializable;
use biome_rowan::TextRange;
use biome_js_syntax::{
binding_ext::AnyJsIdentifierBinding, AnyJsIdentifierUsage, JsExportNamedSpecifier,
};
use biome_rowan::{AstNode, TextRange};
use biome_string_case::{Case, Cases};
use rustc_hash::FxHashSet;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -210,13 +213,20 @@ impl Rule for UseFilenamingConvention {
}
if options.filename_cases.0.contains(&FilenameCase::Export) {
// If no exported binding has the file name, then reports the filename
let model = ctx.model();
model
.all_bindings()
.map(|binding| binding.tree())
.filter(|binding| model.is_exported(binding))
.filter_map(|exported_binding| exported_binding.name_token().ok())
.all(|exported_name_token| exported_name_token.text_trimmed() != name)
ctx.model()
.all_exported_bindings()
.all(|exported_binding| {
exported_binding
.exports()
.filter_map(|export| match AnyJsIdentifierBinding::try_cast(export) {
Ok(id) => id.name_token().ok(),
Err(export) => match JsExportNamedSpecifier::cast(export.parent()?) {
Some(specifier) => specifier.exported_name().ok()?.value().ok(),
None => AnyJsIdentifierUsage::cast(export)?.value_token().ok(),
},
})
.all(|exported_name_token| exported_name_token.text_trimmed() != name)
})
.then_some(FileNamingConventionState::Filename)
} else {
Some(FileNamingConventionState::Filename)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class Invalid_Renamed_Export {}
export { Invalid_Renamed_Export as A }
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: Invalid_Renamed_Export.js
---
# Input
```jsx
class Invalid_Renamed_Export {}
export { Invalid_Renamed_Export as A }
```

# Diagnostics
```
Invalid_Renamed_Export.js lint/style/useFilenamingConvention ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! The filename should be in camelCase or kebab-case or snake_case or equal to the name of an export.
i The filename could be renamed to one of the following names:
invalid-renamed-export.js
invalidRenamedExport.js
invalid_renamed_export.js
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class A {}
export { A as Valid_Renamed_Export }
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: Valid_Renamed_Export.js
---
# Input
```jsx
class A {}
export { A as Valid_Renamed_Export }
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: Valid_Renamed_Export.js
---
# Input
```jsx
class A {}
export { A as Valid_Renamed_Export }
```
1 change: 1 addition & 0 deletions crates/biome_js_semantic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ biome_js_syntax = { workspace = true }
biome_rowan = { workspace = true }
rust-lapper = "1.1.0"
rustc-hash = { workspace = true }
smallvec = { workspace = true }

[dev-dependencies]
biome_console = { path = "../biome_console" }
Expand Down
13 changes: 13 additions & 0 deletions crates/biome_js_semantic/src/semantic_model/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use biome_js_syntax::{binding_ext::AnyJsIdentifierBinding, TextRange, TsTypePara
pub(crate) struct SemanticModelBindingData {
pub(crate) range: TextRange,
pub(crate) references: Vec<SemanticModelReference>,
// We use a SmallVec because most of the time a binding is expected once.
pub(crate) export_by_start: smallvec::SmallVec<[TextSize; 4]>,
}

#[derive(Clone, Copy, Debug)]
Expand Down Expand Up @@ -117,6 +119,17 @@ impl Binding {
std::iter::successors(first, Reference::find_next_write)
}

/// Returns all exports of the binding.
/// The node kind is either an identifier binding (tehd eclaration is self exported)
/// or an identifier usage.
pub fn exports(&self) -> impl Iterator<Item = JsSyntaxNode> + '_ {
let binding = self.data.binding(self.id);
binding
.export_by_start
.iter()
.map(|export_start| self.data.binding_node_by_start[export_start].clone())
}

pub fn is_imported(&self) -> bool {
super::is_imported(self.syntax())
}
Expand Down
12 changes: 10 additions & 2 deletions crates/biome_js_semantic/src/semantic_model/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ impl SemanticModelBuilder {
let binding_id = BindingId::new(self.bindings.len());
self.bindings.push(SemanticModelBindingData {
range,
references: vec![],
references: Vec::new(),
export_by_start: smallvec::SmallVec::new(),
});
self.bindings_by_start.insert(range.start(), binding_id);

Expand Down Expand Up @@ -308,8 +309,15 @@ impl SemanticModelBuilder {
.push(SemanticModelUnresolvedReference { range }),
}
}
Export { declaration_at, .. } => {
Export {
declaration_at,
range,
} => {
self.exported.insert(declaration_at);

let binding_id = self.bindings_by_start[&declaration_at];
let binding = &mut self.bindings[binding_id.index()];
binding.export_by_start.push(range.start());
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions crates/biome_js_semantic/src/semantic_model/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,17 @@ impl SemanticModel {
})
}

pub fn all_exported_bindings(&self) -> impl Iterator<Item = Binding> + '_ {
self.data
.exported
.iter()
.filter_map(|declared_at| self.data.bindings_by_start.get(declared_at).copied())
.map(|id| Binding {
data: self.data.clone(),
id,
})
}

/// Returns the [Binding] of a reference.
/// Can also be called from "binding" extension method.
///
Expand Down

0 comments on commit c5d8d54

Please sign in to comment.