Skip to content

Commit

Permalink
feat(useFilenamingConvention): add the match option
Browse files Browse the repository at this point in the history
  • Loading branch information
Conaclos committed Oct 18, 2024
1 parent 7fffb27 commit a890039
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 16 deletions.
1 change: 1 addition & 0 deletions crates/biome_cli/src/execute/migrate/eslint_unicorn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ impl From<FilenameCaseOptions> for use_filenaming_convention::FilenamingConventi
use_filenaming_convention::FilenamingConventionOptions {
strict_case: true,
require_ascii: true,
matching: None,
filename_cases: filename_cases.unwrap_or_else(|| {
use_filenaming_convention::FilenameCases::from_iter([val.case.into()])
}),
Expand Down
85 changes: 73 additions & 12 deletions crates/biome_js_analyze/src/lint/style/use_filenaming_convention.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::services::semantic::SemanticServices;
use crate::{services::semantic::SemanticServices, utils::regex::RestrictedRegex};
use biome_analyze::{
context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource, RuleSourceKind,
};
Expand All @@ -18,21 +18,24 @@ declare_lint_rule! {
///
/// Enforcing [naming conventions](https://en.wikipedia.org/wiki/Naming_convention_(programming)) helps to keep the codebase consistent.
///
/// A filename consists of two parts: a name and a set of consecutive extension.
/// A filename consists of two parts: a name and a set of consecutive extensions.
/// For instance, `my-filename.test.js` has `my-filename` as name, and two consecutive extensions: `.test` and `.js`.
///
/// The filename can start with a dot or a plus sign, be prefixed and suffixed by underscores `_`.
/// For example, `.filename.js`, `+filename.js`, `__filename__.js`, or even `.__filename__.js`.
/// By default, the rule ensures that the name is either in [`camelCase`], [`kebab-case`], [`snake_case`],
/// or equal to the name of one export in the file.
/// By default, the rule ensures that the extensions are either in [`camelCase`], [`kebab-case`], or [`snake_case`].
///
/// The convention of prefixing a filename with a plus sign is used by
/// [Sveltekit](https://kit.svelte.dev/docs/routing#page) and [Vike](https://vike.dev/route).
/// The rule supports the following exceptions:
///
/// Also, the rule supports dynamic route syntaxes of [Next.js](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments), [SolidStart](https://docs.solidjs.com/solid-start/building-your-application/routing#renaming-index), [Nuxt](https://nuxt.com/docs/guide/directory-structure/server#catch-all-route), and [Astro](https://docs.astro.build/en/guides/routing/#rest-parameters).
/// For example `[...slug].js` and `[[...slug]].js` are valid filenames.
/// - The name of the file can start with a dot or a plus sign, be prefixed and suffixed by underscores `_`.
/// For example, `.filename.js`, `+filename.js`, `__filename__.js`, or even `.__filename__.js`.
///
/// By default, the rule ensures that the filename is either in [`camelCase`], [`kebab-case`], [`snake_case`],
/// or equal to the name of one export in the file.
/// By default, the rule ensures that the extensions are either in [`camelCase`], [`kebab-case`], or [`snake_case`].
/// The convention of prefixing a filename with a plus sign is used by [Sveltekit](https://kit.svelte.dev/docs/routing#page) and [Vike](https://vike.dev/route).
///
/// - Also, the rule supports dynamic route syntaxes of [Next.js](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments), [SolidStart](https://docs.solidjs.com/solid-start/building-your-application/routing#renaming-index), [Nuxt](https://nuxt.com/docs/guide/directory-structure/server#catch-all-route), and [Astro](https://docs.astro.build/en/guides/routing/#rest-parameters).
/// For example `[...slug].js` and `[[...slug]].js` are valid filenames.
///
/// Note that if you specify the `match' option, the previous exceptions will no longer be handled.
///
/// ## Ignoring some files
///
Expand Down Expand Up @@ -68,6 +71,7 @@ declare_lint_rule! {
/// "options": {
/// "strictCase": false,
/// "requireAscii": true,
/// "match": "%?(.+?)[.](.+)",
/// "filenameCases": ["camelCase", "export"]
/// }
/// }
Expand Down Expand Up @@ -96,6 +100,30 @@ declare_lint_rule! {
///
/// **This option will be turned on by default in Biome 2.0.**
///
/// ### match
///
/// `match` defines a regular expression that the filename must match.
/// If the regex has capturing groups, then the first capture is considered as the filename
/// and the second one as file extensions separated by dots.
///
/// For example, given the regular expression `%?(.+?)\.(.+)` and the filename `%index.d.ts`,
/// the filename matches the regular expression with two captures: `index` and `d.ts`.
/// The captures are checked against `filenameCases`.
/// Note that we use the non-greedy quantifier `+?` to stop capturing as soon as we met the next character (`.`).
/// If we use the greedy quantifier `+` instead, then the captures could be `index.d` and `ts`.
///
/// The regular expression supports the following syntaxes:
///
/// - Greedy quantifiers `*`, `?`, `+`, `{n}`, `{n,m}`, `{n,}`, `{m}`
/// - Non-greedy quantifiers `*?`, `??`, `+?`, `{n}?`, `{n,m}?`, `{n,}?`, `{m}?`
/// - Any character matcher `.`
/// - Character classes `[a-z]`, `[xyz]`, `[^a-z]`
/// - Alternations `|`
/// - Capturing groups `()`
/// - Non-capturing groups `(?:)`
/// - A limited set of escaped characters including all special characters
/// and regular string escape characters `\f`, `\n`, `\r`, `\t`, `\v`
///
/// ### filenameCases
///
/// By default, the rule enforces that the filename is either in [`camelCase`], [`kebab-case`], [`snake_case`], or equal to the name of one export in the file.
Expand Down Expand Up @@ -134,7 +162,23 @@ impl Rule for UseFilenamingConvention {
return Some(FileNamingConventionState::Ascii);
}
let first_char = file_name.bytes().next()?;
let (name, mut extensions) = if matches!(first_char, b'(' | b'[') {
let (name, mut extensions) = if let Some(matching) = &options.matching {
let Some(captures) = matching.captures(file_name) else {
return Some(FileNamingConventionState::Match);
};
let mut captures = captures.iter().skip(1).flatten();
let Some(first_capture) = captures.next() else {
// Match without any capture implies a valid case
return None;
};
let name = first_capture.as_str();
if name.is_empty() {
// Empty string are always valid.
return None;
}
let split = captures.next().map_or("", |x| x.as_str()).split('.');
(name, split)
} else if matches!(first_char, b'(' | b'[') {
// Support [Next.js](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments),
// [SolidStart](https://docs.solidjs.com/solid-start/building-your-application/routing#renaming-index),
// [Nuxt](https://nuxt.com/docs/guide/directory-structure/server#catch-all-route),
Expand Down Expand Up @@ -329,6 +373,16 @@ impl Rule for UseFilenamingConvention {
},
))
},
FileNamingConventionState::Match => {
let matching = options.matching.as_ref()?.to_string();
Some(RuleDiagnostic::new(
rule_category!(),
None as Option<TextRange>,
markup! {
"This filename should match the following regex "<Emphasis>"/"{matching}"/"</Emphasis>"."
},
))
}
}
}
}
Expand All @@ -341,6 +395,8 @@ pub enum FileNamingConventionState {
Filename,
/// An extension is not in lowercase
Extension,
/// The filename doesn't match the provided regex
Match,
}

/// Rule's options.
Expand All @@ -357,6 +413,10 @@ pub struct FilenamingConventionOptions {
#[serde(default, skip_serializing_if = "is_default")]
pub require_ascii: bool,

/// Regular expression to enforce
#[serde(default, rename = "match", skip_serializing_if = "Option::is_none")]
pub matching: Option<RestrictedRegex>,

/// Allowed cases for file names.
#[serde(default, skip_serializing_if = "is_default")]
pub filename_cases: FilenameCases,
Expand All @@ -375,6 +435,7 @@ impl Default for FilenamingConventionOptions {
Self {
strict_case: true,
require_ascii: false,
matching: None,
filename_cases: FilenameCases::default(),
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ impl Rule for UseNamingConvention {
start: name_range_start as u16,
end: (name_range_start + name.len()) as u16,
},
suggestion: Suggestion::Match(matching.to_string()),
suggestion: Suggestion::Match(matching.to_string().into_boxed_str()),
});
};
if let Some(first_capture) = capture.iter().skip(1).find_map(|x| x) {
Expand Down Expand Up @@ -756,7 +756,7 @@ impl Rule for UseNamingConvention {
rule_category!(),
name_token_range,
markup! {
"This "<Emphasis>{format_args!("{convention_selector}")}</Emphasis>" name"{trimmed_info}" should match the following regex "<Emphasis>"/"{regex}"/"</Emphasis>"."
"This "<Emphasis>{format_args!("{convention_selector}")}</Emphasis>" name"{trimmed_info}" should match the following regex "<Emphasis>"/"{regex.as_ref()}"/"</Emphasis>"."
},
))
}
Expand Down Expand Up @@ -897,7 +897,7 @@ pub enum Suggestion {
/// Use only ASCII characters
Ascii,
/// Use a name that matches this regex
Match(String),
Match(Box<str>),
/// Use a name that follows one of these formats
Formats(Formats),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
"linter": {
"rules": {
"style": {
"useFilenamingConvention": {
"level": "error",
"options": {
"match": "%(.+?)[.](.+)",
"filenameCases": ["camelCase"]
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const C: number;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: "%validMatch.ts"
---
# Input
```ts
export const C: number;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
"linter": {
"rules": {
"style": {
"useFilenamingConvention": {
"level": "error",
"options": {
"match": "%(.+)[.](.+)",
"filenameCases": ["camelCase"]
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalidMatch.js
---
# Input
```jsx
{
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
"linter": {
"rules": {
"style": {
"useFilenamingConvention": {
"level": "error",
"options": {
"match": "%(.+)[.](.+)",
"filenameCases": ["camelCase"]
}
}
}
}
}
}

```

# Diagnostics
```
invalidMatch.js lint/style/useFilenamingConvention ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! This filename should match the following regex /[^i].*/.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
"linter": {
"rules": {
"style": {
"useFilenamingConvention": {
"level": "error",
"options": {
"match": "[^i].*",
"filenameCases": ["camelCase"]
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalidMatchExtension.INVALID.js
---
# Input
```jsx

```

# Diagnostics
```
invalidMatchExtension.INVALID.js lint/style/useFilenamingConvention ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! The file extension should be in camelCase.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
"linter": {
"rules": {
"style": {
"useFilenamingConvention": {
"level": "error",
"options": {
"match": "(.+?)[.](.+)",
"filenameCases": ["camelCase"]
}
}
}
}
}
}
6 changes: 5 additions & 1 deletion packages/@biomejs/backend-jsonrpc/src/workspace.ts

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

4 changes: 4 additions & 0 deletions packages/@biomejs/biome/configuration_schema.json

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

0 comments on commit a890039

Please sign in to comment.