diff --git a/.github/ISSUE_TEMPLATE/01_formatter_bug.yml b/.github/ISSUE_TEMPLATE/01_formatter_bug.yml index e24b2e5ea7d7..38d176a6dc41 100644 --- a/.github/ISSUE_TEMPLATE/01_formatter_bug.yml +++ b/.github/ISSUE_TEMPLATE/01_formatter_bug.yml @@ -1,7 +1,7 @@ name: πŸ“ Formatter bug report description: Report a bug or regression of the formatter title: "πŸ“ " -labels: [ "S-To triage" ] +labels: [ "S-Needs triage" ] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/02_lint_bug.yml b/.github/ISSUE_TEMPLATE/02_lint_bug.yml index ded21f6e0ab0..0944224f7fb2 100644 --- a/.github/ISSUE_TEMPLATE/02_lint_bug.yml +++ b/.github/ISSUE_TEMPLATE/02_lint_bug.yml @@ -1,7 +1,7 @@ name: πŸ’… Linter bug report description: Report a bug or regression of the linter title: "πŸ’… <TITLE>" -labels: [ "S-To triage" ] +labels: [ "S-Needs triage" ] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/03_bug.yml b/.github/ISSUE_TEMPLATE/03_bug.yml index 2c6fc38bbbc9..66e3aa9f14fe 100644 --- a/.github/ISSUE_TEMPLATE/03_bug.yml +++ b/.github/ISSUE_TEMPLATE/03_bug.yml @@ -1,7 +1,7 @@ name: πŸ› Bug Report description: Report a possible bug or regression title: "πŸ› <TITLE>" -labels: [ "S-To triage" ] +labels: [ "S-Needs triage" ] body: - type: markdown attributes: diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 0fff462a6d5a..3b87649f942f 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -11,6 +11,7 @@ on: - 'crates/**_parser/**/*.rs' - 'crates/**_formatter/**/*.rs' - 'crates/**_analyze/**/*.rs' + - 'crates/biome_grit_patterns/**/*.rs' push: branches: - main @@ -19,6 +20,7 @@ on: - 'crates/**_parser/**/*.rs' - 'crates/**_formatter/**/*.rs' - 'crates/**_analyze/**/*.rs' + - 'crates/biome_grit_patterns/**/*.rs' env: RUST_LOG: info diff --git a/.github/workflows/close-issue.yml b/.github/workflows/close-issue.yml new file mode 100644 index 000000000000..3a976b492301 --- /dev/null +++ b/.github/workflows/close-issue.yml @@ -0,0 +1,22 @@ +name: Close issues + +on: + schedule: + - cron: "0 0 * * *" + + +permissions: + issues: write + +jobs: + close-issues: + if: github.repository == 'biomejs/biome' + runs-on: ubuntu-latest + steps: + - name: Close issue without reproduction + uses: actions-cool/issues-helper@v3 + with: + actions: "close-issues" + token: ${{ secrets.GITHUB_TOKEN }} + labels: "S-Needs repro" + inactive-day: 3 diff --git a/.github/workflows/needs-repro.yml b/.github/workflows/needs-repro.yml new file mode 100644 index 000000000000..f8285a29ee35 --- /dev/null +++ b/.github/workflows/needs-repro.yml @@ -0,0 +1,38 @@ +name: Needs reproduction + +on: + issues: + types: [ labeled ] + +permissions: + issues: write + +jobs: + reply-labeled: + if: github.repository == 'biomejs/biome' + runs-on: ubuntu-latest + steps: + - name: Remove triaging label + if: contains(github.event.issue.labels.*.name, 'S-Bug-confirmed') && contains(github.event.issue.labels.*.name, 'S-Needs triage') + uses: actions-cool/issues-helper@v3 + with: + actions: "remove-labels" + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.issue.number }} + labels: "S-Needs triage" + + - name: Needs reproduction + if: github.event.label.name == 'S-Needs repro' + uses: actions-cool/issues-helper@v3 + with: + actions: "create-comment, remove-labels" + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: | + Hello @${{ github.event.issue.user.login }}, please provide a minimal reproduction. You can use one of the following options: + + - Provide a link to [our playground](https://biomejs.dev/playground), if it's applicable. + - Provide a link to GitHub repository. To easily create a reproduction, you can use our interactive CLI via `npm create @biomejs/biome-reproduction` + + Issues marked with `S-Needs repro` will be **closed** if they have **no activity within 3 days**. + labels: "S-Needs triage" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d36dc709ef94..acb8c3a6ad42 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -124,7 +124,7 @@ jobs: with: node-version: 20 - name: Cache pnpm modules - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: ~/.pnpm-store key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} diff --git a/.github/workflows/pull_request_js.yml b/.github/workflows/pull_request_js.yml index 995a6205637e..6cd4cef01a29 100644 --- a/.github/workflows/pull_request_js.yml +++ b/.github/workflows/pull_request_js.yml @@ -28,7 +28,7 @@ jobs: - name: Free Disk Space uses: ./.github/actions/free-disk-space - name: Cache pnpm modules - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: ~/.pnpm-store key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} diff --git a/.github/workflows/release_cli.yml b/.github/workflows/release_cli.yml index c357fe78d720..214dfad3e305 100644 --- a/.github/workflows/release_cli.yml +++ b/.github/workflows/release_cli.yml @@ -27,7 +27,7 @@ jobs: run: echo "nightly=true" >> $GITHUB_ENV - name: Check version changes - uses: EndBug/version-check@d4be4219408b50d1bbbfd350a47cbcb126878692 # v2.1.4 + uses: EndBug/version-check@36ff30f37c7deabe56a30caa043d127be658c425 # v2.1.5 if: env.nightly != 'true' id: version with: diff --git a/.github/workflows/release_js_api.yml b/.github/workflows/release_js_api.yml index 8ee1b413980e..60bb02596d1d 100644 --- a/.github/workflows/release_js_api.yml +++ b/.github/workflows/release_js_api.yml @@ -27,7 +27,7 @@ jobs: run: echo "nightly=true" >> $GITHUB_ENV - name: Check version changes - uses: EndBug/version-check@d4be4219408b50d1bbbfd350a47cbcb126878692 # v2.1.4 + uses: EndBug/version-check@36ff30f37c7deabe56a30caa043d127be658c425 # v2.1.5 if: env.nightly != 'true' id: version with: @@ -69,7 +69,7 @@ jobs: run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Cache pnpm modules - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: ~/.pnpm-store key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} diff --git a/.github/workflows/release_knope.yml b/.github/workflows/release_knope.yml index 05cdd44840d1..e6cdab75a98b 100644 --- a/.github/workflows/release_knope.yml +++ b/.github/workflows/release_knope.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Check version changes - uses: EndBug/version-check@d4be4219408b50d1bbbfd350a47cbcb126878692 # v2.1.4 + uses: EndBug/version-check@36ff30f37c7deabe56a30caa043d127be658c425 # v2.1.5 id: version with: diff-search: true diff --git a/.github/workflows/repository_dispatch.yml b/.github/workflows/repository_dispatch.yml index 333eb8ee3f9f..092f122a52b5 100644 --- a/.github/workflows/repository_dispatch.yml +++ b/.github/workflows/repository_dispatch.yml @@ -28,7 +28,7 @@ jobs: - name: Warm up wasm-pack cache id: cache-restore - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: | ./target @@ -52,7 +52,7 @@ jobs: continue-on-error: true - name: Save new wasm-pack cache - uses: actions/cache/save@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache/save@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: | ./target diff --git a/CHANGELOG.md b/CHANGELOG.md index 09de48b76e0d..d452c5f77492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,16 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b ### Analyzer +#### Bug fixes + +- Improved the message for unused suppression comments. Contributed by @dyc3 + ### CLI +#### Enhancements + +- The `--summary` reporter now reports parsing diagnostics too. Contributed by @ematipico + ### Configuration #### Bug fixes @@ -29,8 +37,94 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b ### Linter +#### Bug Fixes + +- Biome no longer crashes when it encounters a string that contain a multibyte character ([#4181](https://github.com/biomejs/biome/issues/4181)). + + This fixes a regression introduced in Biome 1.9.3 + The regression affected the following linter rules: + + - nursery/useSortedClasses + - nursery/useTrimStartEnd + - style/useTemplate + - suspicious/noMisleadingCharacterClass + + Contributed by @Conaclos + +- Fix [#4190](https://github.com/biomejs/biome/issues/4190), where the rule `noMissingVarFunction` wrongly reported a variable as missing when used inside a `var()` function that was a newline. Contributed by @ematipico + +- Fix [#4041](https://github.com/biomejs/biome/issues/4041). Now the rule `useSortedClasses` won't be triggered if `className` is composed only by inlined variables. Contributed by @ematipico + +- [useImportType](https://biomejs.dev/linter/rules/use-import-type/) and [useExportType](https://biomejs.dev/linter/rules/use-export-type/) now report useless inline type qualifiers ([#4178](https://github.com/biomejs/biome/issues/4178)). + + The following fix is now proposed: + + ```diff + - import type { type A, B } from ""; + + import type { A, B } from ""; + + - export type { type C, D }; + + export type { C, D }; + ``` + + Contributed by @Conaclos + +- [useExportType](https://biomejs.dev/linter/rules/use-export-type/) now reports ungrouped `export from`. + + The following fix is now proposed: + + ```diff + - export { type A, type B } from ""; + + export type { A, B } from ""; + ``` + + Contributed by @Conaclos + +- [noVoidTypeReturn](https://biomejs.dev/linter/rules/no-void-type-return/) now accepts `void` expressions in return position ([#4173](https://github.com/biomejs/biome/issues/4173)). + + The following code is now accepted: + + ```ts + function f(): void { + return void 0; + } + ``` + + Contributed by @Conaclos + +- Fixes [#4059](https://github.com/biomejs/biome/issues/4059), the rule [noUselessFragments](https://biomejs.dev/linter/rules/no-useless-fragments/) now correctly handles fragments containing HTML escapes (e.g. ` `) inside expression escapes `{ ... }`. +The following code is no longer reported: + +```jsx +function Component() { + return ( + <div key={index}>{line || <> </>}</div> + ) +} +``` + +Contributed by @fireairforce + ### Parser +#### Bug Fixes + +- The CSS parser now accepts more emoji in identifiers ([#3627](https://github.com/biomejs/biome/issues/3627#issuecomment-2392388022)). + + Browsers accept more emoji than the standard allows. + Biome now accepts these additional emoji. + + The following code is now correctly parsed: + + ```css + p { + --✨-color: red; + color: var(--✨-color); + } + ``` + + Contributed by @Conaclos + ## v1.9.3 (2024-10-01) ### CLI diff --git a/Cargo.lock b/Cargo.lock index 39ec0ebcfdd2..934ffc8f3c79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -602,6 +602,7 @@ dependencies = [ "biome_configuration", "biome_formatter", "biome_formatter_test", + "biome_fs", "biome_grit_factory", "biome_grit_parser", "biome_grit_syntax", diff --git a/benchmark/package.json b/benchmark/package.json index db8ddaf88ff5..f45249440307 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -11,9 +11,9 @@ "node": ">20.0.0" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "8.7.0", + "@typescript-eslint/eslint-plugin": "8.8.0", "dprint": "0.47.2", - "eslint": "9.11.1", + "eslint": "9.12.0", "prettier": "3.3.3" } } diff --git a/crates/biome_analyze/CONTRIBUTING.md b/crates/biome_analyze/CONTRIBUTING.md index 4635735b98c2..5a4af4740a06 100644 --- a/crates/biome_analyze/CONTRIBUTING.md +++ b/crates/biome_analyze/CONTRIBUTING.md @@ -170,7 +170,7 @@ use biome_deserialize_macros::Deserializable; pub struct MyRuleOptions { behavior: Behavior, threshold: u8, - behavior_exceptions: Vec<String> + behavior_exceptions: Box<[Box<str>]> } #[derive(Clone, Debug, Default, Deserializable)] @@ -182,6 +182,9 @@ pub enum Behavior { } ``` +Note that we use a boxed slice `Box<[Box<str>]>` instead of `Vec<String>`. +This allows saving memory: [boxed slices and boxed str use 2 words instead of three words](https://nnethercote.github.io/perf-book/type-sizes.html#boxed-slices). + To allow deserializing instances of the types `MyRuleOptions` and `Behavior`, they have to implement the `Deserializable` trait from the `biome_deserialize` crate. This is what the `Deserializable` keyword in the `#[derive]` statements above did. @@ -432,8 +435,10 @@ impl Rule for ForLoopCountReferences { #### Multiple signals Some rules require you to find all possible cases upfront in `run` function. -To achieve that you can change Signals type from `Option<()>` to `Vec<()>`. -This will call the diagnostic/action function for every item of the vec. +To achieve that you can change Signals type from `Option<Self::State>` to an iterable data structure such as `Vec<Self::State>` or `Box<[Self::State]>`. +This will call the diagnostic/action function for every item of the data structure. +We prefer to use `Box<[_]>` over `Vec<_>` because it takes less memory. +You can easily convert a `Vec<_>` into a `Box<[_]>` using the `Vec::into_boxed_slice()` method. Taking previous example and modifying it a bit we can apply diagnostic for each item easily. @@ -441,7 +446,7 @@ Taking previous example and modifying it a bit we can apply diagnostic for each impl Rule for ForLoopCountReferences { type Query = Semantic<JsForStatement>; type State = TextRange; - type Signals = Vec<Self::State>; // Replaced Option with Vec + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { @@ -462,7 +467,7 @@ impl Rule for ForLoopCountReferences { Some(range) }).collect::<Vec<_>>(); - write_ranges + write_ranges.into_boxed_slice() } fn diagnostic(_: &RuleContext<Self>, range: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_analyze/src/lib.rs b/crates/biome_analyze/src/lib.rs index ed5e3e2ad914..e20f13b34f1f 100644 --- a/crates/biome_analyze/src/lib.rs +++ b/crates/biome_analyze/src/lib.rs @@ -182,7 +182,7 @@ where SuppressionDiagnostic::new( category!("suppressions/unused"), suppression.comment_span, - "Suppression comment is not being used", + "Suppression comment has no effect. Remove the suppression or make sure you are suppressing the correct rule.", ) }); @@ -424,7 +424,7 @@ where suppression .suppressed_instances .iter() - .any(|(filter, v)| *filter == entry.rule && v == value) + .any(|(filter, v)| *filter == entry.rule && v == value.as_ref()) }) }); diff --git a/crates/biome_analyze/src/matcher.rs b/crates/biome_analyze/src/matcher.rs index f1a101e22fa8..a88858bcb53d 100644 --- a/crates/biome_analyze/src/matcher.rs +++ b/crates/biome_analyze/src/matcher.rs @@ -138,7 +138,7 @@ pub struct SignalEntry<'phase, L: Language> { /// Unique identifier for the rule that emitted this signal pub rule: RuleKey, /// Optional rule instances being suppressed - pub instances: Vec<String>, + pub instances: Box<[Box<str>]>, /// Text range in the document this signal covers pub text_range: TextRange, } diff --git a/crates/biome_analyze/src/rule.rs b/crates/biome_analyze/src/rule.rs index 4e7b3cc83588..594c75327733 100644 --- a/crates/biome_analyze/src/rule.rs +++ b/crates/biome_analyze/src/rule.rs @@ -85,7 +85,7 @@ impl TryFrom<FixKind> for Applicability { } #[derive(Debug, Clone, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, schemars::JsonSchema))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub enum RuleSource { /// Rules from [Rust Clippy](https://rust-lang.github.io/rust-clippy/master/index.html) @@ -118,7 +118,7 @@ pub enum RuleSource { EslintTypeScript(&'static str), /// Rules from [Eslint Plugin Unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) EslintUnicorn(&'static str), - /// Rules from [Eslint Plugin Unused Imports](https://github.com/sweepline/eslint-plugin-unused-imports) + /// Rules from [Eslint Plugin Unused Imports](https://github.com/sweepline/eslint-plugin-unused-imports) EslintUnusedImports(&'static str), /// Rules from [Eslint Plugin Mysticatea](https://github.com/mysticatea/eslint-plugin) EslintMysticatea(&'static str), @@ -126,8 +126,12 @@ pub enum RuleSource { EslintBarrelFiles(&'static str), /// Rules from [Eslint Plugin N](https://github.com/eslint-community/eslint-plugin-n) EslintN(&'static str), + /// Rules from [Eslint Plugin Next](https://github.com/vercel/next.js/tree/canary/packages/eslint-plugin-next) + EslintNext(&'static str), /// Rules from [Stylelint](https://github.com/stylelint/stylelint) Stylelint(&'static str), + /// Rules from [Eslint Plugin No Secrets](https://github.com/nickdeis/eslint-plugin-no-secrets) + EslintNoSecrets(&'static str), } impl PartialEq for RuleSource { @@ -158,7 +162,9 @@ impl std::fmt::Display for RuleSource { Self::EslintMysticatea(_) => write!(f, "@mysticatea/eslint-plugin"), Self::EslintBarrelFiles(_) => write!(f, "eslint-plugin-barrel-files"), Self::EslintN(_) => write!(f, "eslint-plugin-n"), + Self::EslintNext(_) => write!(f, "@next/eslint-plugin-next"), Self::Stylelint(_) => write!(f, "Stylelint"), + Self::EslintNoSecrets(_) => write!(f, "eslint-plugin-no-secrets"), } } } @@ -207,6 +213,8 @@ impl RuleSource { | Self::EslintMysticatea(rule_name) | Self::EslintBarrelFiles(rule_name) | Self::EslintN(rule_name) + | Self::EslintNext(rule_name) + | Self::EslintNoSecrets(rule_name) | Self::Stylelint(rule_name) => rule_name, } } @@ -231,7 +239,9 @@ impl RuleSource { Self::EslintMysticatea(rule_name) => format!("@mysticatea/{rule_name}"), Self::EslintBarrelFiles(rule_name) => format!("barrel-files/{rule_name}"), Self::EslintN(rule_name) => format!("n/{rule_name}"), + Self::EslintNext(rule_name) => format!("@next/{rule_name}"), Self::Stylelint(rule_name) => format!("stylelint/{rule_name}"), + Self::EslintNoSecrets(rule_name) => format!("no-secrets/{rule_name}"), } } @@ -256,7 +266,9 @@ impl RuleSource { Self::EslintMysticatea(rule_name) => format!("https://github.com/mysticatea/eslint-plugin/blob/master/docs/rules/{rule_name}.md"), Self::EslintBarrelFiles(rule_name) => format!("https://github.com/thepassle/eslint-plugin-barrel-files/blob/main/docs/rules/{rule_name}.md"), Self::EslintN(rule_name) => format!("https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/{rule_name}.md"), + Self::EslintNext(rule_name) => format!("https://nextjs.org/docs/messages/{rule_name}"), Self::Stylelint(rule_name) => format!("https://github.com/stylelint/stylelint/blob/main/lib/rules/{rule_name}/README.md"), + Self::EslintNoSecrets(_) => "https://github.com/nickdeis/eslint-plugin-no-secrets/blob/master/README.md".to_string(), } } @@ -280,7 +292,7 @@ impl RuleSource { } #[derive(Debug, Default, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, schemars::JsonSchema))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub enum RuleSourceKind { /// The rule implements the same logic of the source @@ -788,8 +800,8 @@ pub trait Rule: RuleMeta + Sized { /// *Note: For `noUnusedVariables` the above may not seem very useful (and /// indeed it's not implemented), but for rules such as /// `useExhaustiveDependencies` this is actually desirable.* - fn instances_for_signal(_signal: &Self::State) -> Vec<String> { - Vec::new() + fn instances_for_signal(_signal: &Self::State) -> Box<[Box<str>]> { + Vec::new().into_boxed_slice() } /// Used by the analyzer to associate a range of source text to a signal in diff --git a/crates/biome_cli/src/changed.rs b/crates/biome_cli/src/changed.rs index b3eae9f37621..7b101ae841f1 100644 --- a/crates/biome_cli/src/changed.rs +++ b/crates/biome_cli/src/changed.rs @@ -7,14 +7,14 @@ use std::ffi::OsString; pub(crate) fn get_changed_files( fs: &DynRef<'_, dyn FileSystem>, configuration: &PartialConfiguration, - since: Option<String>, + since: Option<&str>, ) -> Result<Vec<OsString>, CliDiagnostic> { let default_branch = configuration .vcs .as_ref() .and_then(|v| v.default_branch.as_ref()); - let base = match (since.as_ref(), default_branch) { + let base = match (since, default_branch) { (Some(since), Some(_)) => since, (Some(since), None) => since, (None, Some(branch)) => branch, diff --git a/crates/biome_cli/src/commands/check.rs b/crates/biome_cli/src/commands/check.rs index d0584dd2e26f..616f9013e7f6 100644 --- a/crates/biome_cli/src/commands/check.rs +++ b/crates/biome_cli/src/commands/check.rs @@ -1,36 +1,24 @@ +use super::{determine_fix_file_mode, FixFileModeOptions, LoadEditorConfig}; use crate::cli_options::CliOptions; -use crate::commands::{ - get_files_to_process, get_stdin, resolve_manifest, validate_configuration_diagnostics, -}; -use crate::execute::VcsTargeted; -use crate::{ - execute_mode, setup_cli_subscriber, CliDiagnostic, CliSession, Execution, TraversalMode, -}; +use crate::commands::{get_files_to_process_with_cli_options, CommandRunner}; +use crate::{CliDiagnostic, Execution, TraversalMode}; use biome_configuration::analyzer::assists::PartialAssistsConfiguration; use biome_configuration::{ organize_imports::PartialOrganizeImports, PartialConfiguration, PartialFormatterConfiguration, PartialLinterConfiguration, }; -use biome_console::{markup, ConsoleExt}; +use biome_console::Console; use biome_deserialize::Merge; -use biome_diagnostics::PrintDiagnostic; -use biome_service::configuration::{load_editorconfig, PartialConfigurationExt}; -use biome_service::workspace::RegisterProjectFolderParams; -use biome_service::{ - configuration::{load_configuration, LoadedConfiguration}, - workspace::UpdateSettingsParams, -}; +use biome_fs::FileSystem; +use biome_service::{configuration::LoadedConfiguration, DynRef, Workspace, WorkspaceError}; use std::ffi::OsString; -use super::{determine_fix_file_mode, FixFileModeOptions}; - pub(crate) struct CheckCommandPayload { pub(crate) apply: bool, pub(crate) apply_unsafe: bool, pub(crate) write: bool, pub(crate) fix: bool, pub(crate) unsafe_: bool, - pub(crate) cli_options: CliOptions, pub(crate) configuration: Option<PartialConfiguration>, pub(crate) paths: Vec<OsString>, pub(crate) stdin_file_path: Option<String>, @@ -43,169 +31,127 @@ pub(crate) struct CheckCommandPayload { pub(crate) since: Option<String>, } -/// Handler for the "check" command of the Biome CLI -pub(crate) fn check( - session: CliSession, - payload: CheckCommandPayload, -) -> Result<(), CliDiagnostic> { - let CheckCommandPayload { - apply, - apply_unsafe, - write, - fix, - unsafe_, - cli_options, - configuration, - paths, - stdin_file_path, - linter_enabled, - organize_imports_enabled, - formatter_enabled, - since, - assists_enabled, - staged, - changed, - } = payload; - setup_cli_subscriber(cli_options.log_level, cli_options.log_kind); - - let fix_file_mode = determine_fix_file_mode( - FixFileModeOptions { - apply, - apply_unsafe, - write, - fix, - unsafe_, - }, - session.app.console, - )?; - - let loaded_configuration = - load_configuration(&session.app.fs, cli_options.as_configuration_path_hint())?; - validate_configuration_diagnostics( - &loaded_configuration, - session.app.console, - cli_options.verbose, - )?; - - let editorconfig_search_path = loaded_configuration.directory_path.clone(); - let LoadedConfiguration { - configuration: biome_configuration, - directory_path: configuration_path, - .. - } = loaded_configuration; - - let should_use_editorconfig = configuration - .as_ref() - .and_then(|c| c.use_editorconfig()) - .unwrap_or(biome_configuration.use_editorconfig().unwrap_or_default()); - let mut fs_configuration = if should_use_editorconfig { - let (editorconfig, editorconfig_diagnostics) = { - let search_path = editorconfig_search_path.unwrap_or_else(|| { - let fs = &session.app.fs; - fs.working_directory().unwrap_or_default() - }); - load_editorconfig(&session.app.fs, search_path)? - }; - for diagnostic in editorconfig_diagnostics { - session.app.console.error(markup! { - {PrintDiagnostic::simple(&diagnostic)} - }) - } - editorconfig.unwrap_or_default() - } else { - Default::default() - }; - // this makes biome configuration take precedence over editorconfig configuration - fs_configuration.merge_with(biome_configuration); - - let formatter = fs_configuration - .formatter - .get_or_insert_with(PartialFormatterConfiguration::default); - - if formatter_enabled.is_some() { - formatter.enabled = formatter_enabled; +impl LoadEditorConfig for CheckCommandPayload { + fn should_load_editor_config(&self, fs_configuration: &PartialConfiguration) -> bool { + self.configuration + .as_ref() + .and_then(|c| c.use_editorconfig()) + .unwrap_or(fs_configuration.use_editorconfig().unwrap_or_default()) } +} - let linter = fs_configuration - .linter - .get_or_insert_with(PartialLinterConfiguration::default); +impl CommandRunner for CheckCommandPayload { + const COMMAND_NAME: &'static str = "check"; + + fn merge_configuration( + &mut self, + loaded_configuration: LoadedConfiguration, + fs: &DynRef<'_, dyn FileSystem>, + console: &mut dyn Console, + ) -> Result<PartialConfiguration, WorkspaceError> { + let editorconfig_search_path = loaded_configuration.directory_path.clone(); + let LoadedConfiguration { + configuration: biome_configuration, + .. + } = loaded_configuration; + let mut fs_configuration = + self.load_editor_config(editorconfig_search_path, &biome_configuration, fs, console)?; + // this makes biome configuration take precedence over editorconfig configuration + fs_configuration.merge_with(biome_configuration); + + let formatter = fs_configuration + .formatter + .get_or_insert_with(PartialFormatterConfiguration::default); + + if self.formatter_enabled.is_some() { + formatter.enabled = self.formatter_enabled; + } - if linter_enabled.is_some() { - linter.enabled = linter_enabled; - } + let linter = fs_configuration + .linter + .get_or_insert_with(PartialLinterConfiguration::default); - let organize_imports = fs_configuration - .organize_imports - .get_or_insert_with(PartialOrganizeImports::default); + if self.linter_enabled.is_some() { + linter.enabled = self.linter_enabled; + } - if organize_imports_enabled.is_some() { - organize_imports.enabled = organize_imports_enabled; - } + let organize_imports = fs_configuration + .organize_imports + .get_or_insert_with(PartialOrganizeImports::default); - let assists = fs_configuration - .assists - .get_or_insert_with(PartialAssistsConfiguration::default); + if self.organize_imports_enabled.is_some() { + organize_imports.enabled = self.organize_imports_enabled; + } - if assists_enabled.is_some() { - assists.enabled = assists_enabled; - } + let assists = fs_configuration + .assists + .get_or_insert_with(PartialAssistsConfiguration::default); - if let Some(mut configuration) = configuration { - if let Some(linter) = configuration.linter.as_mut() { - // Don't overwrite rules from the CLI configuration. - // Otherwise, rules that are disabled in the config file might - // become re-enabled due to the defaults included in the CLI - // configuration. - linter.rules = None; + if self.assists_enabled.is_some() { + assists.enabled = self.assists_enabled; } - fs_configuration.merge_with(configuration); + + if let Some(mut configuration) = self.configuration.clone() { + if let Some(linter) = configuration.linter.as_mut() { + // Don't overwrite rules from the CLI configuration. + // Otherwise, rules that are disabled in the config file might + // become re-enabled due to the defaults included in the CLI + // configuration. + linter.rules = None; + } + fs_configuration.merge_with(configuration); + } + + Ok(fs_configuration) } - // check if support of git ignore files is enabled - let vcs_base_path = configuration_path.or(session.app.fs.working_directory()); - let (vcs_base_path, gitignore_matches) = - fs_configuration.retrieve_gitignore_matches(&session.app.fs, vcs_base_path.as_deref())?; - - let stdin = get_stdin(stdin_file_path, &mut *session.app.console, "check")?; - - let vcs_targeted_paths = - get_files_to_process(since, changed, staged, &session.app.fs, &fs_configuration)?; - - session - .app - .workspace - .register_project_folder(RegisterProjectFolderParams { - path: session.app.fs.working_directory(), - set_as_current_workspace: true, - })?; - let manifest_data = resolve_manifest(&session.app.fs)?; - - if let Some(manifest_data) = manifest_data { - session - .app - .workspace - .set_manifest_for_project(manifest_data.into())?; + fn get_files_to_process( + &self, + fs: &DynRef<'_, dyn FileSystem>, + configuration: &PartialConfiguration, + ) -> Result<Vec<OsString>, CliDiagnostic> { + let paths = get_files_to_process_with_cli_options( + self.since.as_deref(), + self.changed, + self.staged, + fs, + configuration, + )? + .unwrap_or(self.paths.clone()); + + Ok(paths) } - session - .app - .workspace - .update_settings(UpdateSettingsParams { - workspace_directory: session.app.fs.working_directory(), - configuration: fs_configuration, - vcs_base_path, - gitignore_matches, - })?; - - execute_mode( - Execution::new(TraversalMode::Check { + fn get_stdin_file_path(&self) -> Option<&str> { + self.stdin_file_path.as_deref() + } + + fn should_write(&self) -> bool { + self.write || self.fix + } + + fn get_execution( + &self, + cli_options: &CliOptions, + console: &mut dyn Console, + _workspace: &dyn Workspace, + ) -> Result<Execution, CliDiagnostic> { + let fix_file_mode = determine_fix_file_mode( + FixFileModeOptions { + apply: self.apply, + apply_unsafe: self.apply_unsafe, + write: self.write, + fix: self.fix, + unsafe_: self.unsafe_, + }, + console, + )?; + + Ok(Execution::new(TraversalMode::Check { fix_file_mode, - stdin, - vcs_targeted: VcsTargeted { staged, changed }, + stdin: self.get_stdin(console)?, + vcs_targeted: (self.staged, self.changed).into(), }) - .set_report(&cli_options), - session, - &cli_options, - vcs_targeted_paths.unwrap_or(paths), - ) + .set_report(cli_options)) + } } diff --git a/crates/biome_cli/src/commands/ci.rs b/crates/biome_cli/src/commands/ci.rs index 29b909276813..be4689fcf04c 100644 --- a/crates/biome_cli/src/commands/ci.rs +++ b/crates/biome_cli/src/commands/ci.rs @@ -1,18 +1,15 @@ use crate::changed::get_changed_files; use crate::cli_options::CliOptions; -use crate::commands::{resolve_manifest, validate_configuration_diagnostics}; -use crate::execute::VcsTargeted; -use crate::{execute_mode, setup_cli_subscriber, CliDiagnostic, CliSession, Execution}; +use crate::commands::{CommandRunner, LoadEditorConfig}; +use crate::{CliDiagnostic, Execution}; use biome_configuration::analyzer::assists::PartialAssistsConfiguration; use biome_configuration::{organize_imports::PartialOrganizeImports, PartialConfiguration}; use biome_configuration::{PartialFormatterConfiguration, PartialLinterConfiguration}; -use biome_console::{markup, ConsoleExt}; +use biome_console::Console; use biome_deserialize::Merge; -use biome_diagnostics::PrintDiagnostic; -use biome_service::configuration::{ - load_configuration, load_editorconfig, LoadedConfiguration, PartialConfigurationExt, -}; -use biome_service::workspace::{RegisterProjectFolderParams, UpdateSettingsParams}; +use biome_fs::FileSystem; +use biome_service::configuration::LoadedConfiguration; +use biome_service::{DynRef, Workspace, WorkspaceError}; use std::ffi::OsString; pub(crate) struct CiCommandPayload { @@ -22,163 +19,124 @@ pub(crate) struct CiCommandPayload { pub(crate) assists_enabled: Option<bool>, pub(crate) paths: Vec<OsString>, pub(crate) configuration: Option<PartialConfiguration>, - pub(crate) cli_options: CliOptions, pub(crate) changed: bool, pub(crate) since: Option<String>, } -/// Handler for the "ci" command of the Biome CLI -pub(crate) fn ci(session: CliSession, payload: CiCommandPayload) -> Result<(), CliDiagnostic> { - let CiCommandPayload { - cli_options, - formatter_enabled, - linter_enabled, - organize_imports_enabled, - assists_enabled, - configuration, - mut paths, - since, - changed, - } = payload; - setup_cli_subscriber(cli_options.log_level, cli_options.log_kind); - - let loaded_configuration = - load_configuration(&session.app.fs, cli_options.as_configuration_path_hint())?; - - validate_configuration_diagnostics( - &loaded_configuration, - session.app.console, - cli_options.verbose, - )?; - - let LoadedConfiguration { - configuration: biome_configuration, - directory_path: configuration_path, - .. - } = loaded_configuration; - - let should_use_editorconfig = configuration - .as_ref() - .and_then(|c| c.use_editorconfig()) - .unwrap_or(biome_configuration.use_editorconfig().unwrap_or_default()); - let mut fs_configuration = if should_use_editorconfig { - let (editorconfig, editorconfig_diagnostics) = { - let search_path = configuration_path.clone().unwrap_or_else(|| { - let fs = &session.app.fs; - fs.working_directory().unwrap_or_default() - }); - load_editorconfig(&session.app.fs, search_path)? - }; - for diagnostic in editorconfig_diagnostics { - session.app.console.error(markup! { - {PrintDiagnostic::simple(&diagnostic)} - }) - } - editorconfig.unwrap_or_default() - } else { - Default::default() - }; - // this makes biome configuration take precedence over editorconfig configuration - fs_configuration.merge_with(biome_configuration); - - let formatter = fs_configuration - .formatter - .get_or_insert_with(PartialFormatterConfiguration::default); - - if formatter_enabled.is_some() { - formatter.enabled = formatter_enabled; +impl LoadEditorConfig for CiCommandPayload { + fn should_load_editor_config(&self, fs_configuration: &PartialConfiguration) -> bool { + self.configuration + .as_ref() + .and_then(|c| c.use_editorconfig()) + .unwrap_or(fs_configuration.use_editorconfig().unwrap_or_default()) } +} - let linter = fs_configuration - .linter - .get_or_insert_with(PartialLinterConfiguration::default); +impl CommandRunner for CiCommandPayload { + const COMMAND_NAME: &'static str = "ci"; + + fn merge_configuration( + &mut self, + loaded_configuration: LoadedConfiguration, + fs: &DynRef<'_, dyn FileSystem>, + console: &mut dyn Console, + ) -> Result<PartialConfiguration, WorkspaceError> { + let LoadedConfiguration { + configuration: biome_configuration, + directory_path: configuration_path, + .. + } = loaded_configuration; + + let mut fs_configuration = + self.load_editor_config(configuration_path, &biome_configuration, fs, console)?; + // this makes biome configuration take precedence over editorconfig configuration + fs_configuration.merge_with(biome_configuration); + + let formatter = fs_configuration + .formatter + .get_or_insert_with(PartialFormatterConfiguration::default); + + if self.formatter_enabled.is_some() { + formatter.enabled = self.formatter_enabled; + } - if linter_enabled.is_some() { - linter.enabled = linter_enabled; - } + let linter = fs_configuration + .linter + .get_or_insert_with(PartialLinterConfiguration::default); - let organize_imports = fs_configuration - .organize_imports - .get_or_insert_with(PartialOrganizeImports::default); + if self.linter_enabled.is_some() { + linter.enabled = self.linter_enabled; + } - if organize_imports_enabled.is_some() { - organize_imports.enabled = organize_imports_enabled; - } + let organize_imports = fs_configuration + .organize_imports + .get_or_insert_with(PartialOrganizeImports::default); - let assists = fs_configuration - .assists - .get_or_insert_with(PartialAssistsConfiguration::default); + if self.organize_imports_enabled.is_some() { + organize_imports.enabled = self.organize_imports_enabled; + } - if assists_enabled.is_some() { - assists.enabled = assists_enabled; - } + let assists = fs_configuration + .assists + .get_or_insert_with(PartialAssistsConfiguration::default); - // no point in doing the traversal if all the checks have been disabled - if fs_configuration.is_formatter_disabled() - && fs_configuration.is_linter_disabled() - && fs_configuration.is_organize_imports_disabled() - { - return Err(CliDiagnostic::incompatible_end_configuration("Formatter, linter and organize imports are disabled, can't perform the command. This is probably and error.")); + if self.assists_enabled.is_some() { + assists.enabled = self.assists_enabled; + } + + if let Some(mut configuration) = self.configuration.clone() { + if let Some(linter) = configuration.linter.as_mut() { + // Don't overwrite rules from the CLI configuration. + // Otherwise, rules that are disabled in the config file might + // become re-enabled due to the defaults included in the CLI + // configuration. + linter.rules = None; + } + fs_configuration.merge_with(configuration); + } + + Ok(fs_configuration) } - if let Some(mut configuration) = configuration { - if let Some(linter) = configuration.linter.as_mut() { - // Don't overwrite rules from the CLI configuration. - // Otherwise, rules that are disabled in the config file might - // become re-enabled due to the defaults included in the CLI - // configuration. - linter.rules = None; + fn get_files_to_process( + &self, + fs: &DynRef<'_, dyn FileSystem>, + configuration: &PartialConfiguration, + ) -> Result<Vec<OsString>, CliDiagnostic> { + if self.changed { + get_changed_files(fs, configuration, self.since.as_deref()) + } else { + Ok(self.paths.clone()) } - fs_configuration.merge_with(configuration); } - // check if support of git ignore files is enabled - let vcs_base_path = configuration_path.or(session.app.fs.working_directory()); - let (vcs_base_path, gitignore_matches) = - fs_configuration.retrieve_gitignore_matches(&session.app.fs, vcs_base_path.as_deref())?; + fn get_stdin_file_path(&self) -> Option<&str> { + None + } - if since.is_some() && !changed { - return Err(CliDiagnostic::incompatible_arguments("since", "changed")); + fn should_write(&self) -> bool { + false } - if changed { - paths = get_changed_files(&session.app.fs, &fs_configuration, since)?; + fn get_execution( + &self, + cli_options: &CliOptions, + _console: &mut dyn Console, + _workspace: &dyn Workspace, + ) -> Result<Execution, CliDiagnostic> { + Ok(Execution::new_ci((false, self.changed).into()).set_report(cli_options)) } - session - .app - .workspace - .register_project_folder(RegisterProjectFolderParams { - path: session.app.fs.working_directory(), - set_as_current_workspace: true, - })?; - - let manifest_data = resolve_manifest(&session.app.fs)?; - - if let Some(manifest_data) = manifest_data { - session - .app - .workspace - .set_manifest_for_project(manifest_data.into())?; + fn check_incompatible_arguments(&self) -> Result<(), CliDiagnostic> { + if matches!(self.formatter_enabled, Some(false)) + && matches!(self.linter_enabled, Some(false)) + && matches!(self.organize_imports_enabled, Some(false)) + { + return Err(CliDiagnostic::incompatible_end_configuration("Formatter, linter and organize imports are disabled, can't perform the command. At least one feature needs to be enabled. This is probably and error.")); + } + if self.since.is_some() && !self.changed { + return Err(CliDiagnostic::incompatible_arguments("since", "changed")); + } + Ok(()) } - session - .app - .workspace - .update_settings(UpdateSettingsParams { - configuration: fs_configuration, - workspace_directory: session.app.fs.working_directory(), - vcs_base_path, - gitignore_matches, - })?; - - execute_mode( - Execution::new_ci(VcsTargeted { - staged: false, - changed, - }) - .set_report(&cli_options), - session, - &cli_options, - paths, - ) } diff --git a/crates/biome_cli/src/commands/format.rs b/crates/biome_cli/src/commands/format.rs index 0438e60f5706..6b876ff5691b 100644 --- a/crates/biome_cli/src/commands/format.rs +++ b/crates/biome_cli/src/commands/format.rs @@ -1,28 +1,21 @@ use crate::cli_options::CliOptions; -use crate::commands::{ - get_files_to_process, get_stdin, resolve_manifest, validate_configuration_diagnostics, -}; +use crate::commands::{get_files_to_process_with_cli_options, CommandRunner, LoadEditorConfig}; use crate::diagnostics::DeprecatedArgument; -use crate::execute::VcsTargeted; -use crate::{ - execute_mode, setup_cli_subscriber, CliDiagnostic, CliSession, Execution, TraversalMode, -}; +use crate::{CliDiagnostic, Execution, TraversalMode}; use biome_configuration::vcs::PartialVcsConfiguration; use biome_configuration::{ - PartialCssFormatter, PartialFilesConfiguration, PartialFormatterConfiguration, - PartialGraphqlFormatter, PartialJavascriptFormatter, PartialJsonFormatter, + PartialConfiguration, PartialCssFormatter, PartialFilesConfiguration, + PartialFormatterConfiguration, PartialGraphqlFormatter, PartialJavascriptFormatter, + PartialJsonFormatter, }; -use biome_console::{markup, ConsoleExt}; +use biome_console::{markup, Console, ConsoleExt}; use biome_deserialize::Merge; use biome_diagnostics::PrintDiagnostic; -use biome_service::configuration::{ - load_configuration, load_editorconfig, LoadedConfiguration, PartialConfigurationExt, -}; -use biome_service::workspace::{RegisterProjectFolderParams, UpdateSettingsParams}; +use biome_fs::FileSystem; +use biome_service::configuration::LoadedConfiguration; +use biome_service::{DynRef, Workspace, WorkspaceError}; use std::ffi::OsString; -use super::check_fix_incompatible_arguments; - pub(crate) struct FormatCommandPayload { pub(crate) javascript_formatter: Option<PartialJavascriptFormatter>, pub(crate) json_formatter: Option<PartialJsonFormatter>, @@ -34,227 +27,181 @@ pub(crate) struct FormatCommandPayload { pub(crate) stdin_file_path: Option<String>, pub(crate) write: bool, pub(crate) fix: bool, - pub(crate) cli_options: CliOptions, pub(crate) paths: Vec<OsString>, pub(crate) staged: bool, pub(crate) changed: bool, pub(crate) since: Option<String>, } -/// Handler for the "format" command of the Biome CLI -pub(crate) fn format( - session: CliSession, - payload: FormatCommandPayload, -) -> Result<(), CliDiagnostic> { - let FormatCommandPayload { - mut javascript_formatter, - mut formatter_configuration, - vcs_configuration, - mut paths, - cli_options, - stdin_file_path, - files_configuration, - write, - fix, - mut json_formatter, - css_formatter, - graphql_formatter, - since, - staged, - changed, - } = payload; - setup_cli_subscriber(cli_options.log_level, cli_options.log_kind); - - check_fix_incompatible_arguments(super::FixFileModeOptions { - apply: false, - apply_unsafe: false, - write, - fix, - unsafe_: false, - })?; - - let loaded_configuration = - load_configuration(&session.app.fs, cli_options.as_configuration_path_hint())?; - validate_configuration_diagnostics( - &loaded_configuration, - session.app.console, - cli_options.verbose, - )?; - - let editorconfig_search_path = loaded_configuration.directory_path.clone(); - let LoadedConfiguration { - configuration: biome_configuration, - directory_path: configuration_path, - .. - } = loaded_configuration; - - let should_use_editorconfig = formatter_configuration - .as_ref() - .and_then(|c| c.use_editorconfig) - .unwrap_or(biome_configuration.use_editorconfig().unwrap_or_default()); - let mut fs_configuration = if should_use_editorconfig { - let (editorconfig, editorconfig_diagnostics) = { - let search_path = editorconfig_search_path.unwrap_or_else(|| { - let fs = &session.app.fs; - fs.working_directory().unwrap_or_default() - }); - load_editorconfig(&session.app.fs, search_path)? - }; - for diagnostic in editorconfig_diagnostics { - session.app.console.error(markup! { - {PrintDiagnostic::simple(&diagnostic)} - }) - } - editorconfig.unwrap_or_default() - } else { - Default::default() - }; - // this makes biome configuration take precedence over editorconfig configuration - fs_configuration.merge_with(biome_configuration); - let mut configuration = fs_configuration; - - // TODO: remove in biome 2.0 - let console = &mut *session.app.console; - if let Some(config) = formatter_configuration.as_mut() { - if let Some(indent_size) = config.indent_size { - let diagnostic = DeprecatedArgument::new(markup! { - "The argument "<Emphasis>"--indent-size"</Emphasis>" is deprecated, it will be removed in the next major release. Use "<Emphasis>"--indent-width"</Emphasis>" instead." - }); - console.error(markup! { - {PrintDiagnostic::simple(&diagnostic)} - }); +impl LoadEditorConfig for FormatCommandPayload { + fn should_load_editor_config(&self, fs_configuration: &PartialConfiguration) -> bool { + self.formatter_configuration + .as_ref() + .and_then(|c| c.use_editorconfig) + .unwrap_or(fs_configuration.use_editorconfig().unwrap_or_default()) + } +} - if config.indent_width.is_none() { - config.indent_width = Some(indent_size); +impl CommandRunner for FormatCommandPayload { + const COMMAND_NAME: &'static str = "format"; + + fn merge_configuration( + &mut self, + loaded_configuration: LoadedConfiguration, + fs: &DynRef<'_, dyn FileSystem>, + console: &mut dyn Console, + ) -> Result<PartialConfiguration, WorkspaceError> { + let LoadedConfiguration { + configuration: biome_configuration, + directory_path: configuration_path, + .. + } = loaded_configuration; + let editorconfig_search_path = configuration_path.clone(); + let mut fs_configuration = + self.load_editor_config(editorconfig_search_path, &biome_configuration, fs, console)?; + // this makes biome configuration take precedence over editorconfig configuration + fs_configuration.merge_with(biome_configuration); + let mut configuration = fs_configuration; + + // TODO: remove in biome 2.0 + if let Some(config) = self.formatter_configuration.as_mut() { + if let Some(indent_size) = config.indent_size { + let diagnostic = DeprecatedArgument::new(markup! { + "The argument "<Emphasis>"--indent-size"</Emphasis>" is deprecated, it will be removed in the next major release. Use "<Emphasis>"--indent-width"</Emphasis>" instead." + }); + console.error(markup! { + {PrintDiagnostic::simple(&diagnostic)} + }); + + if config.indent_width.is_none() { + config.indent_width = Some(indent_size); + } } } - } - // TODO: remove in biome 2.0 - if let Some(js_formatter) = javascript_formatter.as_mut() { - if let Some(indent_size) = js_formatter.indent_size { - let diagnostic = DeprecatedArgument::new(markup! { - "The argument "<Emphasis>"--javascript-formatter-indent-size"</Emphasis>" is deprecated, it will be removed in the next major release. Use "<Emphasis>"--javascript-formatter-indent-width"</Emphasis>" instead." - }); - console.error(markup! { - {PrintDiagnostic::simple(&diagnostic)} - }); + // TODO: remove in biome 2.0 + if let Some(js_formatter) = self.javascript_formatter.as_mut() { + if let Some(indent_size) = js_formatter.indent_size { + let diagnostic = DeprecatedArgument::new(markup! { + "The argument "<Emphasis>"--javascript-formatter-indent-size"</Emphasis>" is deprecated, it will be removed in the next major release. Use "<Emphasis>"--javascript-formatter-indent-width"</Emphasis>" instead." + }); + console.error(markup! { + {PrintDiagnostic::simple(&diagnostic)} + }); + + if js_formatter.indent_width.is_none() { + js_formatter.indent_width = Some(indent_size); + } + } - if js_formatter.indent_width.is_none() { - js_formatter.indent_width = Some(indent_size); + if let Some(trailing_comma) = js_formatter.trailing_comma { + let diagnostic = DeprecatedArgument::new(markup! { + "The argument "<Emphasis>"--trailing-comma"</Emphasis>" is deprecated, it will be removed in the next major release. Use "<Emphasis>"--trailing-commas"</Emphasis>" instead." + }); + console.error(markup! { + {PrintDiagnostic::simple(&diagnostic)} + }); + + if js_formatter.trailing_commas.is_none() { + js_formatter.trailing_commas = Some(trailing_comma); + } } } - - if let Some(trailing_comma) = js_formatter.trailing_comma { - let diagnostic = DeprecatedArgument::new(markup! { - "The argument "<Emphasis>"--trailing-comma"</Emphasis>" is deprecated, it will be removed in the next major release. Use "<Emphasis>"--trailing-commas"</Emphasis>" instead." - }); - console.error(markup! { - {PrintDiagnostic::simple(&diagnostic)} - }); - - if js_formatter.trailing_commas.is_none() { - js_formatter.trailing_commas = Some(trailing_comma); + // TODO: remove in biome 2.0 + if let Some(json_formatter) = self.json_formatter.as_mut() { + if let Some(indent_size) = json_formatter.indent_size { + let diagnostic = DeprecatedArgument::new(markup! { + "The argument "<Emphasis>"--json-formatter-indent-size"</Emphasis>" is deprecated, it will be removed in the next major release. Use "<Emphasis>"--json-formatter-indent-width"</Emphasis>" instead." + }); + console.error(markup! { + {PrintDiagnostic::simple(&diagnostic)} + }); + + if json_formatter.indent_width.is_none() { + json_formatter.indent_width = Some(indent_size); + } } } - } - // TODO: remove in biome 2.0 - if let Some(json_formatter) = json_formatter.as_mut() { - if let Some(indent_size) = json_formatter.indent_size { - let diagnostic = DeprecatedArgument::new(markup! { - "The argument "<Emphasis>"--json-formatter-indent-size"</Emphasis>" is deprecated, it will be removed in the next major release. Use "<Emphasis>"--json-formatter-indent-width"</Emphasis>" instead." - }); - console.error(markup! { - {PrintDiagnostic::simple(&diagnostic)} - }); - if json_formatter.indent_width.is_none() { - json_formatter.indent_width = Some(indent_size); + // merge formatter options + if !configuration + .formatter + .as_ref() + .is_some_and(PartialFormatterConfiguration::is_disabled) + { + let formatter = configuration.formatter.get_or_insert_with(Default::default); + if let Some(formatter_configuration) = self.formatter_configuration.clone() { + formatter.merge_with(formatter_configuration); } + + formatter.enabled = Some(true); + } + if self.css_formatter.is_some() { + let css = configuration.css.get_or_insert_with(Default::default); + css.formatter.merge_with(self.css_formatter.clone()); + } + if self.graphql_formatter.is_some() { + let graphql = configuration.graphql.get_or_insert_with(Default::default); + graphql.formatter.merge_with(self.graphql_formatter.clone()); } - } - // merge formatter options - if !configuration - .formatter - .as_ref() - .is_some_and(PartialFormatterConfiguration::is_disabled) - { - let formatter = configuration.formatter.get_or_insert_with(Default::default); - if let Some(formatter_configuration) = formatter_configuration { - formatter.merge_with(formatter_configuration); + if self.javascript_formatter.is_some() { + let javascript = configuration + .javascript + .get_or_insert_with(Default::default); + javascript + .formatter + .merge_with(self.javascript_formatter.clone()); + } + if self.json_formatter.is_some() { + let json = configuration.json.get_or_insert_with(Default::default); + json.formatter.merge_with(self.json_formatter.clone()); } - formatter.enabled = Some(true); - } - if css_formatter.is_some() { - let css = configuration.css.get_or_insert_with(Default::default); - css.formatter.merge_with(css_formatter); - } - if graphql_formatter.is_some() { - let graphql = configuration.graphql.get_or_insert_with(Default::default); - graphql.formatter.merge_with(graphql_formatter); - } + configuration + .files + .merge_with(self.files_configuration.clone()); + configuration.vcs.merge_with(self.vcs_configuration.clone()); - if javascript_formatter.is_some() { - let javascript = configuration - .javascript - .get_or_insert_with(Default::default); - javascript.formatter.merge_with(javascript_formatter); - } - if json_formatter.is_some() { - let json = configuration.json.get_or_insert_with(Default::default); - json.formatter.merge_with(json_formatter); + Ok(configuration) } - configuration.files.merge_with(files_configuration); - configuration.vcs.merge_with(vcs_configuration); - - // check if support of git ignore files is enabled - let vcs_base_path = configuration_path.or(session.app.fs.working_directory()); - let (vcs_base_path, gitignore_matches) = - configuration.retrieve_gitignore_matches(&session.app.fs, vcs_base_path.as_deref())?; + fn get_files_to_process( + &self, + fs: &DynRef<'_, dyn FileSystem>, + configuration: &PartialConfiguration, + ) -> Result<Vec<OsString>, CliDiagnostic> { + let paths = get_files_to_process_with_cli_options( + self.since.as_deref(), + self.changed, + self.staged, + fs, + configuration, + )? + .unwrap_or(self.paths.clone()); - if let Some(_paths) = - get_files_to_process(since, changed, staged, &session.app.fs, &configuration)? - { - paths = _paths; + Ok(paths) } - session - .app - .workspace - .register_project_folder(RegisterProjectFolderParams { - path: session.app.fs.working_directory(), - set_as_current_workspace: true, - })?; - - let manifest_data = resolve_manifest(&session.app.fs)?; - - if let Some(manifest_data) = manifest_data { - session - .app - .workspace - .set_manifest_for_project(manifest_data.into())?; + fn get_stdin_file_path(&self) -> Option<&str> { + self.stdin_file_path.as_deref() } - session - .app - .workspace - .update_settings(UpdateSettingsParams { - workspace_directory: session.app.fs.working_directory(), - configuration, - vcs_base_path, - gitignore_matches, - })?; - - let stdin = get_stdin(stdin_file_path, console, "format")?; - let execution = Execution::new(TraversalMode::Format { - ignore_errors: cli_options.skip_errors, - write: write || fix, - stdin, - vcs_targeted: VcsTargeted { staged, changed }, - }) - .set_report(&cli_options); + fn should_write(&self) -> bool { + self.write || self.fix + } - execute_mode(execution, session, &cli_options, paths) + fn get_execution( + &self, + cli_options: &CliOptions, + console: &mut dyn Console, + _workspace: &dyn Workspace, + ) -> Result<Execution, CliDiagnostic> { + Ok(Execution::new(TraversalMode::Format { + ignore_errors: cli_options.skip_errors, + write: self.should_write(), + stdin: self.get_stdin(console)?, + vcs_targeted: (self.staged, self.changed).into(), + }) + .set_report(cli_options)) + } } diff --git a/crates/biome_cli/src/commands/lint.rs b/crates/biome_cli/src/commands/lint.rs index 84ab59f0f440..065674b2e4c8 100644 --- a/crates/biome_cli/src/commands/lint.rs +++ b/crates/biome_cli/src/commands/lint.rs @@ -1,11 +1,7 @@ +use super::{determine_fix_file_mode, FixFileModeOptions}; use crate::cli_options::CliOptions; -use crate::commands::{ - get_files_to_process, get_stdin, resolve_manifest, validate_configuration_diagnostics, -}; -use crate::execute::VcsTargeted; -use crate::{ - execute_mode, setup_cli_subscriber, CliDiagnostic, CliSession, Execution, TraversalMode, -}; +use crate::commands::{get_files_to_process_with_cli_options, CommandRunner}; +use crate::{CliDiagnostic, Execution, TraversalMode}; use biome_configuration::analyzer::RuleSelector; use biome_configuration::css::PartialCssLinter; use biome_configuration::javascript::PartialJavascriptLinter; @@ -15,22 +11,19 @@ use biome_configuration::{ PartialConfiguration, PartialFilesConfiguration, PartialGraphqlLinter, PartialLinterConfiguration, }; +use biome_console::Console; use biome_deserialize::Merge; -use biome_service::configuration::{ - load_configuration, LoadedConfiguration, PartialConfigurationExt, -}; -use biome_service::workspace::{RegisterProjectFolderParams, UpdateSettingsParams}; +use biome_fs::FileSystem; +use biome_service::configuration::LoadedConfiguration; +use biome_service::{DynRef, Workspace, WorkspaceError}; use std::ffi::OsString; -use super::{determine_fix_file_mode, FixFileModeOptions}; - pub(crate) struct LintCommandPayload { pub(crate) apply: bool, pub(crate) apply_unsafe: bool, pub(crate) write: bool, pub(crate) fix: bool, pub(crate) unsafe_: bool, - pub(crate) cli_options: CliOptions, pub(crate) linter_configuration: Option<PartialLinterConfiguration>, pub(crate) vcs_configuration: Option<PartialVcsConfiguration>, pub(crate) files_configuration: Option<PartialFilesConfiguration>, @@ -47,144 +40,112 @@ pub(crate) struct LintCommandPayload { pub(crate) graphql_linter: Option<PartialGraphqlLinter>, } -/// Handler for the "lint" command of the Biome CLI -pub(crate) fn lint(session: CliSession, payload: LintCommandPayload) -> Result<(), CliDiagnostic> { - let LintCommandPayload { - apply, - apply_unsafe, - write, - fix, - unsafe_, - cli_options, - mut linter_configuration, - paths, - only, - skip, - stdin_file_path, - vcs_configuration, - files_configuration, - staged, - changed, - since, - javascript_linter, - css_linter, - json_linter, - graphql_linter, - } = payload; - setup_cli_subscriber(cli_options.log_level, cli_options.log_kind); +impl CommandRunner for LintCommandPayload { + const COMMAND_NAME: &'static str = "lint"; - let fix_file_mode = determine_fix_file_mode( - FixFileModeOptions { - apply, - apply_unsafe, - write, - fix, - unsafe_, - }, - session.app.console, - )?; + fn merge_configuration( + &mut self, + loaded_configuration: LoadedConfiguration, + _fs: &DynRef<'_, dyn FileSystem>, + _console: &mut dyn Console, + ) -> Result<PartialConfiguration, WorkspaceError> { + let LoadedConfiguration { + configuration: mut fs_configuration, + .. + } = loaded_configuration; - let loaded_configuration = - load_configuration(&session.app.fs, cli_options.as_configuration_path_hint())?; - validate_configuration_diagnostics( - &loaded_configuration, - session.app.console, - cli_options.verbose, - )?; + fs_configuration.merge_with(PartialConfiguration { + linter: if fs_configuration + .linter + .as_ref() + .is_some_and(PartialLinterConfiguration::is_disabled) + { + None + } else { + if let Some(linter) = self.linter_configuration.as_mut() { + // Don't overwrite rules from the CLI configuration. + linter.rules = None; + } + self.linter_configuration.clone() + }, + files: self.files_configuration.clone(), + vcs: self.vcs_configuration.clone(), + ..Default::default() + }); - let LoadedConfiguration { - configuration: mut fs_configuration, - directory_path: configuration_path, - .. - } = loaded_configuration; - fs_configuration.merge_with(PartialConfiguration { - linter: if fs_configuration - .linter - .as_ref() - .is_some_and(PartialLinterConfiguration::is_disabled) - { - None - } else { - if let Some(linter) = linter_configuration.as_mut() { - // Don't overwrite rules from the CLI configuration. - linter.rules = None; - } - linter_configuration - }, - files: files_configuration, - vcs: vcs_configuration, - ..Default::default() - }); + if self.css_linter.is_some() { + let css = fs_configuration.css.get_or_insert_with(Default::default); + css.linter.merge_with(self.css_linter.clone()); + } - if css_linter.is_some() { - let css = fs_configuration.css.get_or_insert_with(Default::default); - css.linter.merge_with(css_linter); - } + if self.graphql_linter.is_some() { + let graphql = fs_configuration + .graphql + .get_or_insert_with(Default::default); + graphql.linter.merge_with(self.graphql_linter.clone()); + } + if self.javascript_linter.is_some() { + let javascript = fs_configuration + .javascript + .get_or_insert_with(Default::default); + javascript.linter.merge_with(self.javascript_linter.clone()); + } + if self.json_linter.is_some() { + let json = fs_configuration.json.get_or_insert_with(Default::default); + json.linter.merge_with(self.json_linter.clone()); + } - if graphql_linter.is_some() { - let graphql = fs_configuration - .graphql - .get_or_insert_with(Default::default); - graphql.linter.merge_with(graphql_linter); + Ok(fs_configuration) } - if javascript_linter.is_some() { - let javascript = fs_configuration - .javascript - .get_or_insert_with(Default::default); - javascript.linter.merge_with(javascript_linter); - } - if json_linter.is_some() { - let json = fs_configuration.json.get_or_insert_with(Default::default); - json.linter.merge_with(json_linter); - } - - let vcs_targeted_paths = - get_files_to_process(since, changed, staged, &session.app.fs, &fs_configuration)?; - - // check if support of git ignore files is enabled - let vcs_base_path = configuration_path.or(session.app.fs.working_directory()); - let (vcs_base_path, gitignore_matches) = - fs_configuration.retrieve_gitignore_matches(&session.app.fs, vcs_base_path.as_deref())?; - let stdin = get_stdin(stdin_file_path, &mut *session.app.console, "lint")?; + fn get_files_to_process( + &self, + fs: &DynRef<'_, dyn FileSystem>, + configuration: &PartialConfiguration, + ) -> Result<Vec<OsString>, CliDiagnostic> { + let paths = get_files_to_process_with_cli_options( + self.since.as_deref(), + self.changed, + self.staged, + fs, + configuration, + )? + .unwrap_or(self.paths.clone()); - session - .app - .workspace - .register_project_folder(RegisterProjectFolderParams { - path: session.app.fs.working_directory(), - set_as_current_workspace: true, - })?; - let manifest_data = resolve_manifest(&session.app.fs)?; + Ok(paths) + } - if let Some(manifest_data) = manifest_data { - session - .app - .workspace - .set_manifest_for_project(manifest_data.into())?; + fn get_stdin_file_path(&self) -> Option<&str> { + self.stdin_file_path.as_deref() } - session - .app - .workspace - .update_settings(UpdateSettingsParams { - workspace_directory: session.app.fs.working_directory(), - configuration: fs_configuration, - vcs_base_path, - gitignore_matches, - })?; + fn should_write(&self) -> bool { + self.write || self.fix + } - execute_mode( - Execution::new(TraversalMode::Lint { + fn get_execution( + &self, + cli_options: &CliOptions, + console: &mut dyn Console, + _workspace: &dyn Workspace, + ) -> Result<Execution, CliDiagnostic> { + let fix_file_mode = determine_fix_file_mode( + FixFileModeOptions { + apply: self.apply, + apply_unsafe: self.apply_unsafe, + write: self.write, + fix: self.fix, + unsafe_: self.unsafe_, + }, + console, + )?; + Ok(Execution::new(TraversalMode::Lint { fix_file_mode, - stdin, - only, - skip, - vcs_targeted: VcsTargeted { staged, changed }, + stdin: self.get_stdin(console)?, + only: self.only.clone(), + skip: self.skip.clone(), + vcs_targeted: (self.staged, self.changed).into(), }) - .set_report(&cli_options), - session, - &cli_options, - vcs_targeted_paths.unwrap_or(paths), - ) + .set_report(cli_options)) + } } diff --git a/crates/biome_cli/src/commands/migrate.rs b/crates/biome_cli/src/commands/migrate.rs index 7f0362f3ab90..ed29e23eb737 100644 --- a/crates/biome_cli/src/commands/migrate.rs +++ b/crates/biome_cli/src/commands/migrate.rs @@ -1,65 +1,93 @@ +use super::{ + check_fix_incompatible_arguments, CommandRunner, FixFileModeOptions, MigrateSubCommand, +}; use crate::cli_options::CliOptions; use crate::diagnostics::MigrationDiagnostic; -use crate::execute::{execute_mode, Execution, TraversalMode}; -use crate::{setup_cli_subscriber, CliDiagnostic, CliSession}; -use biome_console::{markup, ConsoleExt}; -use biome_service::configuration::{load_configuration, LoadedConfiguration}; -use biome_service::workspace::RegisterProjectFolderParams; +use crate::execute::{Execution, TraversalMode}; +use crate::CliDiagnostic; +use biome_configuration::PartialConfiguration; +use biome_console::{markup, Console, ConsoleExt}; +use biome_fs::FileSystem; +use biome_service::configuration::LoadedConfiguration; +use biome_service::{DynRef, Workspace, WorkspaceError}; +use std::ffi::OsString; +use std::path::PathBuf; -use super::{check_fix_incompatible_arguments, FixFileModeOptions, MigrateSubCommand}; +pub(crate) struct MigrateCommandPayload { + pub(crate) write: bool, + pub(crate) fix: bool, + pub(crate) sub_command: Option<MigrateSubCommand>, + pub(crate) configuration_file_path: Option<PathBuf>, + pub(crate) configuration_directory_path: Option<PathBuf>, +} + +impl CommandRunner for MigrateCommandPayload { + const COMMAND_NAME: &'static str = "migrate"; -/// Handler for the "migrate" command of the Biome CLI -pub(crate) fn migrate( - session: CliSession, - cli_options: CliOptions, - write: bool, - fix: bool, - sub_command: Option<MigrateSubCommand>, -) -> Result<(), CliDiagnostic> { - let base_path = cli_options.as_configuration_path_hint(); - let LoadedConfiguration { - configuration: _, - diagnostics: _, - directory_path, - file_path, - } = load_configuration(&session.app.fs, base_path)?; - setup_cli_subscriber(cli_options.log_level, cli_options.log_kind); + fn merge_configuration( + &mut self, + loaded_configuration: LoadedConfiguration, + _fs: &DynRef<'_, dyn FileSystem>, + _console: &mut dyn Console, + ) -> Result<PartialConfiguration, WorkspaceError> { + self.configuration_file_path = loaded_configuration.file_path; + self.configuration_directory_path = loaded_configuration.directory_path; + Ok(loaded_configuration.configuration) + } - check_fix_incompatible_arguments(FixFileModeOptions { - apply: false, - apply_unsafe: false, - write, - fix, - unsafe_: false, - })?; + fn get_files_to_process( + &self, + _fs: &DynRef<'_, dyn FileSystem>, + _configuration: &PartialConfiguration, + ) -> Result<Vec<OsString>, CliDiagnostic> { + Ok(vec![]) + } - session - .app - .workspace - .register_project_folder(RegisterProjectFolderParams { - path: session.app.fs.working_directory(), - set_as_current_workspace: true, - })?; + fn get_stdin_file_path(&self) -> Option<&str> { + None + } - if let (Some(path), Some(directory_path)) = (file_path, directory_path) { - execute_mode( - Execution::new(TraversalMode::Migrate { - write: write || fix, + fn should_write(&self) -> bool { + self.write || self.fix + } + + fn get_execution( + &self, + _cli_options: &CliOptions, + console: &mut dyn Console, + _workspace: &dyn Workspace, + ) -> Result<Execution, CliDiagnostic> { + if let (Some(path), Some(directory_path)) = ( + self.configuration_file_path.clone(), + self.configuration_directory_path.clone(), + ) { + Ok(Execution::new(TraversalMode::Migrate { + write: self.should_write(), configuration_file_path: path, configuration_directory_path: directory_path, - sub_command, - }), - session, - &cli_options, - vec![], - ) - } else { - let console = session.app.console; - console.log(markup! { + sub_command: self.sub_command.clone(), + })) + } else { + console.log(markup! { <Info>"If this project has not yet been set up with Biome yet, please follow the "<Hyperlink href="https://biomejs.dev/guides/getting-started/">"Getting Started guide"</Hyperlink>" first."</Info> }); - Err(CliDiagnostic::MigrateError(MigrationDiagnostic { - reason: "Biome couldn't find the Biome configuration file.".to_string(), - })) + Err(CliDiagnostic::MigrateError(MigrationDiagnostic { + reason: "Biome couldn't find the Biome configuration file.".to_string(), + })) + } + } + + fn check_incompatible_arguments(&self) -> Result<(), CliDiagnostic> { + check_fix_incompatible_arguments(FixFileModeOptions { + apply: false, + apply_unsafe: false, + write: self.write, + fix: self.fix, + unsafe_: false, + }) + } + + fn should_validate_configuration_diagnostics(&self) -> bool { + false } } diff --git a/crates/biome_cli/src/commands/mod.rs b/crates/biome_cli/src/commands/mod.rs index 5e68992a2456..0101d9545236 100644 --- a/crates/biome_cli/src/commands/mod.rs +++ b/crates/biome_cli/src/commands/mod.rs @@ -3,7 +3,9 @@ use crate::cli_options::{cli_options, CliOptions, CliReporter, ColorsArg}; use crate::diagnostics::{DeprecatedArgument, DeprecatedConfigurationFile}; use crate::execute::Stdin; use crate::logging::LoggingKind; -use crate::{CliDiagnostic, LoggingLevel, VERSION}; +use crate::{ + execute_mode, setup_cli_subscriber, CliDiagnostic, CliSession, Execution, LoggingLevel, VERSION, +}; use biome_configuration::analyzer::RuleSelector; use biome_configuration::css::PartialCssLinter; use biome_configuration::javascript::PartialJavascriptLinter; @@ -22,10 +24,12 @@ use biome_configuration::{BiomeDiagnostic, PartialConfiguration}; use biome_console::{markup, Console, ConsoleExt}; use biome_diagnostics::{Diagnostic, PrintDiagnostic}; use biome_fs::{BiomePath, FileSystem}; -use biome_service::configuration::LoadedConfiguration; +use biome_service::configuration::{ + load_configuration, load_editorconfig, LoadedConfiguration, PartialConfigurationExt, +}; use biome_service::documentation::Doc; -use biome_service::workspace::FixFileMode; -use biome_service::{DynRef, WorkspaceError}; +use biome_service::workspace::{FixFileMode, RegisterProjectFolderParams, UpdateSettingsParams}; +use biome_service::{DynRef, Workspace, WorkspaceError}; use bpaf::Bpaf; use std::ffi::OsString; use std::path::PathBuf; @@ -608,7 +612,7 @@ impl BiomeCommand { /// It accepts a [LoadedPartialConfiguration] and it prints the diagnostics emitted during parsing and deserialization. /// -/// If it contains errors, it return an error. +/// If it contains [errors](Severity::Error) or higher, it returns an error. pub(crate) fn validate_configuration_diagnostics( loaded_configuration: &LoadedConfiguration, console: &mut dyn Console, @@ -666,33 +670,8 @@ fn resolve_manifest( Ok(None) } -/// Computes [Stdin] if the CLI has the necessary information. -/// -/// ## Errors -/// - If the user didn't provide anything via `stdin` but the option `--stdin-file-path` is passed. -pub(crate) fn get_stdin( - stdin_file_path: Option<String>, - console: &mut dyn Console, - command_name: &str, -) -> Result<Option<Stdin>, CliDiagnostic> { - let stdin = if let Some(stdin_file_path) = stdin_file_path { - let input_code = console.read(); - if let Some(input_code) = input_code { - let path = PathBuf::from(stdin_file_path); - Some((path, input_code).into()) - } else { - // we provided the argument without a piped stdin, we bail - return Err(CliDiagnostic::missing_argument("stdin", command_name)); - } - } else { - None - }; - - Ok(stdin) -} - -fn get_files_to_process( - since: Option<String>, +fn get_files_to_process_with_cli_options( + since: Option<&str>, changed: bool, staged: bool, fs: &DynRef<'_, dyn FileSystem>, @@ -804,6 +783,182 @@ fn check_fix_incompatible_arguments(options: FixFileModeOptions) -> Result<(), C Ok(()) } +/// Generic interface for executing commands. +/// +/// Consumers must implement the following methods: +/// +/// - [CommandRunner::merge_configuration] +/// - [CommandRunner::get_files_to_process] +/// - [CommandRunner::get_stdin_file_path] +/// - [CommandRunner::should_write] +/// - [CommandRunner::get_execution] +/// +/// Optional methods: +/// - [CommandRunner::check_incompatible_arguments] +pub(crate) trait CommandRunner: Sized { + const COMMAND_NAME: &'static str; + + /// The main command to use. + fn run(&mut self, session: CliSession, cli_options: &CliOptions) -> Result<(), CliDiagnostic> { + setup_cli_subscriber(cli_options.log_level, cli_options.log_kind); + let fs = &session.app.fs; + let console = &mut *session.app.console; + let workspace = &*session.app.workspace; + self.check_incompatible_arguments()?; + let (execution, paths) = self.configure_workspace(fs, console, workspace, cli_options)?; + execute_mode(execution, session, cli_options, paths) + } + + /// This function prepares the workspace with the following: + /// - Loading the configuration file. + /// - Configure the VCS integration + /// - Computes the paths to traverse/handle. This changes based on the VCS arguments that were passed. + /// - Register a project folder using the working directory. + /// - Resolves the closets manifest AKA `package.json` and registers it. + /// - Updates the settings that belong to the project registered + fn configure_workspace( + &mut self, + fs: &DynRef<'_, dyn FileSystem>, + console: &mut dyn Console, + workspace: &dyn Workspace, + cli_options: &CliOptions, + ) -> Result<(Execution, Vec<OsString>), CliDiagnostic> { + let loaded_configuration = + load_configuration(fs, cli_options.as_configuration_path_hint())?; + if self.should_validate_configuration_diagnostics() { + validate_configuration_diagnostics( + &loaded_configuration, + console, + cli_options.verbose, + )?; + } + let configuration_path = loaded_configuration.directory_path.clone(); + let configuration = self.merge_configuration(loaded_configuration, fs, console)?; + let vcs_base_path = configuration_path.or(fs.working_directory()); + let (vcs_base_path, gitignore_matches) = + configuration.retrieve_gitignore_matches(fs, vcs_base_path.as_deref())?; + let paths = self.get_files_to_process(fs, &configuration)?; + workspace.register_project_folder(RegisterProjectFolderParams { + path: fs.working_directory(), + set_as_current_workspace: true, + })?; + + let manifest_data = resolve_manifest(fs)?; + + if let Some(manifest_data) = manifest_data { + workspace.set_manifest_for_project(manifest_data.into())?; + } + workspace.update_settings(UpdateSettingsParams { + workspace_directory: fs.working_directory(), + configuration, + vcs_base_path, + gitignore_matches, + })?; + + let execution = self.get_execution(cli_options, console, workspace)?; + Ok((execution, paths)) + } + + /// Computes [Stdin] if the CLI has the necessary information. + /// + /// ## Errors + /// - If the user didn't provide anything via `stdin` but the option `--stdin-file-path` is passed. + fn get_stdin(&self, console: &mut dyn Console) -> Result<Option<Stdin>, CliDiagnostic> { + let stdin = if let Some(stdin_file_path) = self.get_stdin_file_path() { + let input_code = console.read(); + if let Some(input_code) = input_code { + let path = PathBuf::from(stdin_file_path); + Some((path, input_code).into()) + } else { + // we provided the argument without a piped stdin, we bail + return Err(CliDiagnostic::missing_argument("stdin", Self::COMMAND_NAME)); + } + } else { + None + }; + + Ok(stdin) + } + + // Below, the methods that consumers must implement. + + /// Implements this method if you need to merge CLI arguments to the loaded configuration. + /// + /// The CLI arguments take precedence over the option configured in the configuration file. + fn merge_configuration( + &mut self, + loaded_configuration: LoadedConfiguration, + fs: &DynRef<'_, dyn FileSystem>, + console: &mut dyn Console, + ) -> Result<PartialConfiguration, WorkspaceError>; + + /// It returns the paths that need to be handled/traversed. + fn get_files_to_process( + &self, + fs: &DynRef<'_, dyn FileSystem>, + configuration: &PartialConfiguration, + ) -> Result<Vec<OsString>, CliDiagnostic>; + + /// It returns the file path to use in `stdin` mode. + fn get_stdin_file_path(&self) -> Option<&str>; + + /// Whether the command should write the files. + fn should_write(&self) -> bool; + + /// Returns the [Execution] mode. + fn get_execution( + &self, + cli_options: &CliOptions, + console: &mut dyn Console, + workspace: &dyn Workspace, + ) -> Result<Execution, CliDiagnostic>; + + // Below, methods that consumers can implement + + /// Optional method that can be implemented to check if some CLI arguments aren't compatible. + /// + /// The method is called before loading the configuration from disk. + fn check_incompatible_arguments(&self) -> Result<(), CliDiagnostic> { + Ok(()) + } + + /// Checks whether the configuration has errors. + fn should_validate_configuration_diagnostics(&self) -> bool { + true + } +} + +pub trait LoadEditorConfig: CommandRunner { + /// Whether this command should load the `.editorconfig` file. + fn should_load_editor_config(&self, fs_configuration: &PartialConfiguration) -> bool; + + /// It loads the `.editorconfig` from the file system, parses it and deserialize it into a [PartialConfiguration] + fn load_editor_config( + &self, + configuration_path: Option<PathBuf>, + fs_configuration: &PartialConfiguration, + fs: &DynRef<'_, dyn FileSystem>, + console: &mut dyn Console, + ) -> Result<PartialConfiguration, WorkspaceError> { + Ok(if self.should_load_editor_config(fs_configuration) { + let (editorconfig, editorconfig_diagnostics) = { + let search_path = configuration_path + .clone() + .unwrap_or_else(|| fs.working_directory().unwrap_or_default()); + load_editorconfig(fs, search_path)? + }; + for diagnostic in editorconfig_diagnostics { + console.error(markup! { + {PrintDiagnostic::simple(&diagnostic)} + }) + } + editorconfig.unwrap_or_default() + } else { + Default::default() + }) + } +} + #[cfg(test)] mod tests { use biome_console::BufferConsole; diff --git a/crates/biome_cli/src/commands/search.rs b/crates/biome_cli/src/commands/search.rs index 3e912f1dcec8..233d526b5da6 100644 --- a/crates/biome_cli/src/commands/search.rs +++ b/crates/biome_cli/src/commands/search.rs @@ -1,20 +1,18 @@ use crate::cli_options::CliOptions; -use crate::commands::{get_stdin, resolve_manifest, validate_configuration_diagnostics}; -use crate::{ - execute_mode, setup_cli_subscriber, CliDiagnostic, CliSession, Execution, TraversalMode, +use crate::commands::CommandRunner; +use crate::{CliDiagnostic, Execution, TraversalMode}; +use biome_configuration::{ + vcs::PartialVcsConfiguration, PartialConfiguration, PartialFilesConfiguration, }; -use biome_configuration::{vcs::PartialVcsConfiguration, PartialFilesConfiguration}; +use biome_console::Console; use biome_deserialize::Merge; -use biome_service::configuration::{ - load_configuration, LoadedConfiguration, PartialConfigurationExt, -}; -use biome_service::workspace::{ - ParsePatternParams, RegisterProjectFolderParams, UpdateSettingsParams, -}; +use biome_fs::FileSystem; +use biome_service::configuration::LoadedConfiguration; +use biome_service::workspace::ParsePatternParams; +use biome_service::{DynRef, Workspace, WorkspaceError}; use std::ffi::OsString; pub(crate) struct SearchCommandPayload { - pub(crate) cli_options: CliOptions, pub(crate) files_configuration: Option<PartialFilesConfiguration>, pub(crate) paths: Vec<OsString>, pub(crate) pattern: String, @@ -22,80 +20,57 @@ pub(crate) struct SearchCommandPayload { pub(crate) vcs_configuration: Option<PartialVcsConfiguration>, } -/// Handler for the "search" command of the Biome CLI -pub(crate) fn search( - session: CliSession, - payload: SearchCommandPayload, -) -> Result<(), CliDiagnostic> { - let SearchCommandPayload { - cli_options, - files_configuration, - paths, - pattern, - stdin_file_path, - vcs_configuration, - } = payload; - setup_cli_subscriber(cli_options.log_level, cli_options.log_kind); - - let loaded_configuration = - load_configuration(&session.app.fs, cli_options.as_configuration_path_hint())?; - validate_configuration_diagnostics( - &loaded_configuration, - session.app.console, - cli_options.verbose, - )?; - - let LoadedConfiguration { - mut configuration, - directory_path: configuration_path, - .. - } = loaded_configuration; - - configuration.files.merge_with(files_configuration); - configuration.vcs.merge_with(vcs_configuration); - - // check if support for git ignore files is enabled - let vcs_base_path = configuration_path.or(session.app.fs.working_directory()); - let (vcs_base_path, gitignore_matches) = - configuration.retrieve_gitignore_matches(&session.app.fs, vcs_base_path.as_deref())?; +impl CommandRunner for SearchCommandPayload { + const COMMAND_NAME: &'static str = "search"; - session - .app - .workspace - .register_project_folder(RegisterProjectFolderParams { - path: session.app.fs.working_directory(), - set_as_current_workspace: true, - })?; - let manifest_data = resolve_manifest(&session.app.fs)?; + fn merge_configuration( + &mut self, + loaded_configuration: LoadedConfiguration, + _fs: &DynRef<'_, dyn FileSystem>, + _console: &mut dyn Console, + ) -> Result<PartialConfiguration, WorkspaceError> { + let LoadedConfiguration { + mut configuration, .. + } = loaded_configuration; + configuration + .files + .merge_with(self.files_configuration.clone()); + configuration.vcs.merge_with(self.vcs_configuration.clone()); - if let Some(manifest_data) = manifest_data { - session - .app - .workspace - .set_manifest_for_project(manifest_data.into())?; + Ok(configuration) } - session - .app - .workspace - .update_settings(UpdateSettingsParams { - workspace_directory: session.app.fs.working_directory(), - configuration, - vcs_base_path, - gitignore_matches, - })?; - - let console = &mut *session.app.console; - let stdin = get_stdin(stdin_file_path, console, "search")?; + fn get_files_to_process( + &self, + _fs: &DynRef<'_, dyn FileSystem>, + _configuration: &PartialConfiguration, + ) -> Result<Vec<OsString>, CliDiagnostic> { + Ok(self.paths.clone()) + } - let pattern = session - .app - .workspace - .parse_pattern(ParsePatternParams { pattern })? - .pattern_id; + fn get_stdin_file_path(&self) -> Option<&str> { + self.stdin_file_path.as_deref() + } - let execution = - Execution::new(TraversalMode::Search { pattern, stdin }).set_report(&cli_options); + fn should_write(&self) -> bool { + false + } - execute_mode(execution, session, &cli_options, paths) + fn get_execution( + &self, + cli_options: &CliOptions, + _console: &mut dyn Console, + workspace: &dyn Workspace, + ) -> Result<Execution, CliDiagnostic> { + let pattern = workspace + .parse_pattern(ParsePatternParams { + pattern: self.pattern.clone(), + })? + .pattern_id; + Ok(Execution::new(TraversalMode::Search { + pattern, + stdin: self.get_stdin(_console)?, + }) + .set_report(cli_options)) + } } diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index e804e24520ad..dc0765b17a81 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -14,6 +14,32 @@ pub(crate) fn migrate_eslint_any_rule( let rule = group.no_this_in_static.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "@next/no-head-element" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group.no_head_element.get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } + "@next/no-head-import-in-document" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .no_head_import_in_document + .get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } + "@next/no-img-element" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group.no_img_element.get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "@stylistic/jsx-self-closing-comp" => { let group = rules.style.get_or_insert_with(Default::default); let rule = group diff --git a/crates/biome_cli/src/execute/migrate/eslint_eslint.rs b/crates/biome_cli/src/execute/migrate/eslint_eslint.rs index e12eab4a6de3..ca9320350505 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_eslint.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_eslint.rs @@ -537,7 +537,7 @@ impl Deserializable for Rules { #[derive(Debug, Default, Deserializable)] pub struct NoConsoleOptions { /// Allowed calls on the console object. - pub allow: Vec<String>, + pub allow: Box<[Box<str>]>, } impl From<NoConsoleOptions> for biome_js_analyze::lint::suspicious::no_console::NoConsoleOptions { fn from(val: NoConsoleOptions) -> Self { diff --git a/crates/biome_cli/src/execute/migrate/eslint_jsxa11y.rs b/crates/biome_cli/src/execute/migrate/eslint_jsxa11y.rs index c8b415c394e0..f2bd1db8e0a8 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_jsxa11y.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_jsxa11y.rs @@ -7,7 +7,7 @@ use biome_js_analyze::lint::a11y::use_valid_aria_role; #[derive(Debug, Default, Deserializable)] pub(crate) struct AriaRoleOptions { - allowed_invalid_roles: Vec<String>, + allowed_invalid_roles: Box<[Box<str>]>, #[deserializable(rename = "ignoreNonDOM")] ignore_non_dom: bool, } diff --git a/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs index 4614c68be53d..f9eb1aec77ef 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs @@ -224,13 +224,16 @@ fn migrate_eslint_rule( eslint_eslint::Rule::NoRestrictedGlobals(conf) => { if migrate_eslint_any_rule(rules, &name, conf.severity(), opts, results) { let severity = conf.severity(); - let globals = conf.into_vec().into_iter().map(|g| g.into_name()); + let globals = conf + .into_vec() + .into_iter() + .map(|g| g.into_name().into_boxed_str()); let group = rules.style.get_or_insert_with(Default::default); group.no_restricted_globals = Some(biome_config::RuleConfiguration::WithOptions( biome_config::RuleWithOptions { level: severity.into(), options: Box::new(no_restricted_globals::RestrictedGlobalsOptions { - denied_globals: globals.collect(), + denied_globals: globals.collect::<Vec<_>>().into_boxed_slice(), }), }, )); diff --git a/crates/biome_cli/src/execute/migrate/eslint_typescript.rs b/crates/biome_cli/src/execute/migrate/eslint_typescript.rs index 3344d0450cd5..8514934aab38 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_typescript.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_typescript.rs @@ -160,7 +160,7 @@ impl From<NamingConventionOptions> for use_naming_convention::NamingConventionOp use_naming_convention::NamingConventionOptions { strict_case: false, require_ascii: false, - conventions, + conventions: conventions.into_boxed_slice(), enum_member_case: use_naming_convention::Format::default(), } } diff --git a/crates/biome_cli/src/execute/mod.rs b/crates/biome_cli/src/execute/mod.rs index 58620e90cce7..3cd4a8eb97a9 100644 --- a/crates/biome_cli/src/execute/mod.rs +++ b/crates/biome_cli/src/execute/mod.rs @@ -115,6 +115,12 @@ pub struct VcsTargeted { pub changed: bool, } +impl From<(bool, bool)> for VcsTargeted { + fn from((staged, changed): (bool, bool)) -> Self { + Self { staged, changed } + } +} + #[derive(Debug, Clone)] pub enum TraversalMode { /// This mode is enabled when running the command `biome check` diff --git a/crates/biome_cli/src/lib.rs b/crates/biome_cli/src/lib.rs index 653ebdc1d32f..ca7fff6a4ab6 100644 --- a/crates/biome_cli/src/lib.rs +++ b/crates/biome_cli/src/lib.rs @@ -23,11 +23,13 @@ mod panic; mod reporter; mod service; -use crate::cli_options::ColorsArg; +use crate::cli_options::{CliOptions, ColorsArg}; use crate::commands::check::CheckCommandPayload; use crate::commands::ci::CiCommandPayload; use crate::commands::format::FormatCommandPayload; use crate::commands::lint::LintCommandPayload; +use crate::commands::migrate::MigrateCommandPayload; +use crate::commands::CommandRunner; pub use crate::commands::{biome_command, BiomeCommand}; pub use crate::logging::{setup_cli_subscriber, LoggingLevel}; pub use diagnostics::CliDiagnostic; @@ -103,15 +105,15 @@ impl<'app> CliSession<'app> { staged, changed, since, - } => commands::check::check( + } => run_command( self, + &cli_options, CheckCommandPayload { apply_unsafe, apply, write, fix, unsafe_, - cli_options, configuration, paths, stdin_file_path, @@ -145,15 +147,15 @@ impl<'app> CliSession<'app> { javascript_linter, json_linter, graphql_linter, - } => commands::lint::lint( + } => run_command( self, + &cli_options, LintCommandPayload { apply_unsafe, apply, write, fix, unsafe_, - cli_options, linter_configuration, paths, only, @@ -180,8 +182,9 @@ impl<'app> CliSession<'app> { cli_options, changed, since, - } => commands::ci::ci( + } => run_command( self, + &cli_options, CiCommandPayload { linter_enabled, formatter_enabled, @@ -189,7 +192,6 @@ impl<'app> CliSession<'app> { assists_enabled, configuration, paths, - cli_options, changed, since, }, @@ -210,15 +212,15 @@ impl<'app> CliSession<'app> { staged, changed, since, - } => commands::format::format( + } => run_command( self, + &cli_options, FormatCommandPayload { javascript_formatter, formatter_configuration, stdin_file_path, write, fix, - cli_options, paths, vcs_configuration, files_configuration, @@ -243,7 +245,17 @@ impl<'app> CliSession<'app> { write, fix, sub_command, - } => commands::migrate::migrate(self, cli_options, write, fix, sub_command), + } => run_command( + self, + &cli_options, + MigrateCommandPayload { + write, + fix, + sub_command, + configuration_directory_path: None, + configuration_file_path: None, + }, + ), BiomeCommand::Search { cli_options, files_configuration, @@ -251,10 +263,10 @@ impl<'app> CliSession<'app> { pattern, stdin_file_path, vcs_configuration, - } => commands::search::search( + } => run_command( self, + &cli_options, SearchCommandPayload { - cli_options, files_configuration, paths, pattern, @@ -291,3 +303,12 @@ pub fn to_color_mode(color: Option<&ColorsArg>) -> ColorMode { None => ColorMode::Auto, } } + +pub(crate) fn run_command( + session: CliSession, + cli_options: &CliOptions, + mut command: impl CommandRunner, +) -> Result<(), CliDiagnostic> { + let command = &mut command; + command.run(session, cli_options) +} diff --git a/crates/biome_cli/src/reporter/summary.rs b/crates/biome_cli/src/reporter/summary.rs index 3d78b6f1a87c..90aefff0f65d 100644 --- a/crates/biome_cli/src/reporter/summary.rs +++ b/crates/biome_cli/src/reporter/summary.rs @@ -1,8 +1,11 @@ use crate::reporter::terminal::ConsoleTraversalSummary; use crate::{DiagnosticsPayload, Execution, Reporter, ReporterVisitor, TraversalSummary}; use biome_console::fmt::{Display, Formatter}; -use biome_console::{markup, Console, ConsoleExt, HorizontalLine, Padding, SOFT_LINE}; -use biome_diagnostics::{Resource, Severity}; +use biome_console::{markup, Console, ConsoleExt}; +use biome_diagnostics::{ + category, Advices, Category, Diagnostic, MessageAndDescription, PrintDiagnostic, Resource, + Severity, Visit, +}; use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Debug; @@ -84,6 +87,12 @@ impl<'a> ReporterVisitor for SummaryReporterVisitor<'a> { } } + if let Some(category) = category { + if category.name() == "parse" { + files_to_diagnostics.insert_parse(location); + } + } + if execution.is_check() || execution.is_lint() || execution.is_ci() { if let Some(category) = category { if category.name().starts_with("lint/") @@ -123,6 +132,7 @@ struct FileToDiagnostics { formats: BTreeSet<String>, organize_imports: BTreeSet<String>, lints: LintsByCategory, + parse: BTreeSet<String>, } impl FileToDiagnostics { @@ -138,55 +148,106 @@ impl FileToDiagnostics { fn insert_organize_imports(&mut self, location: &str) { self.organize_imports.insert(location.into()); } + + fn insert_parse(&mut self, location: &str) { + self.parse.insert(location.into()); + } +} + +#[derive(Debug, Diagnostic)] +#[diagnostic( + severity = Information +)] +struct SummaryListDiagnostic<'a> { + #[category] + category: &'static Category, + + #[message] + message: MessageAndDescription, + + #[advice] + list: SummaryListAdvice<'a>, +} + +#[derive(Debug, Diagnostic)] +#[diagnostic( + severity = Information, + category = "reporter/analyzer", + message = "Some analyzer rules were triggered" +)] +struct SummaryTableDiagnostic<'a> { + #[advice] + tables: &'a LintsByCategory, +} + +#[derive(Debug)] +struct SummaryListAdvice<'a>(&'a BTreeSet<String>); + +impl<'a> Advices for SummaryListAdvice<'a> { + fn record(&self, visitor: &mut dyn Visit) -> io::Result<()> { + let list: Vec<_> = self.0.iter().map(|s| s as &dyn Display).collect(); + visitor.record_list(&list) + } } impl Display for FileToDiagnostics { fn fmt(&self, fmt: &mut Formatter) -> io::Result<()> { - if !self.formats.is_empty() { - let header = "Formatter "; - let horizontal_line = HorizontalLine::new(100usize.saturating_sub(header.len())); + if !self.parse.is_empty() { + let diagnostic = SummaryListDiagnostic { + message: MessageAndDescription::from( + markup! { + <Warn>"The following files have parsing errors."</Warn> + } + .to_owned(), + ), + list: SummaryListAdvice(&self.parse), + category: category!("reporter/parse"), + }; fmt.write_markup(markup! { - <Emphasis>{header}</Emphasis>{horizontal_line} + {PrintDiagnostic::simple(&diagnostic)} })?; - SOFT_LINE.fmt(fmt)?; + } + if !self.formats.is_empty() { + let diagnostic = SummaryListDiagnostic { + message: MessageAndDescription::from( + markup! { + <Warn>"The following files needs to be formatted."</Warn> + } + .to_owned(), + ), + list: SummaryListAdvice(&self.formats), + category: category!("reporter/format"), + }; fmt.write_markup(markup! { - <Warn>"The following files needs to be formatted:\n"</Warn> + {PrintDiagnostic::simple(&diagnostic)} })?; - - for file_name in &self.formats { - fmt.write_markup(markup! { - <Emphasis>{file_name}</Emphasis>{SOFT_LINE} - })?; - } - SOFT_LINE.fmt(fmt)?; } if !self.organize_imports.is_empty() { - let header = "Organize Imports "; - let horizontal_line = HorizontalLine::new(100usize.saturating_sub(header.len())); + let diagnostic = SummaryListDiagnostic { + message: MessageAndDescription::from( + markup! { + <Warn>"The following files needs to have their imports sorted."</Warn> + } + .to_owned(), + ), + list: SummaryListAdvice(&self.organize_imports), + category: category!("reporter/organizeImports"), + }; fmt.write_markup(markup! { - <Emphasis>{header}</Emphasis>{horizontal_line} + {PrintDiagnostic::simple(&diagnostic)} })?; - SOFT_LINE.fmt(fmt)?; + } + if !self.lints.0.is_empty() { + let diagnostic = SummaryTableDiagnostic { + tables: &self.lints, + }; fmt.write_markup(markup! { - <Warn>"The following files needs to have their imports sorted:\n"</Warn> + {PrintDiagnostic::simple(&diagnostic)} })?; - - for file_name in &self.organize_imports { - fmt.write_markup(markup! { - <Emphasis>{file_name}</Emphasis>{SOFT_LINE} - })?; - } - SOFT_LINE.fmt(fmt)?; } - - fmt.write_markup(markup! { - {self.lints} - })?; - SOFT_LINE.fmt(fmt)?; - Ok(()) } } @@ -206,62 +267,25 @@ impl LintsByCategory { } } -impl Display for LintsByCategory { - fn fmt(&self, fmt: &mut Formatter) -> io::Result<()> { - let rule_name_str = "Rule Name"; - let diagnostics_str = "Diagnostics"; - let padding = 15usize; - - if !self.0.is_empty() { - let header = "Analyzer "; - let horizontal_line = HorizontalLine::new(100usize.saturating_sub(header.len())); - fmt.write_markup(markup! { - <Emphasis>{header}</Emphasis>{horizontal_line} - })?; - SOFT_LINE.fmt(fmt)?; - fmt.write_markup(markup!( - <Warn>"Some analyzer rules were triggered"</Warn> - ))?; - fmt.write_str("\n\n")?; - let mut iter = self.0.iter().rev(); - // SAFETY: it isn't empty - let (first_name, first_count) = iter.next().unwrap(); - let longest_rule_name = first_name.name_len(); - - fmt.write_markup(markup!( - <Info><Underline>{rule_name_str}</Underline></Info> - ))?; - fmt.write_markup(markup! {{Padding::new(longest_rule_name + padding)}})?; - fmt.write_markup(markup!( - <Info><Underline>{diagnostics_str}</Underline></Info> - ))?; - fmt.write_str("\n")?; - - fmt.write_markup(markup! { - <Emphasis>{first_name}</Emphasis>{Padding::new(padding + rule_name_str.len())}{first_count} - })?; - - fmt.write_str("\n")?; - - for (name, num) in iter { - let current_name_len = name.name_len(); - let extra_padding = longest_rule_name.saturating_sub(current_name_len); - fmt.write_markup(markup! { - <Emphasis>{name}</Emphasis> - })?; - - fmt.write_markup(markup! { - {Padding::new(extra_padding + padding + rule_name_str.len())} - })?; - - fmt.write_markup(markup! { - {num} - })?; - fmt.write_str("\n")?; - } - } - - Ok(()) +impl<'a> Advices for &'a LintsByCategory { + fn record(&self, visitor: &mut dyn Visit) -> io::Result<()> { + let headers = &[ + markup!("Rule Name").to_owned(), + markup!("Diagnostics").to_owned(), + ]; + let (first, second): (Vec<_>, Vec<_>) = self + .0 + .iter() + .rev() + .map(|(rule_name, diagnostic)| { + ( + markup! {{rule_name}}.to_owned(), + markup! {{diagnostic}}.to_owned(), + ) + }) + .unzip(); + let array = [first.as_slice(), second.as_slice()]; + visitor.record_table(15usize, headers, &array) } } @@ -274,12 +298,6 @@ impl AsRef<str> for RuleName { } } -impl RuleName { - fn name_len(&self) -> usize { - self.0.len() - } -} - impl From<&'static str> for RuleName { fn from(value: &'static str) -> Self { Self(value) diff --git a/crates/biome_cli/tests/cases/reporter_summary.rs b/crates/biome_cli/tests/cases/reporter_summary.rs index bb7c49052f5d..dbdf1b1d2e27 100644 --- a/crates/biome_cli/tests/cases/reporter_summary.rs +++ b/crates/biome_cli/tests/cases/reporter_summary.rs @@ -14,10 +14,10 @@ a ==b a ==b a ==b -debugger -debugger -debugger -debugger +debugger +debugger +debugger +debugger let f; let f; @@ -34,10 +34,10 @@ a ==b a ==b a ==b -debugger -debugger -debugger -debugger +debugger +debugger +debugger +debugger let f; let f; @@ -46,6 +46,16 @@ let f; let f; let f;"#; +const MAIN_3: &str = r#" + +.brokenStyle { color: f( } + +.style { + color: + fakeFunction() +} +"#; + #[test] fn reports_diagnostics_summary_check_command() { let mut fs = MemoryFileSystem::default(); @@ -57,6 +67,9 @@ fn reports_diagnostics_summary_check_command() { let file_path2 = Path::new("index.ts"); fs.insert(file_path2.into(), MAIN_2.as_bytes()); + let file_path3 = Path::new("index.css"); + fs.insert(file_path3.into(), MAIN_3.as_bytes()); + let result = run_cli( DynRef::Borrowed(&mut fs), &mut console, @@ -67,6 +80,7 @@ fn reports_diagnostics_summary_check_command() { "--max-diagnostics=200", file_path1.as_os_str().to_str().unwrap(), file_path2.as_os_str().to_str().unwrap(), + file_path3.as_os_str().to_str().unwrap(), ] .as_slice(), ), @@ -94,6 +108,9 @@ fn reports_diagnostics_summary_ci_command() { let file_path2 = Path::new("index.ts"); fs.insert(file_path2.into(), MAIN_2.as_bytes()); + let file_path3 = Path::new("index.css"); + fs.insert(file_path3.into(), MAIN_3.as_bytes()); + let result = run_cli( DynRef::Borrowed(&mut fs), &mut console, @@ -104,6 +121,7 @@ fn reports_diagnostics_summary_ci_command() { "--max-diagnostics=200", file_path1.as_os_str().to_str().unwrap(), file_path2.as_os_str().to_str().unwrap(), + file_path3.as_os_str().to_str().unwrap(), ] .as_slice(), ), @@ -131,6 +149,9 @@ fn reports_diagnostics_summary_lint_command() { let file_path2 = Path::new("index.ts"); fs.insert(file_path2.into(), MAIN_2.as_bytes()); + let file_path3 = Path::new("index.css"); + fs.insert(file_path3.into(), MAIN_3.as_bytes()); + let result = run_cli( DynRef::Borrowed(&mut fs), &mut console, @@ -141,6 +162,7 @@ fn reports_diagnostics_summary_lint_command() { "--max-diagnostics=200", file_path1.as_os_str().to_str().unwrap(), file_path2.as_os_str().to_str().unwrap(), + file_path3.as_os_str().to_str().unwrap(), ] .as_slice(), ), @@ -168,6 +190,9 @@ fn reports_diagnostics_summary_format_command() { let file_path2 = Path::new("index.ts"); fs.insert(file_path2.into(), MAIN_2.as_bytes()); + let file_path3 = Path::new("index.css"); + fs.insert(file_path3.into(), MAIN_3.as_bytes()); + let result = run_cli( DynRef::Borrowed(&mut fs), &mut console, @@ -178,6 +203,7 @@ fn reports_diagnostics_summary_format_command() { "--max-diagnostics=200", file_path1.as_os_str().to_str().unwrap(), file_path2.as_os_str().to_str().unwrap(), + file_path3.as_os_str().to_str().unwrap(), ] .as_slice(), ), diff --git a/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_check_command.snap b/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_check_command.snap index b3007f2e62bb..8960bfc5f713 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_check_command.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_check_command.snap @@ -2,6 +2,20 @@ source: crates/biome_cli/tests/snap_test.rs expression: content --- +## `index.css` + +```css + + +.brokenStyle { color: f( } + +.style { + color: + fakeFunction() +} + +``` + ## `index.ts` ```ts @@ -13,10 +27,10 @@ a ==b a ==b a ==b -debugger -debugger -debugger -debugger +debugger +debugger +debugger +debugger let f; let f; @@ -37,10 +51,10 @@ a ==b a ==b a ==b -debugger -debugger -debugger -debugger +debugger +debugger +debugger +debugger let f; let f; @@ -64,25 +78,38 @@ check ━━━━━━━━━━━━━━━━━━━━━━━━ # Emitted Messages ```block -Formatter ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -The following files needs to be formatted: -index.ts -main.ts +reporter/parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Organize Imports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -The following files needs to have their imports sorted: -index.ts -main.ts + i The following files have parsing errors. + + - index.css + +reporter/format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Analyzer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Some analyzer rules were triggered + i The following files needs to be formatted. + + - index.css + - index.ts + - main.ts + +reporter/organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Rule Name Diagnostics -lint/suspicious/noImplicitAnyLet 12 (12 error(s), 0 warning(s), 0 info(s)) -lint/suspicious/noDoubleEquals 8 (8 error(s), 0 warning(s), 0 info(s)) -lint/suspicious/noRedeclare 12 (12 error(s), 0 warning(s), 0 info(s)) -lint/suspicious/noDebugger 8 (8 error(s), 0 warning(s), 0 info(s)) + i The following files needs to have their imports sorted. + + - index.ts + - main.ts + +reporter/analyzer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + i Some analyzer rules were triggered + + Rule Name Diagnostics + + lint/correctness/noUnknownFunction 2 (2 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noImplicitAnyLet 12 (12 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noDoubleEquals 8 (8 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noRedeclare 12 (12 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noDebugger 8 (8 error(s), 0 warning(s), 0 info(s)) ``` @@ -93,6 +120,6 @@ If you wish to apply the suggested (unsafe) fixes, use the command biome check - ``` ```block -Checked 2 files in <TIME>. No fixes applied. -Found 44 errors. +Checked 3 files in <TIME>. No fixes applied. +Found 49 errors. ``` diff --git a/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_ci_command.snap b/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_ci_command.snap index 77216dc6ef34..de4a7b62f31c 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_ci_command.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_ci_command.snap @@ -2,6 +2,20 @@ source: crates/biome_cli/tests/snap_test.rs expression: content --- +## `index.css` + +```css + + +.brokenStyle { color: f( } + +.style { + color: + fakeFunction() +} + +``` + ## `index.ts` ```ts @@ -13,10 +27,10 @@ a ==b a ==b a ==b -debugger -debugger -debugger -debugger +debugger +debugger +debugger +debugger let f; let f; @@ -37,10 +51,10 @@ a ==b a ==b a ==b -debugger -debugger -debugger -debugger +debugger +debugger +debugger +debugger let f; let f; @@ -64,29 +78,42 @@ ci ━━━━━━━━━━━━━━━━━━━━━━━━━ # Emitted Messages ```block -Formatter ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -The following files needs to be formatted: -index.ts -main.ts +reporter/parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Organize Imports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -The following files needs to have their imports sorted: -index.ts -main.ts + i The following files have parsing errors. + + - index.css + +reporter/format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Analyzer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Some analyzer rules were triggered + i The following files needs to be formatted. + + - index.css + - index.ts + - main.ts + +reporter/organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Rule Name Diagnostics -lint/suspicious/noImplicitAnyLet 12 (12 error(s), 0 warning(s), 0 info(s)) -lint/suspicious/noDoubleEquals 8 (8 error(s), 0 warning(s), 0 info(s)) -lint/suspicious/noRedeclare 12 (12 error(s), 0 warning(s), 0 info(s)) -lint/suspicious/noDebugger 8 (8 error(s), 0 warning(s), 0 info(s)) + i The following files needs to have their imports sorted. + + - index.ts + - main.ts + +reporter/analyzer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + i Some analyzer rules were triggered + + Rule Name Diagnostics + + lint/correctness/noUnknownFunction 2 (2 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noImplicitAnyLet 12 (12 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noDoubleEquals 8 (8 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noRedeclare 12 (12 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noDebugger 8 (8 error(s), 0 warning(s), 0 info(s)) ``` ```block -Checked 2 files in <TIME>. No fixes applied. -Found 44 errors. +Checked 3 files in <TIME>. No fixes applied. +Found 49 errors. ``` diff --git a/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_format_command.snap b/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_format_command.snap index 33277e9937a9..14d4a74aca3d 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_format_command.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_format_command.snap @@ -2,6 +2,20 @@ source: crates/biome_cli/tests/snap_test.rs expression: content --- +## `index.css` + +```css + + +.brokenStyle { color: f( } + +.style { + color: + fakeFunction() +} + +``` + ## `index.ts` ```ts @@ -13,10 +27,10 @@ a ==b a ==b a ==b -debugger -debugger -debugger -debugger +debugger +debugger +debugger +debugger let f; let f; @@ -37,10 +51,10 @@ a ==b a ==b a ==b -debugger -debugger -debugger -debugger +debugger +debugger +debugger +debugger let f; let f; @@ -64,16 +78,24 @@ format ━━━━━━━━━━━━━━━━━━━━━━━━ # Emitted Messages ```block -Formatter ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -The following files needs to be formatted: -index.ts -main.ts +reporter/parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + i The following files have parsing errors. + + - index.css + +reporter/format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + i The following files needs to be formatted. + + - index.css + - index.ts + - main.ts + ``` ```block -Checked 2 files in <TIME>. No fixes applied. -Found 2 errors. +Checked 3 files in <TIME>. No fixes applied. +Found 3 errors. ``` diff --git a/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_lint_command.snap b/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_lint_command.snap index 04eb57c6a072..63544789d4ac 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_lint_command.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_reporter_summary/reports_diagnostics_summary_lint_command.snap @@ -2,6 +2,20 @@ source: crates/biome_cli/tests/snap_test.rs expression: content --- +## `index.css` + +```css + + +.brokenStyle { color: f( } + +.style { + color: + fakeFunction() +} + +``` + ## `index.ts` ```ts @@ -13,10 +27,10 @@ a ==b a ==b a ==b -debugger -debugger -debugger -debugger +debugger +debugger +debugger +debugger let f; let f; @@ -37,10 +51,10 @@ a ==b a ==b a ==b -debugger -debugger -debugger -debugger +debugger +debugger +debugger +debugger let f; let f; @@ -64,19 +78,27 @@ lint ━━━━━━━━━━━━━━━━━━━━━━━━━ # Emitted Messages ```block -Analyzer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Some analyzer rules were triggered +reporter/parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Rule Name Diagnostics -lint/suspicious/noImplicitAnyLet 12 (12 error(s), 0 warning(s), 0 info(s)) -lint/suspicious/noDoubleEquals 8 (8 error(s), 0 warning(s), 0 info(s)) -lint/suspicious/noRedeclare 12 (12 error(s), 0 warning(s), 0 info(s)) -lint/suspicious/noDebugger 8 (8 error(s), 0 warning(s), 0 info(s)) + i The following files have parsing errors. + + - index.css + +reporter/analyzer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + i Some analyzer rules were triggered + + Rule Name Diagnostics + + lint/correctness/noUnknownFunction 2 (2 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noImplicitAnyLet 12 (12 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noDoubleEquals 8 (8 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noRedeclare 12 (12 error(s), 0 warning(s), 0 info(s)) + lint/suspicious/noDebugger 8 (8 error(s), 0 warning(s), 0 info(s)) ``` ```block -Checked 2 files in <TIME>. No fixes applied. -Found 40 errors. +Checked 3 files in <TIME>. No fixes applied. +Found 43 errors. ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_check/fs_files_ignore_symlink.snap b/crates/biome_cli/tests/snapshots/main_commands_check/fs_files_ignore_symlink.snap index 6737afed55d1..622b942847f1 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_check/fs_files_ignore_symlink.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_check/fs_files_ignore_symlink.snap @@ -5,17 +5,17 @@ expression: content # Emitted Messages ```block -internalError/fs DEPRECATED ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +rome.json internalError/fs ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The argument --apply-unsafe is deprecated, it will be removed in the next major release. Use --write --unsafe instead. + ! The configuration file rome.json is deprecated. Use biome.json instead. ``` ```block -rome.json internalError/fs ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +internalError/fs DEPRECATED ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The configuration file rome.json is deprecated. Use biome.json instead. + ! The argument --apply-unsafe is deprecated, it will be removed in the next major release. Use --write --unsafe instead. ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_ci/ci_errors_for_all_disabled_checks.snap b/crates/biome_cli/tests/snapshots/main_commands_ci/ci_errors_for_all_disabled_checks.snap index 1997124eaa79..f5c8f95847b7 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_ci/ci_errors_for_all_disabled_checks.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_ci/ci_errors_for_all_disabled_checks.snap @@ -30,10 +30,8 @@ statement( ) ; let a = !b || !c; internalError/io ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Γ— The combination of configuration and arguments is invalid: - Formatter, linter and organize imports are disabled, can't perform the command. This is probably and error. + Formatter, linter and organize imports are disabled, can't perform the command. At least one feature needs to be enabled. This is probably and error. ``` - - diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate/should_create_biome_json_file.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate/should_create_biome_json_file.snap index 3da5654f333b..b1b918cdb331 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate/should_create_biome_json_file.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate/should_create_biome_json_file.snap @@ -19,5 +19,3 @@ expression: content ```block The configuration rome.json has been successfully migrated. ``` - - diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index aa751b551cdf..2968c84822de 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3301,6 +3301,16 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub no_exported_imports: Option<RuleConfiguration<biome_js_analyze::options::NoExportedImports>>, + #[doc = "Prevent usage of \\<head> element in a Next.js project."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_head_element: Option<RuleConfiguration<biome_js_analyze::options::NoHeadElement>>, + #[doc = "Prevent using the next/head module in pages/_document.js on Next.js projects."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_head_import_in_document: + Option<RuleConfiguration<biome_js_analyze::options::NoHeadImportInDocument>>, + #[doc = "Prevent usage of \\<img> element in a Next.js project."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_img_element: Option<RuleConfiguration<biome_js_analyze::options::NoImgElement>>, #[doc = "Disallows the use of irregular whitespace characters."] #[serde(skip_serializing_if = "Option::is_none")] pub no_irregular_whitespace: @@ -3348,6 +3358,10 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub no_unknown_pseudo_element: Option<RuleConfiguration<biome_css_analyze::options::NoUnknownPseudoElement>>, + #[doc = "Disallow unknown type selectors."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_unknown_type_selector: + Option<RuleConfiguration<biome_css_analyze::options::NoUnknownTypeSelector>>, #[doc = "Disallow unnecessary escape sequence in regular expression literals."] #[serde(skip_serializing_if = "Option::is_none")] pub no_useless_escape_in_regex: @@ -3431,6 +3445,9 @@ impl Nursery { "noDynamicNamespaceImportAccess", "noEnum", "noExportedImports", + "noHeadElement", + "noHeadImportInDocument", + "noImgElement", "noIrregularWhitespace", "noMissingVarFunction", "noNestedTernary", @@ -3444,6 +3461,7 @@ impl Nursery { "noTemplateCurlyInString", "noUnknownPseudoClass", "noUnknownPseudoElement", + "noUnknownTypeSelector", "noUselessEscapeInRegex", "noValueAtRule", "useAdjacentOverloadSignatures", @@ -3468,6 +3486,7 @@ impl Nursery { "noMissingVarFunction", "noUnknownPseudoClass", "noUnknownPseudoElement", + "noUnknownTypeSelector", "noUselessEscapeInRegex", "useAriaPropsSupportedByRole", "useConsistentMemberAccessibility", @@ -3479,14 +3498,15 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3525,6 +3545,10 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3581,146 +3605,166 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_irregular_whitespace.as_ref() { + if let Some(rule) = self.no_head_element.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_missing_var_function.as_ref() { + if let Some(rule) = self.no_head_import_in_document.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_nested_ternary.as_ref() { + if let Some(rule) = self.no_img_element.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_octal_escape.as_ref() { + if let Some(rule) = self.no_irregular_whitespace.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_process_env.as_ref() { + if let Some(rule) = self.no_missing_var_function.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_nested_ternary.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_restricted_types.as_ref() { + if let Some(rule) = self.no_octal_escape.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_secrets.as_ref() { + if let Some(rule) = self.no_process_env.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_static_element_interactions.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_substr.as_ref() { + if let Some(rule) = self.no_restricted_types.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_template_curly_in_string.as_ref() { + if let Some(rule) = self.no_secrets.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { + if let Some(rule) = self.no_static_element_interactions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { + if let Some(rule) = self.no_substr.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { + if let Some(rule) = self.no_template_curly_in_string.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.no_value_at_rule.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { + if let Some(rule) = self.no_unknown_type_selector.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_at_index.as_ref() { + if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_component_export_only_modules.as_ref() { + if let Some(rule) = self.no_value_at_rule.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_consistent_curly_braces.as_ref() { + if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { + if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_at_index.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_explicit_function_return_type.as_ref() { + if let Some(rule) = self.use_component_export_only_modules.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + if let Some(rule) = self.use_explicit_function_return_type.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } + if let Some(rule) = self.use_sorted_classes.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); + } + } + if let Some(rule) = self.use_strict_mode.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); + } + } + if let Some(rule) = self.use_trim_start_end.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); + } + } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet<RuleFilter<'static>> { @@ -3765,146 +3809,166 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_irregular_whitespace.as_ref() { + if let Some(rule) = self.no_head_element.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_missing_var_function.as_ref() { + if let Some(rule) = self.no_head_import_in_document.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_nested_ternary.as_ref() { + if let Some(rule) = self.no_img_element.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_octal_escape.as_ref() { + if let Some(rule) = self.no_irregular_whitespace.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_process_env.as_ref() { + if let Some(rule) = self.no_missing_var_function.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_nested_ternary.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_restricted_types.as_ref() { + if let Some(rule) = self.no_octal_escape.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_secrets.as_ref() { + if let Some(rule) = self.no_process_env.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_static_element_interactions.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_substr.as_ref() { + if let Some(rule) = self.no_restricted_types.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_template_curly_in_string.as_ref() { + if let Some(rule) = self.no_secrets.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { + if let Some(rule) = self.no_static_element_interactions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { + if let Some(rule) = self.no_substr.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { + if let Some(rule) = self.no_template_curly_in_string.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.no_value_at_rule.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { + if let Some(rule) = self.no_unknown_type_selector.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_at_index.as_ref() { + if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_component_export_only_modules.as_ref() { + if let Some(rule) = self.no_value_at_rule.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_consistent_curly_braces.as_ref() { + if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { + if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_at_index.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_explicit_function_return_type.as_ref() { + if let Some(rule) = self.use_component_export_only_modules.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + if let Some(rule) = self.use_explicit_function_return_type.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } + if let Some(rule) = self.use_sorted_classes.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); + } + } + if let Some(rule) = self.use_strict_mode.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); + } + } + if let Some(rule) = self.use_trim_start_end.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); + } + } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3973,6 +4037,18 @@ impl Nursery { .no_exported_imports .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noHeadElement" => self + .no_head_element + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "noHeadImportInDocument" => self + .no_head_import_in_document + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "noImgElement" => self + .no_img_element + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noIrregularWhitespace" => self .no_irregular_whitespace .as_ref() @@ -4025,6 +4101,10 @@ impl Nursery { .no_unknown_pseudo_element .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noUnknownTypeSelector" => self + .no_unknown_type_selector + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noUselessEscapeInRegex" => self .no_useless_escape_in_regex .as_ref() diff --git a/crates/biome_console/src/markup.rs b/crates/biome_console/src/markup.rs index adce8bb4c6fd..f840eb5b6388 100644 --- a/crates/biome_console/src/markup.rs +++ b/crates/biome_console/src/markup.rs @@ -195,6 +195,12 @@ impl MarkupBuf { pub fn len(&self) -> TextSize { self.0.iter().map(|node| TextSize::of(&node.content)).sum() } + + pub fn text_len(&self) -> usize { + self.0 + .iter() + .fold(0, |acc, string| acc + string.content.len()) + } } impl Write for MarkupBuf { diff --git a/crates/biome_css_analyze/src/keywords.rs b/crates/biome_css_analyze/src/keywords.rs index 4b41b8dafdf6..08c87bb74044 100644 --- a/crates/biome_css_analyze/src/keywords.rs +++ b/crates/biome_css_analyze/src/keywords.rs @@ -5433,17 +5433,295 @@ pub const RESET_TO_INITIAL_PROPERTIES_BY_FONT: [&str; 13] = [ "font-variation-settings", ]; +// https://developer.mozilla.org/ja/docs/Web/HTML/Element +// https://github.com/sindresorhus/html-tags/blob/main/html-tags.json +pub(crate) const HTML_TAGS: [&str; 150] = [ + "a", + "abbr", + "acronym", + "address", + "applet", + "area", + "article", + "aside", + "audio", + "b", + "base", + "basefont", + "bdi", + "bdo", + "bgsound", + "big", + "blink", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "center", + "cite", + "code", + "col", + "colgroup", + "content", + "data", + "datalist", + "dd", + "del", + "details", + "dfn", + "dialog", + "dir", + "div", + "dl", + "dt", + "em", + "embed", + "fencedframe", + "fieldset", + "figcaption", + "figure", + "font", + "footer", + "form", + "frame", + "frameset", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hgroup", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "isindex", + "kbd", + "keygen", + "label", + "legend", + "li", + "link", + "listbox", + "listing", + "main", + "map", + "mark", + "marquee", + "math", + "menu", + "menuitem", + "meta", + "meter", + "model", + "multicol", + "nav", + "nextid", + "nobr", + "noembed", + "noframes", + "noscript", + "object", + "ol", + "optgroup", + "option", + "output", + "p", + "param", + "picture", + "plaintext", + "portal", + "pre", + "progress", + "q", + "rb", + "rp", + "rt", + "rtc", + "ruby", + "s", + "samp", + "script", + "search", + "section", + "select", + "selectlist", + "shadow", + "slot", + "small", + "source", + "spacer", + "span", + "strike", + "strong", + "style", + "sub", + "summary", + "sup", + "svg", + "table", + "tbody", + "td", + "template", + "textarea", + "tfoot", + "th", + "thead", + "time", + "title", + "tr", + "track", + "tt", + "u", + "ul", + "var", + "video", + "wbr", + "xmp", +]; + +// https://developer.mozilla.org/ja/docs/Web/SVG/Element +// https://github.com/element-io/svg-tags/blob/master/lib/svg-tags.json +pub(crate) const SVG_TAGS: [&str; 82] = [ + "a", + "altGlyph", + "altGlyphDef", + "altGlyphItem", + "animate", + "animateColor", + "animateMotion", + "animateTransform", + "circle", + "clipPath", + "color-profile", + "cursor", + "defs", + "desc", + "ellipse", + "feBlend", + "feColorMatrix", + "feComponentTransfer", + "feComposite", + "feConvolveMatrix", + "feDiffuseLighting", + "feDisplacementMap", + "feDistantLight", + "feDropShadow", + "feFlood", + "feFuncA", + "feFuncB", + "feFuncG", + "feFuncR", + "feGaussianBlur", + "feImage", + "feMerge", + "feMergeNode", + "feMorphology", + "feOffset", + "fePointLight", + "feSpecularLighting", + "feSpotLight", + "feTile", + "feTurbulence", + "filter", + "font", + "font-face", + "font-face-format", + "font-face-name", + "font-face-src", + "font-face-uri", + "foreignObject", + "g", + "glyph", + "glyphRef", + "hatch", + "hatchpath", + "hkern", + "image", + "line", + "linearGradient", + "marker", + "mask", + "metadata", + "missing-glyph", + "mpath", + "path", + "pattern", + "polygon", + "polyline", + "radialGradient", + "rect", + "script", + "set", + "stop", + "style", + "svg", + "switch", + "symbol", + "text", + "textPath", + "title", + "tspan", + "use", + "view", + "vkern", +]; + +// https://developer.mozilla.org/ja/docs/Web/MathML/Element +pub(crate) const MATH_ML_TAGS: [&str; 32] = [ + "annotation", + "annotation-xml", + "maction", + "math", + "menclose", + "merror", + "mfenced", + "mfrac", + "mi", + "mmultiscripts", + "mn", + "mo", + "mover", + "mpadded", + "mphantom", + "mprescripts", + "mroot", + "mrow", + "ms", + "mspace", + "msqrt", + "mstyle", + "msub", + "msubsup", + "msup", + "mtable", + "mtd", + "mtext", + "mtr", + "munder", + "munderover", + "semantics", +]; + #[cfg(test)] mod tests { use std::collections::HashSet; use super::{ - FUNCTION_KEYWORDS, KNOWN_EDGE_PROPERTIES, KNOWN_EXPLORER_PROPERTIES, + FUNCTION_KEYWORDS, HTML_TAGS, KNOWN_EDGE_PROPERTIES, KNOWN_EXPLORER_PROPERTIES, KNOWN_FIREFOX_PROPERTIES, KNOWN_PROPERTIES, KNOWN_SAFARI_PROPERTIES, KNOWN_SAMSUNG_INTERNET_PROPERTIES, KNOWN_US_BROWSER_PROPERTIES, - LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES, MEDIA_FEATURE_NAMES, + LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES, MATH_ML_TAGS, MEDIA_FEATURE_NAMES, RESET_TO_INITIAL_PROPERTIES_BY_BORDER, RESET_TO_INITIAL_PROPERTIES_BY_FONT, - SHORTHAND_PROPERTIES, + SHORTHAND_PROPERTIES, SVG_TAGS, }; #[test] @@ -5635,4 +5913,25 @@ mod tests { .any(|&x| !set.insert(x)); assert!(!has_duplicates); } + + #[test] + fn test_selector_types() { + for items in HTML_TAGS.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } + } + + #[test] + fn test_svg_tags() { + for items in SVG_TAGS.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } + } + + #[test] + fn test_math_ml_tags() { + for items in MATH_ML_TAGS.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } + } } diff --git a/crates/biome_css_analyze/src/lint/correctness/no_invalid_position_at_import_rule.rs b/crates/biome_css_analyze/src/lint/correctness/no_invalid_position_at_import_rule.rs index 22d1151ec5bc..191d204e319a 100644 --- a/crates/biome_css_analyze/src/lint/correctness/no_invalid_position_at_import_rule.rs +++ b/crates/biome_css_analyze/src/lint/correctness/no_invalid_position_at_import_rule.rs @@ -38,10 +38,10 @@ declare_lint_rule! { impl Rule for NoInvalidPositionAtImportRule { type Query = Ast<CssRuleList>; type State = TextRange; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); - fn run(ctx: &RuleContext<Self>) -> Vec<Self::State> { + fn run(ctx: &RuleContext<Self>) -> Self::Signals { let node = ctx.query(); let mut is_invalid_position = false; let mut invalid_import_list = Vec::new(); @@ -73,7 +73,7 @@ impl Rule for NoInvalidPositionAtImportRule { is_invalid_position = true; } } - invalid_import_list + invalid_import_list.into_boxed_slice() } fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index 7910f2e76559..7e9c91dbf140 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -8,6 +8,7 @@ pub mod no_irregular_whitespace; pub mod no_missing_var_function; pub mod no_unknown_pseudo_class; pub mod no_unknown_pseudo_element; +pub mod no_unknown_type_selector; pub mod no_value_at_rule; declare_lint_group! { @@ -20,6 +21,7 @@ declare_lint_group! { self :: no_missing_var_function :: NoMissingVarFunction , self :: no_unknown_pseudo_class :: NoUnknownPseudoClass , self :: no_unknown_pseudo_element :: NoUnknownPseudoElement , + self :: no_unknown_type_selector :: NoUnknownTypeSelector , self :: no_value_at_rule :: NoValueAtRule , ] } diff --git a/crates/biome_css_analyze/src/lint/nursery/no_descending_specificity.rs b/crates/biome_css_analyze/src/lint/nursery/no_descending_specificity.rs index f8efdb1c4763..24f637375124 100644 --- a/crates/biome_css_analyze/src/lint/nursery/no_descending_specificity.rs +++ b/crates/biome_css_analyze/src/lint/nursery/no_descending_specificity.rs @@ -163,7 +163,7 @@ fn find_descending_selector( impl Rule for NoDescendingSpecificity { type Query = Semantic<CssRoot>; type State = DescendingSelector; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { @@ -180,8 +180,7 @@ impl Rule for NoDescendingSpecificity { &mut descending_selectors, ); } - - descending_selectors + descending_selectors.into_boxed_slice() } fn diagnostic(_: &RuleContext<Self>, node: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_css_analyze/src/lint/nursery/no_irregular_whitespace.rs b/crates/biome_css_analyze/src/lint/nursery/no_irregular_whitespace.rs index 48cadd2ebae9..80f0538caa4f 100644 --- a/crates/biome_css_analyze/src/lint/nursery/no_irregular_whitespace.rs +++ b/crates/biome_css_analyze/src/lint/nursery/no_irregular_whitespace.rs @@ -51,12 +51,12 @@ declare_lint_rule! { impl Rule for NoIrregularWhitespace { type Query = Ast<AnyCssRule>; type State = TextRange; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { let node = ctx.query(); - get_irregular_whitespace(node) + get_irregular_whitespace(node).into_boxed_slice() } fn diagnostic(_: &RuleContext<Self>, range: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_css_analyze/src/lint/nursery/no_missing_var_function.rs b/crates/biome_css_analyze/src/lint/nursery/no_missing_var_function.rs index 38675bd7e392..e192963942c4 100644 --- a/crates/biome_css_analyze/src/lint/nursery/no_missing_var_function.rs +++ b/crates/biome_css_analyze/src/lint/nursery/no_missing_var_function.rs @@ -222,7 +222,7 @@ fn is_wrapped_in_var(node: &CssDashedIdentifier) -> bool { // e.g `color: --custom-property;` // ^^^^^^^^^^^^^^^^ CSS_GENERIC_COMPONENT_VALUE_LIST CssSyntaxKind::CSS_GENERIC_COMPONENT_VALUE_LIST => return false, - CssSyntaxKind::CSS_FUNCTION => return parent.text().starts_with("var"), + CssSyntaxKind::CSS_FUNCTION => return parent.text_trimmed().starts_with("var"), _ => {} } current_node = parent.parent(); diff --git a/crates/biome_css_analyze/src/lint/nursery/no_unknown_type_selector.rs b/crates/biome_css_analyze/src/lint/nursery/no_unknown_type_selector.rs new file mode 100644 index 000000000000..16f521723d54 --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/no_unknown_type_selector.rs @@ -0,0 +1,97 @@ +use biome_analyze::{ + context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, +}; +use biome_console::markup; +use biome_css_syntax::CssTypeSelector; +use biome_rowan::AstNode; + +use crate::utils::is_known_type_selector; + +declare_lint_rule! { + /// Disallow unknown type selectors. + /// + /// This rule considers tags defined in the HTML, SVG, and MathML specifications to be known. + /// For details on known CSS type selectors, see the following links + /// - https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors + /// - https://developer.mozilla.org/ja/docs/Web/HTML/Element + /// - https://developer.mozilla.org/ja/docs/Web/SVG/Element + /// - https://developer.mozilla.org/ja/docs/Web/MathML/Element + /// + /// This rule allows custom elements. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// unknown {} + /// ``` + /// + /// ```css,expect_diagnostic + /// unknown > ul {} + /// ``` + /// + /// ```css,expect_diagnostic + /// x-Foo {} + /// ``` + /// + /// ### Valid + /// + /// ```css + /// input {} + /// ``` + /// + /// ```css + /// ul > li {} + /// ``` + /// + /// ```css + /// x-foo {} + /// ``` + /// + pub NoUnknownTypeSelector { + version: "next", + name: "noUnknownTypeSelector", + language: "css", + recommended: true, + sources: &[RuleSource::Stylelint("selector-type-no-unknown")], + } +} + +impl Rule for NoUnknownTypeSelector { + type Query = Ast<CssTypeSelector>; + type State = CssTypeSelector; + type Signals = Option<Self::State>; + type Options = (); + + fn run(ctx: &RuleContext<Self>) -> Option<Self::State> { + let css_type_selector = ctx.query(); + let type_selector = css_type_selector + .ident() + .ok()? + .value_token() + .ok()? + .token_text_trimmed(); + if !is_known_type_selector(&type_selector) { + return Some(css_type_selector.clone()); + } + None + } + + fn diagnostic(_: &RuleContext<Self>, node: &Self::State) -> Option<RuleDiagnostic> { + let span = node.range(); + Some( + RuleDiagnostic::new( + rule_category!(), + span, + markup! { + "Unknown type selector is not allowed." + }, + ) + .note(markup! { + "See "<Hyperlink href="https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors">"MDN web docs"</Hyperlink>" for more details." + }).note(markup! { + "Consider replacing the unknown type selector with valid one."}) + ) + } +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index c91337050f9b..ae6fc3c26304 100644 --- a/crates/biome_css_analyze/src/options.rs +++ b/crates/biome_css_analyze/src/options.rs @@ -27,6 +27,7 @@ pub type NoUnknownProperty = pub type NoUnknownPseudoClass = <lint::nursery::no_unknown_pseudo_class::NoUnknownPseudoClass as biome_analyze::Rule>::Options; pub type NoUnknownPseudoElement = < lint :: nursery :: no_unknown_pseudo_element :: NoUnknownPseudoElement as biome_analyze :: Rule > :: Options ; +pub type NoUnknownTypeSelector = < lint :: nursery :: no_unknown_type_selector :: NoUnknownTypeSelector as biome_analyze :: Rule > :: Options ; pub type NoUnknownUnit = <lint::correctness::no_unknown_unit::NoUnknownUnit as biome_analyze::Rule>::Options; pub type NoUnmatchableAnbSelector = < lint :: correctness :: no_unmatchable_anb_selector :: NoUnmatchableAnbSelector as biome_analyze :: Rule > :: Options ; diff --git a/crates/biome_css_analyze/src/utils.rs b/crates/biome_css_analyze/src/utils.rs index ecf9ab0ec0bb..33ca5e959b08 100644 --- a/crates/biome_css_analyze/src/utils.rs +++ b/crates/biome_css_analyze/src/utils.rs @@ -2,15 +2,15 @@ use crate::keywords::{ AT_RULE_PAGE_PSEUDO_CLASSES, A_NPLUS_BNOTATION_PSEUDO_CLASSES, A_NPLUS_BOF_SNOTATION_PSEUDO_CLASSES, BASIC_KEYWORDS, FONT_FAMILY_KEYWORDS, FONT_SIZE_KEYWORDS, FONT_STRETCH_KEYWORDS, FONT_STYLE_KEYWORDS, FONT_VARIANTS_KEYWORDS, - FONT_WEIGHT_ABSOLUTE_KEYWORDS, FONT_WEIGHT_NUMERIC_KEYWORDS, FUNCTION_KEYWORDS, + FONT_WEIGHT_ABSOLUTE_KEYWORDS, FONT_WEIGHT_NUMERIC_KEYWORDS, FUNCTION_KEYWORDS, HTML_TAGS, KNOWN_CHROME_PROPERTIES, KNOWN_EDGE_PROPERTIES, KNOWN_EXPLORER_PROPERTIES, KNOWN_FIREFOX_PROPERTIES, KNOWN_PROPERTIES, KNOWN_SAFARI_PROPERTIES, KNOWN_SAMSUNG_INTERNET_PROPERTIES, KNOWN_US_BROWSER_PROPERTIES, LEVEL_ONE_AND_TWO_PSEUDO_ELEMENTS, LINE_HEIGHT_KEYWORDS, LINGUISTIC_PSEUDO_CLASSES, LOGICAL_COMBINATIONS_PSEUDO_CLASSES, LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES, - MEDIA_FEATURE_NAMES, OTHER_PSEUDO_CLASSES, OTHER_PSEUDO_ELEMENTS, + MATH_ML_TAGS, MEDIA_FEATURE_NAMES, OTHER_PSEUDO_CLASSES, OTHER_PSEUDO_ELEMENTS, RESET_TO_INITIAL_PROPERTIES_BY_BORDER, RESET_TO_INITIAL_PROPERTIES_BY_FONT, - RESOURCE_STATE_PSEUDO_CLASSES, SHADOW_TREE_PSEUDO_ELEMENTS, SHORTHAND_PROPERTIES, + RESOURCE_STATE_PSEUDO_CLASSES, SHADOW_TREE_PSEUDO_ELEMENTS, SHORTHAND_PROPERTIES, SVG_TAGS, SYSTEM_FAMILY_NAME_KEYWORDS, VENDOR_PREFIXES, VENDOR_SPECIFIC_PSEUDO_ELEMENTS, }; use biome_css_syntax::{AnyCssGenericComponentValue, AnyCssValue, CssGenericComponentValueList}; @@ -217,3 +217,16 @@ pub fn get_reset_to_initial_properties(shorthand_property: &str) -> &'static [&' _ => &[], } } + +fn is_custom_element(prop: &str) -> bool { + prop.contains('-') && prop.eq(prop.to_lowercase_cow().as_ref()) +} + +/// Check if the input string is a known type selector. +pub fn is_known_type_selector(prop: &str) -> bool { + let input = prop.to_lowercase_cow(); + HTML_TAGS.binary_search(&input.as_ref()).is_ok() + || SVG_TAGS.binary_search(&prop).is_ok() + || MATH_ML_TAGS.binary_search(&input.as_ref()).is_ok() + || is_custom_element(prop) +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noMissingVarFunction/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noMissingVarFunction/valid.css index bbb288bd9017..8ed3b50ccd2e 100644 --- a/crates/biome_css_analyze/tests/specs/nursery/noMissingVarFunction/valid.css +++ b/crates/biome_css_analyze/tests/specs/nursery/noMissingVarFunction/valid.css @@ -66,3 +66,10 @@ a { --foo: red; } } + +:root { + --colors-gray-a7: black; + /* The formatter breaks the line */ + --broken-shadow: 0px 0px 1px + var(--colors-gray-a7); +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noMissingVarFunction/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noMissingVarFunction/valid.css.snap index 04c5c0e97268..9eb37f42e50b 100644 --- a/crates/biome_css_analyze/tests/specs/nursery/noMissingVarFunction/valid.css.snap +++ b/crates/biome_css_analyze/tests/specs/nursery/noMissingVarFunction/valid.css.snap @@ -73,4 +73,11 @@ a { } } +:root { + --colors-gray-a7: black; + /* The formatter breaks the line */ + --broken-shadow: 0px 0px 1px + var(--colors-gray-a7); +} + ``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/invalid.css new file mode 100644 index 000000000000..2fe3d8798769 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/invalid.css @@ -0,0 +1,25 @@ +unknown { +} + +ul unknown { +} + +unknown ul { +} + +li > hoge { +} + +fuga > li { +} + +table, +unknown { +} + +unknown, +article { +} + +x-Foo { +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/invalid.css.snap new file mode 100644 index 000000000000..ec89dee08aef --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/invalid.css.snap @@ -0,0 +1,183 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css +unknown { +} + +ul unknown { +} + +unknown ul { +} + +li > hoge { +} + +fuga > li { +} + +table, +unknown { +} + +unknown, +article { +} + +x-Foo { +} + +``` + +# Diagnostics +``` +invalid.css:1:1 lint/nursery/noUnknownTypeSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unknown type selector is not allowed. + + > 1 β”‚ unknown { + β”‚ ^^^^^^^ + 2 β”‚ } + 3 β”‚ + + i See MDN web docs for more details. + + i Consider replacing the unknown type selector with valid one. + + +``` + +``` +invalid.css:4:4 lint/nursery/noUnknownTypeSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unknown type selector is not allowed. + + 2 β”‚ } + 3 β”‚ + > 4 β”‚ ul unknown { + β”‚ ^^^^^^^ + 5 β”‚ } + 6 β”‚ + + i See MDN web docs for more details. + + i Consider replacing the unknown type selector with valid one. + + +``` + +``` +invalid.css:7:1 lint/nursery/noUnknownTypeSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unknown type selector is not allowed. + + 5 β”‚ } + 6 β”‚ + > 7 β”‚ unknown ul { + β”‚ ^^^^^^^ + 8 β”‚ } + 9 β”‚ + + i See MDN web docs for more details. + + i Consider replacing the unknown type selector with valid one. + + +``` + +``` +invalid.css:10:6 lint/nursery/noUnknownTypeSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unknown type selector is not allowed. + + 8 β”‚ } + 9 β”‚ + > 10 β”‚ li > hoge { + β”‚ ^^^^ + 11 β”‚ } + 12 β”‚ + + i See MDN web docs for more details. + + i Consider replacing the unknown type selector with valid one. + + +``` + +``` +invalid.css:13:1 lint/nursery/noUnknownTypeSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unknown type selector is not allowed. + + 11 β”‚ } + 12 β”‚ + > 13 β”‚ fuga > li { + β”‚ ^^^^ + 14 β”‚ } + 15 β”‚ + + i See MDN web docs for more details. + + i Consider replacing the unknown type selector with valid one. + + +``` + +``` +invalid.css:17:1 lint/nursery/noUnknownTypeSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unknown type selector is not allowed. + + 16 β”‚ table, + > 17 β”‚ unknown { + β”‚ ^^^^^^^ + 18 β”‚ } + 19 β”‚ + + i See MDN web docs for more details. + + i Consider replacing the unknown type selector with valid one. + + +``` + +``` +invalid.css:20:1 lint/nursery/noUnknownTypeSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unknown type selector is not allowed. + + 18 β”‚ } + 19 β”‚ + > 20 β”‚ unknown, + β”‚ ^^^^^^^ + 21 β”‚ article { + 22 β”‚ } + + i See MDN web docs for more details. + + i Consider replacing the unknown type selector with valid one. + + +``` + +``` +invalid.css:24:1 lint/nursery/noUnknownTypeSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unknown type selector is not allowed. + + 22 β”‚ } + 23 β”‚ + > 24 β”‚ x-Foo { + β”‚ ^^^^^ + 25 β”‚ } + 26 β”‚ + + i See MDN web docs for more details. + + i Consider replacing the unknown type selector with valid one. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/valid.css new file mode 100644 index 000000000000..cbe72617a5c2 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/valid.css @@ -0,0 +1,21 @@ +input { +} + +ul li { +} + +li > a { +} + +table, +tr { +} + +x-foo { +} + +g { +} + +mfrac { +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/valid.css.snap new file mode 100644 index 000000000000..fde7a93556c3 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownTypeSelector/valid.css.snap @@ -0,0 +1,29 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: valid.css +--- +# Input +```css +input { +} + +ul li { +} + +li > a { +} + +table, +tr { +} + +x-foo { +} + +g { +} + +mfrac { +} + +``` diff --git a/crates/biome_css_formatter/tests/specs/prettier/css/character-escaping/character_escaping.css.snap b/crates/biome_css_formatter/tests/specs/prettier/css/character-escaping/character_escaping.css.snap index ecdbf573379a..dd6f0f1dbe22 100644 --- a/crates/biome_css_formatter/tests/specs/prettier/css/character-escaping/character_escaping.css.snap +++ b/crates/biome_css_formatter/tests/specs/prettier/css/character-escaping/character_escaping.css.snap @@ -72,29 +72,24 @@ info: css/character-escaping/character_escaping.css ```diff --- Prettier +++ Biome -@@ -1,15 +1,9 @@ --#β™₯ { --} +@@ -1,13 +1,10 @@ + #β™₯ { + } -#Β© { -} -#β€œβ€˜β€™β€ { -} --#β˜Ίβ˜ƒ { --} --#⌘βŒ₯ { --} --#π„žβ™ͺ♩♫♬ { --} -+#β™₯ {} +#Β© {} +#β€œβ€˜β€™β€ {} -+#β˜Ίβ˜ƒ {} + #β˜Ίβ˜ƒ { + } +-#⌘βŒ₯ { +-} +#⌘βŒ₯ {} -+#π„žβ™ͺ♩♫♬ {} - #πŸ’© { + #π„žβ™ͺ♩♫♬ { } - #\? { -@@ -50,7 +44,7 @@ + #πŸ’© { +@@ -50,7 +47,7 @@ } #\3A hover { } @@ -108,12 +103,15 @@ info: css/character-escaping/character_escaping.css # Output ```css -#β™₯ {} +#β™₯ { +} #Β© {} #β€œβ€˜β€™β€ {} -#β˜Ίβ˜ƒ {} +#β˜Ίβ˜ƒ { +} #⌘βŒ₯ {} -#π„žβ™ͺ♩♫♬ {} +#π„žβ™ͺ♩♫♬ { +} #πŸ’© { } #\? { @@ -204,15 +202,6 @@ info: css/character-escaping/character_escaping.css # Errors ``` -character_escaping.css:1:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Γ— unexpected character `β™₯` - - > 1 β”‚ #β™₯ {} - β”‚ ^ - 2 β”‚ #Β© {} - 3 β”‚ #β€œβ€˜β€™β€ {} - character_escaping.css:2:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Γ— unexpected character `Β©` @@ -267,39 +256,6 @@ character_escaping.css:3:5 parse ━━━━━━━━━━━━━━━ 4 β”‚ #β˜Ίβ˜ƒ {} 5 β”‚ #⌘βŒ₯ {} -character_escaping.css:4:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Γ— unexpected character `☺` - - 2 β”‚ #Β© {} - 3 β”‚ #β€œβ€˜β€™β€ {} - > 4 β”‚ #β˜Ίβ˜ƒ {} - β”‚ ^ - 5 β”‚ #⌘βŒ₯ {} - 6 β”‚ #π„žβ™ͺ♩♫♬ {} - -character_escaping.css:4:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Γ— unexpected character `β˜ƒ` - - 2 β”‚ #Β© {} - 3 β”‚ #β€œβ€˜β€™β€ {} - > 4 β”‚ #β˜Ίβ˜ƒ {} - β”‚ ^ - 5 β”‚ #⌘βŒ₯ {} - 6 β”‚ #π„žβ™ͺ♩♫♬ {} - -character_escaping.css:5:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Γ— unexpected character `⌘` - - 3 β”‚ #β€œβ€˜β€™β€ {} - 4 β”‚ #β˜Ίβ˜ƒ {} - > 5 β”‚ #⌘βŒ₯ {} - β”‚ ^ - 6 β”‚ #π„žβ™ͺ♩♫♬ {} - 7 β”‚ #πŸ’© {} - character_escaping.css:5:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Γ— unexpected character `βŒ₯` @@ -311,54 +267,10 @@ character_escaping.css:5:3 parse ━━━━━━━━━━━━━━━ 6 β”‚ #π„žβ™ͺ♩♫♬ {} 7 β”‚ #πŸ’© {} -character_escaping.css:6:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Γ— unexpected character `β™ͺ` - - 4 β”‚ #β˜Ίβ˜ƒ {} - 5 β”‚ #⌘βŒ₯ {} - > 6 β”‚ #π„žβ™ͺ♩♫♬ {} - β”‚ ^ - 7 β”‚ #πŸ’© {} - 8 β”‚ #\? {} - -character_escaping.css:6:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Γ— unexpected character `β™©` - - 4 β”‚ #β˜Ίβ˜ƒ {} - 5 β”‚ #⌘βŒ₯ {} - > 6 β”‚ #π„žβ™ͺ♩♫♬ {} - β”‚ ^ - 7 β”‚ #πŸ’© {} - 8 β”‚ #\? {} - -character_escaping.css:6:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Γ— unexpected character `β™«` - - 4 β”‚ #β˜Ίβ˜ƒ {} - 5 β”‚ #⌘βŒ₯ {} - > 6 β”‚ #π„žβ™ͺ♩♫♬ {} - β”‚ ^ - 7 β”‚ #πŸ’© {} - 8 β”‚ #\? {} - -character_escaping.css:6:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Γ— unexpected character `♬` - - 4 β”‚ #β˜Ίβ˜ƒ {} - 5 β”‚ #⌘βŒ₯ {} - > 6 β”‚ #π„žβ™ͺ♩♫♬ {} - β”‚ ^ - 7 β”‚ #πŸ’© {} - 8 β”‚ #\? {} - ``` # Lines exceeding max width of 80 characters ``` - 27: #\+\+\+\+\+\+\+\+\+\+\[\>\+\+\+\+\+\+\+\>\+\+\+\+\+\+\+\+\+\+\>\+\+\+\>\+\<\<\<\<\-\]\>\+\+\.\>\+\.\+\+\+\+\+\+\+\.\.\+\+\+\.\>\+\+\.\<\<\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\.\>\.\+\+\+\.\-\-\-\-\-\-\.\-\-\-\-\-\-\-\-\.\>\+\.\>\. { + 30: #\+\+\+\+\+\+\+\+\+\+\[\>\+\+\+\+\+\+\+\>\+\+\+\+\+\+\+\+\+\+\>\+\+\+\>\+\<\<\<\<\-\]\>\+\+\.\>\+\.\+\+\+\+\+\+\+\.\.\+\+\+\.\>\+\+\.\<\<\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\.\>\.\+\+\+\.\-\-\-\-\-\-\.\-\-\-\-\-\-\-\-\.\>\+\.\>\. { ``` diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/property/property-with-emoji.css b/crates/biome_css_parser/tests/css_test_suite/ok/property/property-with-emoji.css index 8a970615c189..16d8eee95e14 100644 --- a/crates/biome_css_parser/tests/css_test_suite/ok/property/property-with-emoji.css +++ b/crates/biome_css_parser/tests/css_test_suite/ok/property/property-with-emoji.css @@ -1,4 +1,6 @@ p { --πŸ₯”-color: red; + --β˜‚-color: red; + --✨-color: red; color: var(--πŸ₯”-color); } \ No newline at end of file diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/property/property-with-emoji.css.snap b/crates/biome_css_parser/tests/css_test_suite/ok/property/property-with-emoji.css.snap index 0b438105b320..669d534ad0e6 100644 --- a/crates/biome_css_parser/tests/css_test_suite/ok/property/property-with-emoji.css.snap +++ b/crates/biome_css_parser/tests/css_test_suite/ok/property/property-with-emoji.css.snap @@ -7,6 +7,8 @@ expression: snapshot ```css p { --πŸ₯”-color: red; + --β˜‚-color: red; + --✨-color: red; color: var(--πŸ₯”-color); } ``` @@ -51,54 +53,88 @@ CssRoot { }, semicolon_token: SEMICOLON@23..24 ";" [] [], }, + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssDashedIdentifier { + value_token: IDENT@24..38 "--β˜‚-color" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@38..40 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@40..43 "red" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@43..44 ";" [] [], + }, + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssDashedIdentifier { + value_token: IDENT@44..58 "--✨-color" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@58..60 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@60..63 "red" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@63..64 ";" [] [], + }, CssDeclarationWithSemicolon { declaration: CssDeclaration { property: CssGenericProperty { name: CssIdentifier { - value_token: IDENT@24..32 "color" [Newline("\n"), Whitespace(" ")] [], + value_token: IDENT@64..72 "color" [Newline("\n"), Whitespace(" ")] [], }, - colon_token: COLON@32..34 ":" [] [Whitespace(" ")], + colon_token: COLON@72..74 ":" [] [Whitespace(" ")], value: CssGenericComponentValueList [ CssFunction { name: CssIdentifier { - value_token: IDENT@34..37 "var" [] [], + value_token: IDENT@74..77 "var" [] [], }, - l_paren_token: L_PAREN@37..38 "(" [] [], + l_paren_token: L_PAREN@77..78 "(" [] [], items: CssParameterList [ CssParameter { any_css_expression: CssListOfComponentValuesExpression { css_component_value_list: CssComponentValueList [ CssDashedIdentifier { - value_token: IDENT@38..50 "--πŸ₯”-color" [] [], + value_token: IDENT@78..90 "--πŸ₯”-color" [] [], }, ], }, }, ], - r_paren_token: R_PAREN@50..51 ")" [] [], + r_paren_token: R_PAREN@90..91 ")" [] [], }, ], }, important: missing (optional), }, - semicolon_token: SEMICOLON@51..52 ";" [] [], + semicolon_token: SEMICOLON@91..92 ";" [] [], }, ], - r_curly_token: R_CURLY@52..54 "}" [Newline("\n")] [], + r_curly_token: R_CURLY@92..94 "}" [Newline("\n")] [], }, }, ], - eof_token: EOF@54..54 "" [] [], + eof_token: EOF@94..94 "" [] [], } ``` ## CST ``` -0: CSS_ROOT@0..54 +0: CSS_ROOT@0..94 0: (empty) - 1: CSS_RULE_LIST@0..54 - 0: CSS_QUALIFIED_RULE@0..54 + 1: CSS_RULE_LIST@0..94 + 0: CSS_QUALIFIED_RULE@0..94 0: CSS_SELECTOR_LIST@0..2 0: CSS_COMPOUND_SELECTOR@0..2 0: CSS_NESTED_SELECTOR_LIST@0..0 @@ -107,9 +143,9 @@ CssRoot { 1: CSS_IDENTIFIER@0..2 0: IDENT@0..2 "p" [] [Whitespace(" ")] 2: CSS_SUB_SELECTOR_LIST@2..2 - 1: CSS_DECLARATION_OR_RULE_BLOCK@2..54 + 1: CSS_DECLARATION_OR_RULE_BLOCK@2..94 0: L_CURLY@2..3 "{" [] [] - 1: CSS_DECLARATION_OR_RULE_LIST@3..52 + 1: CSS_DECLARATION_OR_RULE_LIST@3..92 0: CSS_DECLARATION_WITH_SEMICOLON@3..24 0: CSS_DECLARATION@3..23 0: CSS_GENERIC_PROPERTY@3..23 @@ -121,27 +157,49 @@ CssRoot { 0: IDENT@20..23 "red" [] [] 1: (empty) 1: SEMICOLON@23..24 ";" [] [] - 1: CSS_DECLARATION_WITH_SEMICOLON@24..52 - 0: CSS_DECLARATION@24..51 - 0: CSS_GENERIC_PROPERTY@24..51 - 0: CSS_IDENTIFIER@24..32 - 0: IDENT@24..32 "color" [Newline("\n"), Whitespace(" ")] [] - 1: COLON@32..34 ":" [] [Whitespace(" ")] - 2: CSS_GENERIC_COMPONENT_VALUE_LIST@34..51 - 0: CSS_FUNCTION@34..51 - 0: CSS_IDENTIFIER@34..37 - 0: IDENT@34..37 "var" [] [] - 1: L_PAREN@37..38 "(" [] [] - 2: CSS_PARAMETER_LIST@38..50 - 0: CSS_PARAMETER@38..50 - 0: CSS_LIST_OF_COMPONENT_VALUES_EXPRESSION@38..50 - 0: CSS_COMPONENT_VALUE_LIST@38..50 - 0: CSS_DASHED_IDENTIFIER@38..50 - 0: IDENT@38..50 "--πŸ₯”-color" [] [] - 3: R_PAREN@50..51 ")" [] [] + 1: CSS_DECLARATION_WITH_SEMICOLON@24..44 + 0: CSS_DECLARATION@24..43 + 0: CSS_GENERIC_PROPERTY@24..43 + 0: CSS_DASHED_IDENTIFIER@24..38 + 0: IDENT@24..38 "--β˜‚-color" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@38..40 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@40..43 + 0: CSS_IDENTIFIER@40..43 + 0: IDENT@40..43 "red" [] [] + 1: (empty) + 1: SEMICOLON@43..44 ";" [] [] + 2: CSS_DECLARATION_WITH_SEMICOLON@44..64 + 0: CSS_DECLARATION@44..63 + 0: CSS_GENERIC_PROPERTY@44..63 + 0: CSS_DASHED_IDENTIFIER@44..58 + 0: IDENT@44..58 "--✨-color" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@58..60 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@60..63 + 0: CSS_IDENTIFIER@60..63 + 0: IDENT@60..63 "red" [] [] + 1: (empty) + 1: SEMICOLON@63..64 ";" [] [] + 3: CSS_DECLARATION_WITH_SEMICOLON@64..92 + 0: CSS_DECLARATION@64..91 + 0: CSS_GENERIC_PROPERTY@64..91 + 0: CSS_IDENTIFIER@64..72 + 0: IDENT@64..72 "color" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@72..74 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@74..91 + 0: CSS_FUNCTION@74..91 + 0: CSS_IDENTIFIER@74..77 + 0: IDENT@74..77 "var" [] [] + 1: L_PAREN@77..78 "(" [] [] + 2: CSS_PARAMETER_LIST@78..90 + 0: CSS_PARAMETER@78..90 + 0: CSS_LIST_OF_COMPONENT_VALUES_EXPRESSION@78..90 + 0: CSS_COMPONENT_VALUE_LIST@78..90 + 0: CSS_DASHED_IDENTIFIER@78..90 + 0: IDENT@78..90 "--πŸ₯”-color" [] [] + 3: R_PAREN@90..91 ")" [] [] 1: (empty) - 1: SEMICOLON@51..52 ";" [] [] - 2: R_CURLY@52..54 "}" [Newline("\n")] [] - 2: EOF@54..54 "" [] [] + 1: SEMICOLON@91..92 ";" [] [] + 2: R_CURLY@92..94 "}" [Newline("\n")] [] + 2: EOF@94..94 "" [] [] ``` diff --git a/crates/biome_deserialize/src/impls.rs b/crates/biome_deserialize/src/impls.rs index b87a53d0fafe..29d54a0fb4c1 100644 --- a/crates/biome_deserialize/src/impls.rs +++ b/crates/biome_deserialize/src/impls.rs @@ -498,6 +498,16 @@ impl Deserializable for String { } } +impl Deserializable for Box<str> { + fn deserialize( + value: &impl DeserializableValue, + name: &str, + diagnostics: &mut Vec<DeserializationDiagnostic>, + ) -> Option<Self> { + String::deserialize(value, name, diagnostics).map(|s| s.into_boxed_str()) + } +} + impl Deserializable for PathBuf { fn deserialize( value: &impl DeserializableValue, @@ -556,6 +566,16 @@ impl<T: Deserializable> Deserializable for Vec<T> { } } +impl<T: Deserializable> Deserializable for Box<[T]> { + fn deserialize( + value: &impl DeserializableValue, + name: &str, + diagnostics: &mut Vec<DeserializationDiagnostic>, + ) -> Option<Self> { + Deserializable::deserialize(value, name, diagnostics).map(Vec::into_boxed_slice) + } +} + #[cfg(feature = "smallvec")] impl<T: Deserializable, const L: usize> Deserializable for smallvec::SmallVec<[T; L]> { fn deserialize( diff --git a/crates/biome_deserialize/src/validator.rs b/crates/biome_deserialize/src/validator.rs index 31697ee982a0..44ea089a740d 100644 --- a/crates/biome_deserialize/src/validator.rs +++ b/crates/biome_deserialize/src/validator.rs @@ -50,8 +50,20 @@ impl IsEmpty for String { } } +impl IsEmpty for Box<str> { + fn is_empty(&self) -> bool { + str::is_empty(self) + } +} + impl<T> IsEmpty for Vec<T> { fn is_empty(&self) -> bool { Vec::is_empty(self) } } + +impl<T> IsEmpty for Box<[T]> { + fn is_empty(&self) -> bool { + <[_]>::is_empty(self) + } +} diff --git a/crates/biome_diagnostics/src/advice.rs b/crates/biome_diagnostics/src/advice.rs index 4520a2fcd328..4919577b3153 100644 --- a/crates/biome_diagnostics/src/advice.rs +++ b/crates/biome_diagnostics/src/advice.rs @@ -5,7 +5,7 @@ use crate::{ Location, }; use biome_console::fmt::{self, Display}; -use biome_console::markup; +use biome_console::{markup, MarkupBuf}; use biome_text_edit::TextEdit; use serde::{Deserialize, Serialize}; use std::io; @@ -66,6 +66,20 @@ pub trait Visit { let _ = (title, advice); Ok(()) } + + /// ## Warning + /// + /// The implementation of the table, for now, is tailored for two columns, and it assumes that + /// the longest cell is on top. + fn record_table( + &mut self, + padding: usize, + headers: &[MarkupBuf], + columns: &[&[MarkupBuf]], + ) -> io::Result<()> { + let _ = (headers, columns, padding); + Ok(()) + } } /// The category for a log advice, defines how the message should be presented diff --git a/crates/biome_diagnostics/src/display.rs b/crates/biome_diagnostics/src/display.rs index 849c088637ca..5317659b18a2 100644 --- a/crates/biome_diagnostics/src/display.rs +++ b/crates/biome_diagnostics/src/display.rs @@ -1,9 +1,10 @@ -use std::path::Path; -use std::{env, io, iter}; - use biome_console::fmt::MarkupElements; -use biome_console::{fmt, markup, HorizontalLine, Markup, MarkupBuf, MarkupElement, MarkupNode}; +use biome_console::{ + fmt, markup, HorizontalLine, Markup, MarkupBuf, MarkupElement, MarkupNode, Padding, +}; use biome_text_edit::TextEdit; +use std::path::Path; +use std::{env, io, iter}; use unicode_width::UnicodeWidthStr; mod backtrace; @@ -466,6 +467,61 @@ impl Visit for PrintAdvices<'_, '_> { let mut visitor = PrintAdvices(&mut fmt); advice.record(&mut visitor) } + + fn record_table( + &mut self, + padding: usize, + headers: &[MarkupBuf], + columns: &[&[MarkupBuf]], + ) -> io::Result<()> { + debug_assert_eq!( + headers.len(), + columns.len(), + "headers and columns must have the same number length" + ); + + if columns.is_empty() { + return Ok(()); + } + + let mut headers_iter = headers.iter().enumerate(); + let rows_number = columns[0].len(); + let columns_number = columns.len(); + + let mut longest_cell = 0; + for current_row_index in 0..rows_number { + for current_column_index in 0..columns_number { + let cell = columns + .get(current_column_index) + .and_then(|c| c.get(current_row_index)); + if let Some(cell) = cell { + if current_column_index == 0 && current_row_index == 0 { + longest_cell = cell.text_len(); + for (index, header_cell) in headers_iter.by_ref() { + self.0.write_markup(markup!({ header_cell }))?; + if index < headers.len() - 1 { + self.0.write_markup( + markup! {{Padding::new(padding + longest_cell - header_cell.text_len())}}, + )?; + } + } + + self.0.write_markup(markup! {"\n\n"})?; + } + let extra_padding = longest_cell.saturating_sub(cell.text_len()); + + self.0.write_markup(markup!({ cell }))?; + if columns_number != current_column_index + 1 { + self.0 + .write_markup(markup! {{Padding::new(padding + extra_padding)}})?; + } + } + } + self.0.write_markup(markup!("\n"))?; + } + + Ok(()) + } } /// Print the fatal and internal tags for the diagnostic as log advices. diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 6dbdda2803f7..c748ad922cab 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -145,6 +145,9 @@ define_categories! { "lint/nursery/noDynamicNamespaceImportAccess": "https://biomejs.dev/linter/rules/no-dynamic-namespace-import-access", "lint/nursery/noEnum": "https://biomejs.dev/linter/rules/no-enum", "lint/nursery/noExportedImports": "https://biomejs.dev/linter/rules/no-exported-imports", + "lint/nursery/noHeadElement": "https://biomejs.dev/linter/rules/no-head-element", + "lint/nursery/noHeadImportInDocument": "https://biomejs.dev/linter/rules/no-head-import-in-document", + "lint/nursery/noImgElement": "https://biomejs.dev/linter/rules/no-img-element", "lint/nursery/noImportantInKeyframe": "https://biomejs.dev/linter/rules/no-important-in-keyframe", "lint/nursery/noInvalidDirectionInLinearGradient": "https://biomejs.dev/linter/rules/no-invalid-direction-in-linear-gradient", "lint/nursery/noInvalidGridAreas": "https://biomejs.dev/linter/rules/use-consistent-grid-areas", @@ -171,6 +174,7 @@ define_categories! { "lint/nursery/noUnknownPseudoClassSelector": "https://biomejs.dev/linter/rules/no-unknown-pseudo-class-selector", "lint/nursery/noUnknownPseudoElement": "https://biomejs.dev/linter/rules/no-unknown-selector-pseudo-element", "lint/nursery/noUnknownSelectorPseudoElement": "https://biomejs.dev/linter/rules/no-unknown-selector-pseudo-element", + "lint/nursery/noUnknownTypeSelector": "https://biomejs.dev/linter/rules/no-unknown-type-selector", "lint/nursery/noUnknownUnit": "https://biomejs.dev/linter/rules/no-unknown-unit", "lint/nursery/noUnmatchableAnbSelector": "https://biomejs.dev/linter/rules/no-unmatchable-anb-selector", "lint/nursery/noUnusedFunctionParameters": "https://biomejs.dev/linter/rules/no-unused-function-parameters", @@ -343,6 +347,10 @@ define_categories! { "internalError/io", "internalError/fs", "internalError/panic", + "reporter/parse", + "reporter/format", + "reporter/analyzer", + "reporter/organizeImports", // parse categories "parse", diff --git a/crates/biome_graphql_analyze/src/lint/nursery/no_duplicated_fields.rs b/crates/biome_graphql_analyze/src/lint/nursery/no_duplicated_fields.rs index 77676cf462df..118a29b41e2b 100644 --- a/crates/biome_graphql_analyze/src/lint/nursery/no_duplicated_fields.rs +++ b/crates/biome_graphql_analyze/src/lint/nursery/no_duplicated_fields.rs @@ -49,10 +49,10 @@ declare_lint_rule! { impl Rule for NoDuplicatedFields { type Query = Ast<AnyGraphqlOperationDefinition>; type State = DuplicatedField; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); - fn run(ctx: &RuleContext<Self>) -> Vec<Self::State> { + fn run(ctx: &RuleContext<Self>) -> Self::Signals { let operation = ctx.query(); let mut duplicated_fields = vec![]; match operation { @@ -68,8 +68,7 @@ impl Rule for NoDuplicatedFields { duplicated_fields.extend(check_duplicated_selection_fields(selection_set)) } }; - - duplicated_fields + duplicated_fields.into_boxed_slice() } fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_grit_formatter/Cargo.toml b/crates/biome_grit_formatter/Cargo.toml index 399c4eda2216..eaebbafeaa7c 100644 --- a/crates/biome_grit_formatter/Cargo.toml +++ b/crates/biome_grit_formatter/Cargo.toml @@ -20,6 +20,7 @@ biome_rowan = { workspace = true } [dev-dependencies] biome_configuration = { path = "../biome_configuration" } biome_formatter_test = { path = "../biome_formatter_test" } +biome_fs = { path = "../biome_fs" } biome_grit_factory = { path = "../biome_grit_factory" } biome_grit_parser = { path = "../biome_grit_parser" } biome_parser = { path = "../biome_parser" } diff --git a/crates/biome_grit_formatter/src/context.rs b/crates/biome_grit_formatter/src/context.rs index 43812f0b78e5..3b4040af3b61 100644 --- a/crates/biome_grit_formatter/src/context.rs +++ b/crates/biome_grit_formatter/src/context.rs @@ -2,8 +2,9 @@ use crate::comments::{FormatGritLeadingComment, GritCommentStyle, GritComments}; use biome_formatter::printer::PrinterOptions; use biome_formatter::{ AttributePosition, BracketSpacing, CstFormatContext, FormatContext, FormatOptions, IndentStyle, - IndentWidth, LineEnding, LineWidth, QuoteStyle, TransformSourceMap, + IndentWidth, LineEnding, LineWidth, TransformSourceMap, }; +use biome_grit_syntax::file_source::GritFileSource; use biome_grit_syntax::GritLanguage; use std::fmt::Display; use std::rc::Rc; @@ -54,27 +55,28 @@ impl CstFormatContext for GritFormatContext { } } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct GritFormatOptions { indent_style: IndentStyle, indent_width: IndentWidth, line_ending: LineEnding, line_width: LineWidth, - quote_style: QuoteStyle, attribute_position: AttributePosition, + _file_source: GritFileSource, } impl GritFormatOptions { - pub fn new() -> Self { + pub fn new(file_source: GritFileSource) -> Self { Self { + _file_source: file_source, indent_style: IndentStyle::default(), indent_width: IndentWidth::default(), line_ending: LineEnding::default(), line_width: LineWidth::default(), - quote_style: QuoteStyle::default(), attribute_position: AttributePosition::default(), } } + pub fn with_indent_style(mut self, indent_style: IndentStyle) -> Self { self.indent_style = indent_style; self @@ -95,11 +97,6 @@ impl GritFormatOptions { self } - pub fn with_quote_style(mut self, quote_style: QuoteStyle) -> Self { - self.quote_style = quote_style; - self - } - pub fn set_indent_style(&mut self, indent_style: IndentStyle) { self.indent_style = indent_style; } @@ -116,14 +113,6 @@ impl GritFormatOptions { self.line_width = line_width; } - pub fn set_quote_style(&mut self, quote_style: QuoteStyle) { - self.quote_style = quote_style; - } - - pub fn quote_style(&self) -> QuoteStyle { - self.quote_style - } - pub fn attribute_position(&self) -> AttributePosition { self.attribute_position } diff --git a/crates/biome_grit_formatter/src/grit/lists/definition_list.rs b/crates/biome_grit_formatter/src/grit/lists/definition_list.rs index 78a127a982c1..fc6c963ff715 100644 --- a/crates/biome_grit_formatter/src/grit/lists/definition_list.rs +++ b/crates/biome_grit_formatter/src/grit/lists/definition_list.rs @@ -5,6 +5,17 @@ pub(crate) struct FormatGritDefinitionList; impl FormatRule<GritDefinitionList> for FormatGritDefinitionList { type Context = GritFormatContext; fn fmt(&self, node: &GritDefinitionList, f: &mut GritFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + let mut join = f.join_nodes_with_hardline(); + + for definition in node { + let definition = definition?; + + join.entry( + definition.syntax(), + &format_or_verbatim(definition.format()), + ); + } + + join.finish() } } diff --git a/crates/biome_grit_formatter/src/grit/lists/predicate_list.rs b/crates/biome_grit_formatter/src/grit/lists/predicate_list.rs index 0a93e84841eb..bc94f77bd127 100644 --- a/crates/biome_grit_formatter/src/grit/lists/predicate_list.rs +++ b/crates/biome_grit_formatter/src/grit/lists/predicate_list.rs @@ -5,6 +5,13 @@ pub(crate) struct FormatGritPredicateList; impl FormatRule<GritPredicateList> for FormatGritPredicateList { type Context = GritFormatContext; fn fmt(&self, node: &GritPredicateList, f: &mut GritFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + let mut join = f.join_nodes_with_hardline(); + + for predicate in node { + let predicate = predicate?; + join.entry(predicate.syntax(), &format_or_verbatim(predicate.format())); + } + + join.finish() } } diff --git a/crates/biome_grit_formatter/src/grit/patterns/pattern_where.rs b/crates/biome_grit_formatter/src/grit/patterns/pattern_where.rs index bbdfa34e0aec..5f24ffec270e 100644 --- a/crates/biome_grit_formatter/src/grit/patterns/pattern_where.rs +++ b/crates/biome_grit_formatter/src/grit/patterns/pattern_where.rs @@ -1,10 +1,27 @@ use crate::prelude::*; -use biome_grit_syntax::GritPatternWhere; -use biome_rowan::AstNode; +use biome_formatter::write; +use biome_grit_syntax::{GritPatternWhere, GritPatternWhereFields}; + #[derive(Debug, Clone, Default)] pub(crate) struct FormatGritPatternWhere; impl FormatNodeRule<GritPatternWhere> for FormatGritPatternWhere { fn fmt_fields(&self, node: &GritPatternWhere, f: &mut GritFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + let GritPatternWhereFields { + pattern, + side_condition, + where_token, + } = node.as_fields(); + + write!( + f, + [ + space(), + pattern.format(), + space(), + where_token.format(), + space(), + side_condition.format(), + ] + ) } } diff --git a/crates/biome_grit_formatter/src/grit/predicates/predicate_and.rs b/crates/biome_grit_formatter/src/grit/predicates/predicate_and.rs index d48416ef1524..890c07c23a7c 100644 --- a/crates/biome_grit_formatter/src/grit/predicates/predicate_and.rs +++ b/crates/biome_grit_formatter/src/grit/predicates/predicate_and.rs @@ -1,6 +1,5 @@ use crate::prelude::*; use biome_grit_syntax::GritPredicateAnd; -use biome_rowan::AstNode; #[derive(Debug, Clone, Default)] pub(crate) struct FormatGritPredicateAnd; impl FormatNodeRule<GritPredicateAnd> for FormatGritPredicateAnd { diff --git a/crates/biome_grit_formatter/tests/language.rs b/crates/biome_grit_formatter/tests/language.rs index 24edd18515ba..13f278bb367e 100644 --- a/crates/biome_grit_formatter/tests/language.rs +++ b/crates/biome_grit_formatter/tests/language.rs @@ -1,7 +1,9 @@ use biome_formatter_test::TestFormatLanguage; +use biome_fs::BiomePath; use biome_grit_formatter::{context::GritFormatContext, GritFormatLanguage}; use biome_grit_parser::parse_grit; use biome_grit_syntax::GritLanguage; +use biome_service::settings::ServiceLanguage; #[derive(Default)] pub struct GritTestFormatLanguage; @@ -17,9 +19,17 @@ impl TestFormatLanguage for GritTestFormatLanguage { fn to_format_language( &self, - _settings: &biome_service::settings::Settings, - _file_source: &biome_service::workspace::DocumentFileSource, + settings: &biome_service::settings::Settings, + file_source: &biome_service::workspace::DocumentFileSource, ) -> Self::FormatLanguage { - todo!() + let language_settings = &settings.languages.grit.formatter; + let options = Self::ServiceLanguage::resolve_format_options( + Some(&settings.formatter), + Some(&settings.override_settings), + Some(language_settings), + &BiomePath::new(""), + file_source, + ); + GritFormatLanguage::new(options) } } diff --git a/crates/biome_grit_formatter/tests/quick_test.rs b/crates/biome_grit_formatter/tests/quick_test.rs index d8605439ac83..41c9ea68e749 100644 --- a/crates/biome_grit_formatter/tests/quick_test.rs +++ b/crates/biome_grit_formatter/tests/quick_test.rs @@ -1,4 +1,4 @@ -use biome_formatter::{IndentStyle, LineWidth, QuoteStyle}; +use biome_formatter::{IndentStyle, LineWidth}; use biome_formatter_test::check_reformat::CheckReformat; use biome_grit_formatter::context::GritFormatOptions; use biome_grit_formatter::{format_node, GritFormatLanguage}; @@ -22,10 +22,9 @@ fn quick_test() { } "#; let tree = parse_grit(src); - let options = GritFormatOptions::new() + let options = GritFormatOptions::default() .with_indent_style(IndentStyle::Space) - .with_line_width(LineWidth::try_from(80).unwrap()) - .with_quote_style(QuoteStyle::Double); + .with_line_width(LineWidth::try_from(80).unwrap()); let doc = format_node(options.clone(), &tree.syntax()).unwrap(); let result = doc.print().unwrap(); diff --git a/crates/biome_grit_formatter/tests/specs/grit/patterns/if_pattern.grit b/crates/biome_grit_formatter/tests/specs/grit/patterns/if_pattern.grit deleted file mode 100644 index 97bc4bb4f96f..000000000000 --- a/crates/biome_grit_formatter/tests/specs/grit/patterns/if_pattern.grit +++ /dev/null @@ -1,7 +0,0 @@ -`$method('$message')` where { - if ($message <: r"Hello, .*!") { - $method => `console.info` - } else { - $method => `console.warn` - } -} diff --git a/crates/biome_grit_formatter/tests/specs/grit/patterns/if_pattern.grit.snap b/crates/biome_grit_formatter/tests/specs/grit/patterns/if_pattern.grit.snap deleted file mode 100644 index 354668dcdfa0..000000000000 --- a/crates/biome_grit_formatter/tests/specs/grit/patterns/if_pattern.grit.snap +++ /dev/null @@ -1,47 +0,0 @@ ---- -source: crates/biome_formatter_test/src/snapshot_builder.rs -info: grit/patterns/if_pattern.grit ---- -# Input - -```grit -`$method('$message')` where { - if ($message <: r"Hello, .*!") { - $method => `console.info` - } else { - $method => `console.warn` - } -} - -``` - - -============================= - -# Outputs - -## Output 1 - ------ -Indent style: Tab -Indent width: 2 -Line ending: LF -Line width: 80 -Attribute Position: Auto ------ - -```grit -`$method('$message')` where { - if ($message <: r"Hello, .*!") { - $method => `console.info` - } else { - $method => `console.warn` - } -} -``` - - - -## Unimplemented nodes/tokens - -"`$method('$message')` where {\n if ($message <: r\"Hello, .*!\") {\n $method => `console.info`\n } else {\n $method => `console.warn`\n }\n}" => 0..141 diff --git a/crates/biome_grit_formatter/tests/specs/grit/patterns/where_pattern.grit b/crates/biome_grit_formatter/tests/specs/grit/patterns/where_pattern.grit new file mode 100644 index 000000000000..f23458480d4c --- /dev/null +++ b/crates/biome_grit_formatter/tests/specs/grit/patterns/where_pattern.grit @@ -0,0 +1,3 @@ +`$method('$message')`where{ + +} diff --git a/crates/biome_grit_formatter/tests/specs/grit/patterns/where_pattern.grit.snap b/crates/biome_grit_formatter/tests/specs/grit/patterns/where_pattern.grit.snap new file mode 100644 index 000000000000..f3db11c84cac --- /dev/null +++ b/crates/biome_grit_formatter/tests/specs/grit/patterns/where_pattern.grit.snap @@ -0,0 +1,40 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: grit/patterns/where_pattern.grit +--- +# Input + +```grit +`$method('$message')`where{ + +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Attribute Position: Auto +----- + +```grit +`$method('$message')` where { + +} +``` + + + +## Unimplemented nodes/tokens + +"`$method('$message')`" => 0..21 +" {\n\n" => 27..31 diff --git a/crates/biome_grit_patterns/src/grit_built_in_functions.rs b/crates/biome_grit_patterns/src/grit_built_in_functions.rs index 1c95dbc4289e..00f8d110e198 100644 --- a/crates/biome_grit_patterns/src/grit_built_in_functions.rs +++ b/crates/biome_grit_patterns/src/grit_built_in_functions.rs @@ -14,7 +14,6 @@ use grit_pattern_matcher::{ }, }; use grit_util::AnalysisLogs; -use im::Vector; use path_absolutize::Absolutize; use rand::{seq::SliceRandom, Rng}; use std::borrow::Cow; @@ -135,10 +134,10 @@ fn distinct_fn<'a>( match arg1 { GritResolvedPattern::List(list) => { - let mut unique_list = Vector::new(); + let mut unique_list = Vec::new(); for item in list { if !unique_list.contains(&item) { - unique_list.push_back(item); + unique_list.push(item); } } Ok(GritResolvedPattern::List(unique_list)) @@ -149,11 +148,11 @@ fn distinct_fn<'a>( bail!("distinct() requires a list as the first argument"); }; - let mut unique_list = Vector::new(); + let mut unique_list = Vec::new(); for item in list_items { let resolved = ResolvedPattern::from_node_binding(item); if !unique_list.contains(&resolved) { - unique_list.push_back(resolved); + unique_list.push(resolved); } } Ok(GritResolvedPattern::List(unique_list)) diff --git a/crates/biome_grit_patterns/src/grit_resolved_pattern.rs b/crates/biome_grit_patterns/src/grit_resolved_pattern.rs index ebb61c17a3ff..c5b32cffd709 100644 --- a/crates/biome_grit_patterns/src/grit_resolved_pattern.rs +++ b/crates/biome_grit_patterns/src/grit_resolved_pattern.rs @@ -16,15 +16,14 @@ use grit_pattern_matcher::pattern::{ ResolvedPattern, ResolvedSnippet, State, }; use grit_util::{AnalysisLogs, Ast, CodeRange, Range}; -use im::{vector, Vector}; use std::borrow::Cow; use std::collections::{BTreeMap, HashMap}; #[derive(Clone, Debug, PartialEq)] pub enum GritResolvedPattern<'a> { - Binding(Vector<GritBinding<'a>>), - Snippets(Vector<ResolvedSnippet<'a, GritQueryContext>>), - List(Vector<GritResolvedPattern<'a>>), + Binding(Vec<GritBinding<'a>>), + Snippets(Vec<ResolvedSnippet<'a, GritQueryContext>>), + List(Vec<GritResolvedPattern<'a>>), Map(BTreeMap<String, GritResolvedPattern<'a>>), File(GritFile<'a>), Files(Box<GritResolvedPattern<'a>>), @@ -40,10 +39,10 @@ impl<'a> GritResolvedPattern<'a> { Self::from_binding(GritBinding::from_node(tree.root_node())) } - fn to_snippets(&self) -> Result<Vector<ResolvedSnippet<'a, GritQueryContext>>> { + fn to_snippets(&self) -> Result<Vec<ResolvedSnippet<'a, GritQueryContext>>> { match self { Self::Snippets(snippets) => Ok(snippets.clone()), - Self::Binding(bindings) => Ok(vector![ResolvedSnippet::from_binding( + Self::Binding(bindings) => Ok(vec![ResolvedSnippet::from_binding( bindings .last() .ok_or_else(|| { @@ -59,7 +58,7 @@ impl<'a> GritResolvedPattern<'a> { snippets.push(ResolvedSnippet::Text(" ".into())); } snippets.pop(); - Ok(snippets.into()) + Ok(snippets) } Self::Map(map) => { let mut snippets = Vec::new(); @@ -71,7 +70,7 @@ impl<'a> GritResolvedPattern<'a> { } snippets.pop(); snippets.push(ResolvedSnippet::Text("}".into())); - Ok(snippets.into()) + Ok(snippets) } Self::File(_) => Err(anyhow::anyhow!( "cannot convert ResolvedPattern::File to ResolvedSnippet" @@ -80,7 +79,7 @@ impl<'a> GritResolvedPattern<'a> { "cannot convert ResolvedPattern::Files to ResolvedSnippet" )), Self::Constant(constant) => { - Ok(vector![ResolvedSnippet::Text(constant.to_string().into())]) + Ok(vec![ResolvedSnippet::Text(constant.to_string().into())]) } } } @@ -88,7 +87,7 @@ impl<'a> GritResolvedPattern<'a> { impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern<'a> { fn from_binding(binding: GritBinding<'a>) -> Self { - Self::Binding(vector![binding]) + Self::Binding(vec![binding]) } fn from_constant(constant: Constant) -> Self { @@ -108,11 +107,11 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern<'a> { } fn from_string(string: String) -> Self { - Self::Snippets(vector![ResolvedSnippet::Text(string.into())]) + Self::Snippets(vec![ResolvedSnippet::Text(string.into())]) } fn from_resolved_snippet(snippet: ResolvedSnippet<'a, GritQueryContext>) -> Self { - Self::Snippets(vector![snippet]) + Self::Snippets(vec![snippet]) } fn from_dynamic_snippet( @@ -145,7 +144,7 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern<'a> { } } } - Ok(Self::Snippets(parts.into())) + Ok(Self::Snippets(parts)) } fn from_dynamic_pattern( @@ -176,7 +175,7 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern<'a> { for element in &list.elements { elements.push(Self::from_dynamic_pattern(element, state, context, logs)?); } - Ok(Self::List(elements.into())) + Ok(Self::List(elements)) } DynamicPattern::Snippet(snippet) => { Self::from_dynamic_snippet(snippet, state, context, logs) @@ -234,7 +233,7 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern<'a> { Pattern::CallBuiltIn(built_in) => built_in.call(state, context, logs), Pattern::CallFunction(func) => func.call(state, context, logs), Pattern::CallForeignFunction(_) => unimplemented!(), - Pattern::StringConstant(string) => Ok(Self::Snippets(vector![ResolvedSnippet::Text( + Pattern::StringConstant(string) => Ok(Self::Snippets(vec![ResolvedSnippet::Text( (&string.text).into(), )])), Pattern::IntConstant(int) => Ok(Self::Constant(Constant::Integer(int.value))), @@ -256,7 +255,7 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern<'a> { .patterns .iter() .map(|pattern| Self::from_pattern(pattern, state, context, logs)) - .collect::<Result<Vector<_>>>() + .collect::<Result<Vec<_>>>() .map(Self::List), Pattern::ListIndex(index) => Self::from_list_index(index, state, context, logs), Pattern::Map(map) => map @@ -331,7 +330,7 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern<'a> { fn extend( &mut self, _with: Self, - _effects: &mut Vector<Effect<'a, GritQueryContext>>, + _effects: &mut im::Vector<Effect<'a, GritQueryContext>>, _language: &<GritQueryContext as QueryContext>::Language<'a>, ) -> anyhow::Result<()> { bail!("Not implemented") // TODO: Implement rewriting @@ -558,7 +557,7 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern<'a> { bail!("can only push to bindings"); }; - bindings.push_back(binding); + bindings.push(binding); Ok(()) } diff --git a/crates/biome_grit_patterns/src/grit_target_language.rs b/crates/biome_grit_patterns/src/grit_target_language.rs index 00e89cfa9761..371f630d9db3 100644 --- a/crates/biome_grit_patterns/src/grit_target_language.rs +++ b/crates/biome_grit_patterns/src/grit_target_language.rs @@ -187,19 +187,32 @@ impl GritTargetLanguage { pub fn parse_snippet_contexts(&self, source: &str) -> Vec<SnippetTree<GritTargetTree>> { let source = self.substitute_metavariable_prefix(source); - self.snippet_context_strings() - .iter() - .map(|(pre, post)| self.get_parser().parse_snippet(pre, &source, post)) - .filter(|result| { - result - .tree + + let mut snippet_trees: Vec<SnippetTree<GritTargetTree>> = Vec::new(); + for (pre, post) in self.snippet_context_strings() { + let parse_result = self.get_parser().parse_snippet(pre, &source, post); + + let has_errors = parse_result + .tree + .root_node() + .descendants() + .map_or(false, |mut descendants| { + descendants.any(|descendant| descendant.kind().is_bogus()) + }); + if has_errors { + continue; + } + + if !snippet_trees.iter().any(|tree| { + tree.tree .root_node() - .descendants() - .map_or(false, |mut descendants| { - !descendants.any(|descendant| descendant.kind().is_bogus()) - }) - }) - .collect() + .matches_kinds_recursively_with(&parse_result.tree.root_node()) + }) { + snippet_trees.push(parse_result); + } + } + + snippet_trees } } diff --git a/crates/biome_grit_patterns/src/grit_target_node.rs b/crates/biome_grit_patterns/src/grit_target_node.rs index 70c051860633..fbbc60b26b3e 100644 --- a/crates/biome_grit_patterns/src/grit_target_node.rs +++ b/crates/biome_grit_patterns/src/grit_target_node.rs @@ -261,6 +261,52 @@ impl<'a> GritTargetNode<'a> { let trimmed_range = self.text_trimmed_range(); &self.source()[trimmed_range.start().into()..trimmed_range.end().into()] } + + /// Matches the `kind` of this node, and those of all its children, with + /// those of another node. + /// + /// This is a relatively cheap way to discover whether two parsed snippets + /// are identical when they were parsed from the same source string, but + /// with different contexts. In that use case, we already know the snippet + /// is essentially the same, but we only detect a meaningful difference in + /// context by looking for a variance in node kinds. + pub fn matches_kinds_recursively_with(&self, other: &Self) -> bool { + let mut cursor_a = self.walk(); + let mut cursor_b = other.walk(); + + // Are we navigating back up? If so, we shouldn't try to visit any + // children until we've visited another sibling, or we'd run in circles. + let mut up = false; + + loop { + if cursor_a.node().kind() != cursor_b.node().kind() { + break false; + } + + if !up && cursor_a.goto_first_child() { + if !cursor_b.goto_first_child() { + break false; + } + } else if cursor_a.goto_next_sibling() { + if cursor_b.goto_first_child() || !cursor_b.goto_next_sibling() { + break false; + } + + up = false; + } else if cursor_a.goto_parent() { + if (!up && cursor_b.goto_first_child()) + || cursor_b.goto_next_sibling() + || !cursor_b.goto_parent() + { + break false; + } + + up = true; + } else { + break true; + } + } + } } impl<'a> Debug for GritTargetNode<'a> { diff --git a/crates/biome_js_analyze/src/assists/source/sort_jsx_props.rs b/crates/biome_js_analyze/src/assists/source/sort_jsx_props.rs index 7687b359d79c..8bae5121ef0c 100644 --- a/crates/biome_js_analyze/src/assists/source/sort_jsx_props.rs +++ b/crates/biome_js_analyze/src/assists/source/sort_jsx_props.rs @@ -52,7 +52,7 @@ declare_source_rule! { impl Rule for SortJsxProps { type Query = Ast<JsxAttributeList>; type State = PropGroup; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { @@ -72,7 +72,7 @@ impl Rule for SortJsxProps { } } prop_groups.push(current_prop_group); - prop_groups + prop_groups.into_boxed_slice() } fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> { diff --git a/crates/biome_js_analyze/src/lint/a11y/no_label_without_control.rs b/crates/biome_js_analyze/src/lint/a11y/no_label_without_control.rs index 39fe4c431c8d..76470398ec3d 100644 --- a/crates/biome_js_analyze/src/lint/a11y/no_label_without_control.rs +++ b/crates/biome_js_analyze/src/lint/a11y/no_label_without_control.rs @@ -155,11 +155,11 @@ impl Rule for NoLabelWithoutControl { #[serde(rename_all = "camelCase", deny_unknown_fields, default)] pub struct NoLabelWithoutControlOptions { /// Array of component names that should be considered the same as an `input` element. - pub input_components: Vec<String>, + pub input_components: Box<[Box<str>]>, /// Array of attributes that should be treated as the `label` accessible text content. - pub label_attributes: Vec<String>, + pub label_attributes: Box<[Box<str>]>, /// Array of component names that should be considered the same as a `label` element. - pub label_components: Vec<String>, + pub label_components: Box<[Box<str>]>, } impl NoLabelWithoutControlOptions { @@ -175,7 +175,7 @@ impl NoLabelWithoutControlOptions { && !self .label_attributes .iter() - .any(|name| name == attribute_name) + .any(|name| name.as_ref() == attribute_name) { return false; } @@ -245,7 +245,7 @@ impl NoLabelWithoutControlOptions { || self .input_components .iter() - .any(|name| name == element_name) + .any(|name| name.as_ref() == element_name) { return true; } @@ -260,7 +260,7 @@ impl NoLabelWithoutControlOptions { fn has_element_name(&self, element_name: &str) -> bool { self.label_components .iter() - .any(|label_component_name| label_component_name == element_name) + .any(|label_component_name| label_component_name.as_ref() == element_name) } } diff --git a/crates/biome_js_analyze/src/lint/a11y/use_aria_props_for_role.rs b/crates/biome_js_analyze/src/lint/a11y/use_aria_props_for_role.rs index a2380c1493c4..779f6bd3463d 100644 --- a/crates/biome_js_analyze/src/lint/a11y/use_aria_props_for_role.rs +++ b/crates/biome_js_analyze/src/lint/a11y/use_aria_props_for_role.rs @@ -49,7 +49,7 @@ declare_lint_rule! { #[derive(Default, Debug)] pub struct UseAriaPropsForRoleState { - missing_aria_props: Vec<String>, + missing_aria_props: Box<[String]>, attribute: Option<(JsxAttribute, String)>, } @@ -121,7 +121,7 @@ impl Rule for UseAriaPropsForRole { if !missing_aria_props.is_empty() { return Some(UseAriaPropsForRoleState { attribute: Some((role_attribute, name.text().to_string())), - missing_aria_props, + missing_aria_props: missing_aria_props.into_boxed_slice(), }); } } diff --git a/crates/biome_js_analyze/src/lint/a11y/use_valid_aria_props.rs b/crates/biome_js_analyze/src/lint/a11y/use_valid_aria_props.rs index 42632c7ecad1..6015d05773e8 100644 --- a/crates/biome_js_analyze/src/lint/a11y/use_valid_aria_props.rs +++ b/crates/biome_js_analyze/src/lint/a11y/use_valid_aria_props.rs @@ -37,7 +37,7 @@ declare_lint_rule! { impl Rule for UseValidAriaProps { type Query = Aria<AnyJsxElement>; type State = JsxAttribute; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { @@ -64,11 +64,11 @@ impl Rule for UseValidAriaProps { } }) .collect(); - attributes } else { - vec![] + Vec::new() } + .into_boxed_slice() } fn diagnostic(ctx: &RuleContext<Self>, attribute: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_js_analyze/src/lint/a11y/use_valid_aria_role.rs b/crates/biome_js_analyze/src/lint/a11y/use_valid_aria_role.rs index 62ac2e2d1403..0db71e14cb57 100644 --- a/crates/biome_js_analyze/src/lint/a11y/use_valid_aria_role.rs +++ b/crates/biome_js_analyze/src/lint/a11y/use_valid_aria_role.rs @@ -78,7 +78,7 @@ declare_lint_rule! { #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields, default)] pub struct ValidAriaRoleOptions { - pub allow_invalid_roles: Vec<String>, + pub allow_invalid_roles: Box<[Box<str>]>, pub ignore_non_dom: bool, } @@ -107,7 +107,10 @@ impl Rule for UseValidAriaRole { let is_valid = role_attribute_value.all(|val| { let role_data = aria_roles.get_role(val); - allowed_invalid_roles.contains(&val.to_string()) || role_data.is_some() + allowed_invalid_roles + .iter() + .any(|role| role.as_ref() == val) + || role_data.is_some() }); if is_valid { diff --git a/crates/biome_js_analyze/src/lint/complexity/no_useless_fragments.rs b/crates/biome_js_analyze/src/lint/complexity/no_useless_fragments.rs index a846f71944b5..21479cd1b84d 100644 --- a/crates/biome_js_analyze/src/lint/complexity/no_useless_fragments.rs +++ b/crates/biome_js_analyze/src/lint/complexity/no_useless_fragments.rs @@ -11,7 +11,7 @@ use biome_js_factory::make::{ use biome_js_syntax::{ AnyJsxChild, AnyJsxElementName, AnyJsxTag, JsLanguage, JsLogicalExpression, JsParenthesizedExpression, JsSyntaxKind, JsxChildList, JsxElement, JsxExpressionAttributeValue, - JsxFragment, JsxTagExpression, JsxText, T, + JsxExpressionChild, JsxFragment, JsxTagExpression, JsxText, T, }; use biome_rowan::{declare_node_union, AstNode, AstNodeList, BatchMutation, BatchMutationExt}; @@ -124,6 +124,7 @@ impl Rule for NoUselessFragments { let model = ctx.model(); let mut in_jsx_attr_expr = false; let mut in_js_logical_expr = false; + let mut in_jsx_expr = false; match node { NoUselessFragmentsQuery::JsxFragment(fragment) => { let parents_where_fragments_must_be_preserved = node.syntax().parent().map_or( @@ -139,6 +140,9 @@ impl Rule for NoUselessFragments { if JsLogicalExpression::can_cast(parent.kind()) { in_js_logical_expr = true; } + if JsxExpressionChild::can_cast(parent.kind()) { + in_jsx_expr = true; + } match JsParenthesizedExpression::try_cast(parent) { Ok(parenthesized_expression) => { parenthesized_expression.syntax().parent() @@ -190,7 +194,19 @@ impl Rule for NoUselessFragments { } } JsSyntaxKind::JSX_TEXT => { - if !child.syntax().text().to_string().trim().is_empty() { + // We need to whitespaces and newlines from the original string. + // Since in the JSX newlines aren't trivia, we require to allocate a string to trim from those characters. + let original_text = child.text(); + let child_text = original_text.trim(); + + if (in_jsx_expr || in_js_logical_expr) + && contains_html_character_references(child_text) + { + children_where_fragments_must_preserved = true; + break; + } + + if !child_text.is_empty() { significant_children += 1; if first_significant_child.is_none() { first_significant_child = Some(child); @@ -401,3 +417,9 @@ impl Rule for NoUselessFragments { })) } } + +fn contains_html_character_references(s: &str) -> bool { + let and = s.find('&'); + let semi = s.find(';'); + matches!((and, semi), (Some(and), Some(semi)) if and < semi) +} diff --git a/crates/biome_js_analyze/src/lint/complexity/no_useless_switch_case.rs b/crates/biome_js_analyze/src/lint/complexity/no_useless_switch_case.rs index 7230b79aacce..34a502fb6526 100644 --- a/crates/biome_js_analyze/src/lint/complexity/no_useless_switch_case.rs +++ b/crates/biome_js_analyze/src/lint/complexity/no_useless_switch_case.rs @@ -71,7 +71,7 @@ declare_lint_rule! { impl Rule for NoUselessSwitchCase { type Query = Ast<JsDefaultClause>; type State = JsCaseClause; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { @@ -107,10 +107,11 @@ impl Rule for NoUselessSwitchCase { .filter_map(JsCaseClause::cast) .find(|case| !case.consequent().is_empty()), ) - .collect() + .collect::<Vec<_>>() } else { - it.collect() + it.collect::<Vec<_>>() } + .into_boxed_slice() } fn diagnostic(ctx: &RuleContext<Self>, useless_case: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs b/crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs index 94b9df8df5c8..35ee5df4390e 100644 --- a/crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs +++ b/crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs @@ -60,8 +60,8 @@ declare_lint_rule! { impl Rule for NoUselessUndefinedInitialization { type Query = Ast<JsVariableStatement>; - type State = (String, TextRange); - type Signals = Vec<Self::State>; + type State = (Box<str>, TextRange); + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { @@ -69,13 +69,13 @@ impl Rule for NoUselessUndefinedInitialization { let mut signals = vec![]; let Ok(node) = statement.declaration() else { - return signals; + return signals.into_boxed_slice(); }; let let_or_var_kind = node.is_let() || node.is_var(); if !let_or_var_kind { - return signals; + return signals.into_boxed_slice(); } for declarator in node.declarators() { @@ -98,11 +98,11 @@ impl Rule for NoUselessUndefinedInitialization { let Some(binding_name) = decl.id().ok().map(|id| id.text()) else { continue; }; - signals.push((binding_name, decl_range)); + signals.push((binding_name.into(), decl_range)); } } - signals + signals.into_boxed_slice() } fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { @@ -110,7 +110,7 @@ impl Rule for NoUselessUndefinedInitialization { rule_category!(), state.1, markup! { - "It's not necessary to initialize "<Emphasis>{state.0}</Emphasis>" to undefined." + "It's not necessary to initialize "<Emphasis>{state.0.as_ref()}</Emphasis>" to undefined." }).note("A variable that is declared and not initialized to any value automatically gets the value of undefined.") ) } @@ -126,7 +126,17 @@ impl Rule for NoUselessUndefinedInitialization { .clone() .into_iter() .filter_map(|declarator| declarator.ok()) - .find(|decl| decl.id().is_ok_and(|id| id.text() == state.0))?; + .find(|decl| { + decl.id() + .ok() + .and_then(|id| { + id.as_any_js_binding()? + .as_js_identifier_binding()? + .name_token() + .ok() + }) + .is_some_and(|id| id.text_trimmed() == state.0.as_ref()) + })?; let current_initializer = current_declaration.initializer()?; diff --git a/crates/biome_js_analyze/src/lint/complexity/use_simple_number_keys.rs b/crates/biome_js_analyze/src/lint/complexity/use_simple_number_keys.rs index afff5011e0c0..569301a21598 100644 --- a/crates/biome_js_analyze/src/lint/complexity/use_simple_number_keys.rs +++ b/crates/biome_js_analyze/src/lint/complexity/use_simple_number_keys.rs @@ -57,28 +57,28 @@ declare_lint_rule! { pub enum NumberLiteral { Binary { node: JsLiteralMemberName, - value: String, + value: Box<str>, big_int: bool, }, Decimal { node: JsLiteralMemberName, - value: String, + value: Box<str>, big_int: bool, underscore: bool, }, Octal { node: JsLiteralMemberName, - value: String, + value: Box<str>, big_int: bool, }, Hexadecimal { node: JsLiteralMemberName, - value: String, + value: Box<str>, big_int: bool, }, FloatingPoint { node: JsLiteralMemberName, - value: String, + value: Box<str>, exponent: bool, underscore: bool, }, @@ -149,7 +149,7 @@ impl TryFrom<AnyJsObjectMember> for NumberLiteral { if contains_dot { return Ok(Self::FloatingPoint { node: literal_member_name, - value, + value: value.into_boxed_str(), exponent, underscore, }); @@ -157,7 +157,7 @@ impl TryFrom<AnyJsObjectMember> for NumberLiteral { if !is_first_char_zero { return Ok(Self::Decimal { node: literal_member_name, - value, + value: value.into_boxed_str(), big_int, underscore, }); @@ -167,21 +167,21 @@ impl TryFrom<AnyJsObjectMember> for NumberLiteral { Some(b'b' | b'B') => { return Ok(Self::Binary { node: literal_member_name, - value, + value: value.into_boxed_str(), big_int, }) } Some(b'o' | b'O') => { return Ok(Self::Octal { node: literal_member_name, - value, + value: value.into_boxed_str(), big_int, }) } Some(b'x' | b'X') => { return Ok(Self::Hexadecimal { node: literal_member_name, - value, + value: value.into_boxed_str(), big_int, }) } @@ -191,14 +191,14 @@ impl TryFrom<AnyJsObjectMember> for NumberLiteral { if largest_digit < b'8' { return Ok(Self::Octal { node: literal_member_name, - value, + value: value.into_boxed_str(), big_int, }); } Ok(Self::Decimal { node: literal_member_name, - value, + value: value.into_boxed_str(), big_int, underscore, }) @@ -229,13 +229,13 @@ impl NumberLiteral { } } - fn value(&self) -> &String { + fn value(&self) -> &str { match self { - Self::Decimal { value, .. } => value, - Self::Binary { value, .. } => value, - Self::FloatingPoint { value, .. } => value, - Self::Octal { value, .. } => value, - Self::Hexadecimal { value, .. } => value, + Self::Decimal { value, .. } => value.as_ref(), + Self::Binary { value, .. } => value.as_ref(), + Self::FloatingPoint { value, .. } => value.as_ref(), + Self::Octal { value, .. } => value.as_ref(), + Self::Hexadecimal { value, .. } => value.as_ref(), } } } @@ -267,13 +267,12 @@ pub struct RuleState(WrongNumberLiteralName, NumberLiteral); impl Rule for UseSimpleNumberKeys { type Query = Ast<JsObjectExpression>; type State = RuleState; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { - let mut signals: Self::Signals = Vec::new(); + let mut result = Vec::new(); let node = ctx.query(); - for number_literal in node .members() .into_iter() @@ -282,32 +281,31 @@ impl Rule for UseSimpleNumberKeys { { match number_literal { NumberLiteral::Decimal { big_int: true, .. } => { - signals.push(RuleState(WrongNumberLiteralName::BigInt, number_literal)) + result.push(RuleState(WrongNumberLiteralName::BigInt, number_literal)) } NumberLiteral::FloatingPoint { underscore: true, .. } | NumberLiteral::Decimal { underscore: true, .. - } => signals.push(RuleState( + } => result.push(RuleState( WrongNumberLiteralName::WithUnderscore, number_literal, )), NumberLiteral::Binary { .. } => { - signals.push(RuleState(WrongNumberLiteralName::Binary, number_literal)) + result.push(RuleState(WrongNumberLiteralName::Binary, number_literal)) } - NumberLiteral::Hexadecimal { .. } => signals.push(RuleState( + NumberLiteral::Hexadecimal { .. } => result.push(RuleState( WrongNumberLiteralName::Hexadecimal, number_literal, )), NumberLiteral::Octal { .. } => { - signals.push(RuleState(WrongNumberLiteralName::Octal, number_literal)) + result.push(RuleState(WrongNumberLiteralName::Octal, number_literal)) } _ => (), } } - - signals + result.into_boxed_slice() } fn diagnostic( diff --git a/crates/biome_js_analyze/src/lint/correctness/no_empty_character_class_in_regex.rs b/crates/biome_js_analyze/src/lint/correctness/no_empty_character_class_in_regex.rs index 6744b85a9be7..63317a26c4d0 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_empty_character_class_in_regex.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_empty_character_class_in_regex.rs @@ -52,14 +52,14 @@ declare_lint_rule! { impl Rule for NoEmptyCharacterClassInRegex { type Query = Ast<JsRegexLiteralExpression>; type State = Range<usize>; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { let mut empty_classes = vec![]; let regex = ctx.query(); let Ok((pattern, flags)) = regex.decompose() else { - return empty_classes; + return empty_classes.into_boxed_slice(); }; let has_v_flag = flags.text().contains('v'); let trimmed_text = pattern.text(); @@ -95,7 +95,7 @@ impl Rule for NoEmptyCharacterClassInRegex { _ => {} } } - empty_classes + empty_classes.into_boxed_slice() } fn diagnostic( diff --git a/crates/biome_js_analyze/src/lint/correctness/no_invalid_use_before_declaration.rs b/crates/biome_js_analyze/src/lint/correctness/no_invalid_use_before_declaration.rs index 27d92e6379c5..b884ea27d0ab 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_invalid_use_before_declaration.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_invalid_use_before_declaration.rs @@ -72,7 +72,7 @@ declare_lint_rule! { impl Rule for NoInvalidUseBeforeDeclaration { type Query = SemanticServices; type State = InvalidUseBeforeDeclaration; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { @@ -156,7 +156,7 @@ impl Rule for NoInvalidUseBeforeDeclaration { } } } - result + result.into_boxed_slice() } fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_js_analyze/src/lint/correctness/no_nonoctal_decimal_escape.rs b/crates/biome_js_analyze/src/lint/correctness/no_nonoctal_decimal_escape.rs index 248b44983a1b..10918b2ff100 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_nonoctal_decimal_escape.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_nonoctal_decimal_escape.rs @@ -71,26 +71,26 @@ pub enum FixSuggestionKind { pub struct RuleState { kind: FixSuggestionKind, diagnostics_text_range: TextRange, - replace_from: String, - replace_to: String, + replace_from: Box<str>, + replace_to: Box<str>, replace_string_range: Range<usize>, } impl Rule for NoNonoctalDecimalEscape { type Query = Ast<JsStringLiteralExpression>; type State = RuleState; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { let node = ctx.query(); - let mut signals: Self::Signals = Vec::new(); + let mut result = Vec::new(); let Some(token) = node.value_token().ok() else { - return signals; + return result.into_boxed_slice(); }; let text = token.text(); if !is_octal_escape_sequence(text) { - return signals; + return result.into_boxed_slice(); } let matches = lex_escape_sequences(text); @@ -133,11 +133,11 @@ impl Rule for NoNonoctalDecimalEscape { previous_escape_range_start..*decimal_escape_string_end; // \0\8 -> \u00008 - signals.push(RuleState { + result.push(RuleState { kind: FixSuggestionKind::Refactor, diagnostics_text_range: unicode_escape_text_range, - replace_from: format!("{previous_escape}{decimal_escape}"), - replace_to: format!("{unicode_escape}{decimal_char}"), + replace_from: format!("{previous_escape}{decimal_escape}").into(), + replace_to: format!("{unicode_escape}{decimal_char}").into(), replace_string_range, }); } @@ -147,20 +147,20 @@ impl Rule for NoNonoctalDecimalEscape { continue; }; // \8 -> \u0038 - signals.push(RuleState { + result.push(RuleState { kind: FixSuggestionKind::Refactor, diagnostics_text_range: decimal_escape_range, - replace_from: decimal_escape.to_string(), - replace_to: decimal_char_unicode_escaped, + replace_from: decimal_escape.clone().into_boxed_str(), + replace_to: decimal_char_unicode_escaped.into_boxed_str(), replace_string_range, }); } else { // \8 -> 8 - signals.push(RuleState { + result.push(RuleState { kind: FixSuggestionKind::Refactor, diagnostics_text_range: decimal_escape_range, - replace_from: decimal_escape.to_string(), - replace_to: decimal_char.to_string(), + replace_from: decimal_escape.clone().into_boxed_str(), + replace_to: decimal_char.to_string().into_boxed_str(), replace_string_range, }) } @@ -168,8 +168,8 @@ impl Rule for NoNonoctalDecimalEscape { } let mut seen = FxHashSet::default(); - signals.retain(|rule_state| seen.insert(rule_state.diagnostics_text_range)); - signals + result.retain(|rule_state| seen.insert(rule_state.diagnostics_text_range)); + result.into_boxed_slice() } fn diagnostic( @@ -220,7 +220,7 @@ impl Rule for NoNonoctalDecimalEscape { ctx.metadata().applicability(), match kind { FixSuggestionKind::Refactor => { - markup! ("Replace "<Emphasis>{replace_from}</Emphasis>" with "<Emphasis>{replace_to}</Emphasis>". This maintains the current functionality.").to_owned() + markup! ("Replace "<Emphasis>{replace_from.as_ref()}</Emphasis>" with "<Emphasis>{replace_to.as_ref()}</Emphasis>". This maintains the current functionality.").to_owned() } }, mutation, diff --git a/crates/biome_js_analyze/src/lint/correctness/no_self_assign.rs b/crates/biome_js_analyze/src/lint/correctness/no_self_assign.rs index a4d4f45865a3..c8e060e8f9d4 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_self_assign.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_self_assign.rs @@ -78,7 +78,7 @@ declare_lint_rule! { impl Rule for NoSelfAssign { type Query = Ast<JsAssignmentExpression>; type State = IdentifiersLike; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { @@ -86,8 +86,7 @@ impl Rule for NoSelfAssign { let left = node.left().ok(); let right = node.right().ok(); let operator = node.operator().ok(); - - let mut state = vec![]; + let mut result = vec![]; if let Some(operator) = operator { if matches!( operator, @@ -98,12 +97,12 @@ impl Rule for NoSelfAssign { ) { if let (Some(left), Some(right)) = (left, right) { if let Ok(pair) = AnyAssignmentLike::try_from((left, right)) { - compare_assignment_like(pair, &mut state); + compare_assignment_like(pair, &mut result); } } } } - state + result.into_boxed_slice() } fn diagnostic(_: &RuleContext<Self>, identifier_like: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_js_analyze/src/lint/correctness/no_string_case_mismatch.rs b/crates/biome_js_analyze/src/lint/correctness/no_string_case_mismatch.rs index 3efdd136e8b7..32ed618ad9bd 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_string_case_mismatch.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_string_case_mismatch.rs @@ -48,10 +48,10 @@ declare_lint_rule! { impl Rule for NoStringCaseMismatch { type Query = Ast<QueryCandidate>; type State = CaseMismatchInfo; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); - fn run(ctx: &RuleContext<Self>) -> Vec<Self::State> { + fn run(ctx: &RuleContext<Self>) -> Self::Signals { let query = ctx.query(); match query { QueryCandidate::JsBinaryExpression(expr) => CaseMismatchInfo::from_binary_expr(expr) @@ -59,6 +59,7 @@ impl Rule for NoStringCaseMismatch { .collect(), QueryCandidate::JsSwitchStatement(stmt) => CaseMismatchInfo::from_switch_stmt(stmt), } + .into_boxed_slice() } fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_js_analyze/src/lint/correctness/no_switch_declarations.rs b/crates/biome_js_analyze/src/lint/correctness/no_switch_declarations.rs index 942e0a6f7fa4..3470d4a7b063 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_switch_declarations.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_switch_declarations.rs @@ -83,7 +83,7 @@ declare_lint_rule! { impl Rule for NoSwitchDeclarations { type Query = Ast<AnyJsSwitchClause>; type State = TextRange; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { @@ -101,7 +101,8 @@ impl Rule for NoSwitchDeclarations { None } }) - .collect() + .collect::<Vec<_>>() + .into_boxed_slice() } fn diagnostic(ctx: &RuleContext<Self>, decl_range: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs b/crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs index b70a1776502b..2714e9907ad8 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs @@ -41,8 +41,8 @@ declare_lint_rule! { impl Rule for NoUndeclaredVariables { type Query = SemanticServices; - type State = (TextRange, String); - type Signals = Vec<Self::State>; + type State = (TextRange, Box<str>); + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { @@ -88,10 +88,11 @@ impl Rule for NoUndeclaredVariables { } let span = token.text_trimmed_range(); - let text = text.to_string(); + let text = text.to_string().into_boxed_str(); Some((span, text)) }) - .collect() + .collect::<Vec<_>>() + .into_boxed_slice() } fn diagnostic(_ctx: &RuleContext<Self>, (span, name): &Self::State) -> Option<RuleDiagnostic> { @@ -99,7 +100,7 @@ impl Rule for NoUndeclaredVariables { rule_category!(), *span, markup! { - "The "<Emphasis>{name}</Emphasis>" variable is undeclared." + "The "<Emphasis>{name.as_ref()}</Emphasis>" variable is undeclared." }, ).note(markup! { "By default, Biome recognizes browser and Node.js globals.\nYou can ignore more globals using the "<Hyperlink href="https://biomejs.dev/reference/configuration/#javascriptglobals">"javascript.globals"</Hyperlink>" configuration." diff --git a/crates/biome_js_analyze/src/lint/correctness/no_unreachable.rs b/crates/biome_js_analyze/src/lint/correctness/no_unreachable.rs index b78f4cd149dc..dd5acfdb6705 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_unreachable.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_unreachable.rs @@ -92,7 +92,7 @@ impl Rule for NoUnreachable { // Pluralize and adapt the error message accordingly based on the // number and position of secondary labels match state.terminators.as_slice() { - // The CFG didn't contain enough informations to determine a cause + // The CFG didn't contain enough information to determine a cause // for this range being unreachable [] => {} // A single node is responsible for this range being unreachable @@ -137,7 +137,7 @@ impl Rule for NoUnreachable { } // The range has three or more dominating terminator instructions terminators => { - // SAFETY: This substraction is safe since the match expression + // SAFETY: This subtraction is safe since the match expression // ensures the slice has at least 3 elements let last = terminators.len() - 1; @@ -206,7 +206,7 @@ impl Rule for NoUnreachable { const COMPLEXITY_THRESHOLD: u32 = 20; /// Returns true if the "complexity score" for the [JsControlFlowGraph] is higher -/// than [COMPLEXITY_THRESHOLD]. This score is an arbritrary value (the formula +/// than [COMPLEXITY_THRESHOLD]. This score is an arbitrary value (the formula /// is similar to the cyclomatic complexity of the function but this is only /// approximative) used to determine whether the NoDeadCode rule should perform /// a fine reachability analysis or fall back to a simpler algorithm to avoid @@ -656,7 +656,7 @@ impl UnreachableRanges { if let Some(terminator) = terminator { // Terminator labels are also stored in ascending order to - // faciliate the generation of labels when the diagnostic + // facilitate the generation of labels when the diagnostic // gets emitted let terminator_insertion = entry .terminators diff --git a/crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs b/crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs index 1f3c722d24dd..618ad074f2ad 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs @@ -77,18 +77,18 @@ declare_node_union! { impl Rule for NoUnusedPrivateClassMembers { type Query = Ast<JsClassDeclaration>; type State = AnyMember; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { let node = ctx.query(); let private_members: FxHashSet<AnyMember> = get_all_declared_private_members(node); - if private_members.is_empty() { - return vec![]; + Vec::new() + } else { + traverse_members_usage(node.syntax(), private_members) } - - traverse_members_usage(node.syntax(), private_members) + .into_boxed_slice() } fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_js_analyze/src/lint/correctness/no_void_type_return.rs b/crates/biome_js_analyze/src/lint/correctness/no_void_type_return.rs index 10cabae79b5b..1e32d40a6d89 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_void_type_return.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_void_type_return.rs @@ -2,9 +2,10 @@ use biome_analyze::context::RuleContext; use biome_analyze::{declare_lint_rule, Ast, Rule, RuleDiagnostic}; use biome_console::markup; use biome_js_syntax::{ - AnyTsReturnType, JsArrowFunctionExpression, JsFunctionDeclaration, + AnyJsExpression, AnyTsReturnType, JsArrowFunctionExpression, JsFunctionDeclaration, JsFunctionExportDefaultDeclaration, JsFunctionExpression, JsGetterClassMember, JsGetterObjectMember, JsMethodClassMember, JsMethodObjectMember, JsReturnStatement, + JsSyntaxKind, }; use biome_rowan::{declare_node_union, AstNode}; @@ -92,47 +93,28 @@ declare_lint_rule! { } } -declare_node_union! { - pub JsFunctionMethod = JsArrowFunctionExpression | JsFunctionDeclaration | JsFunctionExportDefaultDeclaration | JsFunctionExpression | JsGetterClassMember | JsGetterObjectMember | JsMethodClassMember | JsMethodObjectMember -} - -pub(crate) fn return_type(func: &JsFunctionMethod) -> Option<AnyTsReturnType> { - match func { - JsFunctionMethod::JsArrowFunctionExpression(func) => { - func.return_type_annotation()?.ty().ok() - } - JsFunctionMethod::JsFunctionDeclaration(func) => func.return_type_annotation()?.ty().ok(), - JsFunctionMethod::JsFunctionExportDefaultDeclaration(func) => { - func.return_type_annotation()?.ty().ok() - } - JsFunctionMethod::JsFunctionExpression(func) => func.return_type_annotation()?.ty().ok(), - JsFunctionMethod::JsGetterClassMember(func) => { - Some(AnyTsReturnType::AnyTsType(func.return_type()?.ty().ok()?)) - } - JsFunctionMethod::JsGetterObjectMember(func) => { - Some(AnyTsReturnType::AnyTsType(func.return_type()?.ty().ok()?)) - } - JsFunctionMethod::JsMethodClassMember(func) => func.return_type_annotation()?.ty().ok(), - JsFunctionMethod::JsMethodObjectMember(func) => func.return_type_annotation()?.ty().ok(), - } -} - impl Rule for NoVoidTypeReturn { type Query = Ast<JsReturnStatement>; - type State = JsFunctionMethod; + type State = AnyJsFunctionMethodWithReturnType; type Signals = Option<Self::State>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { let ret = ctx.query(); - // Do not take arg-less returns into account - let _arg = ret.argument()?; + // Ignore arg-less returns such as `return;` + let arg = ret.argument()?; + if let AnyJsExpression::JsUnaryExpression(expr) = arg { + if expr.operator_token().ok()?.kind() == JsSyntaxKind::VOID_KW { + // Ignore `return void <foo>;` + return None; + } + } let func = ret .syntax() .ancestors() .find(|x| AnyJsControlFlowRoot::can_cast(x.kind())) - .and_then(JsFunctionMethod::cast)?; - let ret_type = return_type(&func)?; + .and_then(AnyJsFunctionMethodWithReturnType::cast)?; + let ret_type = func.return_type()?; ret_type.as_any_ts_type()?.as_ts_void_type().and(Some(func)) } @@ -149,3 +131,28 @@ impl Rule for NoVoidTypeReturn { )) } } + +declare_node_union! { + pub AnyJsFunctionMethodWithReturnType = JsArrowFunctionExpression | JsFunctionDeclaration | JsFunctionExportDefaultDeclaration | JsFunctionExpression | JsGetterClassMember | JsGetterObjectMember | JsMethodClassMember | JsMethodObjectMember +} + +impl AnyJsFunctionMethodWithReturnType { + pub fn return_type(&self) -> Option<AnyTsReturnType> { + match self { + Self::JsArrowFunctionExpression(func) => func.return_type_annotation()?.ty().ok(), + Self::JsFunctionDeclaration(func) => func.return_type_annotation()?.ty().ok(), + Self::JsFunctionExportDefaultDeclaration(func) => { + func.return_type_annotation()?.ty().ok() + } + Self::JsFunctionExpression(func) => func.return_type_annotation()?.ty().ok(), + Self::JsGetterClassMember(func) => { + Some(AnyTsReturnType::AnyTsType(func.return_type()?.ty().ok()?)) + } + Self::JsGetterObjectMember(func) => { + Some(AnyTsReturnType::AnyTsType(func.return_type()?.ty().ok()?)) + } + Self::JsMethodClassMember(func) => func.return_type_annotation()?.ty().ok(), + Self::JsMethodObjectMember(func) => func.return_type_annotation()?.ty().ok(), + } + } +} diff --git a/crates/biome_js_analyze/src/lint/correctness/use_exhaustive_dependencies.rs b/crates/biome_js_analyze/src/lint/correctness/use_exhaustive_dependencies.rs index b2b75f0b6968..5daf84f2bf8e 100644 --- a/crates/biome_js_analyze/src/lint/correctness/use_exhaustive_dependencies.rs +++ b/crates/biome_js_analyze/src/lint/correctness/use_exhaustive_dependencies.rs @@ -304,7 +304,7 @@ pub struct UseExhaustiveDependenciesOptions { /// List of hooks of which the dependencies should be validated. #[serde(default)] #[deserializable(validate = "non_empty")] - pub hooks: Vec<Hook>, + pub hooks: Box<[Hook]>, } impl Default for UseExhaustiveDependenciesOptions { @@ -312,7 +312,7 @@ impl Default for UseExhaustiveDependenciesOptions { Self { report_unnecessary_dependencies: report_unnecessary_dependencies_default(), report_missing_dependencies_array: false, - hooks: vec![], + hooks: Vec::new().into_boxed_slice(), } } } @@ -417,18 +417,18 @@ pub enum Fix { /// When a dependency needs to be added. AddDependency { function_name_range: TextRange, - captures: (String, Vec<TextRange>), + captures: (Box<str>, Box<[TextRange]>), dependencies_len: usize, }, /// When a dependency needs to be removed. RemoveDependency { function_name_range: TextRange, component_function: JsSyntaxNode, - dependencies: Vec<AnyJsExpression>, + dependencies: Box<[AnyJsExpression]>, }, /// When a dependency is too unstable (changes every render). DependencyTooUnstable { - dependency_name: String, + dependency_name: Box<str>, dependency_range: TextRange, kind: UnstableDependencyKind, }, @@ -437,7 +437,7 @@ pub enum Fix { function_name_range: TextRange, capture_range: TextRange, dependency_range: TextRange, - dependency_text: String, + dependency_text: Box<str>, }, } @@ -736,14 +736,14 @@ fn compare_member_depth(a: &JsSyntaxNode, b: &JsSyntaxNode) -> (bool, bool) { impl Rule for UseExhaustiveDependencies { type Query = Semantic<JsCallExpression>; type State = Fix; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = Box<UseExhaustiveDependenciesOptions>; - fn run(ctx: &RuleContext<Self>) -> Vec<Self::State> { + fn run(ctx: &RuleContext<Self>) -> Self::Signals { let options = ctx.options(); let hook_config_maps = HookConfigMaps::new(options); - let mut signals = vec![]; + let mut signals = Vec::new(); let call = ctx.query(); let model = ctx.model(); @@ -752,16 +752,17 @@ impl Rule for UseExhaustiveDependencies { react_hook_with_dependency(call, &hook_config_maps.hooks_config, model) { let Some(component_function) = function_of_hook_call(call) else { - return vec![]; + return Vec::new().into_boxed_slice(); }; if result.dependencies_node.is_none() { if options.report_missing_dependencies_array { return vec![Fix::MissingDependenciesArray { function_name_range: result.function_name_range, - }]; + }] + .into_boxed_slice(); } else { - return vec![]; + return Vec::new().into_boxed_slice(); } } @@ -798,7 +799,7 @@ impl Rule for UseExhaustiveDependencies { let deps: Vec<_> = result.all_dependencies().collect(); let dependencies_len = deps.len(); - let mut add_deps: BTreeMap<String, Vec<TextRange>> = BTreeMap::new(); + let mut add_deps: BTreeMap<Box<str>, Vec<TextRange>> = BTreeMap::new(); // Evaluate all the captures for (capture_text, capture_range, capture_path) in captures.iter() { @@ -836,7 +837,7 @@ impl Rule for UseExhaustiveDependencies { function_name_range: result.function_name_range, capture_range: *capture_range, dependency_range: dep.syntax().text_trimmed_range(), - dependency_text: dep.syntax().text_trimmed().to_string(), + dependency_text: dep.syntax().text_trimmed().to_string().into(), }); } _ => {} @@ -848,7 +849,7 @@ impl Rule for UseExhaustiveDependencies { } if !is_captured_covered { - let captures = add_deps.entry(capture_text.clone()).or_default(); + let captures = add_deps.entry(capture_text.clone().into()).or_default(); captures.push(*capture_range); } } @@ -872,7 +873,7 @@ impl Rule for UseExhaustiveDependencies { signals.push(Fix::RemoveDependency { function_name_range: result.function_name_range, component_function: component_function.clone(), - dependencies: vec![dep.clone()], + dependencies: vec![dep.clone()].into_boxed_slice(), }); continue; } @@ -887,10 +888,10 @@ impl Rule for UseExhaustiveDependencies { }); // Generate signals - for captures in add_deps { + for (name, ranges) in add_deps { signals.push(Fix::AddDependency { function_name_range: result.function_name_range, - captures, + captures: (name, ranges.into_boxed_slice()), dependencies_len, }); } @@ -899,38 +900,39 @@ impl Rule for UseExhaustiveDependencies { signals.push(Fix::RemoveDependency { function_name_range: result.function_name_range, component_function, - dependencies: excessive_deps, + dependencies: excessive_deps.into_boxed_slice(), }); } for (unstable_dep, kind) in unstable_deps { signals.push(Fix::DependencyTooUnstable { - dependency_name: unstable_dep.syntax().to_string(), + dependency_name: unstable_dep.syntax().to_string().into_boxed_str(), dependency_range: unstable_dep.range(), kind, }); } } - signals + signals.into_boxed_slice() } - fn instances_for_signal(signal: &Self::State) -> Vec<String> { + fn instances_for_signal(signal: &Self::State) -> Box<[Box<str>]> { match signal { Fix::MissingDependenciesArray { function_name_range: _, - } => vec![], - Fix::AddDependency { captures, .. } => vec![captures.0.clone()], + } => vec![].into_boxed_slice(), + Fix::AddDependency { captures, .. } => vec![captures.0.clone()].into(), Fix::RemoveDependency { dependencies, .. } => dependencies .iter() - .map(|dep| dep.syntax().text_trimmed().to_string()) - .collect(), + .map(|dep| dep.syntax().text_trimmed().to_string().into_boxed_str()) + .collect::<Vec<_>>() + .into_boxed_slice(), Fix::DependencyTooUnstable { dependency_name, .. - } => vec![dependency_name.clone()], + } => vec![dependency_name.clone()].into(), Fix::DependencyTooDeep { dependency_text, .. - } => vec![dependency_text.clone()], + } => vec![dependency_text.clone()].into(), } } @@ -954,7 +956,7 @@ impl Rule for UseExhaustiveDependencies { let mut diag = RuleDiagnostic::new( rule_category!(), function_name_range, - markup! {"This hook does not specify all of its dependencies: "{capture_text}""}, + markup! {"This hook does not specify all of its dependencies: "{capture_text.as_ref()}""}, ); for range in captures_range { @@ -1023,11 +1025,11 @@ impl Rule for UseExhaustiveDependencies { rule_category!(), dependency_range, markup! { - <Emphasis>{dependency_name}</Emphasis>" changes on every re-render and should not be used as a hook dependency." + <Emphasis>{dependency_name.as_ref()}</Emphasis>" changes on every re-render and should not be used as a hook dependency." }, ) .note(markup! { - "To fix this, wrap the definition of "<Emphasis>{dependency_name}</Emphasis>" in its own "<Emphasis>{suggested_hook}</Emphasis>" hook." + "To fix this, wrap the definition of "<Emphasis>{dependency_name.as_ref()}</Emphasis>" in its own "<Emphasis>{suggested_hook}</Emphasis>" hook." }); Some(diag) } @@ -1041,7 +1043,7 @@ impl Rule for UseExhaustiveDependencies { rule_category!(), function_name_range, markup! { - "This hook specifies a dependency more specific that its captures: "{dependency_text}"" + "This hook specifies a dependency more specific that its captures: "{dependency_text.as_ref()}"" }, ) .detail(capture_range, "This capture is more generic than...") diff --git a/crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs b/crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs index 1ab6b8d3c662..bb497acc7b9b 100644 --- a/crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs +++ b/crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs @@ -103,7 +103,7 @@ impl AnyJsFunctionOrMethod { pub enum Suggestion { None { hook_name_range: TextRange, - path: Vec<TextRange>, + path: Box<[TextRange]>, early_return: Option<TextRange>, is_nested: bool, }, @@ -233,12 +233,12 @@ fn is_nested_function_inside_component_or_hook(function: &AnyJsFunctionOrMethod) }) } -/// Model for tracking which function calls are preceeded by an early return. +/// Model for tracking which function calls are preceded by an early return. /// /// The keys in the model are call sites and each value is the text range of an -/// early return that preceeds such call site. Call sites without preceeding +/// early return that precedes such call site. Call sites without preceding /// early returns are not included in the model. For call sites that are -/// preceeded by multiple early returns, the return statement that we map to is +/// preceded by multiple early returns, the return statement that we map to is /// implementation-defined. #[derive(Clone, Default)] struct EarlyReturnsModel(FxHashMap<JsCallExpression, TextRange>); @@ -440,7 +440,7 @@ impl Rule for UseHookAtTopLevel { // hooks to be called from the top-level. return Some(Suggestion::None { hook_name_range: get_hook_name_range()?, - path, + path: path.into_boxed_slice(), early_return: None, is_nested: true, }); @@ -449,7 +449,7 @@ impl Rule for UseHookAtTopLevel { if let Some(early_return) = early_returns.get(&call) { return Some(Suggestion::None { hook_name_range: get_hook_name_range()?, - path, + path: path.into_boxed_slice(), early_return: Some(*early_return), is_nested: false, }); @@ -468,7 +468,7 @@ impl Rule for UseHookAtTopLevel { } else { return Some(Suggestion::None { hook_name_range: get_hook_name_range()?, - path, + path: path.into_boxed_slice(), early_return: None, is_nested: false, }); diff --git a/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs b/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs index 53c09213b93d..29587ca64e40 100644 --- a/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs +++ b/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs @@ -130,7 +130,7 @@ declare_lint_rule! { pub struct UseImportExtensionsOptions { /// A map of custom import extension mappings, where the key is the inspected file extension, /// and the value is a pair of `module` extension and `component` import extension - pub suggested_extensions: FxHashMap<String, SuggestedExtensionMapping>, + pub suggested_extensions: FxHashMap<Box<str>, SuggestedExtensionMapping>, } #[derive(Debug, Clone, Default, Deserializable, Deserialize, Serialize, Eq, PartialEq)] @@ -138,9 +138,9 @@ pub struct UseImportExtensionsOptions { #[serde(rename_all = "camelCase", deny_unknown_fields, default)] pub struct SuggestedExtensionMapping { /// Extension that should be used for module imports - pub module: String, + pub module: Box<str>, /// Extension that should be used for component file imports - pub component: String, + pub component: Box<str>, } impl Rule for UseImportExtensions { @@ -209,7 +209,7 @@ pub struct UseImportExtensionsState { fn get_extensionless_import( file_ext: &str, node: &AnyJsImportLike, - custom_suggested_imports: &FxHashMap<String, SuggestedExtensionMapping>, + custom_suggested_imports: &FxHashMap<Box<str>, SuggestedExtensionMapping>, ) -> Option<UseImportExtensionsState> { let module_name_token = node.module_name_token()?; let module_path = inner_string_text(&module_name_token); @@ -293,7 +293,7 @@ fn get_extensionless_import( fn resolve_import_extension<'a>( file_ext: &str, path: &Path, - custom_suggested_imports: &'a FxHashMap<String, SuggestedExtensionMapping>, + custom_suggested_imports: &'a FxHashMap<Box<str>, SuggestedExtensionMapping>, ) -> &'a str { let (potential_ext, potential_component_ext): (&str, &str) = if let Some(custom_mapping) = custom_suggested_imports.get(file_ext) { diff --git a/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs b/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs index b33c447fe54d..53fb89b8da18 100644 --- a/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs +++ b/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs @@ -56,19 +56,19 @@ declare_node_union! { impl Rule for UseJsxKeyInIterable { type Query = Semantic<UseJsxKeyInIterableQuery>; type State = TextRange; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { let node = ctx.query(); let model = ctx.model(); - match node { UseJsxKeyInIterableQuery::JsArrayExpression(node) => handle_collections(node, model), UseJsxKeyInIterableQuery::JsCallExpression(node) => { handle_iterators(node, model).unwrap_or_default() } } + .into_boxed_slice() } fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 4228deb21b6d..9f15f5405e11 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -7,6 +7,9 @@ pub mod no_duplicate_else_if; pub mod no_dynamic_namespace_import_access; pub mod no_enum; pub mod no_exported_imports; +pub mod no_head_element; +pub mod no_head_import_in_document; +pub mod no_img_element; pub mod no_irregular_whitespace; pub mod no_nested_ternary; pub mod no_octal_escape; @@ -40,6 +43,9 @@ declare_lint_group! { self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess , self :: no_enum :: NoEnum , self :: no_exported_imports :: NoExportedImports , + self :: no_head_element :: NoHeadElement , + self :: no_head_import_in_document :: NoHeadImportInDocument , + self :: no_img_element :: NoImgElement , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_nested_ternary :: NoNestedTernary , self :: no_octal_escape :: NoOctalEscape , diff --git a/crates/biome_js_analyze/src/lint/nursery/no_duplicate_else_if.rs b/crates/biome_js_analyze/src/lint/nursery/no_duplicate_else_if.rs index 5993e28b6af2..6daf7421b23c 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_duplicate_else_if.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_duplicate_else_if.rs @@ -58,7 +58,6 @@ declare_lint_rule! { impl Rule for NoDuplicateElseIf { type Query = Ast<JsIfStatement>; type State = TextRange; - type Signals = Option<Self::State>; type Options = (); diff --git a/crates/biome_js_analyze/src/lint/nursery/no_dynamic_namespace_import_access.rs b/crates/biome_js_analyze/src/lint/nursery/no_dynamic_namespace_import_access.rs index 29c997f99056..837b59b6a54a 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_dynamic_namespace_import_access.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_dynamic_namespace_import_access.rs @@ -67,11 +67,13 @@ declare_lint_rule! { impl Rule for NoDynamicNamespaceImportAccess { type Query = Semantic<JsImportNamespaceClause>; type State = TextRange; - type Signals = Vec<Self::State>; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext<Self>) -> Self::Signals { - find_dynamic_namespace_import_accesses(ctx).map_or(vec![], |range| range) + find_dynamic_namespace_import_accesses(ctx) + .map_or(Vec::new(), |x| x) + .into_boxed_slice() } fn diagnostic(_: &RuleContext<Self>, range: &Self::State) -> Option<RuleDiagnostic> { diff --git a/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs b/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs new file mode 100644 index 000000000000..8384ae9f9368 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs @@ -0,0 +1,89 @@ +use biome_analyze::RuleSourceKind; +use biome_analyze::{ + context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, +}; +use biome_console::markup; +use biome_js_syntax::JsxOpeningElement; +use biome_rowan::AstNode; +use biome_rowan::TextRange; + +declare_lint_rule! { + /// Prevent usage of `<head>` element in a Next.js project. + /// + /// Next.js provides a specialized `<Head />` component from `next/head` that manages + /// the `<head>` tag for optimal server-side rendering, client-side navigation, and + /// automatic deduplication of tags such as `<meta>` and `<title>`. + /// + /// This rule only checks files that are outside of the [`app/` directory](https://nextjs.org/docs/app), as it's typically + /// handled differently in Next.js. + /// + /// ## Examples + /// + /// ### Invalid + /// ```jsx,expect_diagnostic + /// function Index() { + /// return ( + /// <head> + /// <title>Invalid + /// + /// ) + /// } + /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// import Head from 'next/head' + /// + /// function Index() { + /// return ( + /// + /// All good! + /// + /// ) + /// } + /// ``` + pub NoHeadElement { + version: "next", + name: "noHeadElement", + language: "jsx", + sources: &[RuleSource::EslintNext("no-head-element")], + source_kind: RuleSourceKind::SameLogic, + recommended: false, + } +} + +impl Rule for NoHeadElement { + type Query = Ast; + type State = TextRange; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let element = ctx.query(); + let name = element.name().ok()?.name_value_token()?; + + if name.text_trimmed() == "head" { + let is_in_app_dir = ctx + .file_path() + .ancestors() + .any(|a| a.file_name().map_or(false, |f| f == "app" && a.is_dir())); + + if !is_in_app_dir { + return Some(element.syntax().text_range()); + } + } + + None + } + + fn diagnostic(_: &RuleContext, range: &Self::State) -> Option { + return Some(RuleDiagnostic::new( + rule_category!(), + range, + markup! { "Don't use """" element." }, + ).note(markup! { + "Using the """" element can cause unexpected behavior in a Next.js application. Use """" from ""next/head"" instead." + })); + } +} diff --git a/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs b/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs new file mode 100644 index 000000000000..ffc1ea7e40c8 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs @@ -0,0 +1,122 @@ +use biome_analyze::{ + context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, RuleSourceKind, +}; +use biome_console::markup; +use biome_js_syntax::{JsFileSource, JsImport}; +use biome_rowan::AstNode; +use std::path::MAIN_SEPARATOR; + +declare_lint_rule! { + /// Prevent using the `next/head` module in `pages/_document.js` on Next.js projects. + /// + /// Importing `next/head` within the custom `pages/_document.js` file can cause + /// unexpected behavior in your application. The `next/head` component is designed + /// to be used at the page level, and when used in the custom document it can interfere + /// with the global document structure, which leads to issues with rendering and SEO. + /// + /// To modify `` elements across all pages, you should use the `` + /// component from the `next/document` module. + /// + /// ## Examples + /// + /// ### Valid + /// + /// ```jsx + /// // pages/_document.js + /// import Document, { Html, Head, Main, NextScript } from "next/document"; + /// + /// class MyDocument extends Document { + /// static async getInitialProps(ctx) { + /// //... + /// } + /// + /// render() { + /// return ( + /// + /// + /// + /// ); + /// } + /// } + /// + /// export default MyDocument; + /// ``` + /// + pub NoHeadImportInDocument { + version: "next", + name: "noHeadImportInDocument", + language: "jsx", + sources: &[RuleSource::EslintNext("no-head-import-in-document")], + source_kind: RuleSourceKind::SameLogic, + recommended: false, + } +} + +impl Rule for NoHeadImportInDocument { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + if !ctx.source_type::().is_jsx() { + return None; + } + + let import = ctx.query(); + let import_source = import.import_clause().ok()?.source().ok()?; + let module_name = import_source.inner_string_text().ok()?; + + if module_name != "next/head" { + return None; + } + + let path = ctx.file_path(); + + if !path + .ancestors() + .filter_map(|a| a.file_name()) + .any(|f| f == "pages") + { + return None; + } + + let file_name = path.file_stem()?.to_str()?; + + // pages/_document.(jsx|tsx) + if file_name == "_document" { + return Some(()); + } + + let parent_name = path.parent()?.file_stem()?.to_str()?; + + // pages/_document/index.(jsx|tsx) + if parent_name == "_document" && file_name == "index" { + return Some(()); + } + + None + } + + fn diagnostic(ctx: &RuleContext, _: &Self::State) -> Option { + let path = ctx.file_path().to_str()?.split("pages").nth(1)?; + let path = if cfg!(debug_assertions) { + path.replace(MAIN_SEPARATOR, "/") + } else { + path.to_string() + }; + + return Some( + RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "Don't use ""next/head"" in pages"{path}"" + }, + ) + .note(markup! { + "Using the ""next/head"" in document pages can cause unexpected issues. Use """" from ""next/document"" instead." + }) + ); + } +} diff --git a/crates/biome_js_analyze/src/lint/nursery/no_img_element.rs b/crates/biome_js_analyze/src/lint/nursery/no_img_element.rs new file mode 100644 index 000000000000..6164b9409f98 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_img_element.rs @@ -0,0 +1,113 @@ +use biome_analyze::RuleSourceKind; +use biome_analyze::{ + context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, +}; +use biome_console::markup; +use biome_js_syntax::jsx_ext::AnyJsxElement; +use biome_js_syntax::{JsxChildList, JsxElement}; +use biome_rowan::{AstNode, AstNodeList}; + +declare_lint_rule! { + /// Prevent usage of `` element in a Next.js project. + /// + /// Using the `` element can result in slower Largest Contentful Paint (LCP) + /// and higher bandwidth usage, as it lacks the optimizations provided by the `` + /// component from `next/image`. Next.js's `` automatically optimizes images + /// by serving responsive sizes and using modern formats, improving performance and reducing bandwidth. + /// + /// If your project is self-hosted, ensure that you have sufficient storage and have + /// installed the `sharp` package to support optimized images. When deploying to managed + /// hosting providers, be aware of potential additional costs or usage. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```jsx,expect_diagnostic + /// Foo + /// ``` + /// + /// ```jsx,expect_diagnostic + ///
+ /// Foo + ///
+ /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// + /// ``` + /// + /// ```jsx + /// + /// ``` + /// + /// ```jsx + /// + /// + /// + /// + /// + /// ``` + /// + pub NoImgElement { + version: "next", + name: "noImgElement", + language: "jsx", + sources: &[RuleSource::EslintNext("no-img-element")], + source_kind: RuleSourceKind::SameLogic, + recommended: false, + } +} + +impl Rule for NoImgElement { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + + if node.name().ok()?.name_value_token()?.text_trimmed() != "img" + || node.attributes().is_empty() + { + return None; + } + + if let AnyJsxElement::JsxSelfClosingElement(jsx) = &node { + let Some(parent) = jsx.parent::() else { + return Some(()); + }; + let Some(parent) = parent.parent::() else { + return Some(()); + }; + let Some(opening_element) = parent.opening_element().ok() else { + return Some(()); + }; + let name = opening_element.name().ok()?.name_value_token()?; + + if name.text_trimmed() == "picture" { + return None; + } + } + + Some(()) + } + + fn diagnostic(ctx: &RuleContext, _: &Self::State) -> Option { + return Some( + RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "Don't use """" element." + }, + ) + .note(markup! { + "Using the """" can lead to slower LCP and higher bandwidth. Consider using """" from ""next/image"" to automatically optimize images." + }) + ); + } +} diff --git a/crates/biome_js_analyze/src/lint/nursery/no_irregular_whitespace.rs b/crates/biome_js_analyze/src/lint/nursery/no_irregular_whitespace.rs index 769687e8b804..38438b2bdafc 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_irregular_whitespace.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_irregular_whitespace.rs @@ -50,12 +50,12 @@ declare_lint_rule! { impl Rule for NoIrregularWhitespace { type Query = Ast; type State = TextRange; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { let node = ctx.query(); - get_irregular_whitespace(node) + get_irregular_whitespace(node).into_boxed_slice() } fn diagnostic(_ctx: &RuleContext, range: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/nursery/no_restricted_imports.rs b/crates/biome_js_analyze/src/lint/nursery/no_restricted_imports.rs index 76002050d0bd..ead4e83692a9 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_restricted_imports.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_restricted_imports.rs @@ -43,7 +43,7 @@ declare_lint_rule! { pub struct RestrictedImportsOptions { /// A list of names that should trigger the rule #[serde(skip_serializing_if = "FxHashMap::is_empty")] - paths: FxHashMap, + paths: FxHashMap, Box>, } impl Rule for NoRestrictedImports { diff --git a/crates/biome_js_analyze/src/lint/nursery/no_restricted_types.rs b/crates/biome_js_analyze/src/lint/nursery/no_restricted_types.rs index e1d39dc949bb..260cbe6bb176 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_restricted_types.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_restricted_types.rs @@ -126,7 +126,7 @@ impl Rule for NoRestrictedTypes { #[cfg_attr(feature = "schemars", derive(JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields, default)] pub struct NoRestrictedTypesOptions { - types: FxHashMap, + types: FxHashMap, CustomRestrictedType>, } #[derive( diff --git a/crates/biome_js_analyze/src/lint/nursery/no_secrets.rs b/crates/biome_js_analyze/src/lint/nursery/no_secrets.rs index 6504a2bd1dda..279a061d05fb 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_secrets.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_secrets.rs @@ -10,14 +10,58 @@ use regex::Regex; use std::sync::LazyLock; +use biome_deserialize_macros::Deserializable; +use serde::{Deserialize, Serialize}; + // TODO: Try to get this to work in JavaScript comments as well declare_lint_rule! { /// Disallow usage of sensitive data such as API keys and tokens. /// /// This rule checks for high-entropy strings and matches common patterns - /// for secrets, such as AWS keys, Slack tokens, and private keys. + /// for secrets, including AWS keys, Slack tokens, and private keys. + /// It aims to help users identify immediate potential secret leaks in their codebase, + /// especially for those who may not be aware of the risks associated with + /// sensitive data exposure. + /// + /// ## Detected Secrets + /// + /// The following list contains the patterns we detect: + /// + /// - **JSON Web Token (JWT)**: Tokens in the format of `ey...` + /// - **Base64-encoded JWT**: Base64-encoded JWT tokens with various parameters (alg, aud, iss, etc.) + /// - **Slack Token**: Tokens such as `xox[baprs]-...` + /// - **Slack Webhook URL**: URLs like `https://hooks.slack.com/services/...` + /// - **GitHub Token**: GitHub tokens with lengths between 35-40 characters + /// - **Twitter OAuth Token**: Twitter OAuth tokens with lengths between 35-44 characters + /// - **Facebook OAuth Token**: Facebook OAuth tokens with possible lengths up to 42 characters + /// - **Google OAuth Token**: Google OAuth tokens in the format `ya29...` + /// - **AWS API Key**: Keys that begin with `AKIA` followed by 16 alphanumeric characters + /// - **Passwords in URLs**: Passwords included in URL credentials (`protocol://user:pass@...`) + /// - **Google Service Account**: JSON structure with the service-account identifier + /// - **Twilio API Key**: API keys starting with `SK...` followed by 32 characters + /// - **RSA Private Key**: Key blocks that start with `-----BEGIN RSA PRIVATE KEY-----` + /// - **OpenSSH Private Key**: Key blocks that start with `-----BEGIN OPENSSH PRIVATE KEY-----` + /// - **DSA Private Key**: Key blocks that start with `-----BEGIN DSA PRIVATE KEY-----` + /// - **EC Private Key**: Key blocks that start with `-----BEGIN EC PRIVATE KEY-----` + /// - **PGP Private Key Block**: Key blocks that start with `-----BEGIN PGP PRIVATE KEY BLOCK-----` + /// + /// ## Entropy Check + /// + /// In addition to detecting the above patterns, we also employ a **string entropy checker** to catch potential secrets based on their entropy (randomness). The entropy checker is configurable through the `Options`, allowing customization of thresholds for string entropy to fine-tune detection and minimize false positives. + /// + /// ## Disclaimer + /// + /// While this rule helps with most common cases, it is not intended to handle all of them. + /// Therefore, always review your code carefully and consider implementing additional security + /// measures, such as automated secret scanning in your CI/CD and git pipeline. /// - /// While this rule is helpful, it's not infallible. Always review your code carefully and consider implementing additional security measures like automated secret scanning in your CI/CD and git pipeline, such as GitGuardian or GitHub protections. + /// ## Recommendations + /// + /// Some recommended tools for more comprehensive secret detection include: + /// - [SonarQube](https://www.sonarsource.com/products/sonarqube/downloads/): Clean Code scanning solution with a secret scanner (Community version). + /// - [Gitleaks](https://github.com/gitleaks/gitleaks/): A mature secret scanning tool. + /// - [Trufflehog](https://github.com/trufflesecurity/trufflehog): A tool for finding secrets in git history. + /// - [Sensleak](https://github.com/crates-pro/sensleak-rs): A Rust-based solution for secret detection. /// /// ## Examples /// @@ -37,7 +81,7 @@ declare_lint_rule! { name: "noSecrets", language: "js", recommended: false, - sources: &[RuleSource::Eslint("no-secrets/no-secrets")], + sources: &[RuleSource::EslintNoSecrets("no-secrets")], source_kind: RuleSourceKind::Inspired, } } @@ -46,7 +90,7 @@ impl Rule for NoSecrets { type Query = Ast; type State = &'static str; type Signals = Option; - type Options = (); + type Options = NoSecretsOptions; fn run(ctx: &RuleContext) -> Self::Signals { let node = ctx.query(); @@ -57,11 +101,17 @@ impl Rule for NoSecrets { return None; } + let has_spaces = text.contains(' '); + for sensitive_pattern in SENSITIVE_PATTERNS.iter() { if text.len() < sensitive_pattern.min_len { continue; } + if has_spaces && !sensitive_pattern.allows_spaces { + continue; + } + let matched = match &sensitive_pattern.pattern { Pattern::Regex(re) => re.is_match(text), Pattern::Contains(substring) => text.contains(substring), @@ -72,11 +122,15 @@ impl Rule for NoSecrets { } } - if is_high_entropy(text) { - Some("The string has a high entropy value") - } else { - None + if is_path(text) { + return None; } + + let entropy_threshold = ctx + .options() + .entropy_threshold + .unwrap_or(DEFAULT_HIGH_ENTROPY_THRESHOLD); + detect_secret(text, &entropy_threshold) } fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { @@ -92,16 +146,37 @@ impl Rule for NoSecrets { "Storing secrets in source code is a security risk. Consider the following steps:" "\n1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree." "\n2. If needed, use environment variables or a secure secret management system to store sensitive data." - "\n3. If this is a false positive, consider adding an inline disable comment." + "\n3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options ""in our docs." + "\nThis rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: ""https://biomejs.dev/linter/rules/no-secrets/#recommendations" }) ) } } -const HIGH_ENTROPY_THRESHOLD: f64 = 4.5; +#[derive(Clone, Debug, Default, Deserialize, Deserializable, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NoSecretsOptions { + /// Set entropy threshold (default is 41). + entropy_threshold: Option, +} + +fn is_path(text: &str) -> bool { + // Check for common path indicators + text.starts_with("./") || text.starts_with("../") +} + +const DEFAULT_HIGH_ENTROPY_THRESHOLD: u16 = 41; + +// Known sensitive patterns start here +static JWT_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r#"\b(ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/\\_-]{17,}\.(?:[a-zA-Z0-9\/\\_-]{10,}={0,2})?)(?:['|\"|\n|\r|\s|\x60|;]|$)"#).unwrap() +}); + +static JWT_BASE64_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r#"\bZXlK(?:(?PaGJHY2lPaU)|(?PaGNIVWlPaU)|(?PaGNIWWlPaU)|(?PaGRXUWlPaU)|(?PaU5qUWlP)|(?PamNtbDBJanBi)|(?PamRIa2lPaU)|(?PbGNHc2lPbn)|(?PbGJtTWlPaU)|(?PcWEzVWlPaU)|(?PcWQyc2lPb)|(?PcGMzTWlPaU)|(?PcGRpSTZJ)|(?PcmFXUWlP)|(?PclpYbGZiM0J6SWpwY)|(?PcmRIa2lPaUp)|(?PdWIyNWpaU0k2)|(?Pd01tTWlP)|(?Pd01uTWlPaU)|(?Pd2NIUWlPaU)|(?PemRXSWlPaU)|(?PemRuUWlP)|(?PMFlXY2lPaU)|(?PMGVYQWlPaUp)|(?PMWNtd2l)|(?PMWMyVWlPaUp)|(?PMlpYSWlPaU)|(?PMlpYSnphVzl1SWpv)|(?PNElqb2)|(?PNE5XTWlP)|(?PNE5YUWlPaU)|(?PNE5YUWpVekkxTmlJNkl)|(?PNE5YVWlPaU)|(?PNmFYQWlPaU))[a-zA-Z0-9\/\\_+\-\r\n]{40,}={0,2}"#).unwrap() +}); -// Workaround: Since I couldn't figure out how to declare them inline, -// declare the LazyLock patterns separately static SLACK_TOKEN_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"xox[baprs]-([0-9a-zA-Z]{10,48})?").unwrap()); @@ -121,13 +196,6 @@ static TWITTER_OAUTH_REGEX: LazyLock = static FACEBOOK_OAUTH_REGEX: LazyLock = LazyLock::new(|| Regex::new(r#"[fF][aA][cC][eE][bB][oO][oO][kK].*(?:.{0,42})"#).unwrap()); -static HEROKU_API_KEY_REGEX: LazyLock = LazyLock::new(|| { - Regex::new( - r"[hH][eE][rR][oO][kK][uU].*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", - ) - .unwrap() -}); - static PASSWORD_IN_URL_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r#"[a-zA-Z]{3,10}://[^/\s:@]{3,20}:[^/\s:@]{3,20}@.{1,100}['"\s]"#).unwrap() }); @@ -155,119 +223,282 @@ struct SensitivePattern { pattern: Pattern, comment: &'static str, min_len: usize, + allows_spaces: bool, } static SENSITIVE_PATTERNS: &[SensitivePattern] = &[ + SensitivePattern { + pattern: Pattern::Regex(&JWT_REGEX), + comment: "JSON Web Token (JWT)", + min_len: 100, + allows_spaces: false, + }, + SensitivePattern { + pattern: Pattern::Regex(&JWT_BASE64_REGEX), + comment: "Base64-encoded JWT", + min_len: 100, + allows_spaces: false, + }, SensitivePattern { pattern: Pattern::Regex(&SLACK_TOKEN_REGEX), comment: "Slack Token", min_len: 32, + allows_spaces: false, }, SensitivePattern { pattern: Pattern::Regex(&SLACK_WEBHOOK_REGEX), comment: "Slack Webhook", min_len: 24, + allows_spaces: false, }, SensitivePattern { pattern: Pattern::Regex(&GITHUB_TOKEN_REGEX), comment: "GitHub", min_len: 35, + allows_spaces: false, }, SensitivePattern { pattern: Pattern::Regex(&TWITTER_OAUTH_REGEX), comment: "Twitter OAuth", min_len: 35, + allows_spaces: false, }, SensitivePattern { pattern: Pattern::Regex(&FACEBOOK_OAUTH_REGEX), comment: "Facebook OAuth", min_len: 32, + allows_spaces: false, }, SensitivePattern { pattern: Pattern::Regex(&GOOGLE_OAUTH_REGEX), comment: "Google OAuth", min_len: 24, + allows_spaces: false, }, SensitivePattern { pattern: Pattern::Regex(&AWS_API_KEY_REGEX), comment: "AWS API Key", min_len: 16, - }, - SensitivePattern { - pattern: Pattern::Regex(&HEROKU_API_KEY_REGEX), - comment: "Heroku API Key", - min_len: 12, + allows_spaces: false, }, SensitivePattern { pattern: Pattern::Regex(&PASSWORD_IN_URL_REGEX), comment: "Password in URL", min_len: 14, + allows_spaces: false, }, SensitivePattern { pattern: Pattern::Regex(&GOOGLE_SERVICE_ACCOUNT_REGEX), comment: "Google (GCP) Service-account", min_len: 14, + allows_spaces: true, }, SensitivePattern { pattern: Pattern::Regex(&TWILIO_API_KEY_REGEX), comment: "Twilio API Key", min_len: 32, + allows_spaces: false, }, SensitivePattern { pattern: Pattern::Contains("-----BEGIN RSA PRIVATE KEY-----"), comment: "RSA Private Key", min_len: 64, + allows_spaces: true, }, SensitivePattern { pattern: Pattern::Contains("-----BEGIN OPENSSH PRIVATE KEY-----"), comment: "SSH (OPENSSH) Private Key", min_len: 64, + allows_spaces: true, }, SensitivePattern { pattern: Pattern::Contains("-----BEGIN DSA PRIVATE KEY-----"), comment: "SSH (DSA) Private Key", min_len: 64, + allows_spaces: true, }, SensitivePattern { pattern: Pattern::Contains("-----BEGIN EC PRIVATE KEY-----"), comment: "SSH (EC) Private Key", min_len: 64, + allows_spaces: true, }, SensitivePattern { pattern: Pattern::Contains("-----BEGIN PGP PRIVATE KEY BLOCK-----"), comment: "PGP Private Key Block", min_len: 64, + allows_spaces: true, }, ]; +const MIN_PATTERN_LEN: usize = 14; + +// Known safe patterns start here +static BASE64_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$").unwrap() +}); +static URL_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^https?://[a-zA-Z0-9.-]+(/[a-zA-Z0-9./_-]*)?$").unwrap()); +static RELATIVE_PATH_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^(?:\.\./|\./|[a-zA-Z0-9_-]+)/?$").unwrap()); +static UNIX_PATH_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^(/[^/]+)+/?$").unwrap()); +static WINDOWS_PATH_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-zA-Z]:\\(?:[^\\]+\\?)*$").unwrap()); +static EMAIL_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()); +static PHONE_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^\+?[1-9]\d{1,14}$").unwrap()); // E.164 format + +// Combine all known safe patterns into a single list +static KNOWN_SAFE_PATTERNS: &[&LazyLock] = &[ + &BASE64_REGEX, + &URL_REGEX, + &RELATIVE_PATH_REGEX, + &UNIX_PATH_REGEX, + &WINDOWS_PATH_REGEX, + &EMAIL_REGEX, + &PHONE_REGEX, +]; + +fn is_known_safe_pattern(data: &str) -> bool { + for pattern in KNOWN_SAFE_PATTERNS { + if pattern.is_match(data) { + return true; + } + } + false +} + +fn detect_secret(data: &str, entropy_threshold: &u16) -> Option<&'static str> { + if is_known_safe_pattern(data) { + return None; + } -const MIN_PATTERN_LEN: usize = 12; + let tokens = data + .split([' ', '\t', '\n', '.', ',', ';', ':', '/', '-', '_', '@']) + .filter(|s| !s.is_empty()); -fn is_high_entropy(text: &str) -> bool { - let entropy = calculate_shannon_entropy(text); - entropy > HIGH_ENTROPY_THRESHOLD // TODO: Make this optional, or controllable + for token in tokens { + if token.len() >= MIN_PATTERN_LEN { + if is_known_safe_pattern(token) { + continue; + } + + let entropy = + calculate_entropy_with_case_and_classes(token, *entropy_threshold as f64, 15.0); + if (entropy as u16) > *entropy_threshold { + return Some("Detected high entropy string"); + } + } + } + None } -/// Inspired by https://github.com/nickdeis/eslint-plugin-no-secrets/blob/master/utils.js#L93 -/// Adapted from https://docs.rs/entropy/latest/src/entropy/lib.rs.html#14-33 -/// Calculates Shannon entropy to measure the randomness of data. High entropy values indicate potentially -/// secret or sensitive information, as such data is typically more random and less predictable than regular text. -/// Useful for detecting API keys, passwords, and other secrets within code or configuration files. -fn calculate_shannon_entropy(data: &str) -> f64 { +/* +Uses Shannon Entropy as a base algorithm, then adds "boosts" for special patterns/occurrences. +For example, Continuous mixed cases (lIkE tHiS) are more likely to contribute to a higher score than single cases. +Symbols also contribute highly to secrets. + +TODO: This needs work. False positives/negatives are highlighted in valid.js and invalid.js. + +References: +- ChatGPT chat: https://chatgpt.com/share/670370bf-3e18-8011-8454-f3bd01be0319 +- Original paper for Shannon Entropy: https://ieeexplore.ieee.org/abstract/document/6773024/ +*/ +fn calculate_entropy_with_case_and_classes( + data: &str, + base_threshold: f64, + scaling_factor: f64, +) -> f64 { let mut freq = [0usize; 256]; let len = data.len(); + for &byte in data.as_bytes() { freq[byte as usize] += 1; } - let mut entropy = 0.0; + let mut shannon_entropy = 0.0; + let mut letter_count = 0; + let mut uppercase_count = 0; + let mut lowercase_count = 0; + let mut digit_count = 0; + let mut symbol_count = 0; + let mut case_switches = 0; + let mut previous_char_was_upper = false; + for count in freq.iter() { if *count > 0 { let p = *count as f64 / len as f64; - entropy -= p * p.log2(); + shannon_entropy -= p * p.log2(); } } - entropy + // Letter classification and case switching + for (i, c) in data.chars().enumerate() { + if c.is_ascii_alphabetic() { + letter_count += 1; + if c.is_uppercase() { + uppercase_count += 1; + if i > 0 && !previous_char_was_upper { + case_switches += 1; + } + previous_char_was_upper = true; + } else { + lowercase_count += 1; + if i > 0 && previous_char_was_upper { + case_switches += 1; + } + previous_char_was_upper = false; + } + } else if c.is_ascii_digit() { + digit_count += 1; + } else if !c.is_whitespace() { + symbol_count += 1; + } + } + + // Adjust entropy: case switches and symbol boosts + let case_entropy_boost = if uppercase_count > 0 && lowercase_count > 0 { + (case_switches as f64 / letter_count as f64) * 2.0 + } else { + 0.0 + }; + + let symbol_entropy_boost = if symbol_count > 0 { + symbol_count as f64 / len as f64 + } else { + 0.0 + }; + + let digit_entropy_boost = if digit_count > 0 { + digit_count as f64 / len as f64 + } else { + 0.0 + }; + + let adjusted_entropy = shannon_entropy + + (case_entropy_boost * 2.5) + + (symbol_entropy_boost * 1.5) + + digit_entropy_boost; + + // Apply exponential scaling to avoid excessive boosting for long, structured tokens + apply_exponential_entropy_scaling(adjusted_entropy, len, base_threshold, scaling_factor) +} + +/* + A simple mechanism to scale entropy as the string length increases, the reason being that + large length strings are likely to be secrets. + TODO: However, at some point there should definitely be a cutoff i.e. 100 characters, because it's + probably base64 data or something similar at that point. + This was taken from GPT, and I sadly couldn't find references for it. +*/ +fn apply_exponential_entropy_scaling( + entropy: f64, + token_length: usize, + base_threshold: f64, + scaling_factor: f64, +) -> f64 { + // We will apply a logarithmic dampening to prevent excessive scaling for long tokens + let scaling_adjustment = (token_length as f64 / scaling_factor).ln(); + base_threshold + entropy * scaling_adjustment } #[cfg(test)] diff --git a/crates/biome_js_analyze/src/lint/nursery/use_adjacent_overload_signatures.rs b/crates/biome_js_analyze/src/lint/nursery/use_adjacent_overload_signatures.rs index 72a5fd08c96f..b12ba7997c26 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_adjacent_overload_signatures.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_adjacent_overload_signatures.rs @@ -99,7 +99,7 @@ declare_lint_rule! { impl Rule for UseAdjacentOverloadSignatures { type Query = Ast; - type State = Vec<(TokenText, TextRange)>; + type State = Box<[(TokenText, TextRange)]>; type Signals = Option; type Options = (); @@ -136,7 +136,7 @@ impl Rule for UseAdjacentOverloadSignatures { }; if !methods.is_empty() { - Some(methods) + Some(methods.into_boxed_slice()) } else { None } diff --git a/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs b/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs index e11484ba5100..c32ae1532b99 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs @@ -82,7 +82,7 @@ declare_lint_rule! { /// /// ### `allowExportNames` /// - /// If you use a framework that handles [Hot Mudule Replacement(HMR)] of some specific exports, you can use this option to avoid warning for them. + /// If you use a framework that handles [Hot Module Replacement(HMR)] of some specific exports, you can use this option to avoid warning for them. /// /// Example for [Remix](https://remix.run/docs/en/main/discussion/hot-module-replacement#supported-exports): /// ```json @@ -95,7 +95,7 @@ declare_lint_rule! { /// ``` /// /// [`meta` in Remix]: https://remix.run/docs/en/main/route/meta - /// [Hot Mudule Replacement(HMR)]: https://remix.run/docs/en/main/discussion/hot-module-replacement + /// [Hot Module Replacement(HMR)]: https://remix.run/docs/en/main/discussion/hot-module-replacement /// [`React Fast Refresh`]: https://github.com/facebook/react/tree/main/packages/react-refresh /// [Remix]: https://remix.run/ /// [Vite]: https://vitejs.dev/ @@ -119,8 +119,8 @@ pub struct UseComponentExportOnlyModulesOptions { #[serde(default)] allow_constant_export: bool, /// A list of names that can be additionally exported from the module This option is for exports that do not hinder [React Fast Refresh](https://github.com/facebook/react/tree/main/packages/react-refresh), such as [`meta` in Remix](https://remix.run/docs/en/main/route/meta) - #[serde(default, skip_serializing_if = "Vec::is_empty")] - allow_export_names: Vec, + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + allow_export_names: Box<[Box]>, } enum ErrorType { @@ -139,13 +139,13 @@ const JSX_FILE_EXT: [&str; 2] = [".jsx", ".tsx"]; impl Rule for UseComponentExportOnlyModules { type Query = Ast; type State = UseComponentExportOnlyModulesState; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = UseComponentExportOnlyModulesOptions; fn run(ctx: &RuleContext) -> Self::Signals { if let Some(file_name) = ctx.file_path().file_name().and_then(|x| x.to_str()) { if !JSX_FILE_EXT.iter().any(|ext| file_name.ends_with(ext)) { - return vec![]; + return Vec::new().into_boxed_slice(); } } let root = ctx.query(); @@ -175,12 +175,14 @@ impl Rule for UseComponentExportOnlyModules { continue; } // Allow exporting specific names - if let Some(exported_item_id) = &exported_item.identifier { - if ctx - .options() - .allow_export_names - .contains(&exported_item_id.text()) - { + if let Some(exported_item_id) = exported_item + .identifier + .as_ref() + .and_then(|x| x.name_token()) + { + if ctx.options().allow_export_names.iter().any(|export_name| { + export_name.as_ref() == exported_item_id.text_trimmed() + }) { continue; } } @@ -240,7 +242,8 @@ impl Rule for UseComponentExportOnlyModules { }, range: id, }) - .collect::>() + .collect::>() + .into_boxed_slice() } fn diagnostic(_ctx: &RuleContext, state: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/nursery/use_explicit_function_return_type.rs b/crates/biome_js_analyze/src/lint/nursery/use_explicit_function_return_type.rs index 3cd1520f13cd..46000228b2e5 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_explicit_function_return_type.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_explicit_function_return_type.rs @@ -4,14 +4,16 @@ use biome_analyze::{ use biome_console::markup; use biome_js_semantic::HasClosureAstNode; use biome_js_syntax::{ - AnyJsBinding, AnyJsExpression, AnyJsFunctionBody, AnyJsStatement, AnyTsType, JsFileSource, - JsStatementList, JsSyntaxKind, + AnyJsBinding, AnyJsExpression, AnyJsFunctionBody, AnyJsStatement, AnyTsType, JsCallExpression, + JsFileSource, JsFormalParameter, JsInitializerClause, JsLanguage, JsObjectExpression, + JsParenthesizedExpression, JsPropertyClassMember, JsPropertyObjectMember, JsStatementList, + JsSyntaxKind, JsVariableDeclarator, }; use biome_js_syntax::{ AnyJsFunction, JsGetterClassMember, JsGetterObjectMember, JsMethodClassMember, JsMethodObjectMember, }; -use biome_rowan::{declare_node_union, AstNode, SyntaxNodeOptionExt, TextRange}; +use biome_rowan::{declare_node_union, AstNode, SyntaxNode, SyntaxNodeOptionExt, TextRange}; declare_lint_rule! { /// Require explicit return types on functions and class methods. @@ -168,6 +170,38 @@ declare_lint_rule! { /// } /// ``` /// + /// The following patterns are considered correct for type annotations on variables in function expressions: + /// + /// ```ts + /// // A function with a type assertion using `as` + /// const asTyped = (() => '') as () => string; + /// ``` + /// + /// ```ts + /// // A function with a type assertion using `<>` + /// const castTyped = <() => string>(() => ''); + /// ``` + /// + /// ```ts + /// // A variable declarator with a type annotation. + /// type FuncType = () => string; + /// const arrowFn: FuncType = () => 'test'; + /// ``` + /// + /// ```ts + /// // A function is a default parameter with a type annotation + /// type CallBack = () => void; + /// const f = (gotcha: CallBack = () => { }): void => { }; + /// ``` + /// + /// ```ts + /// // A class property with a type annotation + /// type MethodType = () => void; + /// class App { + /// private method: MethodType = () => { }; + /// } + /// ``` + /// pub UseExplicitFunctionReturnType { version: "1.9.3", name: "useExplicitFunctionReturnType", @@ -204,7 +238,11 @@ impl Rule for UseExplicitFunctionReturnType { return None; } - if is_function_used_in_argument_or_expression_list(func) { + if is_iife(func) { + return None; + } + + if is_function_used_in_argument_or_array(func) { return None; } @@ -212,6 +250,10 @@ impl Rule for UseExplicitFunctionReturnType { return None; } + if is_typed_function_expressions(func) { + return None; + } + let func_range = func.syntax().text_range(); if let Ok(Some(AnyJsBinding::JsIdentifierBinding(id))) = func.id() { return Some(TextRange::new( @@ -313,22 +355,27 @@ fn is_direct_const_assertion_in_arrow_functions(func: &AnyJsFunction) -> bool { /// JS_ARRAY_ELEMENT_LIST: /// - `[function () {}, () => {}];` /// -/// JS_PARENTHESIZED_EXPRESSION: -/// - `(function () {});` -/// - `(() => {})();` -fn is_function_used_in_argument_or_expression_list(func: &AnyJsFunction) -> bool { +fn is_function_used_in_argument_or_array(func: &AnyJsFunction) -> bool { matches!( func.syntax().parent().kind(), - Some( - JsSyntaxKind::JS_CALL_ARGUMENT_LIST - | JsSyntaxKind::JS_ARRAY_ELEMENT_LIST - // We include JS_PARENTHESIZED_EXPRESSION for IIFE (Immediately Invoked Function Expressions). - // We also assume that the parent of the parent is a call expression. - | JsSyntaxKind::JS_PARENTHESIZED_EXPRESSION - ) + Some(JsSyntaxKind::JS_CALL_ARGUMENT_LIST | JsSyntaxKind::JS_ARRAY_ELEMENT_LIST) ) } +/// Checks if a function is an IIFE (Immediately Invoked Function Expressions) +/// +/// # Examples +/// +/// ```typescript +/// (function () {}); +/// (() => {})(); +/// ``` +fn is_iife(func: &AnyJsFunction) -> bool { + func.parent::() + .and_then(|expr| expr.parent::()) + .is_some() +} + /// Checks whether the given function is a higher-order function, i.e., a function /// that returns another function either directly in its body or as an expression. /// @@ -384,7 +431,7 @@ fn is_first_statement_function_return(statements: JsStatementList) -> bool { None } }) - .map_or(false, |args| { + .is_some_and(|args| { matches!( args, AnyJsExpression::JsFunctionExpression(_) @@ -392,3 +439,114 @@ fn is_first_statement_function_return(statements: JsStatementList) -> bool { ) }) } + +/// Checks if a given function expression has a type annotation. +fn is_typed_function_expressions(func: &AnyJsFunction) -> bool { + let syntax = func.syntax(); + is_type_assertion(syntax) + || is_variable_declarator_with_type_annotation(syntax) + || is_default_function_parameter_with_type_annotation(syntax) + || is_class_property_with_type_annotation(syntax) + || is_property_of_object_with_type(syntax) +} + +/// Checks if a function is a variable declarator with a type annotation. +/// +/// # Examples +/// +/// ```typescript +/// type FuncType = () => string; +/// const arrowFn: FuncType = () => 'test'; +/// ``` +fn is_variable_declarator_with_type_annotation(syntax: &SyntaxNode) -> bool { + syntax + .parent() + .and_then(JsInitializerClause::cast) + .and_then(|init| init.parent::()) + .is_some_and(|decl| decl.variable_annotation().is_some()) +} + +/// Checks if a function is a default parameter with a type annotation. +/// +/// # Examples +/// +/// ```typescript +/// type CallBack = () => void; +/// const f = (gotcha: CallBack = () => { }): void => { }; +/// ``` +fn is_default_function_parameter_with_type_annotation(syntax: &SyntaxNode) -> bool { + syntax + .parent() + .and_then(JsInitializerClause::cast) + .and_then(|init| init.parent::()) + .is_some_and(|param| param.type_annotation().is_some()) +} + +/// Checks if a function is a class property with a type annotation. +/// +/// # Examples +/// +/// ```typescript +/// type MethodType = () => void; +/// class App { +/// private method: MethodType = () => { }; +/// } +/// ``` +fn is_class_property_with_type_annotation(syntax: &SyntaxNode) -> bool { + syntax + .parent() + .and_then(JsInitializerClause::cast) + .and_then(|init| init.parent::()) + .is_some_and(|prop| prop.property_annotation().is_some()) +} + +/// Checks if a function is a property or a nested property of a typed object. +/// +/// # Examples +/// +/// ```typescript +/// const x: Foo = { prop: () => {} } +/// const x = { prop: () => {} } as Foo +/// const x = { prop: () => {} } +/// const x: Foo = { bar: { prop: () => {} } } +/// ``` +fn is_property_of_object_with_type(syntax: &SyntaxNode) -> bool { + syntax + .parent() + .and_then(JsPropertyObjectMember::cast) + .and_then(|prop| prop.syntax().grand_parent()) + .and_then(JsObjectExpression::cast) + .is_some_and(|obj_expression| { + let obj_syntax = obj_expression.syntax(); + is_type_assertion(obj_syntax) + || is_variable_declarator_with_type_annotation(obj_syntax) + || is_property_of_object_with_type(obj_syntax) + }) +} + +/// Checks if a function has a type assertion. +/// +/// # Examples +/// +/// ```typescript +/// const asTyped = (() => '') as () => string; +/// const castTyped = <() => string>(() => ''); +/// ``` +fn is_type_assertion(syntax: &SyntaxNode) -> bool { + fn is_assertion_kind(kind: JsSyntaxKind) -> bool { + matches!( + kind, + JsSyntaxKind::TS_AS_EXPRESSION | JsSyntaxKind::TS_TYPE_ASSERTION_EXPRESSION + ) + } + + syntax.parent().map_or(false, |parent| { + if parent.kind() == JsSyntaxKind::JS_PARENTHESIZED_EXPRESSION { + parent + .parent() + .is_some_and(|grandparent| is_assertion_kind(grandparent.kind())) + } else { + is_assertion_kind(parent.kind()) + } + }) +} diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs index 9d596ff2e189..f24e595c64f3 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs @@ -165,6 +165,9 @@ impl Rule for UseSortedClasses { let ignore_postfix = should_ignore_postfix(node); let sorted_value = sort_class_name(&value, &SORT_CONFIG, ignore_prefix, ignore_postfix); + if sorted_value.is_empty() { + return None; + } if value.text() != sorted_value { return Some(sorted_value); } diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/options.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/options.rs index 42637d925fcd..861a1813eae9 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/options.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/options.rs @@ -10,32 +10,23 @@ use serde::{Deserialize, Serialize}; /// Attributes that are always targets. const CLASS_ATTRIBUTES: [&str; 2] = ["class", "className"]; -#[derive(Deserialize, Serialize, Eq, PartialEq, Debug, Clone)] +#[derive(Default, Deserialize, Serialize, Eq, PartialEq, Debug, Clone)] #[cfg_attr(feature = "schemars", derive(JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields, default)] pub struct UtilityClassSortingOptions { /// Additional attributes that will be sorted. #[serde(skip_serializing_if = "Option::is_none")] - pub attributes: Option>, + pub attributes: Option]>>, /// Names of the functions or tagged templates that will be sorted. #[serde(skip_serializing_if = "Option::is_none")] - pub functions: Option>, -} - -impl Default for UtilityClassSortingOptions { - fn default() -> Self { - UtilityClassSortingOptions { - attributes: Some(CLASS_ATTRIBUTES.iter().map(|&s| s.to_string()).collect()), - functions: None, - } - } + pub functions: Option>>, } impl UtilityClassSortingOptions { pub(crate) fn has_function(&self, name: &str) -> bool { let iter = self.functions.iter().flatten(); for v in iter { - if v.as_str() == name { + if v.as_ref() == name { return true; } } @@ -43,13 +34,8 @@ impl UtilityClassSortingOptions { } pub(crate) fn has_attribute(&self, name: &str) -> bool { - let iter = self.attributes.iter().flatten(); - for v in iter { - if v.as_str() == name { - return true; - } - } - false + CLASS_ATTRIBUTES.contains(&name) + || self.attributes.iter().flatten().any(|v| v.as_ref() == name) } } @@ -80,6 +66,7 @@ impl DeserializationVisitor for UtilityClassSortingOptionsVisitor { ) -> Option { let mut result = UtilityClassSortingOptions::default(); + let mut attributes = Vec::new(); for (key, value) in members.flatten() { let Some(key_text) = Text::deserialize(&key, "", diagnostics) else { continue; @@ -89,10 +76,7 @@ impl DeserializationVisitor for UtilityClassSortingOptionsVisitor { if let Some(attributes_option) = Deserializable::deserialize(&value, &key_text, diagnostics) { - result - .attributes - .get_or_insert_with(Vec::new) - .extend::>(attributes_option); + attributes.extend::>>(attributes_option); } } "functions" => { @@ -105,7 +89,11 @@ impl DeserializationVisitor for UtilityClassSortingOptionsVisitor { )), } } - + result.attributes = if attributes.is_empty() { + None + } else { + Some(attributes.into_boxed_slice()) + }; Some(result) } } diff --git a/crates/biome_js_analyze/src/lint/nursery/use_valid_autocomplete.rs b/crates/biome_js_analyze/src/lint/nursery/use_valid_autocomplete.rs index d8b27ab87dac..a9d7ca02de7e 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_valid_autocomplete.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_valid_autocomplete.rs @@ -140,7 +140,7 @@ const BILLING_AND_SHIPPING_ADDRESS: &[&str; 11] = &[ #[serde(rename_all = "camelCase", deny_unknown_fields, default)] pub struct UseValidAutocompleteOptions { /// `input` like custom components that should be checked. - pub input_components: Vec, + pub input_components: Box<[Box]>, } impl Rule for UseValidAutocomplete { @@ -156,7 +156,9 @@ impl Rule for UseValidAutocomplete { UseValidAutocompleteQuery::JsxOpeningElement(elem) => { let elem_name = elem.name().ok()?.name_value_token()?; let elem_name = elem_name.text_trimmed(); - if !(elem_name == "input" || input_components.contains(&elem_name.to_string())) { + if !(elem_name == "input" + || input_components.iter().any(|x| x.as_ref() == elem_name)) + { return None; } let attributes = elem.attributes(); @@ -183,7 +185,9 @@ impl Rule for UseValidAutocomplete { UseValidAutocompleteQuery::JsxSelfClosingElement(elem) => { let elem_name = elem.name().ok()?.name_value_token()?; let elem_name = elem_name.text_trimmed(); - if !(elem_name == "input" || input_components.contains(&elem_name.to_string())) { + if !(elem_name == "input" + || input_components.iter().any(|x| x.as_ref() == elem_name)) + { return None; } let attributes = elem.attributes(); diff --git a/crates/biome_js_analyze/src/lint/style/no_arguments.rs b/crates/biome_js_analyze/src/lint/style/no_arguments.rs index efd5bf81f5af..e846722336e4 100644 --- a/crates/biome_js_analyze/src/lint/style/no_arguments.rs +++ b/crates/biome_js_analyze/src/lint/style/no_arguments.rs @@ -36,12 +36,11 @@ declare_lint_rule! { impl Rule for NoArguments { type Query = SemanticServices; type State = TextRange; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { let model = ctx.query(); - let mut found_arguments = vec![]; for unresolved_reference in model.all_unresolved_references() { @@ -51,7 +50,7 @@ impl Rule for NoArguments { } } - found_arguments + found_arguments.into_boxed_slice() } fn diagnostic(_: &RuleContext, range: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/style/no_restricted_globals.rs b/crates/biome_js_analyze/src/lint/style/no_restricted_globals.rs index 88d4e026842a..7f45ef22e073 100644 --- a/crates/biome_js_analyze/src/lint/style/no_restricted_globals.rs +++ b/crates/biome_js_analyze/src/lint/style/no_restricted_globals.rs @@ -62,14 +62,14 @@ const RESTRICTED_GLOBALS: [&str; 2] = ["event", "error"]; #[serde(rename_all = "camelCase", deny_unknown_fields, default)] pub struct RestrictedGlobalsOptions { /// A list of names that should trigger the rule - #[serde(skip_serializing_if = "Vec::is_empty")] - pub denied_globals: Vec, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + pub denied_globals: Box<[Box]>, } impl Rule for NoRestrictedGlobals { type Query = SemanticServices; - type State = (TextRange, String); - type Signals = Vec; + type State = (TextRange, Box); + type Signals = Box<[Self::State]>; type Options = Box; fn run(ctx: &RuleContext) -> Self::Signals { @@ -103,9 +103,10 @@ impl Rule for NoRestrictedGlobals { let denied_globals: Vec<_> = options.denied_globals.iter().map(AsRef::as_ref).collect(); is_restricted(text, &binding, denied_globals.as_slice()) - .map(|text| (token.text_trimmed_range(), text)) + .map(|text| (token.text_trimmed_range(), text.into_boxed_str())) }) - .collect() + .collect::>() + .into_boxed_slice() } fn diagnostic(_ctx: &RuleContext, (span, text): &Self::State) -> Option { @@ -114,7 +115,7 @@ impl Rule for NoRestrictedGlobals { rule_category!(), *span, markup! { - "Do not use the global variable "{text}"." + "Do not use the global variable "{text.as_ref()}"." }, ) .note(markup! { diff --git a/crates/biome_js_analyze/src/lint/style/no_useless_else.rs b/crates/biome_js_analyze/src/lint/style/no_useless_else.rs index 9e8856488d42..58f90043a760 100644 --- a/crates/biome_js_analyze/src/lint/style/no_useless_else.rs +++ b/crates/biome_js_analyze/src/lint/style/no_useless_else.rs @@ -101,15 +101,15 @@ declare_lint_rule! { impl Rule for NoUselessElse { type Query = Ast; type State = JsIfStatement; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { - let mut result = vec![]; + let mut result = Vec::new(); let if_stmt = ctx.query(); // Check an `if` statement only once. if if_stmt.syntax().parent().kind() == Some(JsSyntaxKind::JS_ELSE_CLAUSE) { - return result; + return result.into_boxed_slice(); } let mut if_stmt = Cow::Borrowed(if_stmt); while let (Ok(if_consequent), Some(else_clause)) = @@ -131,7 +131,7 @@ impl Rule for NoUselessElse { }; if_stmt = Cow::Owned(stmt); } - result + result.into_boxed_slice() } fn diagnostic(_ctx: &RuleContext, if_stmt: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/style/use_export_type.rs b/crates/biome_js_analyze/src/lint/style/use_export_type.rs index 63382da59aa6..d47077b45120 100644 --- a/crates/biome_js_analyze/src/lint/style/use_export_type.rs +++ b/crates/biome_js_analyze/src/lint/style/use_export_type.rs @@ -5,17 +5,20 @@ use biome_analyze::{ }; use biome_console::markup; use biome_js_factory::make; -use biome_js_syntax::{AnyJsExportNamedSpecifier, JsExportNamedClause, JsFileSource, T}; +use biome_js_syntax::{ + AnyJsExportNamedSpecifier, JsExportNamedClause, JsExportNamedFromClause, JsFileSource, + JsSyntaxToken, T, +}; use biome_rowan::{ - chain_trivia_pieces, trim_leading_trivia_pieces, AstNode, AstSeparatedList, BatchMutationExt, - TriviaPieceKind, + chain_trivia_pieces, declare_node_union, trim_leading_trivia_pieces, AstNode, AstSeparatedList, + BatchMutationExt, TriviaPieceKind, }; declare_lint_rule! { /// Promotes the use of `export type` for types. /// - /// _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. + /// _TypeScript_ allows adding the `type` keyword on an `export` to indicate that the `export` doesn't exist at runtime. + /// This allows compilers to safely drop exports of types without looking for their definition. /// /// The rule ensures that types are exported using a type-only `export`. /// It also groups inline type exports into a grouped `export type`. @@ -69,7 +72,7 @@ declare_lint_rule! { } impl Rule for UseExportType { - type Query = Semantic; + type Query = Semantic; type State = ExportTypeFix; type Signals = Option; type Options = (); @@ -80,77 +83,127 @@ impl Rule for UseExportType { return None; } let export_named_clause = ctx.query(); - if export_named_clause.type_token().is_some() { - // `export type {}` - return None; - } - let mut exports_only_types = true; - let mut specifiers_requiring_type_marker = Vec::new(); - let specifiers = export_named_clause.specifiers(); - if specifiers.is_empty() { - // Don't report `export {}` - return None; - } - for specifier in specifiers { - let Ok((ref_name, specifier)) = - specifier.and_then(|specifier| Ok((specifier.local_name()?, specifier))) - else { - exports_only_types = false; - continue; - }; - if specifier.type_token().is_some() { - // `export { type }` - continue; + match export_named_clause { + AnyJsExportNamedClause::JsExportNamedClause(clause) => { + let specifiers = clause.specifiers(); + if specifiers.is_empty() { + // Don't report `export {}` + None + } else if clause.type_token().is_some() { + // `export type { ... }` + let useless_type_tokens: Vec<_> = specifiers + .iter() + .filter_map(|specifier| specifier.ok()?.type_token()) + .collect(); + if useless_type_tokens.is_empty() { + None + } else { + Some(ExportTypeFix::RemoveInlineTypeQualifiers( + useless_type_tokens, + )) + } + } else { + let mut exports_only_types = true; + let mut specifiers_requiring_type_marker = Vec::new(); + for specifier in specifiers { + let Ok((ref_name, specifier)) = specifier + .and_then(|specifier| Ok((specifier.local_name()?, specifier))) + else { + exports_only_types = false; + continue; + }; + if specifier.type_token().is_some() { + // `export { type }` + 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::UseExportType) + } else if specifiers_requiring_type_marker.is_empty() { + None + } else { + Some(ExportTypeFix::AddInlineTypeQualifiers( + specifiers_requiring_type_marker, + )) + } + } } - 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; + AnyJsExportNamedClause::JsExportNamedFromClause(clause) => { + let specifiers = clause.specifiers(); + if specifiers.is_empty() { + None + } else if clause.type_token().is_some() { + let useless_type_tokens: Vec<_> = specifiers + .iter() + .filter_map(|specifier| specifier.ok()?.type_token()) + .collect(); + if useless_type_tokens.is_empty() { + None + } else { + Some(ExportTypeFix::RemoveInlineTypeQualifiers( + useless_type_tokens, + )) + } + } else if specifiers + .iter() + .all(|x| x.is_ok_and(|x| x.type_token().is_some())) + { + Some(ExportTypeFix::UseExportType) + } else { + None + } } } - if exports_only_types { - Some(ExportTypeFix::UseExportType) - } else if specifiers_requiring_type_marker.is_empty() { - None - } else { - Some(ExportTypeFix::AddInlineTypeQualifiers( - specifiers_requiring_type_marker, - )) - } } fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { - let range = ctx.query().range(); - let (diagnostic_range, diagnostic_message) = match state { - ExportTypeFix::UseExportType => ( - range, - markup! { - "All exports are only types and should thus use ""export type""." - }, + let named_export_clause = ctx.query(); + let diagnostic = match state { + ExportTypeFix::UseExportType => RuleDiagnostic::new( + rule_category!(), + named_export_clause.range(), + "All exports are only types.", ), ExportTypeFix::AddInlineTypeQualifiers(specifiers) => { - let range = specifiers - .iter() - .map(|x| x.range()) - .reduce(|acc, x| acc.cover(x)) - .unwrap_or(range); - ( - range, + let mut diagnostic = RuleDiagnostic::new( + rule_category!(), + named_export_clause.range(), + "Some exports are only types.", + ); + for specifier in specifiers { + diagnostic = diagnostic.detail(specifier.range(), "This export is a type.") + } + diagnostic + } + ExportTypeFix::RemoveInlineTypeQualifiers(type_tokens) => { + let mut diagnostic = RuleDiagnostic::new( + rule_category!(), + named_export_clause.type_token()?.text_trimmed_range(), markup! { - "Several exports are only types and should thus use ""export type""." + "This ""type"" keyword makes all inline ""type"" keywords useless." }, - ) + ); + for type_token in type_tokens { + diagnostic = diagnostic.detail( + type_token.text_trimmed_range(), + markup! { + "This inline ""type"" keyword is useless." + }, + ) + } + return Some(diagnostic); } }; - Some(RuleDiagnostic::new( - rule_category!(), - diagnostic_range, - diagnostic_message, - ).note(markup! { - "Using ""export type"" allows transpilers to safely drop exports of types without looking for their definition." + Some(diagnostic.note(markup! { + "Using ""export type"" allows compilers to safely drop exports of types without looking for their definition." })) } @@ -159,43 +212,86 @@ impl Rule for UseExportType { let mut mutation = ctx.root().begin(); let diagnostic = match state { ExportTypeFix::UseExportType => { - 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 new_specifier = specifier - .with_type_token(None) - .trim_leading_trivia()? - .prepend_trivia_pieces(chain_trivia_pieces( - type_token.leading_trivia().pieces(), - trim_leading_trivia_pieces(type_token.trailing_trivia().pieces()), - ))?; - new_specifiers.push(new_specifier); - } else { - new_specifiers.push(specifier) + match export_named_clause { + AnyJsExportNamedClause::JsExportNamedClause(clause) => { + let specifier_list = 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 new_specifier = specifier + .with_type_token(None) + .trim_leading_trivia()? + .prepend_trivia_pieces(chain_trivia_pieces( + type_token.leading_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::>(), + ); + mutation.replace_node( + clause.clone(), + clause + .clone() + .with_type_token(Some( + make::token(T![type]) + .with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), + )) + .with_specifiers(new_specifier_list), + ); + } + AnyJsExportNamedClause::JsExportNamedFromClause(clause) => { + let specifier_list = 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 new_specifier = specifier + .with_type_token(None) + .trim_leading_trivia()? + .prepend_trivia_pieces(chain_trivia_pieces( + type_token.leading_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_from_specifier_list( + new_specifiers, + specifier_list + .separators() + .filter_map(|sep| sep.ok()) + .collect::>(), + ); + mutation.replace_node( + clause.clone(), + clause + .clone() + .with_type_token(Some( + make::token(T![type]) + .with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), + )) + .with_specifiers(new_specifier_list), + ); } } - let new_specifier_list = make::js_export_named_specifier_list( - new_specifiers, - specifier_list - .separators() - .filter_map(|sep| sep.ok()) - .collect::>(), - ); - mutation.replace_node( - export_named_clause.clone(), - export_named_clause - .clone() - .with_type_token(Some( - make::token(T![type]) - .with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), - )) - .with_specifiers(new_specifier_list), - ); JsRuleAction::new( ActionCategory::QuickFix, ctx.metadata().applicability(), - markup! { "Use a grouped ""export type""." }.to_owned(), + markup! { "Use ""export type""." }.to_owned(), mutation, ) } @@ -218,7 +314,19 @@ impl Rule for UseExportType { JsRuleAction::new( ActionCategory::QuickFix, ctx.metadata().applicability(), - markup! { "Use inline ""type"" exports." }.to_owned(), + markup! { "Add inline ""type"" keywords." }.to_owned(), + mutation, + ) + } + ExportTypeFix::RemoveInlineTypeQualifiers(type_tokens) => { + for type_token in type_tokens { + mutation.remove_token(type_token.clone()); + } + JsRuleAction::new( + ActionCategory::QuickFix, + ctx.metadata().applicability(), + markup! { "Remove useless inline ""type"" keywords." } + .to_owned(), mutation, ) } @@ -227,6 +335,19 @@ impl Rule for UseExportType { } } +declare_node_union! { + pub AnyJsExportNamedClause = JsExportNamedClause | JsExportNamedFromClause +} + +impl AnyJsExportNamedClause { + fn type_token(&self) -> Option { + match self { + Self::JsExportNamedClause(clause) => clause.type_token(), + Self::JsExportNamedFromClause(clause) => clause.type_token(), + } + } +} + #[derive(Debug)] pub enum ExportTypeFix { /** @@ -234,4 +355,5 @@ pub enum ExportTypeFix { */ UseExportType, AddInlineTypeQualifiers(Vec), + RemoveInlineTypeQualifiers(Vec), } diff --git a/crates/biome_js_analyze/src/lint/style/use_import_type.rs b/crates/biome_js_analyze/src/lint/style/use_import_type.rs index 678fec49aa97..3bf082d239af 100644 --- a/crates/biome_js_analyze/src/lint/style/use_import_type.rs +++ b/crates/biome_js_analyze/src/lint/style/use_import_type.rs @@ -26,8 +26,8 @@ use rustc_hash::FxHashSet; declare_lint_rule! { /// Promotes the use of `import type` for types. /// - /// _TypeScript_ allows specifying a `type` qualifier on an `import` to indicate that the `import` doesn't exist at runtime. - /// This allows transpilers to safely drop imports of types without looking for their definition. + /// _TypeScript_ allows specifying a `type` keyword on an `import` to indicate that the `import` doesn't exist at runtime. + /// This allows compilers to safely drop imports of types without looking for their definition. /// This also ensures that some modules are not loaded at runtime. /// /// The rule ensures that all imports used only as a type use a type-only `import`. @@ -35,9 +35,9 @@ declare_lint_rule! { /// /// If you use the TypeScript Compiler (TSC) to compile your code into JavaScript, /// then you can disable this rule, as TSC can remove imports only used as types. - /// However, for consistency and compatibility with other transpilers, you may want to enable this rule. + /// However, for consistency and compatibility with other compilers, you may want to enable this rule. /// In that case we recommend to enable TSC's [`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax). - /// This configuration ensures that TSC preserves imports not marked with the `type` qualifier. + /// This configuration ensures that TSC preserves imports not marked with the `type` keyword. /// /// You may also want to enable the editor setting [`typescript.preferences.preferTypeOnlyAutoImports`](https://devblogs.microsoft.com/typescript/announcing-typescript-5-3-rc/#settings-to-prefer-type-auto-imports) from the TypeScript LSP. /// This setting is available in Visual Studio Code. @@ -136,10 +136,8 @@ impl Rule for UseImportType { if import_clause.assertion().is_some() { return None; } - if import_clause.type_token().is_some() || - // Import attributes and type-only imports are not compatible. - import_clause.assertion().is_some() - { + // Import attributes and type-only imports are not compatible. + if import_clause.assertion().is_some() { return None; } let model = ctx.model(); @@ -157,7 +155,7 @@ impl Rule for UseImportType { }; match clause.specifier().ok()? { AnyJsCombinedSpecifier::JsNamedImportSpecifiers(named_specifiers) => { - match named_import_type_fix(model, &named_specifiers) { + match named_import_type_fix(model, &named_specifiers, false) { Some(NamedImportTypeFix::UseImportType(specifiers)) => { if is_default_used_as_type { Some(ImportTypeFix::UseImportType) @@ -166,7 +164,7 @@ impl Rule for UseImportType { // when the default import is not only used as a type. None } else { - // Prefer adding type qualifier instead of + // Prefer adding type keyword instead of // splitting the import statement into two import statements Some(ImportTypeFix::AddInlineTypeQualifiers(specifiers)) } @@ -180,6 +178,10 @@ impl Rule for UseImportType { Some(ImportTypeFix::AddInlineTypeQualifiers(specifiers)) } } + Some(NamedImportTypeFix::RemoveInlineTypeQualifiers(_)) => { + // Should not be reached because we pass `false` to `named_import_type_fix`. + None + } None => is_default_used_as_type .then_some(ImportTypeFix::ExtractDefaultImportType(vec![])), } @@ -206,6 +208,9 @@ impl Rule for UseImportType { } } AnyJsImportClause::JsImportDefaultClause(clause) => { + if clause.type_token().is_some() { + return None; + } let default_binding = clause.default_specifier().ok()?.local_name().ok()?; let default_binding = default_binding.as_js_identifier_binding()?; if ctx.jsx_runtime() == JsxRuntime::ReactClassic @@ -217,14 +222,24 @@ impl Rule for UseImportType { is_only_used_as_type(model, default_binding).then_some(ImportTypeFix::UseImportType) } AnyJsImportClause::JsImportNamedClause(clause) => { - match named_import_type_fix(model, &clause.named_specifiers().ok()?)? { + match named_import_type_fix( + model, + &clause.named_specifiers().ok()?, + clause.type_token().is_some(), + )? { NamedImportTypeFix::UseImportType(_) => Some(ImportTypeFix::UseImportType), NamedImportTypeFix::AddInlineTypeQualifiers(specifiers) => { Some(ImportTypeFix::AddInlineTypeQualifiers(specifiers)) } + NamedImportTypeFix::RemoveInlineTypeQualifiers(type_tokens) => { + Some(ImportTypeFix::RemoveTypeQualifiers(type_tokens)) + } } } AnyJsImportClause::JsImportNamespaceClause(clause) => { + if clause.type_token().is_some() { + return None; + } let namespace_binding = clause.namespace_specifier().ok()?.local_name().ok()?; let namespace_binding = namespace_binding.as_js_identifier_binding()?; if ctx.jsx_runtime() == JsxRuntime::ReactClassic @@ -245,20 +260,20 @@ impl Rule for UseImportType { let diagnostic = match state { ImportTypeFix::UseImportType => RuleDiagnostic::new( rule_category!(), - import.range(), + import_clause.range(), "All these imports are only used as types.", ), ImportTypeFix::ExtractDefaultImportType(named_specifiers) => { if named_specifiers.is_empty() { RuleDiagnostic::new( rule_category!(), - import.range(), + import_clause.range(), "The default import is only used as a type.", ) } else { let mut diagnostic = RuleDiagnostic::new( rule_category!(), - import.range(), + import_clause.range(), "The default import and some named imports are only used as types.", ); for specifier in named_specifiers { @@ -291,7 +306,7 @@ impl Rule for UseImportType { ImportTypeFix::AddInlineTypeQualifiers(named_specifiers) => { let mut diagnostic = RuleDiagnostic::new( rule_category!(), - import.range(), + import_clause.range(), "Some named imports are only used as types.", ); for specifier in named_specifiers { @@ -300,9 +315,27 @@ impl Rule for UseImportType { } diagnostic } + ImportTypeFix::RemoveTypeQualifiers(type_tokens) => { + let mut diagnostic = RuleDiagnostic::new( + rule_category!(), + import_clause.type_token()?.text_trimmed_range(), + markup! { + "This ""type"" keyword makes all inline ""type"" keywords useless." + }, + ); + for type_token in type_tokens { + diagnostic = diagnostic.detail( + type_token.text_trimmed_range(), + markup! { + "This inline ""type"" keyword is useless." + }, + ) + } + return Some(diagnostic); + } }; Some(diagnostic.note(markup! { - "Importing the types with ""import type"" ensures that they are removed by the transpilers and avoids loading unnecessary modules." + "Importing the types with ""import type"" ensures that they are removed by the compilers and avoids loading unnecessary modules." })) } @@ -526,6 +559,24 @@ impl Rule for UseImportType { )); mutation.replace_node(specifier.clone(), new_specifier); } + return Some(JsRuleAction::new( + ActionCategory::QuickFix, + ctx.metadata().applicability(), + markup! { "Add inline ""type"" keywords." }.to_owned(), + mutation, + )); + } + ImportTypeFix::RemoveTypeQualifiers(type_tokens) => { + for type_token in type_tokens { + mutation.remove_token(type_token.clone()); + } + return Some(JsRuleAction::new( + ActionCategory::QuickFix, + ctx.metadata().applicability(), + markup! { "Remove useless inline ""type"" keywords." } + .to_owned(), + mutation, + )); } } Some(JsRuleAction::new( @@ -543,6 +594,7 @@ pub enum ImportTypeFix { ExtractDefaultImportType(Vec), ExtractCombinedImportType, AddInlineTypeQualifiers(Vec), + RemoveTypeQualifiers(Vec), } /// Returns `true` if all references of `binding` are only used as a type. @@ -564,50 +616,66 @@ fn is_only_used_as_type(model: &SemanticModel, binding: &JsIdentifierBinding) -> pub enum NamedImportTypeFix { UseImportType(Vec), AddInlineTypeQualifiers(Vec), + RemoveInlineTypeQualifiers(Vec), } fn named_import_type_fix( model: &SemanticModel, named_specifiers: &JsNamedImportSpecifiers, + has_type_token: bool, ) -> Option { let specifiers = named_specifiers.specifiers(); if specifiers.is_empty() { return None; }; - let mut imports_only_types = true; - let mut specifiers_requiring_type_marker = Vec::with_capacity(specifiers.len()); - for specifier in specifiers.iter() { - let Ok(specifier) = specifier else { - imports_only_types = false; - continue; - }; - if specifier.type_token().is_none() { - if specifier - .local_name() - .and_then(|local_name| { - Some(is_only_used_as_type( - model, - local_name.as_js_identifier_binding()?, - )) - }) - .unwrap_or(false) - { - specifiers_requiring_type_marker.push(specifier); - } else { + if has_type_token { + let useless_type_tokens: Vec<_> = specifiers + .iter() + .filter_map(|specifier| specifier.ok()?.type_token()) + .collect(); + if useless_type_tokens.is_empty() { + None + } else { + Some(NamedImportTypeFix::RemoveInlineTypeQualifiers( + useless_type_tokens, + )) + } + } else { + let mut imports_only_types = true; + let mut specifiers_requiring_type_marker = Vec::with_capacity(specifiers.len()); + for specifier in specifiers.iter() { + let Ok(specifier) = specifier else { imports_only_types = false; + continue; + }; + if specifier.type_token().is_none() { + if specifier + .local_name() + .and_then(|local_name| { + Some(is_only_used_as_type( + model, + local_name.as_js_identifier_binding()?, + )) + }) + .unwrap_or(false) + { + specifiers_requiring_type_marker.push(specifier); + } else { + imports_only_types = false; + } } } - } - if imports_only_types { - Some(NamedImportTypeFix::UseImportType( - specifiers_requiring_type_marker, - )) - } else if specifiers_requiring_type_marker.is_empty() { - None - } else { - Some(NamedImportTypeFix::AddInlineTypeQualifiers( - specifiers_requiring_type_marker, - )) + if imports_only_types { + Some(NamedImportTypeFix::UseImportType( + specifiers_requiring_type_marker, + )) + } else if specifiers_requiring_type_marker.is_empty() { + None + } else { + Some(NamedImportTypeFix::AddInlineTypeQualifiers( + specifiers_requiring_type_marker, + )) + } } } diff --git a/crates/biome_js_analyze/src/lint/style/use_literal_enum_members.rs b/crates/biome_js_analyze/src/lint/style/use_literal_enum_members.rs index 9902da9a07f6..a5f068fb0e93 100644 --- a/crates/biome_js_analyze/src/lint/style/use_literal_enum_members.rs +++ b/crates/biome_js_analyze/src/lint/style/use_literal_enum_members.rs @@ -75,7 +75,7 @@ declare_lint_rule! { impl Rule for UseLiteralEnumMembers { type Query = Ast; type State = TextRange; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { @@ -83,13 +83,13 @@ impl Rule for UseLiteralEnumMembers { let mut result = Vec::new(); let mut enum_member_names = FxHashSet::default(); let Ok(enum_name) = enum_declaration.id() else { - return result; + return result.into_boxed_slice(); }; let Some(enum_name) = enum_name .as_js_identifier_binding() .and_then(|x| x.name_token().ok()) else { - return result; + return result.into_boxed_slice(); }; let enum_name = enum_name.text_trimmed(); for enum_member in enum_declaration.members() { @@ -111,7 +111,7 @@ impl Rule for UseLiteralEnumMembers { } } } - result + result.into_boxed_slice() } fn diagnostic( diff --git a/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs b/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs index 1a5dd54ed6a3..ca7ccf3cc432 100644 --- a/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs +++ b/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs @@ -248,7 +248,7 @@ declare_lint_rule! { /// /// Note that some declarations are always ignored. /// You cannot apply a convention to them. - /// This is the cas eof: + /// This is the case of: /// /// - Member names that are not identifiers /// @@ -946,7 +946,7 @@ fn renamable( pub struct NamingConventionOptions { /// If `false`, then consecutive uppercase are allowed in _camel_ and _pascal_ cases. /// This does not affect other [Case]. - #[serde(default = "enabled", skip_serializing_if = "is_enabled")] + #[serde(default = "enabled", skip_serializing_if = "bool::clone")] pub strict_case: bool, /// If `false`, then non-ASCII characters are allowed. @@ -954,8 +954,8 @@ pub struct NamingConventionOptions { pub require_ascii: bool, /// Custom conventions. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub conventions: Vec, + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub conventions: Box<[Convention]>, /// Allowed cases for _TypeScript_ `enum` member names. #[serde(default, skip_serializing_if = "is_default")] @@ -966,7 +966,7 @@ impl Default for NamingConventionOptions { Self { strict_case: true, require_ascii: false, - conventions: Vec::new(), + conventions: Vec::new().into_boxed_slice(), enum_member_case: Format::default(), } } @@ -975,9 +975,6 @@ impl Default for NamingConventionOptions { const fn enabled() -> bool { true } -const fn is_enabled(value: &bool) -> bool { - *value -} fn is_default(value: &T) -> bool { value == &T::default() } diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_catch_assign.rs b/crates/biome_js_analyze/src/lint/suspicious/no_catch_assign.rs index 0719a9657f16..86e78a395401 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_catch_assign.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_catch_assign.rs @@ -52,13 +52,12 @@ impl Rule for NoCatchAssign { // The first element of `State` is the reassignment of catch parameter, // the second element of `State` is the declaration of catch clause. type State = (TextRange, TextRange); - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); - fn run(ctx: &RuleContext) -> Vec { + fn run(ctx: &RuleContext) -> Self::Signals { let catch_clause = ctx.query(); let model = ctx.model(); - catch_clause .declaration() .and_then(|decl| { @@ -69,7 +68,7 @@ impl Rule for NoCatchAssign { .as_any_js_binding()? .as_js_identifier_binding()?; let catch_binding_syntax = catch_binding.syntax(); - let mut invalid_assignment = vec![]; + let mut invalid_assignment = Vec::new(); for reference in identifier_binding.all_writes(model) { invalid_assignment.push(( reference.syntax().text_trimmed_range(), @@ -80,6 +79,7 @@ impl Rule for NoCatchAssign { Some(invalid_assignment) }) .unwrap_or_default() + .into_boxed_slice() } fn diagnostic(_ctx: &RuleContext, state: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_class_assign.rs b/crates/biome_js_analyze/src/lint/suspicious/no_class_assign.rs index cb91cb816aed..c0d50889bf1c 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_class_assign.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_class_assign.rs @@ -77,7 +77,7 @@ declare_lint_rule! { impl Rule for NoClassAssign { type Query = Semantic; type State = Reference; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { @@ -86,11 +86,14 @@ impl Rule for NoClassAssign { if let Some(id) = node.id() { if let Some(id_binding) = id.as_js_identifier_binding() { - return id_binding.all_writes(model).collect(); + return id_binding + .all_writes(model) + .collect::>() + .into_boxed_slice(); } } - Vec::new() + Vec::new().into_boxed_slice() } fn diagnostic(ctx: &RuleContext, reference: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_console.rs b/crates/biome_js_analyze/src/lint/suspicious/no_console.rs index 989cfe54d3c7..8307f48308b3 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_console.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_console.rs @@ -70,7 +70,7 @@ impl Rule for NoConsole { .options() .allow .iter() - .any(|allowed| allowed == member_name) + .any(|allowed| allowed.as_ref() == member_name) { return None; } @@ -122,5 +122,5 @@ impl Rule for NoConsole { #[serde(deny_unknown_fields)] pub struct NoConsoleOptions { /// Allowed calls on the console object. - pub allow: Vec, + pub allow: Box<[Box]>, } diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_control_characters_in_regex.rs b/crates/biome_js_analyze/src/lint/suspicious/no_control_characters_in_regex.rs index 00403108a429..11a64da69773 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_control_characters_in_regex.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_control_characters_in_regex.rs @@ -90,7 +90,7 @@ fn collect_control_characters( flags: &str, is_pattern_in_str: bool, ) -> Option> { - let mut control_chars: Vec = Vec::new(); + let mut control_chars = Vec::new(); let is_unicode_flag_set = flags.contains('u') || flags.contains('v'); let bytes = pattern.as_bytes(); let mut iter = pattern.bytes().enumerate(); @@ -196,7 +196,7 @@ fn collect_control_characters_from_expression( impl Rule for NoControlCharactersInRegex { type Query = Ast; type State = TextRange; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { @@ -223,6 +223,7 @@ impl Rule for NoControlCharactersInRegex { .unwrap_or_default() } } + .into_boxed_slice() } fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_case.rs b/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_case.rs index cad413ba6b41..8e13a86c856c 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_case.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_case.rs @@ -92,7 +92,7 @@ declare_lint_rule! { impl Rule for NoDuplicateCase { type Query = Ast; type State = (TextRange, TextRange); - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { @@ -113,7 +113,7 @@ impl Rule for NoDuplicateCase { } } } - signals + signals.into_boxed_slice() } fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_class_members.rs b/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_class_members.rs index 398c42ef346e..471cd7afbad2 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_class_members.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_class_members.rs @@ -179,7 +179,7 @@ struct MemberState { impl Rule for NoDuplicateClassMembers { type Query = Ast; type State = AnyClassMemberDefinition; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { @@ -215,7 +215,8 @@ impl Rule for NoDuplicateClassMembers { None }) - .collect() + .collect::>() + .into_boxed_slice() } fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_object_keys.rs b/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_object_keys.rs index 6355af12c287..b568d75e77c5 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_object_keys.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_duplicate_object_keys.rs @@ -213,14 +213,14 @@ impl DefinedProperty { impl Rule for NoDuplicateObjectKeys { type Query = Ast; type State = PropertyConflict; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { let node = ctx.query(); let mut defined_properties = FxHashMap::default(); - let mut signals: Self::Signals = Vec::new(); + let mut signals = Vec::new(); for member_definition in node .members() @@ -251,7 +251,7 @@ impl Rule for NoDuplicateObjectKeys { } } - signals + signals.into_boxed_slice() } fn diagnostic( diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_fallthrough_switch_clause.rs b/crates/biome_js_analyze/src/lint/suspicious/no_fallthrough_switch_clause.rs index 6104427252b0..68394b2696bb 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_fallthrough_switch_clause.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_fallthrough_switch_clause.rs @@ -66,16 +66,16 @@ declare_lint_rule! { impl Rule for NoFallthroughSwitchClause { type Query = ControlFlowGraph; type State = TextRange; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { let cfg = ctx.query(); - let mut fallthrough: Vec = vec![]; + let mut fallthrough = Vec::new(); // Return early if the graph doesn't contain any switch statements. // This avoids to allocate some memory. if !has_switch_statement(&cfg.node) { - return fallthrough; + return fallthrough.into_boxed_slice(); } // block to process. let mut block_stack = vec![ROOT_BLOCK_ID]; @@ -189,7 +189,7 @@ impl Rule for NoFallthroughSwitchClause { } switch_clauses.clear(); } - fallthrough + fallthrough.into_boxed_slice() } fn diagnostic( diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_function_assign.rs b/crates/biome_js_analyze/src/lint/suspicious/no_function_assign.rs index 8102a184545b..a9266e249ff4 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_function_assign.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_function_assign.rs @@ -104,7 +104,7 @@ declare_lint_rule! { pub struct State { id: JsIdentifierBinding, - all_writes: Vec, + all_writes: Box<[Reference]>, } impl Rule for NoFunctionAssign { @@ -126,7 +126,7 @@ impl Rule for NoFunctionAssign { } else { Some(State { id: id.clone(), - all_writes, + all_writes: all_writes.into_boxed_slice(), }) } } diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_global_assign.rs b/crates/biome_js_analyze/src/lint/suspicious/no_global_assign.rs index d75b92a10560..5e4df7d806b0 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_global_assign.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_global_assign.rs @@ -50,12 +50,12 @@ declare_lint_rule! { impl Rule for NoGlobalAssign { type Query = SemanticServices; type State = TextRange; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { let global_refs = ctx.query().all_unresolved_references(); - let mut result = vec![]; + let mut result = Vec::new(); for global_ref in global_refs { let is_write = global_ref.syntax().kind() == JsSyntaxKind::JS_IDENTIFIER_ASSIGNMENT; if is_write { @@ -67,7 +67,7 @@ impl Rule for NoGlobalAssign { } } } - result + result.into_boxed_slice() } fn diagnostic(_ctx: &RuleContext, range: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_import_assign.rs b/crates/biome_js_analyze/src/lint/suspicious/no_import_assign.rs index 0b7239be08e4..88ae2e5ccab1 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_import_assign.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_import_assign.rs @@ -59,12 +59,12 @@ impl Rule for NoImportAssign { type Query = Semantic; /// The first element of the tuple is the invalid `JsIdentifierAssignment`, the second element of the tuple is the imported `JsIdentifierBinding`. type State = (JsIdentifierAssignment, JsIdentifierBinding); - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); - fn run(ctx: &RuleContext) -> Vec { + fn run(ctx: &RuleContext) -> Self::Signals { let label_statement = ctx.query(); - let mut invalid_assign_list = vec![]; + let mut invalid_assign_list = Vec::new(); let local_name_binding = match label_statement { // `import {x as xx} from 'y'` // ^^^^^^^ @@ -102,6 +102,7 @@ impl Rule for NoImportAssign { Some(invalid_assign_list) }) .unwrap_or_default() + .into_boxed_slice() } fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_redeclare.rs b/crates/biome_js_analyze/src/lint/suspicious/no_redeclare.rs index 93bf20c887d6..7b88df8d155e 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_redeclare.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_redeclare.rs @@ -72,7 +72,7 @@ declare_lint_rule! { #[derive(Debug)] pub struct Redeclaration { - name: String, + name: Box, declaration: TextRange, redeclaration: TextRange, } @@ -80,7 +80,7 @@ pub struct Redeclaration { impl Rule for NoRedeclare { type Query = SemanticServices; type State = Redeclaration; - type Signals = Vec; + type Signals = Box<[Redeclaration]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { @@ -88,7 +88,7 @@ impl Rule for NoRedeclare { for scope in ctx.query().scopes() { check_redeclarations_in_single_scope(&scope, &mut redeclarations); } - redeclarations + redeclarations.into_boxed_slice() } fn diagnostic(_ctx: &RuleContext, state: &Self::State) -> Option { @@ -101,13 +101,13 @@ impl Rule for NoRedeclare { rule_category!(), redeclaration, markup! { - "Shouldn't redeclare '"{ name }"'. Consider to delete it or rename it." + "Shouldn't redeclare '"{ name.as_ref() }"'. Consider to delete it or rename it." }, ) .detail( declaration, markup! { - "'"{ name }"' is defined here:" + "'"{ name.as_ref() }"' is defined here:" }, ); Some(diag) @@ -172,7 +172,7 @@ fn check_redeclarations_in_single_scope(scope: &Scope, redeclarations: &mut Vec< && first_decl.syntax().parent() != decl.syntax().parent()) { redeclarations.push(Redeclaration { - name, + name: name.into_boxed_str(), declaration: *first_text_range, redeclaration: id_binding.syntax().text_trimmed_range(), }) diff --git a/crates/biome_js_analyze/src/lint/suspicious/use_getter_return.rs b/crates/biome_js_analyze/src/lint/suspicious/use_getter_return.rs index 2fc5fcdea990..71f37d5d83e2 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/use_getter_return.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/use_getter_return.rs @@ -69,7 +69,7 @@ declare_lint_rule! { impl Rule for UseGetterReturn { type Query = ControlFlowGraph; type State = InvalidGetterReturn; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { @@ -78,7 +78,7 @@ impl Rule for UseGetterReturn { let mut invalid_returns = Vec::new(); if !JsGetterClassMember::can_cast(node_kind) && !JsGetterObjectMember::can_cast(node_kind) { // The node is not a getter. - return invalid_returns; + return invalid_returns.into_boxed_slice(); } // stack of blocks to process let mut block_stack = vec![ROOT_BLOCK_ID]; @@ -130,7 +130,7 @@ impl Rule for UseGetterReturn { } } } - invalid_returns + invalid_returns.into_boxed_slice() } fn diagnostic(ctx: &RuleContext, invalid_return: &Self::State) -> Option { diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 32283ef96874..3a32afd8290d 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -111,8 +111,13 @@ pub type NoGlobalIsFinite = pub type NoGlobalIsNan = ::Options; pub type NoGlobalObjectCalls = < lint :: correctness :: no_global_object_calls :: NoGlobalObjectCalls as biome_analyze :: Rule > :: Options ; +pub type NoHeadElement = + ::Options; +pub type NoHeadImportInDocument = < lint :: nursery :: no_head_import_in_document :: NoHeadImportInDocument as biome_analyze :: Rule > :: Options ; pub type NoHeaderScope = ::Options; +pub type NoImgElement = + ::Options; pub type NoImplicitAnyLet = ::Options; pub type NoImplicitBoolean = diff --git a/crates/biome_js_analyze/src/syntax/correctness/no_duplicate_private_class_members.rs b/crates/biome_js_analyze/src/syntax/correctness/no_duplicate_private_class_members.rs index 25ba5240342d..09e9d5577130 100644 --- a/crates/biome_js_analyze/src/syntax/correctness/no_duplicate_private_class_members.rs +++ b/crates/biome_js_analyze/src/syntax/correctness/no_duplicate_private_class_members.rs @@ -30,13 +30,12 @@ enum MemberType { impl Rule for NoDuplicatePrivateClassMembers { type Query = Ast; - type State = (String, TextRange); - type Signals = Vec; + type State = (Box, TextRange); + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { - let mut defined_members: FxHashMap> = FxHashMap::default(); - + let mut defined_members: FxHashMap, FxHashSet> = FxHashMap::default(); let node = ctx.query(); node.into_iter() .filter_map(|member| { @@ -44,7 +43,11 @@ impl Rule for NoDuplicatePrivateClassMembers { .name() .ok()?? .as_js_private_class_member_name()? - .text(); + .id_token() + .ok()? + .text_trimmed() + .to_string() + .into_boxed_str(); let member_type = match member { AnyJsClassMember::JsGetterClassMember(_) => MemberType::Getter, AnyJsClassMember::JsMethodClassMember(_) => MemberType::Normal, @@ -71,17 +74,16 @@ impl Rule for NoDuplicatePrivateClassMembers { None }) - .collect() + .collect::>() + .into_boxed_slice() } fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { let (member_name, range) = state; - let diagnostic = RuleDiagnostic::new( + Some(RuleDiagnostic::new( rule_category!(), range, - format!("Duplicate private class member {member_name:?}"), - ); - - Some(diagnostic) + format!("Duplicate private class member \"#{member_name}\""), + )) } } diff --git a/crates/biome_js_analyze/tests/quick_test.rs b/crates/biome_js_analyze/tests/quick_test.rs index 3da9ec568628..5ff05a2ad6cb 100644 --- a/crates/biome_js_analyze/tests/quick_test.rs +++ b/crates/biome_js_analyze/tests/quick_test.rs @@ -13,9 +13,8 @@ use std::{ffi::OsStr, fs::read_to_string, path::Path, slice}; // use this test check if your snippet produces the diagnostics you wish, without using a snapshot #[ignore] #[test] -fn run_test() { - let input_file = - Path::new("tests/specs/correctness/useExhaustiveDependencies/ignoredDependencies.js"); +fn quick_test() { + let input_file = Path::new("tests/specs/nursery/useSortedClasses/issue.jsx"); let file_name = input_file.file_name().and_then(OsStr::to_str).unwrap(); let (group, rule) = parse_test_path(input_file); diff --git a/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx b/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx new file mode 100644 index 000000000000..2bd7ec6335c5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx @@ -0,0 +1,17 @@ +function MyComponent() { + return ( +
{line || <> }
+ ) +} + +function MyComponent2() { + return ( +
{<> }
+ ) +} + +function MyComponent3() { + return ( +
{value ?? <> }
+ ) +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx.snap b/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx.snap new file mode 100644 index 000000000000..4dc837242e07 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx.snap @@ -0,0 +1,24 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: issue_4059.jsx +--- +# Input +```jsx +function MyComponent() { + return ( +
{line || <> }
+ ) +} + +function MyComponent2() { + return ( +
{<> }
+ ) +} + +function MyComponent3() { + return ( +
{value ?? <> }
+ ) +} +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnreachable/SuppressionComments.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnreachable/SuppressionComments.js.snap index 0f1ed4c0a12e..6dfcb5c4920d 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnreachable/SuppressionComments.js.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnreachable/SuppressionComments.js.snap @@ -92,7 +92,7 @@ SuppressionComments.js:11:5 suppressions/deprecatedSuppressionComment FIXABLE ``` SuppressionComments.js:1:1 suppressions/unused ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! Suppression comment is not being used + ! Suppression comment has no effect. Remove the suppression or make sure you are suppressing the correct rule. > 1 β”‚ // rome-ignore lint/correctness/noUnreachable: this comment does nothing β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/biome_js_analyze/tests/specs/correctness/noVoidTypeReturn/valid.ts b/crates/biome_js_analyze/tests/specs/correctness/noVoidTypeReturn/valid.ts index 3b97ec4aeca2..44d8a6049640 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noVoidTypeReturn/valid.ts +++ b/crates/biome_js_analyze/tests/specs/correctness/noVoidTypeReturn/valid.ts @@ -13,3 +13,7 @@ class B { function f(): void { return; } + +function g(): void { + return void 0; +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noVoidTypeReturn/valid.ts.snap b/crates/biome_js_analyze/tests/specs/correctness/noVoidTypeReturn/valid.ts.snap index b206ff930924..541761a26e85 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noVoidTypeReturn/valid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noVoidTypeReturn/valid.ts.snap @@ -20,6 +20,8 @@ function f(): void { return; } -``` - +function g(): void { + return void 0; +} +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/ignoredDependencies.js.snap b/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/ignoredDependencies.js.snap index 766207a60e42..31b558b07d3a 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/ignoredDependencies.js.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/ignoredDependencies.js.snap @@ -57,7 +57,7 @@ ignoredDependencies.js:8:5 lint/correctness/useExhaustiveDependencies ━━━ ``` ignoredDependencies.js:16:5 suppressions/unused ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! Suppression comment is not being used + ! Suppression comment has no effect. Remove the suppression or make sure you are suppressing the correct rule. 14 β”‚ function IgnoredDependencies2() { 15 β”‚ let a = 1; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx new file mode 100644 index 000000000000..c5f12a3b2fb7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx @@ -0,0 +1,3 @@ + + No diagnostic + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx.snap new file mode 100644 index 000000000000..6b213094e0c7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx.snap @@ -0,0 +1,12 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsx +--- +# Input +```jsx + + No diagnostic + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx new file mode 100644 index 000000000000..c426e2e2e3d4 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx @@ -0,0 +1,3 @@ + + Invalid + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx.snap new file mode 100644 index 000000000000..96d3af536e97 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx.snap @@ -0,0 +1,27 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.jsx +--- +# Input +```jsx + + Invalid + + +``` + +# Diagnostics +``` +invalid.jsx:1:1 lint/nursery/noHeadElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use element. + + > 1 β”‚ + β”‚ ^^^^^^ + 2 β”‚ Invalid + 3 β”‚ + + i Using the element can cause unexpected behavior in a Next.js application. Use from next/head instead. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx new file mode 100644 index 000000000000..4281d09e26aa --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx @@ -0,0 +1,4 @@ + + Valid + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx.snap new file mode 100644 index 000000000000..9c402b696a51 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsx +--- +# Input +```jsx + + Valid + + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/app/_document.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/app/_document.jsx new file mode 100644 index 000000000000..52b51acb60f6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/app/_document.jsx @@ -0,0 +1 @@ +import Head from "next/head"; \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/app/_document.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/app/_document.jsx.snap new file mode 100644 index 000000000000..6383b1de66a4 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/app/_document.jsx.snap @@ -0,0 +1,9 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: _document.jsx +--- +# Input +```jsx +import Head from "next/head"; +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document.jsx new file mode 100644 index 000000000000..52b51acb60f6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document.jsx @@ -0,0 +1 @@ +import Head from "next/head"; \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document.jsx.snap new file mode 100644 index 000000000000..7713d3276bcd --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document.jsx.snap @@ -0,0 +1,23 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: _document.jsx +--- +# Input +```jsx +import Head from "next/head"; +``` + +# Diagnostics +``` +_document.jsx:1:1 lint/nursery/noHeadImportInDocument ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use next/head in pages/_document.jsx + + > 1 β”‚ import Head from "next/head"; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using the next/head in document pages can cause unexpected issues. Use from next/document instead. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document/index.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document/index.jsx new file mode 100644 index 000000000000..52b51acb60f6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document/index.jsx @@ -0,0 +1 @@ +import Head from "next/head"; \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document/index.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document/index.jsx.snap new file mode 100644 index 000000000000..d5dc0ffbe4f6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/_document/index.jsx.snap @@ -0,0 +1,23 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: index.jsx +--- +# Input +```jsx +import Head from "next/head"; +``` + +# Diagnostics +``` +index.jsx:1:1 lint/nursery/noHeadImportInDocument ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use next/head in pages/_document/index.jsx + + > 1 β”‚ import Head from "next/head"; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using the next/head in document pages can cause unexpected issues. Use from next/document instead. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/index.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/index.jsx new file mode 100644 index 000000000000..52b51acb60f6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/index.jsx @@ -0,0 +1 @@ +import Head from "next/head"; \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/index.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/index.jsx.snap new file mode 100644 index 000000000000..477d390af909 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadImportInDocument/pages/index.jsx.snap @@ -0,0 +1,9 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: index.jsx +--- +# Input +```jsx +import Head from "next/head"; +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx new file mode 100644 index 000000000000..c4ed80a56d64 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx @@ -0,0 +1,7 @@ +<> + Foo + +
+ Foo +
+ diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx.snap new file mode 100644 index 000000000000..3140dd7edddc --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx.snap @@ -0,0 +1,49 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: invalid.jsx +--- +# Input +```jsx +<> + Foo + +
+ Foo +
+ + +``` + +# Diagnostics +``` +invalid.jsx:2:2 lint/nursery/noImgElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use element. + + 1 β”‚ <> + > 2 β”‚ Foo + β”‚ ^^^^^^^^^^^^^^^^^ + 3 β”‚ + 4 β”‚
+ + i Using the can lead to slower LCP and higher bandwidth. Consider using from next/image to automatically optimize images. + + +``` + +``` +invalid.jsx:5:3 lint/nursery/noImgElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use element. + + 4 β”‚
+ > 5 β”‚ Foo + β”‚ ^^^^^^^^^^^^^^^^^ + 6 β”‚
+ 7 β”‚ + + i Using the can lead to slower LCP and higher bandwidth. Consider using from next/image to automatically optimize images. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx new file mode 100644 index 000000000000..f47d1e4ada55 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx @@ -0,0 +1,7 @@ +<> + + + + Foo + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx.snap new file mode 100644 index 000000000000..2fb934209539 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsx +--- +# Input +```jsx +<> + + + + Foo + + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noSecrets/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noSecrets/invalid.js index f9a465f17bcf..c64c6425d3a7 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noSecrets/invalid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noSecrets/invalid.js @@ -1,13 +1,24 @@ -const awsApiKey = "AKIA1234567890EXAMPLE" +const JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" +const JWT_BASE64 = "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJbXRwWkNJNkltRmtaVzUwYVdGc0lpd2laR1ZtYjNkellUWXpJaXdpYVdGMElqb3hPREF3T0RNeE9UZzFPVGs1TkN3aWZRPT0" const slackToken = "xoxb-not-a-real-token-this-will-not-work"; +const awsApiKey = "AKIA1234567890EXAMPLE" const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; -const clientSecret = "abcdefghijklmnopqrstuvwxyz" -const herokuApiKey = "heroku_api_key_1234abcd-1234-1234-1234-1234abcd5678"; -const genericSecret = "secret_1234567890abcdefghijklmnopqrstuvwxyz"; -const genericApiKey = "api_key_1234567890abcdefghijklmnopqrstuvwxyz"; +const hexEncodedSecret = "4d79207365706f7261746f722068656c6c6f20776f726c6421"; +const base64Secret = "TXkgc2VjcmV0IGtleSBwYXNzd29yZA=="; const slackKey = "https://hooks.slack.com/services/T12345678/B12345678/abcdefghijklmnopqrstuvwx" const twilioApiKey = "SK1234567890abcdefghijklmnopqrstuv"; const dbUrl = "postgres://user:password123@example.com:5432/dbname"; +const A_SECRET = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; +const A_LOWERCASE_SECRET = "zwvtjpqsdhwrgl204hc51ycsritmizn8b=/p9uyex7xu6kkagqfm3fj+oobldneva"; +const A_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F"; +const VAULT = { + token: "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F" +}; + +// TODO: Get these to work, they seem common and important +// const herokuApiKey = "abcd1234-5678-90ef-ghij-klmnopqrstuv"; +// const BASIC_AUTH_HEADER = "Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l"; +// const password = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noSecrets/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noSecrets/invalid.js.snap index fde82b9501ff..8ddf29a626ce 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noSecrets/invalid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noSecrets/invalid.js.snap @@ -4,39 +4,51 @@ expression: invalid.js --- # Input ```jsx -const awsApiKey = "AKIA1234567890EXAMPLE" +const JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" +const JWT_BASE64 = "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJbXRwWkNJNkltRmtaVzUwYVdGc0lpd2laR1ZtYjNkellUWXpJaXdpYVdGMElqb3hPREF3T0RNeE9UZzFPVGs1TkN3aWZRPT0" const slackToken = "xoxb-not-a-real-token-this-will-not-work"; +const awsApiKey = "AKIA1234567890EXAMPLE" const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; -const clientSecret = "abcdefghijklmnopqrstuvwxyz" -const herokuApiKey = "heroku_api_key_1234abcd-1234-1234-1234-1234abcd5678"; -const genericSecret = "secret_1234567890abcdefghijklmnopqrstuvwxyz"; -const genericApiKey = "api_key_1234567890abcdefghijklmnopqrstuvwxyz"; +const hexEncodedSecret = "4d79207365706f7261746f722068656c6c6f20776f726c6421"; +const base64Secret = "TXkgc2VjcmV0IGtleSBwYXNzd29yZA=="; const slackKey = "https://hooks.slack.com/services/T12345678/B12345678/abcdefghijklmnopqrstuvwx" const twilioApiKey = "SK1234567890abcdefghijklmnopqrstuv"; const dbUrl = "postgres://user:password123@example.com:5432/dbname"; +const A_SECRET = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; +const A_LOWERCASE_SECRET = "zwvtjpqsdhwrgl204hc51ycsritmizn8b=/p9uyex7xu6kkagqfm3fj+oobldneva"; +const A_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F"; +const VAULT = { + token: "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F" +}; + +// TODO: Get these to work, they seem common and important +// const herokuApiKey = "abcd1234-5678-90ef-ghij-klmnopqrstuv"; +// const BASIC_AUTH_HEADER = "Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l"; +// const password = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; ``` # Diagnostics ``` -invalid.js:1:19 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.js:1:13 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Potential secret found. - > 1 β”‚ const awsApiKey = "AKIA1234567890EXAMPLE" - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^ - 2 β”‚ const slackToken = "xoxb-not-a-real-token-this-will-not-work"; - 3 β”‚ const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." + > 1 β”‚ const JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 2 β”‚ const JWT_BASE64 = "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJbXRwWkNJNkltRmtaVzUwYVdGc0lpd2laR1ZtYjNkellUWXpJaXdpYVdGMElqb3hPREF3T0RNeE9UZzFPVGs1TkN3aWZRPT0" + 3 β”‚ const slackToken = "xoxb-not-a-real-token-this-will-not-work"; - i Type of secret detected: AWS API Key + i Type of secret detected: JSON Web Token (JWT) i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` @@ -46,172 +58,203 @@ invalid.js:2:20 lint/nursery/noSecrets ━━━━━━━━━━━━━ ! Potential secret found. - 1 β”‚ const awsApiKey = "AKIA1234567890EXAMPLE" - > 2 β”‚ const slackToken = "xoxb-not-a-real-token-this-will-not-work"; + 1 β”‚ const JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + > 2 β”‚ const JWT_BASE64 = "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJbXRwWkNJNkltRmtaVzUwYVdGc0lpd2laR1ZtYjNkellUWXpJaXdpYVdGMElqb3hPREF3T0RNeE9UZzFPVGs1TkN3aWZRPT0" + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 β”‚ const slackToken = "xoxb-not-a-real-token-this-will-not-work"; + 4 β”‚ const awsApiKey = "AKIA1234567890EXAMPLE" + + i Type of secret detected: Base64-encoded JWT + + i Storing secrets in source code is a security risk. Consider the following steps: + 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. + 2. If needed, use environment variables or a secure secret management system to store sensitive data. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations + + +``` + +``` +invalid.js:3:20 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Potential secret found. + + 1 β”‚ const JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + 2 β”‚ const JWT_BASE64 = "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJbXRwWkNJNkltRmtaVzUwYVdGc0lpd2laR1ZtYjNkellUWXpJaXdpYVdGMElqb3hPREF3T0RNeE9UZzFPVGs1TkN3aWZRPT0" + > 3 β”‚ const slackToken = "xoxb-not-a-real-token-this-will-not-work"; β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 3 β”‚ const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." - 4 β”‚ const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; + 4 β”‚ const awsApiKey = "AKIA1234567890EXAMPLE" + 5 β”‚ const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." i Type of secret detected: Slack Token i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` ``` -invalid.js:3:23 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.js:4:19 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Potential secret found. - 1 β”‚ const awsApiKey = "AKIA1234567890EXAMPLE" - 2 β”‚ const slackToken = "xoxb-not-a-real-token-this-will-not-work"; - > 3 β”‚ const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 4 β”‚ const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; - 5 β”‚ const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; + 2 β”‚ const JWT_BASE64 = "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJbXRwWkNJNkltRmtaVzUwYVdGc0lpd2laR1ZtYjNkellUWXpJaXdpYVdGMElqb3hPREF3T0RNeE9UZzFPVGs1TkN3aWZRPT0" + 3 β”‚ const slackToken = "xoxb-not-a-real-token-this-will-not-work"; + > 4 β”‚ const awsApiKey = "AKIA1234567890EXAMPLE" + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^ + 5 β”‚ const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." + 6 β”‚ const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; - i Type of secret detected: RSA Private Key + i Type of secret detected: AWS API Key i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` ``` -invalid.js:4:23 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.js:5:23 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Potential secret found. - 2 β”‚ const slackToken = "xoxb-not-a-real-token-this-will-not-work"; - 3 β”‚ const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." - > 4 β”‚ const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 5 β”‚ const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; - 6 β”‚ const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; + 3 β”‚ const slackToken = "xoxb-not-a-real-token-this-will-not-work"; + 4 β”‚ const awsApiKey = "AKIA1234567890EXAMPLE" + > 5 β”‚ const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 β”‚ const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; + 7 β”‚ const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; - i Type of secret detected: Facebook OAuth + i Type of secret detected: RSA Private Key i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` ``` -invalid.js:5:23 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.js:6:23 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Potential secret found. - 3 β”‚ const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." - 4 β”‚ const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; - > 5 β”‚ const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 6 β”‚ const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; - 7 β”‚ const clientSecret = "abcdefghijklmnopqrstuvwxyz" + 4 β”‚ const awsApiKey = "AKIA1234567890EXAMPLE" + 5 β”‚ const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." + > 6 β”‚ const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 7 β”‚ const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; + 8 β”‚ const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; - i Type of secret detected: Twitter OAuth + i Type of secret detected: Facebook OAuth i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` ``` -invalid.js:6:21 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.js:7:23 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Potential secret found. - 4 β”‚ const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; - 5 β”‚ const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; - > 6 β”‚ const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 7 β”‚ const clientSecret = "abcdefghijklmnopqrstuvwxyz" - 8 β”‚ const herokuApiKey = "heroku_api_key_1234abcd-1234-1234-1234-1234abcd5678"; + 5 β”‚ const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..." + 6 β”‚ const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; + > 7 β”‚ const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 β”‚ const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; + 9 β”‚ const hexEncodedSecret = "4d79207365706f7261746f722068656c6c6f20776f726c6421"; - i Type of secret detected: GitHub + i Type of secret detected: Twitter OAuth i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` ``` -invalid.js:7:22 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.js:8:21 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Potential secret found. - 5 β”‚ const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; - 6 β”‚ const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; - > 7 β”‚ const clientSecret = "abcdefghijklmnopqrstuvwxyz" - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 8 β”‚ const herokuApiKey = "heroku_api_key_1234abcd-1234-1234-1234-1234abcd5678"; - 9 β”‚ const genericSecret = "secret_1234567890abcdefghijklmnopqrstuvwxyz"; + 6 β”‚ const facebookToken = "facebook_app_id_12345abcde67890fghij12345"; + 7 β”‚ const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; + > 8 β”‚ const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 9 β”‚ const hexEncodedSecret = "4d79207365706f7261746f722068656c6c6f20776f726c6421"; + 10 β”‚ const base64Secret = "TXkgc2VjcmV0IGtleSBwYXNzd29yZA=="; - i Type of secret detected: The string has a high entropy value + i Type of secret detected: GitHub i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` ``` -invalid.js:9:23 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.js:9:26 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Potential secret found. - 7 β”‚ const clientSecret = "abcdefghijklmnopqrstuvwxyz" - 8 β”‚ const herokuApiKey = "heroku_api_key_1234abcd-1234-1234-1234-1234abcd5678"; - > 9 β”‚ const genericSecret = "secret_1234567890abcdefghijklmnopqrstuvwxyz"; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 10 β”‚ const genericApiKey = "api_key_1234567890abcdefghijklmnopqrstuvwxyz"; + 7 β”‚ const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz"; + 8 β”‚ const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; + > 9 β”‚ const hexEncodedSecret = "4d79207365706f7261746f722068656c6c6f20776f726c6421"; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 β”‚ const base64Secret = "TXkgc2VjcmV0IGtleSBwYXNzd29yZA=="; 11 β”‚ const slackKey = "https://hooks.slack.com/services/T12345678/B12345678/abcdefghijklmnopqrstuvwx" - i Type of secret detected: The string has a high entropy value + i Type of secret detected: Detected high entropy string i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` ``` -invalid.js:10:23 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.js:10:22 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Potential secret found. - 8 β”‚ const herokuApiKey = "heroku_api_key_1234abcd-1234-1234-1234-1234abcd5678"; - 9 β”‚ const genericSecret = "secret_1234567890abcdefghijklmnopqrstuvwxyz"; - > 10 β”‚ const genericApiKey = "api_key_1234567890abcdefghijklmnopqrstuvwxyz"; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 β”‚ const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz"; + 9 β”‚ const hexEncodedSecret = "4d79207365706f7261746f722068656c6c6f20776f726c6421"; + > 10 β”‚ const base64Secret = "TXkgc2VjcmV0IGtleSBwYXNzd29yZA=="; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 11 β”‚ const slackKey = "https://hooks.slack.com/services/T12345678/B12345678/abcdefghijklmnopqrstuvwx" 12 β”‚ const twilioApiKey = "SK1234567890abcdefghijklmnopqrstuv"; - i Type of secret detected: The string has a high entropy value + i Type of secret detected: Detected high entropy string i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` @@ -221,8 +264,8 @@ invalid.js:11:18 lint/nursery/noSecrets ━━━━━━━━━━━━━ ! Potential secret found. - 9 β”‚ const genericSecret = "secret_1234567890abcdefghijklmnopqrstuvwxyz"; - 10 β”‚ const genericApiKey = "api_key_1234567890abcdefghijklmnopqrstuvwxyz"; + 9 β”‚ const hexEncodedSecret = "4d79207365706f7261746f722068656c6c6f20776f726c6421"; + 10 β”‚ const base64Secret = "TXkgc2VjcmV0IGtleSBwYXNzd29yZA=="; > 11 β”‚ const slackKey = "https://hooks.slack.com/services/T12345678/B12345678/abcdefghijklmnopqrstuvwx" β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 12 β”‚ const twilioApiKey = "SK1234567890abcdefghijklmnopqrstuv"; @@ -233,7 +276,8 @@ invalid.js:11:18 lint/nursery/noSecrets ━━━━━━━━━━━━━ i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` @@ -243,19 +287,20 @@ invalid.js:12:22 lint/nursery/noSecrets ━━━━━━━━━━━━━ ! Potential secret found. - 10 β”‚ const genericApiKey = "api_key_1234567890abcdefghijklmnopqrstuvwxyz"; + 10 β”‚ const base64Secret = "TXkgc2VjcmV0IGtleSBwYXNzd29yZA=="; 11 β”‚ const slackKey = "https://hooks.slack.com/services/T12345678/B12345678/abcdefghijklmnopqrstuvwx" > 12 β”‚ const twilioApiKey = "SK1234567890abcdefghijklmnopqrstuv"; β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 β”‚ const dbUrl = "postgres://user:password123@example.com:5432/dbname"; - 14 β”‚ + 14 β”‚ const A_SECRET = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; i Type of secret detected: Twilio API Key i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` @@ -269,14 +314,108 @@ invalid.js:13:15 lint/nursery/noSecrets ━━━━━━━━━━━━━ 12 β”‚ const twilioApiKey = "SK1234567890abcdefghijklmnopqrstuv"; > 13 β”‚ const dbUrl = "postgres://user:password123@example.com:5432/dbname"; β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 14 β”‚ + 14 β”‚ const A_SECRET = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; + 15 β”‚ const A_LOWERCASE_SECRET = "zwvtjpqsdhwrgl204hc51ycsritmizn8b=/p9uyex7xu6kkagqfm3fj+oobldneva"; i Type of secret detected: Password in URL i Storing secrets in source code is a security risk. Consider the following steps: 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. 2. If needed, use environment variables or a secure secret management system to store sensitive data. - 3. If this is a false positive, consider adding an inline disable comment. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations + + +``` + +``` +invalid.js:14:18 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Potential secret found. + + 12 β”‚ const twilioApiKey = "SK1234567890abcdefghijklmnopqrstuv"; + 13 β”‚ const dbUrl = "postgres://user:password123@example.com:5432/dbname"; + > 14 β”‚ const A_SECRET = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 15 β”‚ const A_LOWERCASE_SECRET = "zwvtjpqsdhwrgl204hc51ycsritmizn8b=/p9uyex7xu6kkagqfm3fj+oobldneva"; + 16 β”‚ const A_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F"; + + i Type of secret detected: Detected high entropy string + + i Storing secrets in source code is a security risk. Consider the following steps: + 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. + 2. If needed, use environment variables or a secure secret management system to store sensitive data. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations + + +``` + +``` +invalid.js:15:28 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Potential secret found. + + 13 β”‚ const dbUrl = "postgres://user:password123@example.com:5432/dbname"; + 14 β”‚ const A_SECRET = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; + > 15 β”‚ const A_LOWERCASE_SECRET = "zwvtjpqsdhwrgl204hc51ycsritmizn8b=/p9uyex7xu6kkagqfm3fj+oobldneva"; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 16 β”‚ const A_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F"; + 17 β”‚ const VAULT = { + + i Type of secret detected: Detected high entropy string + + i Storing secrets in source code is a security risk. Consider the following steps: + 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. + 2. If needed, use environment variables or a secure secret management system to store sensitive data. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations + + +``` + +``` +invalid.js:16:24 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Potential secret found. + + 14 β”‚ const A_SECRET = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; + 15 β”‚ const A_LOWERCASE_SECRET = "zwvtjpqsdhwrgl204hc51ycsritmizn8b=/p9uyex7xu6kkagqfm3fj+oobldneva"; + > 16 β”‚ const A_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F"; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 17 β”‚ const VAULT = { + 18 β”‚ token: "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F" + + i Type of secret detected: Detected high entropy string + + i Storing secrets in source code is a security risk. Consider the following steps: + 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. + 2. If needed, use environment variables or a secure secret management system to store sensitive data. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations + + +``` + +``` +invalid.js:18:10 lint/nursery/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Potential secret found. + + 16 β”‚ const A_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F"; + 17 β”‚ const VAULT = { + > 18 β”‚ token: "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F" + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 19 β”‚ }; + 20 β”‚ + + i Type of secret detected: Detected high entropy string + + i Storing secrets in source code is a security risk. Consider the following steps: + 1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree. + 2. If needed, use environment variables or a secure secret management system to store sensitive data. + 3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs. + This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noSecrets/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noSecrets/valid.js index 0f4cb540dee8..81c61b9c1444 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noSecrets/valid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noSecrets/valid.js @@ -4,3 +4,39 @@ const count = 10; const nonSecret = "hello world" const nonSecretLong = "hello world, this is a looong string which I needed to create for some reason" const dbUrl = `postgres://user:${process.env.DB_PASSWORD}@example.com:5432/dbname`; +const NOT_A_SECRET = "I'm not a secret, I think"; +const NOT_A_SECRET_TEMPLATE = `A template that isn't a secret. ${1+1} = 2`; +const CSS_CLASSNAME = "hey-it-s-a-css-class-not-a-secret and-neither-this-one"; + +// From user tests +const codeCheck = "\nconst DEFAULT_CONFIG = /* @__PURE__ */ bare.Config({})\n"; +const otpCheck = 'Verify OTP Google Mobile Authenticator (2FAS)' +const bitcoinString = { + key: "0 USD,,. for {bitlocus|string}.", +}; +const textString = { + key: 'Verifying takes 15 approved the following 3.' +}; +const facebookAndAwsString = { + key: 'facebook.com |console.aws.amazon.com' +}; +const IsoString = { + key: 'ISO-27001 information , GDPR' +}; + +// Postgres json path query +const isNumeric = '@.scoreDisplayMode == "numeric" || @.scoreDisplayMode == "metricSavings"' +const tailwindClassNames = 'whitespace-nowrap bg-base-4 px-1 text-[0.65rem] group-hover:w-auto group-hover:overflow-visible' +const tailwindConfigOptions = { + theme: { + animation: { + slideDown: 'slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1)', + } + } +} +export const url = 'https://www.nytimes.com/2024/03/05/arts/design/pritzker-prize-riken-yamamoto-architecture.html' + +// TODO: Remove these false positives, they unfortunately hurt the user experience. +// const NAMESPACE_CLASSNAME = 'Validation.JSONSchemaValidationUtilsImplFactory'; +// const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; +// const webpackFriendlyConsole = require('./config/webpack/webpackFriendlyConsole'); \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noSecrets/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noSecrets/valid.js.snap index 6a72ffebcdcd..a973ab6979aa 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noSecrets/valid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noSecrets/valid.js.snap @@ -10,5 +10,40 @@ const count = 10; const nonSecret = "hello world" const nonSecretLong = "hello world, this is a looong string which I needed to create for some reason" const dbUrl = `postgres://user:${process.env.DB_PASSWORD}@example.com:5432/dbname`; +const NOT_A_SECRET = "I'm not a secret, I think"; +const NOT_A_SECRET_TEMPLATE = `A template that isn't a secret. ${1+1} = 2`; +const CSS_CLASSNAME = "hey-it-s-a-css-class-not-a-secret and-neither-this-one"; +// From user tests +const codeCheck = "\nconst DEFAULT_CONFIG = /* @__PURE__ */ bare.Config({})\n"; +const otpCheck = 'Verify OTP Google Mobile Authenticator (2FAS)' +const bitcoinString = { + key: "0 USD,,. for {bitlocus|string}.", +}; +const textString = { + key: 'Verifying takes 15 approved the following 3.' +}; +const facebookAndAwsString = { + key: 'facebook.com |console.aws.amazon.com' +}; +const IsoString = { + key: 'ISO-27001 information , GDPR' +}; + +// Postgres json path query +const isNumeric = '@.scoreDisplayMode == "numeric" || @.scoreDisplayMode == "metricSavings"' +const tailwindClassNames = 'whitespace-nowrap bg-base-4 px-1 text-[0.65rem] group-hover:w-auto group-hover:overflow-visible' +const tailwindConfigOptions = { + theme: { + animation: { + slideDown: 'slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1)', + } + } +} +export const url = 'https://www.nytimes.com/2024/03/05/arts/design/pritzker-prize-riken-yamamoto-architecture.html' + +// TODO: Remove these false positives, they unfortunately hurt the user experience. +// const NAMESPACE_CLASSNAME = 'Validation.JSONSchemaValidationUtilsImplFactory'; +// const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; +// const webpackFriendlyConsole = require('./config/webpack/webpackFriendlyConsole'); ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts index 16fe67e58b09..1cb6ba5f394e 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts @@ -89,4 +89,7 @@ function fn() { return function (): string { return str; }; -} \ No newline at end of file +} + +const x = { prop: () => {} } +const x = { bar: { prop: () => {} } } \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts.snap index 9b84cc810709..f64962a5730d 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts.snap @@ -96,6 +96,9 @@ function fn() { return str; }; } + +const x = { prop: () => {} } +const x = { bar: { prop: () => {} } } ``` # Diagnostics @@ -527,3 +530,37 @@ invalid.ts:85:2 lint/nursery/useExplicitFunctionReturnType ━━━━━━━ ``` + +``` +invalid.ts:94:19 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 92 β”‚ } + 93 β”‚ + > 94 β”‚ const x = { prop: () => {} } + β”‚ ^^^^^^^^^ + 95 β”‚ const x = { bar: { prop: () => {} } } + + i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. + + i Add a return type annotation. + + +``` + +``` +invalid.ts:95:26 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 94 β”‚ const x = { prop: () => {} } + > 95 β”‚ const x = { bar: { prop: () => {} } } + β”‚ ^^^^^^^^^ + + i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. + + i Add a return type annotation. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts index 0c381b08a967..41f4283c04c6 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts @@ -47,6 +47,8 @@ node.addEventListener('click', function () {}); const foo = arr.map(i => i * i); fn(() => {}); fn(function () {}); +new Promise(resolve => {}); +new Foo(1, () => {}); [function () {}, () => {}]; (function () { console.log("This is an IIFE"); @@ -62,4 +64,42 @@ const arrowFn = () => (): void => {}; const arrowFn = () => function(): void {} const arrowFn = () => { return (): void => { }; -} \ No newline at end of file +} + + +// type assertion +const asTyped = (() => '') as () => string; +const castTyped = <() => string>(() => ''); + +// variable declarator with a type annotation +type FuncType = () => string; +const arrowFn: FuncType = () => 'test'; +const funcExpr: FuncType = function () { + return 'test'; +}; + +// default parameter with a type annotation +type CallBack = () => void; +const f = (gotcha: CallBack = () => { }): void => { }; +function f(gotcha: CallBack = () => {}): void {} + +// class property with a type annotation +type MethodType = () => void; +class App { + private method: MethodType = () => { }; +} + +// function as a property or a nested property of a typed object +const x: Foo = { prop: () => {} } +const x = { prop: () => {} } as Foo +const x = { prop: () => {} } + +const x: Foo = { bar: { prop: () => {} } } + +class Accumulator { + private count: number = 0; + public accumulate(fn: () => number): void { + this.count += fn(); + } +} +new Accumulator().accumulate(() => 1); \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts.snap index 80b8221a7297..45a4d83905d7 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts.snap @@ -53,6 +53,8 @@ node.addEventListener('click', function () {}); const foo = arr.map(i => i * i); fn(() => {}); fn(function () {}); +new Promise(resolve => {}); +new Foo(1, () => {}); [function () {}, () => {}]; (function () { console.log("This is an IIFE"); @@ -69,4 +71,42 @@ const arrowFn = () => function(): void {} const arrowFn = () => { return (): void => { }; } + + +// type assertion +const asTyped = (() => '') as () => string; +const castTyped = <() => string>(() => ''); + +// variable declarator with a type annotation +type FuncType = () => string; +const arrowFn: FuncType = () => 'test'; +const funcExpr: FuncType = function () { + return 'test'; +}; + +// default parameter with a type annotation +type CallBack = () => void; +const f = (gotcha: CallBack = () => { }): void => { }; +function f(gotcha: CallBack = () => {}): void {} + +// class property with a type annotation +type MethodType = () => void; +class App { + private method: MethodType = () => { }; +} + +// function as a property or a nested property of a typed object +const x: Foo = { prop: () => {} } +const x = { prop: () => {} } as Foo +const x = { prop: () => {} } + +const x: Foo = { bar: { prop: () => {} } } + +class Accumulator { + private count: number = 0; + public accumulate(fn: () => number): void { + this.count += fn(); + } +} +new Accumulator().accumulate(() => 1); ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/issue_4041.jsx b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/issue_4041.jsx new file mode 100644 index 000000000000..2983f707b47b --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/issue_4041.jsx @@ -0,0 +1 @@ +
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/issue_4041.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/issue_4041.jsx.snap new file mode 100644 index 000000000000..6852ac4f2f56 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/issue_4041.jsx.snap @@ -0,0 +1,9 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: issue_4041.jsx +--- +# Input +```jsx +
+ +``` diff --git a/crates/biome_js_analyze/tests/specs/style/useExportType/invalid.ts b/crates/biome_js_analyze/tests/specs/style/useExportType/invalid.ts index 901be26b8335..b02368290d37 100644 --- a/crates/biome_js_analyze/tests/specs/style/useExportType/invalid.ts +++ b/crates/biome_js_analyze/tests/specs/style/useExportType/invalid.ts @@ -38,3 +38,8 @@ export { import type * as Ns from "" export { Ns } + +import { type T9, type T10 } from "./mod.ts"; +export { type T9, type T10 }; + +export { type T11, type T12 } from "./mod.ts"; diff --git a/crates/biome_js_analyze/tests/specs/style/useExportType/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/style/useExportType/invalid.ts.snap index cb7624470d80..65831e5f9e06 100644 --- a/crates/biome_js_analyze/tests/specs/style/useExportType/invalid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/style/useExportType/invalid.ts.snap @@ -45,13 +45,26 @@ export { import type * as Ns from "" export { Ns } +import { type T9, type T10 } from "./mod.ts"; +export { type T9, type T10 }; + +export { type T11, type T12 } from "./mod.ts"; + ``` # Diagnostics ``` -invalid.ts:2:10 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:2:8 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! Several exports are only types and should thus use export type. + ! Some exports are only types. + + 1 β”‚ import { type T1, V1 } from "./mod.ts"; + > 2 β”‚ export { T1, V1 }; + β”‚ ^^^^^^^^^^^ + 3 β”‚ + 4 β”‚ import type { T2, T3 } from "./mod.ts"; + + i This export is a type. 1 β”‚ import { type T1, V1 } from "./mod.ts"; > 2 β”‚ export { T1, V1 }; @@ -59,9 +72,9 @@ invalid.ts:2:10 lint/style/useExportType FIXABLE ━━━━━━━━━ 3 β”‚ 4 β”‚ import type { T2, T3 } from "./mod.ts"; - i Using export type allows transpilers to safely drop exports of types without looking for their definition. + i Using export type allows compilers to safely drop exports of types without looking for their definition. - i Safe fix: Use inline type exports. + i Safe fix: Add inline type keywords. 2 β”‚ exportΒ·{Β·typeΒ·T1,Β·V1Β·}; β”‚ +++++ @@ -71,7 +84,7 @@ invalid.ts:2:10 lint/style/useExportType FIXABLE ━━━━━━━━━ ``` invalid.ts:5:8 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! All exports are only types and should thus use export type. + ! All exports are only types. 4 β”‚ import type { T2, T3 } from "./mod.ts"; > 5 β”‚ export { T2, T3 }; @@ -79,9 +92,9 @@ invalid.ts:5:8 lint/style/useExportType FIXABLE ━━━━━━━━━━ 6 β”‚ 7 β”‚ import type T4 from "./mod.ts"; - i Using export type allows transpilers to safely drop exports of types without looking for their definition. + i Using export type allows compilers to safely drop exports of types without looking for their definition. - i Safe fix: Use a grouped export type. + i Safe fix: Use export type. 5 β”‚ exportΒ·typeΒ·{Β·T2,Β·T3Β·}; β”‚ +++++ @@ -91,7 +104,7 @@ invalid.ts:5:8 lint/style/useExportType FIXABLE ━━━━━━━━━━ ``` invalid.ts:8:8 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! All exports are only types and should thus use export type. + ! All exports are only types. 7 β”‚ import type T4 from "./mod.ts"; > 8 β”‚ export { T4 }; @@ -99,9 +112,9 @@ invalid.ts:8:8 lint/style/useExportType FIXABLE ━━━━━━━━━━ 9 β”‚ 10 β”‚ // multiline - i Using export type allows transpilers to safely drop exports of types without looking for their definition. + i Using export type allows compilers to safely drop exports of types without looking for their definition. - i Safe fix: Use a grouped export type. + i Safe fix: Use export type. 8 β”‚ exportΒ·typeΒ·{Β·T4Β·}; β”‚ +++++ @@ -109,22 +122,44 @@ invalid.ts:8:8 lint/style/useExportType FIXABLE ━━━━━━━━━━ ``` ``` -invalid.ts:14:5 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:12:8 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! Several exports are only types and should thus use export type. + ! Some exports are only types. + + 10 β”‚ // multiline + 11 β”‚ import { type T5, type T6, V2 } from "./mod.ts"; + > 12 β”‚ export { + β”‚ ^ + > 13 β”‚ // leading comment + > 14 β”‚ T5, + > 15 β”‚ T6, + > 16 β”‚ V2, + > 17 β”‚ }; + β”‚ ^^ + 18 β”‚ + 19 β”‚ import type * as ns from "./mod.ts"; + + i This export is a type. 12 β”‚ export { 13 β”‚ // leading comment > 14 β”‚ T5, - β”‚ ^^^ + β”‚ ^^ + 15 β”‚ T6, + 16 β”‚ V2, + + i This export is a type. + + 13 β”‚ // leading comment + 14 β”‚ T5, > 15 β”‚ T6, β”‚ ^^ 16 β”‚ V2, 17 β”‚ }; - i Using export type allows transpilers to safely drop exports of types without looking for their definition. + i Using export type allows compilers to safely drop exports of types without looking for their definition. - i Safe fix: Use inline type exports. + i Safe fix: Add inline type keywords. 12 12 β”‚ export { 13 13 β”‚ // leading comment @@ -141,7 +176,7 @@ invalid.ts:14:5 lint/style/useExportType FIXABLE ━━━━━━━━━ ``` invalid.ts:20:8 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! All exports are only types and should thus use export type. + ! All exports are only types. 19 β”‚ import type * as ns from "./mod.ts"; > 20 β”‚ export { ns }; @@ -149,9 +184,9 @@ invalid.ts:20:8 lint/style/useExportType FIXABLE ━━━━━━━━━ 21 β”‚ 22 β”‚ interface Interface {} - i Using export type allows transpilers to safely drop exports of types without looking for their definition. + i Using export type allows compilers to safely drop exports of types without looking for their definition. - i Safe fix: Use a grouped export type. + i Safe fix: Use export type. 20 β”‚ exportΒ·typeΒ·{Β·nsΒ·}; β”‚ +++++ @@ -159,20 +194,38 @@ invalid.ts:20:8 lint/style/useExportType FIXABLE ━━━━━━━━━ ``` ``` -invalid.ts:27:10 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:27:8 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! Several exports are only types and should thus use export type. + ! Some exports are only types. + + 25 β”‚ function func() {} + 26 β”‚ class Class {} + > 27 β”‚ export { Interface, TypeAlias, Enum, func as f, Class }; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 28 β”‚ + 29 β”‚ export /*0*/ { /*1*/ type /*2*/ func /*3*/, /*4*/ type Class as C /*5*/ } /*6*/; + + i This export is a type. + + 25 β”‚ function func() {} + 26 β”‚ class Class {} + > 27 β”‚ export { Interface, TypeAlias, Enum, func as f, Class }; + β”‚ ^^^^^^^^^ + 28 β”‚ + 29 β”‚ export /*0*/ { /*1*/ type /*2*/ func /*3*/, /*4*/ type Class as C /*5*/ } /*6*/; + + i This export is a type. 25 β”‚ function func() {} 26 β”‚ class Class {} > 27 β”‚ export { Interface, TypeAlias, Enum, func as f, Class }; - β”‚ ^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^ 28 β”‚ 29 β”‚ export /*0*/ { /*1*/ type /*2*/ func /*3*/, /*4*/ type Class as C /*5*/ } /*6*/; - i Using export type allows transpilers to safely drop exports of types without looking for their definition. + i Using export type allows compilers to safely drop exports of types without looking for their definition. - i Safe fix: Use inline type exports. + i Safe fix: Add inline type keywords. 27 β”‚ exportΒ·{Β·typeΒ·Interface,Β·typeΒ·TypeAlias,Β·Enum,Β·funcΒ·asΒ·f,Β·ClassΒ·}; β”‚ +++++ +++++ @@ -182,7 +235,7 @@ invalid.ts:27:10 lint/style/useExportType FIXABLE ━━━━━━━━━ ``` invalid.ts:29:14 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! All exports are only types and should thus use export type. + ! All exports are only types. 27 β”‚ export { Interface, TypeAlias, Enum, func as f, Class }; 28 β”‚ @@ -191,9 +244,9 @@ invalid.ts:29:14 lint/style/useExportType FIXABLE ━━━━━━━━━ 30 β”‚ 31 β”‚ import { type T7, type T8 } from "./mod.ts"; - i Using export type allows transpilers to safely drop exports of types without looking for their definition. + i Using export type allows compilers to safely drop exports of types without looking for their definition. - i Safe fix: Use a grouped export type. + i Safe fix: Use export type. 27 27 β”‚ export { Interface, TypeAlias, Enum, func as f, Class }; 28 28 β”‚ @@ -208,7 +261,7 @@ invalid.ts:29:14 lint/style/useExportType FIXABLE ━━━━━━━━━ ``` invalid.ts:32:8 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! All exports are only types and should thus use export type. + ! All exports are only types. 31 β”‚ import { type T7, type T8 } from "./mod.ts"; > 32 β”‚ export { @@ -222,9 +275,9 @@ invalid.ts:32:8 lint/style/useExportType FIXABLE ━━━━━━━━━ 38 β”‚ 39 β”‚ import type * as Ns from "" - i Using export type allows transpilers to safely drop exports of types without looking for their definition. + i Using export type allows compilers to safely drop exports of types without looking for their definition. - i Safe fix: Use a grouped export type. + i Safe fix: Use export type. 30 30 β”‚ 31 31 β”‚ import { type T7, type T8 } from "./mod.ts"; @@ -245,18 +298,68 @@ invalid.ts:32:8 lint/style/useExportType FIXABLE ━━━━━━━━━ ``` invalid.ts:40:8 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! All exports are only types and should thus use export type. + ! All exports are only types. 39 β”‚ import type * as Ns from "" > 40 β”‚ export { Ns } β”‚ ^^^^^^ 41 β”‚ + 42 β”‚ import { type T9, type T10 } from "./mod.ts"; - i Using export type allows transpilers to safely drop exports of types without looking for their definition. + i Using export type allows compilers to safely drop exports of types without looking for their definition. - i Safe fix: Use a grouped export type. + i Safe fix: Use export type. 40 β”‚ exportΒ·typeΒ·{Β·NsΒ·} β”‚ +++++ ``` + +``` +invalid.ts:43:8 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! All exports are only types. + + 42 β”‚ import { type T9, type T10 } from "./mod.ts"; + > 43 β”‚ export { type T9, type T10 }; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^ + 44 β”‚ + 45 β”‚ export { type T11, type T12 } from "./mod.ts"; + + i Using export type allows compilers to safely drop exports of types without looking for their definition. + + i Safe fix: Use export type. + + 41 41 β”‚ + 42 42 β”‚ import { type T9, type T10 } from "./mod.ts"; + 43 β”‚ - exportΒ·{Β·typeΒ·T9,Β·typeΒ·T10Β·}; + 43 β”‚ + exportΒ·typeΒ·{Β·T9,Β·T10Β·}; + 44 44 β”‚ + 45 45 β”‚ export { type T11, type T12 } from "./mod.ts"; + + +``` + +``` +invalid.ts:45:8 lint/style/useExportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! All exports are only types. + + 43 β”‚ export { type T9, type T10 }; + 44 β”‚ + > 45 β”‚ export { type T11, type T12 } from "./mod.ts"; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 46 β”‚ + + i Using export type allows compilers to safely drop exports of types without looking for their definition. + + i Safe fix: Use export type. + + 43 43 β”‚ export { type T9, type T10 }; + 44 44 β”‚ + 45 β”‚ - exportΒ·{Β·typeΒ·T11,Β·typeΒ·T12Β·}Β·fromΒ·"./mod.ts"; + 45 β”‚ + exportΒ·typeΒ·{Β·T11,Β·T12Β·}Β·fromΒ·"./mod.ts"; + 46 46 β”‚ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/style/useExportType/valid.ts b/crates/biome_js_analyze/tests/specs/style/useExportType/valid.ts index 9659d32c79f1..a405ab8b5aaf 100644 --- a/crates/biome_js_analyze/tests/specs/style/useExportType/valid.ts +++ b/crates/biome_js_analyze/tests/specs/style/useExportType/valid.ts @@ -41,3 +41,12 @@ declare class AmbientFunction {} export { AmbientClass, AmbientEnum, AmbientFunction } export {} + +function f3() {} +class Class3 {} +export { type Class3, f3 } + +function f4() {} +export { f4 } + +export { type T1, V1 } from "./mod.ts"; diff --git a/crates/biome_js_analyze/tests/specs/style/useExportType/valid.ts.snap b/crates/biome_js_analyze/tests/specs/style/useExportType/valid.ts.snap index 0edcc931e75f..1405afc89d56 100644 --- a/crates/biome_js_analyze/tests/specs/style/useExportType/valid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/style/useExportType/valid.ts.snap @@ -48,4 +48,13 @@ export { AmbientClass, AmbientEnum, AmbientFunction } export {} +function f3() {} +class Class3 {} +export { type Class3, f3 } + +function f4() {} +export { f4 } + +export { type T1, V1 } from "./mod.ts"; + ``` diff --git a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-combined.ts.snap b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-combined.ts.snap index 13ee785fe212..29c44c4c3adf 100644 --- a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-combined.ts.snap +++ b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-combined.ts.snap @@ -37,20 +37,20 @@ export { T, type U }; # Diagnostics ``` -invalid-combined.ts:2:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-combined.ts:2:12 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! All these imports are only used as types. 1 β”‚ // Leading comment > 2 β”‚ import/*1*/A/*2*/ - β”‚ ^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^ > 3 β”‚ // Comma comment > 4 β”‚ ,/*3*/{ B }/*4*/from/*5*/""/*6*/; // Trailing comment - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 5 β”‚ // Comment 6 β”‚ export type { A, B }; - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. i Safe fix: Use import type. @@ -67,14 +67,14 @@ invalid-combined.ts:2:1 lint/style/useImportType FIXABLE ━━━━━━━ ``` ``` -invalid-combined.ts:8:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-combined.ts:8:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! The default import and some named imports are only used as types. 6 β”‚ export type { A, B }; 7 β”‚ > 8 β”‚ import C, { D, E, F } from ""; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^^^^^^^^^ 9 β”‚ export { type C, type D, type E, F }; 10 β”‚ @@ -96,7 +96,7 @@ invalid-combined.ts:8:1 lint/style/useImportType FIXABLE ━━━━━━━ 9 β”‚ export { type C, type D, type E, F }; 10 β”‚ - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. i Safe fix: Use import type. @@ -112,18 +112,18 @@ invalid-combined.ts:8:1 lint/style/useImportType FIXABLE ━━━━━━━ ``` ``` -invalid-combined.ts:11:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-combined.ts:11:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! All these imports are only used as types. 9 β”‚ export { type C, type D, type E, F }; 10 β”‚ > 11 β”‚ import G, { type H, I } from ""; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^ 12 β”‚ export type { G, H, I }; 13 β”‚ - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. i Safe fix: Use import type. @@ -139,20 +139,20 @@ invalid-combined.ts:11:1 lint/style/useImportType FIXABLE ━━━━━━ ``` ``` -invalid-combined.ts:15:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-combined.ts:15:13 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! All these imports are only used as types. 14 β”‚ // Leading comment > 15 β”‚ import /*1*/M/*2*/ - β”‚ ^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^ > 16 β”‚ // Comma comment > 17 β”‚ ,/*3*/*/*4*/as/*5*/N/*6*/from/*7*/""/*8*/;/*9*/ - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 β”‚ // Comment 19 β”‚ export type { M, N }; - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. i Safe fix: Use import type. @@ -170,18 +170,18 @@ invalid-combined.ts:15:1 lint/style/useImportType FIXABLE ━━━━━━ ``` ``` -invalid-combined.ts:21:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-combined.ts:21:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! The default import is only used as a type. 19 β”‚ export type { M, N }; 20 β”‚ > 21 β”‚ import O, * as P from ""; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^^^^ 22 β”‚ export { type O, P }; 23 β”‚ - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. i Safe fix: Use import type. @@ -208,7 +208,7 @@ invalid-combined.ts:24:11 lint/style/useImportType FIXABLE ━━━━━━ 25 β”‚ export { Q, type R }; 26 β”‚ - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. i Safe fix: Use import type. @@ -224,14 +224,14 @@ invalid-combined.ts:24:11 lint/style/useImportType FIXABLE ━━━━━━ ``` ``` -invalid-combined.ts:27:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-combined.ts:27:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Some named imports are only used as types. 25 β”‚ export { Q, type R }; 26 β”‚ > 27 β”‚ import T, { U } from ""; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^^^ 28 β”‚ export { T, type U }; 29 β”‚ @@ -244,13 +244,11 @@ invalid-combined.ts:27:1 lint/style/useImportType FIXABLE ━━━━━━ 28 β”‚ export { T, type U }; 29 β”‚ - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. - i Safe fix: Use import type. + i Safe fix: Add inline type keywords. 27 β”‚ importΒ·T,Β·{Β·typeΒ·UΒ·}Β·fromΒ·""; β”‚ +++++ ``` - - diff --git a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-default-imports.ts.snap b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-default-imports.ts.snap index d0318fb7dd5f..bfd18b741ed3 100644 --- a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-default-imports.ts.snap +++ b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-default-imports.ts.snap @@ -13,16 +13,16 @@ let a: A; # Diagnostics ``` -invalid-default-imports.ts:1:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-default-imports.ts:1:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! All these imports are only used as types. > 1 β”‚ import A from "" - β”‚ ^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^ 2 β”‚ type AA = A; 3 β”‚ export { type A }; - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. i Safe fix: Use import type. @@ -30,5 +30,3 @@ invalid-default-imports.ts:1:1 lint/style/useImportType FIXABLE ━━━━ β”‚ +++++ ``` - - diff --git a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-named-imports.ts b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-named-imports.ts index b956804017cd..26589a020c15 100644 --- a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-named-imports.ts +++ b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-named-imports.ts @@ -9,11 +9,10 @@ import { X, Y } from ""; type XX = X; const YY = Y; -//import { type U, V } from ""; -//type VV = V; +import { type H, type I, type J } from ""; +export type { H, I, J }; -import { type X, type Y, type Z } from ""; -export type { X, Y, Z }; +import type { type M, N, type O } from ""; // multiline import { diff --git a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-named-imports.ts.snap b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-named-imports.ts.snap index 27f83ea298df..3bb4fa401e19 100644 --- a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-named-imports.ts.snap +++ b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-named-imports.ts.snap @@ -15,11 +15,10 @@ import { X, Y } from ""; type XX = X; const YY = Y; -//import { type U, V } from ""; -//type VV = V; +import { type H, type I, type J } from ""; +export type { H, I, J }; -import { type X, type Y, type Z } from ""; -export type { X, Y, Z }; +import type { type M, N, type O } from ""; // multiline import { @@ -34,12 +33,12 @@ export { U, type V, type W }; # Diagnostics ``` -invalid-named-imports.ts:1:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-named-imports.ts:1:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Some named imports are only used as types. > 1 β”‚ import { A, B, C, D, E } from ""; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^ 2 β”‚ type AA = A; 3 β”‚ type BB = typeof B; @@ -64,9 +63,9 @@ invalid-named-imports.ts:1:1 lint/style/useImportType FIXABLE ━━━━━ 2 β”‚ type AA = A; 3 β”‚ type BB = typeof B; - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. - i Safe fix: Use import type. + i Safe fix: Add inline type keywords. 1 β”‚ importΒ·{Β·typeΒ·A,Β·typeΒ·B,Β·typeΒ·C,Β·D,Β·EΒ·}Β·fromΒ·""; β”‚ +++++ +++++ +++++ @@ -74,14 +73,14 @@ invalid-named-imports.ts:1:1 lint/style/useImportType FIXABLE ━━━━━ ``` ``` -invalid-named-imports.ts:8:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-named-imports.ts:8:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Some named imports are only used as types. 6 β”‚ const EE = E; 7 β”‚ > 8 β”‚ import { X, Y } from ""; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^^^ 9 β”‚ type XX = X; 10 β”‚ const YY = Y; @@ -94,9 +93,9 @@ invalid-named-imports.ts:8:1 lint/style/useImportType FIXABLE ━━━━━ 9 β”‚ type XX = X; 10 β”‚ const YY = Y; - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. - i Safe fix: Use import type. + i Safe fix: Add inline type keywords. 8 β”‚ importΒ·{Β·typeΒ·X,Β·YΒ·}Β·fromΒ·""; β”‚ +++++ @@ -104,81 +103,116 @@ invalid-named-imports.ts:8:1 lint/style/useImportType FIXABLE ━━━━━ ``` ``` -invalid-named-imports.ts:15:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-named-imports.ts:12:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! All these imports are only used as types. - 13 β”‚ //type VV = V; + 10 β”‚ const YY = Y; + 11 β”‚ + > 12 β”‚ import { type H, type I, type J } from ""; + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 13 β”‚ export type { H, I, J }; 14 β”‚ - > 15 β”‚ import { type X, type Y, type Z } from ""; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 16 β”‚ export type { X, Y, Z }; - 17 β”‚ - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. i Safe fix: Use import type. - 13 13 β”‚ //type VV = V; + 10 10 β”‚ const YY = Y; + 11 11 β”‚ + 12 β”‚ - importΒ·{Β·typeΒ·H,Β·typeΒ·I,Β·typeΒ·JΒ·}Β·fromΒ·""; + 12 β”‚ + importΒ·typeΒ·{Β·H,Β·I,Β·JΒ·}Β·fromΒ·""; + 13 13 β”‚ export type { H, I, J }; 14 14 β”‚ - 15 β”‚ - importΒ·{Β·typeΒ·X,Β·typeΒ·Y,Β·typeΒ·ZΒ·}Β·fromΒ·""; - 15 β”‚ + importΒ·typeΒ·{Β·X,Β·Y,Β·ZΒ·}Β·fromΒ·""; - 16 16 β”‚ export type { X, Y, Z }; - 17 17 β”‚ ``` ``` -invalid-named-imports.ts:19:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-named-imports.ts:15:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This type keyword makes all inline type keywords useless. + + 13 β”‚ export type { H, I, J }; + 14 β”‚ + > 15 β”‚ import type { type M, N, type O } from ""; + β”‚ ^^^^ + 16 β”‚ + 17 β”‚ // multiline + + i This inline type keyword is useless. + + 13 β”‚ export type { H, I, J }; + 14 β”‚ + > 15 β”‚ import type { type M, N, type O } from ""; + β”‚ ^^^^ + 16 β”‚ + 17 β”‚ // multiline + + i This inline type keyword is useless. + + 13 β”‚ export type { H, I, J }; + 14 β”‚ + > 15 β”‚ import type { type M, N, type O } from ""; + β”‚ ^^^^ + 16 β”‚ + 17 β”‚ // multiline + + i Safe fix: Remove useless inline type keywords. + + 15 β”‚ importΒ·typeΒ·{Β·typeΒ·M,Β·N,Β·typeΒ·OΒ·}Β·fromΒ·""; + β”‚ ----- ----- + +``` + +``` +invalid-named-imports.ts:18:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Some named imports are only used as types. - 18 β”‚ // multiline - > 19 β”‚ import { - β”‚ ^^^^^^^^ - > 20 β”‚ U, - > 21 β”‚ V, - > 22 β”‚ // leading comment - > 23 β”‚ W, - > 24 β”‚ } from ""; - β”‚ ^^^^^^^^^^ - 25 β”‚ export { U, type V, type W }; - 26 β”‚ + 17 β”‚ // multiline + > 18 β”‚ import { + β”‚ ^ + > 19 β”‚ U, + > 20 β”‚ V, + > 21 β”‚ // leading comment + > 22 β”‚ W, + > 23 β”‚ } from ""; + β”‚ ^^^^^^^^^ + 24 β”‚ export { U, type V, type W }; + 25 β”‚ i This import is only used as a type. - 19 β”‚ import { - 20 β”‚ U, - > 21 β”‚ V, + 18 β”‚ import { + 19 β”‚ U, + > 20 β”‚ V, β”‚ ^ - 22 β”‚ // leading comment - 23 β”‚ W, + 21 β”‚ // leading comment + 22 β”‚ W, i This import is only used as a type. - 21 β”‚ V, - 22 β”‚ // leading comment - > 23 β”‚ W, + 20 β”‚ V, + 21 β”‚ // leading comment + > 22 β”‚ W, β”‚ ^ - 24 β”‚ } from ""; - 25 β”‚ export { U, type V, type W }; + 23 β”‚ } from ""; + 24 β”‚ export { U, type V, type W }; - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. - i Safe fix: Use import type. + i Safe fix: Add inline type keywords. - 19 19 β”‚ import { - 20 20 β”‚ U, - 21 β”‚ - Β·Β·Β·Β·V, - 21 β”‚ + Β·Β·Β·Β·typeΒ·V, - 22 22 β”‚ // leading comment - 23 β”‚ - Β·Β·Β·Β·W, - 23 β”‚ + Β·Β·Β·Β·typeΒ·W, - 24 24 β”‚ } from ""; - 25 25 β”‚ export { U, type V, type W }; + 18 18 β”‚ import { + 19 19 β”‚ U, + 20 β”‚ - Β·Β·Β·Β·V, + 20 β”‚ + Β·Β·Β·Β·typeΒ·V, + 21 21 β”‚ // leading comment + 22 β”‚ - Β·Β·Β·Β·W, + 22 β”‚ + Β·Β·Β·Β·typeΒ·W, + 23 23 β”‚ } from ""; + 24 24 β”‚ export { U, type V, type W }; ``` - - diff --git a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-namesapce-imports.ts.snap b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-namesapce-imports.ts.snap index bb8955d01d66..099e00fb63d0 100644 --- a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-namesapce-imports.ts.snap +++ b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-namesapce-imports.ts.snap @@ -13,16 +13,16 @@ let a: A.Type1; # Diagnostics ``` -invalid-namesapce-imports.ts:1:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-namesapce-imports.ts:1:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! All these imports are only used as types. > 1 β”‚ import * as A from ""; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^ 2 β”‚ export { type A } 3 β”‚ type AA = typeof A.Class; - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. i Safe fix: Use import type. @@ -30,5 +30,3 @@ invalid-namesapce-imports.ts:1:1 lint/style/useImportType FIXABLE ━━━━ β”‚ +++++ ``` - - diff --git a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-unused-react-types.tsx.snap b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-unused-react-types.tsx.snap index 520493d4b4aa..6d43b4ea4d9f 100644 --- a/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-unused-react-types.tsx.snap +++ b/crates/biome_js_analyze/tests/specs/style/useImportType/invalid-unused-react-types.tsx.snap @@ -16,16 +16,16 @@ function Component() { # Diagnostics ``` -invalid-unused-react-types.tsx:1:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid-unused-react-types.tsx:1:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! All these imports are only used as types. > 1 β”‚ import * as ReactTypes from "react"; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 β”‚ 3 β”‚ function Component() { - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. i Safe fix: Use import type. diff --git a/crates/biome_js_analyze/tests/specs/style/useImportType/valid-named-imports.ts b/crates/biome_js_analyze/tests/specs/style/useImportType/valid-named-imports.ts index 7ffbbfade399..6e0384de814a 100644 --- a/crates/biome_js_analyze/tests/specs/style/useImportType/valid-named-imports.ts +++ b/crates/biome_js_analyze/tests/specs/style/useImportType/valid-named-imports.ts @@ -13,3 +13,10 @@ export type { C }; import { D } from ""; let a: D = new D(); + +import { type E, F } from ""; +export type { E }; +export { F }; + +import type { G } from ""; +export type { G }; diff --git a/crates/biome_js_analyze/tests/specs/style/useImportType/valid-named-imports.ts.snap b/crates/biome_js_analyze/tests/specs/style/useImportType/valid-named-imports.ts.snap index 6ef270364b09..2d9fb077771c 100644 --- a/crates/biome_js_analyze/tests/specs/style/useImportType/valid-named-imports.ts.snap +++ b/crates/biome_js_analyze/tests/specs/style/useImportType/valid-named-imports.ts.snap @@ -20,6 +20,11 @@ export type { C }; import { D } from ""; let a: D = new D(); -``` +import { type E, F } from ""; +export type { E }; +export { F }; +import type { G } from ""; +export type { G }; +``` diff --git a/crates/biome_js_analyze/tests/specs/style/useImportType/valid-unused-react-combined.tsx.snap b/crates/biome_js_analyze/tests/specs/style/useImportType/valid-unused-react-combined.tsx.snap index ed50131cbf2b..54821076c9e5 100644 --- a/crates/biome_js_analyze/tests/specs/style/useImportType/valid-unused-react-combined.tsx.snap +++ b/crates/biome_js_analyze/tests/specs/style/useImportType/valid-unused-react-combined.tsx.snap @@ -17,12 +17,12 @@ function Component() { # Diagnostics ``` -valid-unused-react-combined.tsx:1:1 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +valid-unused-react-combined.tsx:1:8 lint/style/useImportType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Some named imports are only used as types. > 1 β”‚ import React, { MouseEvent } from 'react'; - β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + β”‚ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 β”‚ 3 β”‚ function Component() { @@ -33,9 +33,9 @@ valid-unused-react-combined.tsx:1:1 lint/style/useImportType FIXABLE ━━━ 2 β”‚ 3 β”‚ function Component() { - i Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules. + i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules. - i Safe fix: Use import type. + i Safe fix: Add inline type keywords. 1 β”‚ importΒ·React,Β·{Β·typeΒ·MouseEventΒ·}Β·fromΒ·'react'; β”‚ +++++ diff --git a/crates/biome_js_factory/src/utils.rs b/crates/biome_js_factory/src/utils.rs index e47e71cabd0b..0e1ffd69f556 100644 --- a/crates/biome_js_factory/src/utils.rs +++ b/crates/biome_js_factory/src/utils.rs @@ -31,7 +31,7 @@ pub fn escape<'a>( iter.next(); } else { for candidate in needs_escaping { - if unescaped_string[idx..].starts_with(candidate) { + if unescaped_string.as_bytes()[idx..].starts_with(candidate.as_bytes()) { if escaped.is_empty() { escaped = String::with_capacity(unescaped_string.len() * 2 - idx); } @@ -70,6 +70,7 @@ mod tests { escape("abc ${} ${} bca", &["${"], b'\\'), r"abc \${} \${} bca" ); + assert_eq!(escape("€", &["'"], b'\\'), "€"); assert_eq!(escape(r"\`", &["`"], b'\\'), r"\`"); assert_eq!(escape(r"\${}", &["${"], b'\\'), r"\${}"); @@ -77,6 +78,8 @@ mod tests { assert_eq!(escape(r"\\${}", &["${"], b'\\'), r"\\\${}"); assert_eq!(escape(r"\\\`", &["`"], b'\\'), r"\\\`"); assert_eq!(escape(r"\\\${}", &["${"], b'\\'), r"\\\${}"); + assert_eq!(escape("€", &["€"], b'\\'), r"\€"); + assert_eq!(escape("πŸ˜€β‚¬", &["€"], b'\\'), r"πŸ˜€\€"); assert_eq!(escape("abc", &["${", "`"], b'\\'), "abc"); assert_eq!(escape("${} `", &["${", "`"], b'\\'), r"\${} \`"); diff --git a/crates/biome_js_syntax/src/export_ext.rs b/crates/biome_js_syntax/src/export_ext.rs index 248ce81cdab6..e7e92977a1d7 100644 --- a/crates/biome_js_syntax/src/export_ext.rs +++ b/crates/biome_js_syntax/src/export_ext.rs @@ -11,6 +11,22 @@ declare_node_union! { pub AnyIdentifier = AnyJsBindingPattern | AnyTsIdentifierBinding | JsIdentifierExpression | JsLiteralExportName | JsReferenceIdentifier } +impl AnyIdentifier { + pub fn name_token(&self) -> Option { + match self { + Self::AnyJsBindingPattern(id) => id + .as_any_js_binding()? + .as_js_identifier_binding()? + .name_token(), + Self::AnyTsIdentifierBinding(id) => id.as_ts_identifier_binding()?.name_token(), + Self::JsIdentifierExpression(id) => id.name().ok()?.value_token(), + Self::JsLiteralExportName(id) => id.value(), + Self::JsReferenceIdentifier(id) => id.value_token(), + } + .ok() + } +} + declare_node_union! { pub AnyJsExported = AnyJsExpression | AnyJsExportClause | AnyIdentifier | AnyTsType | TsEnumDeclaration } diff --git a/crates/biome_json_analyze/src/lint/suspicious/no_duplicate_object_keys.rs b/crates/biome_json_analyze/src/lint/suspicious/no_duplicate_object_keys.rs index 73d0473055ef..0477ab7d4e0f 100644 --- a/crates/biome_json_analyze/src/lint/suspicious/no_duplicate_object_keys.rs +++ b/crates/biome_json_analyze/src/lint/suspicious/no_duplicate_object_keys.rs @@ -37,7 +37,7 @@ declare_lint_rule! { impl Rule for NoDuplicateObjectKeys { type Query = Ast; type State = (JsonMemberName, Vec); - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { @@ -62,10 +62,8 @@ impl Rule for NoDuplicateObjectKeys { } } } - let duplicated_keys: Vec<_> = names.into_iter().collect(); - - duplicated_keys + duplicated_keys.into_boxed_slice() } fn diagnostic(_ctx: &RuleContext, state: &Self::State) -> Option { diff --git a/crates/biome_migrate/src/analyzers/nursery_rules.rs b/crates/biome_migrate/src/analyzers/nursery_rules.rs index d9063e983150..8b4912ef99d8 100644 --- a/crates/biome_migrate/src/analyzers/nursery_rules.rs +++ b/crates/biome_migrate/src/analyzers/nursery_rules.rs @@ -135,12 +135,12 @@ const RULES_TO_MIGRATE: &[(&str, (&str, &str))] = &[ impl Rule for NurseryRules { type Query = Ast; type State = MigrateRuleState; - type Signals = Vec; + type Signals = Box<[Self::State]>; type Options = (); fn run(ctx: &RuleContext) -> Self::Signals { let node = ctx.query(); - let mut rules_to_migrate = vec![]; + let mut rules_to_migrate = Vec::new(); if let Some(nursery_group) = find_group_by_name(node, "nursery") { let mut rules_should_be_migrated = FxHashMap::default(); @@ -153,7 +153,7 @@ impl Rule for NurseryRules { .ok() .and_then(|node| node.as_json_object_value().cloned()) else { - return rules_to_migrate; + return rules_to_migrate.into_boxed_slice(); }; let mut separator_iterator = nursery_group_object @@ -195,7 +195,7 @@ impl Rule for NurseryRules { } } - rules_to_migrate + rules_to_migrate.into_boxed_slice() } fn diagnostic(_ctx: &RuleContext, state: &Self::State) -> Option { diff --git a/crates/biome_service/src/file_handlers/grit.rs b/crates/biome_service/src/file_handlers/grit.rs index afb9529d2fe2..cf56af7a4596 100644 --- a/crates/biome_service/src/file_handlers/grit.rs +++ b/crates/biome_service/src/file_handlers/grit.rs @@ -3,7 +3,7 @@ use crate::{ WorkspaceError, }; use biome_analyze::{AnalyzerConfiguration, AnalyzerOptions}; -use biome_formatter::Printed; +use biome_formatter::{IndentStyle, IndentWidth, LineEnding, LineWidth, Printed}; use biome_fs::BiomePath; use biome_grit_formatter::{context::GritFormatOptions, format_node}; use biome_grit_parser::parse_grit_with_cache; @@ -16,8 +16,30 @@ use super::{ FormatterCapabilities, ParseResult, ParserCapabilities, SearchCapabilities, }; +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +pub struct GritFormatterSettings { + pub line_ending: Option, + pub line_width: Option, + pub indent_width: Option, + pub indent_style: Option, + pub enabled: Option, +} + +impl Default for GritFormatterSettings { + fn default() -> Self { + Self { + enabled: Some(false), + indent_style: Default::default(), + indent_width: Default::default(), + line_ending: Default::default(), + line_width: Default::default(), + } + } +} + impl ServiceLanguage for GritLanguage { - type FormatterSettings = (); + type FormatterSettings = GritFormatterSettings; type LinterSettings = (); type OrganizeImportsSettings = (); type FormatOptions = GritFormatOptions; @@ -30,13 +52,40 @@ impl ServiceLanguage for GritLanguage { } fn resolve_format_options( - _global: Option<&crate::settings::FormatSettings>, - _overrides: Option<&crate::settings::OverrideSettings>, - _language: Option<&Self::FormatterSettings>, - _path: &biome_fs::BiomePath, - _file_source: &super::DocumentFileSource, + global: Option<&crate::settings::FormatSettings>, + overrides: Option<&crate::settings::OverrideSettings>, + language: Option<&Self::FormatterSettings>, + path: &biome_fs::BiomePath, + file_source: &super::DocumentFileSource, ) -> Self::FormatOptions { - GritFormatOptions::default() + let indent_style = language + .and_then(|l| l.indent_style) + .or(global.and_then(|g| g.indent_style)) + .unwrap_or_default(); + let line_width = language + .and_then(|l| l.line_width) + .or(global.and_then(|g| g.line_width)) + .unwrap_or_default(); + let indent_width = language + .and_then(|l| l.indent_width) + .or(global.and_then(|g| g.indent_width)) + .unwrap_or_default(); + + let line_ending = language + .and_then(|l| l.line_ending) + .or(global.and_then(|g| g.line_ending)) + .unwrap_or_default(); + + let options = GritFormatOptions::new(file_source.to_grit_file_source().unwrap_or_default()) + .with_indent_style(indent_style) + .with_indent_width(indent_width) + .with_line_width(line_width) + .with_line_ending(line_ending); + if let Some(overrides) = overrides { + overrides.to_override_grit_format_options(path, options) + } else { + options + } } fn resolve_analyzer_options( diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs index eb249430a9b6..adbc67c57024 100644 --- a/crates/biome_service/src/file_handlers/mod.rs +++ b/crates/biome_service/src/file_handlers/mod.rs @@ -317,6 +317,13 @@ impl DocumentFileSource { } } + pub fn to_grit_file_source(&self) -> Option { + match self { + DocumentFileSource::Grit(grit) => Some(*grit), + _ => None, + } + } + pub fn to_css_file_source(&self) -> Option { match self { DocumentFileSource::Css(css) => Some(*css), diff --git a/crates/biome_service/src/settings.rs b/crates/biome_service/src/settings.rs index e484d9e4134c..77089af01bcb 100644 --- a/crates/biome_service/src/settings.rs +++ b/crates/biome_service/src/settings.rs @@ -23,6 +23,7 @@ use biome_formatter::{ use biome_fs::BiomePath; use biome_graphql_formatter::context::GraphqlFormatOptions; use biome_graphql_syntax::GraphqlLanguage; +use biome_grit_formatter::context::GritFormatOptions; use biome_grit_syntax::GritLanguage; use biome_html_formatter::HtmlFormatOptions; use biome_html_syntax::HtmlLanguage; @@ -1005,6 +1006,19 @@ impl OverrideSettings { options } + pub fn to_override_grit_format_options( + &self, + path: &Path, + mut options: GritFormatOptions, + ) -> GritFormatOptions { + for pattern in self.patterns.iter() { + if pattern.include.matches_path(path) && !pattern.exclude.matches_path(path) { + pattern.apply_overrides_to_grit_format_options(&mut options); + } + } + options + } + pub fn to_override_html_format_options( &self, path: &Path, @@ -1168,6 +1182,7 @@ pub struct OverrideSettingPattern { pub(crate) cached_js_format_options: RwLock>, pub(crate) cached_json_format_options: RwLock>, pub(crate) cached_css_format_options: RwLock>, + pub(crate) cached_grit_format_options: RwLock>, pub(crate) cached_graphql_format_options: RwLock>, pub(crate) cached_html_format_options: RwLock>, pub(crate) cached_js_parser_options: RwLock>, @@ -1339,6 +1354,36 @@ impl OverrideSettingPattern { } } + fn apply_overrides_to_grit_format_options(&self, options: &mut GritFormatOptions) { + if let Ok(readonly_cache) = self.cached_grit_format_options.read() { + if let Some(cached_options) = readonly_cache.as_ref() { + *options = cached_options.clone(); + return; + } + } + + let grit_formatter = &self.languages.grit.formatter; + let formatter = &self.formatter; + + if let Some(indent_style) = grit_formatter.indent_style.or(formatter.indent_style) { + options.set_indent_style(indent_style); + } + if let Some(indent_width) = grit_formatter.indent_width.or(formatter.indent_width) { + options.set_indent_width(indent_width) + } + if let Some(line_ending) = grit_formatter.line_ending.or(formatter.line_ending) { + options.set_line_ending(line_ending); + } + if let Some(line_width) = grit_formatter.line_width.or(formatter.line_width) { + options.set_line_width(line_width); + } + + if let Ok(mut writeonly_cache) = self.cached_grit_format_options.write() { + let options = options.clone(); + let _ = writeonly_cache.insert(options); + } + } + fn apply_overrides_to_html_format_options(&self, options: &mut HtmlFormatOptions) { if let Ok(readonly_cache) = self.cached_html_format_options.read() { if let Some(cached_options) = readonly_cache.as_ref() { diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 8906f0cf7e06..6191e2eccc95 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -204,13 +204,19 @@ pub fn code_fix_to_string(source: &str, action: AnalyzerActi /// corresponding to the directory name. E.g., `style/useWhile/test.js` /// will be analyzed with just the `style/useWhile` rule. pub fn parse_test_path(file: &Path) -> (&str, &str) { - let rule_folder = file.parent().unwrap(); - let rule_name = rule_folder.file_name().unwrap(); + let mut group_name = ""; + let mut rule_name = ""; - let group_folder = rule_folder.parent().unwrap(); - let group_name = group_folder.file_name().unwrap(); + for component in file.iter().rev() { + if component == "specs" || component == "suppression" { + break; + } + + rule_name = group_name; + group_name = component.to_str().unwrap_or_default(); + } - (group_name.to_str().unwrap(), rule_name.to_str().unwrap()) + (group_name, rule_name) } /// This check is used in the parser test to ensure it doesn't emit diff --git a/crates/biome_unicode_table/src/lib.rs b/crates/biome_unicode_table/src/lib.rs index 0077e68f8893..12de5f1df2c8 100644 --- a/crates/biome_unicode_table/src/lib.rs +++ b/crates/biome_unicode_table/src/lib.rs @@ -13,7 +13,14 @@ pub fn is_html_id_start(c: char) -> bool { } /// Is `c` a CSS non-ascii character. -/// See https://drafts.csswg.org/css-syntax-3/#ident-token-diagram +/// See +/// See +/// +/// In contrast to the standard we also accept all characters from: +/// - the Miscellaneous Symbols Unicode block +/// - the Dingbats Unicode block +/// +/// We also accept some characters of the Miscellaneous Technical Unicode block. #[inline] pub fn is_css_non_ascii(c: char) -> bool { matches!( @@ -28,6 +35,13 @@ pub fn is_css_non_ascii(c: char) -> bool { | 0x203F | 0x2040 | 0x2070..=0x218F + // https://en.wikipedia.org/wiki/List_of_Unicode_characters#Miscellaneous_Technical + | 0x2318 | 0x231A | 0x231B | 0x2328 | 0x2399 + | 0x23E9..=0x23F3 + | 0x23F9..=0x23FE + // https://en.wikipedia.org/wiki/List_of_Unicode_characters#Miscellaneous_Symbols + // https://en.wikipedia.org/wiki/Dingbats_(Unicode_block) + | 0x2600..=0x27BF | 0x2C00..=0x2FEF | 0x3001..=0xD7FF | 0xF900..=0xFDCF diff --git a/package.json b/package.json index 96fa0253293a..f030b4388889 100644 --- a/package.json +++ b/package.json @@ -10,5 +10,5 @@ "keywords": [], "author": "Biome Developers and Contributors", "license": "MIT OR Apache-2.0", - "packageManager": "pnpm@9.11.0" + "packageManager": "pnpm@9.12.0" } diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index a101e077bb3b..563e6984626a 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1250,6 +1250,18 @@ export interface Nursery { * Disallow exporting an imported variable. */ noExportedImports?: RuleConfiguration_for_Null; + /** + * Prevent usage of \ element in a Next.js project. + */ + noHeadElement?: RuleConfiguration_for_Null; + /** + * Prevent using the next/head module in pages/_document.js on Next.js projects. + */ + noHeadImportInDocument?: RuleConfiguration_for_Null; + /** + * Prevent usage of \ element in a Next.js project. + */ + noImgElement?: RuleConfiguration_for_Null; /** * Disallows the use of irregular whitespace characters. */ @@ -1281,7 +1293,7 @@ export interface Nursery { /** * Disallow usage of sensitive data such as API keys and tokens. */ - noSecrets?: RuleConfiguration_for_Null; + noSecrets?: RuleConfiguration_for_NoSecretsOptions; /** * Enforce that static, visible elements (such as \
) that have click handlers use the valid role attribute. */ @@ -1302,6 +1314,10 @@ export interface Nursery { * Disallow unknown pseudo-element selectors. */ noUnknownPseudoElement?: RuleConfiguration_for_Null; + /** + * Disallow unknown type selectors. + */ + noUnknownTypeSelector?: RuleConfiguration_for_Null; /** * Disallow unnecessary escape sequence in regular expression literals. */ @@ -2004,6 +2020,9 @@ export type RuleConfiguration_for_RestrictedImportsOptions = export type RuleFixConfiguration_for_NoRestrictedTypesOptions = | RulePlainConfiguration | RuleWithFixOptions_for_NoRestrictedTypesOptions; +export type RuleConfiguration_for_NoSecretsOptions = + | RulePlainConfiguration + | RuleWithOptions_for_NoSecretsOptions; export type RuleConfiguration_for_UseComponentExportOnlyModulesOptions = | RulePlainConfiguration | RuleWithOptions_for_UseComponentExportOnlyModulesOptions; @@ -2165,6 +2184,16 @@ export interface RuleWithFixOptions_for_NoRestrictedTypesOptions { */ options: NoRestrictedTypesOptions; } +export interface RuleWithOptions_for_NoSecretsOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: NoSecretsOptions; +} export interface RuleWithOptions_for_UseComponentExportOnlyModulesOptions { /** * The severity of the emitted diagnostics by the rule @@ -2361,6 +2390,12 @@ export interface RestrictedImportsOptions { export interface NoRestrictedTypesOptions { types?: {}; } +export interface NoSecretsOptions { + /** + * Set entropy threshold (default is 41). + */ + entropyThreshold?: number; +} export interface UseComponentExportOnlyModulesOptions { /** * Allows the export of constants. This option is for environments that support it, such as [Vite](https://vitejs.dev/) @@ -2858,6 +2893,9 @@ export type Category = | "lint/nursery/noDynamicNamespaceImportAccess" | "lint/nursery/noEnum" | "lint/nursery/noExportedImports" + | "lint/nursery/noHeadElement" + | "lint/nursery/noHeadImportInDocument" + | "lint/nursery/noImgElement" | "lint/nursery/noImportantInKeyframe" | "lint/nursery/noInvalidDirectionInLinearGradient" | "lint/nursery/noInvalidGridAreas" @@ -2884,6 +2922,7 @@ export type Category = | "lint/nursery/noUnknownPseudoClassSelector" | "lint/nursery/noUnknownPseudoElement" | "lint/nursery/noUnknownSelectorPseudoElement" + | "lint/nursery/noUnknownTypeSelector" | "lint/nursery/noUnknownUnit" | "lint/nursery/noUnmatchableAnbSelector" | "lint/nursery/noUnusedFunctionParameters" @@ -3049,6 +3088,10 @@ export type Category = | "internalError/io" | "internalError/fs" | "internalError/panic" + | "reporter/parse" + | "reporter/format" + | "reporter/analyzer" + | "reporter/organizeImports" | "parse" | "lint" | "lint/a11y" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 40c3c31c6b5a..91f74cb8dd00 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2059,6 +2059,24 @@ }, "additionalProperties": false }, + "NoSecretsConfiguration": { + "anyOf": [ + { "$ref": "#/definitions/RulePlainConfiguration" }, + { "$ref": "#/definitions/RuleWithNoSecretsOptions" } + ] + }, + "NoSecretsOptions": { + "type": "object", + "properties": { + "entropyThreshold": { + "description": "Set entropy threshold (default is 41).", + "type": ["integer", "null"], + "format": "uint16", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Nursery": { "description": "A list of rules that belong to this group", "type": "object", @@ -2123,6 +2141,27 @@ { "type": "null" } ] }, + "noHeadElement": { + "description": "Prevent usage of \\ element in a Next.js project.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, + "noHeadImportInDocument": { + "description": "Prevent using the next/head module in pages/_document.js on Next.js projects.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, + "noImgElement": { + "description": "Prevent usage of \\ element in a Next.js project.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noIrregularWhitespace": { "description": "Disallows the use of irregular whitespace characters.", "anyOf": [ @@ -2175,7 +2214,7 @@ "noSecrets": { "description": "Disallow usage of sensitive data such as API keys and tokens.", "anyOf": [ - { "$ref": "#/definitions/RuleConfiguration" }, + { "$ref": "#/definitions/NoSecretsConfiguration" }, { "type": "null" } ] }, @@ -2214,6 +2253,13 @@ { "type": "null" } ] }, + "noUnknownTypeSelector": { + "description": "Disallow unknown type selectors.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUselessEscapeInRegex": { "description": "Disallow unnecessary escape sequence in regular expression literals.", "anyOf": [ @@ -2811,6 +2857,21 @@ }, "additionalProperties": false }, + "RuleWithNoSecretsOptions": { + "type": "object", + "required": ["level"], + "properties": { + "level": { + "description": "The severity of the emitted diagnostics by the rule", + "allOf": [{ "$ref": "#/definitions/RulePlainConfiguration" }] + }, + "options": { + "description": "Rule's options", + "allOf": [{ "$ref": "#/definitions/NoSecretsOptions" }] + } + }, + "additionalProperties": false + }, "RuleWithRestrictedGlobalsOptions": { "type": "object", "required": ["level"], @@ -4085,7 +4146,6 @@ "properties": { "attributes": { "description": "Additional attributes that will be sorted.", - "default": ["class", "className"], "type": ["array", "null"], "items": { "type": "string" } }, diff --git a/packages/@biomejs/biome/package.json b/packages/@biomejs/biome/package.json index 8c37801d3730..3c1d2bd25e61 100644 --- a/packages/@biomejs/biome/package.json +++ b/packages/@biomejs/biome/package.json @@ -50,13 +50,13 @@ "provenance": true }, "optionalDependencies": { - "@biomejs/cli-win32-x64": "1.9.2", - "@biomejs/cli-win32-arm64": "1.9.2", - "@biomejs/cli-darwin-x64": "1.9.2", - "@biomejs/cli-darwin-arm64": "1.9.2", - "@biomejs/cli-linux-x64": "1.9.2", - "@biomejs/cli-linux-arm64": "1.9.2", - "@biomejs/cli-linux-x64-musl": "1.9.2", - "@biomejs/cli-linux-arm64-musl": "1.9.2" + "@biomejs/cli-win32-x64": "1.9.3", + "@biomejs/cli-win32-arm64": "1.9.3", + "@biomejs/cli-darwin-x64": "1.9.3", + "@biomejs/cli-darwin-arm64": "1.9.3", + "@biomejs/cli-linux-x64": "1.9.3", + "@biomejs/cli-linux-arm64": "1.9.3", + "@biomejs/cli-linux-x64-musl": "1.9.3", + "@biomejs/cli-linux-arm64-musl": "1.9.3" } } diff --git a/packages/@biomejs/js-api/package.json b/packages/@biomejs/js-api/package.json index f351409c57ac..e554d78726e9 100644 --- a/packages/@biomejs/js-api/package.json +++ b/packages/@biomejs/js-api/package.json @@ -54,9 +54,9 @@ "vitest": "1.6.0" }, "peerDependencies": { - "@biomejs/wasm-bundler": "^1.9.2", - "@biomejs/wasm-nodejs": "^1.9.2", - "@biomejs/wasm-web": "^1.9.2" + "@biomejs/wasm-bundler": "^1.9.3", + "@biomejs/wasm-nodejs": "^1.9.3", + "@biomejs/wasm-web": "^1.9.3" }, "peerDependenciesMeta": { "@biomejs/wasm-bundler": { diff --git a/packages/aria-data/package.json b/packages/aria-data/package.json index 444141fabccd..a7d46436b2fd 100644 --- a/packages/aria-data/package.json +++ b/packages/aria-data/package.json @@ -6,7 +6,7 @@ "license": "MIT OR Apache-2.0", "author": "Victorien Elvinger", "engines": { - "node": ">=20.17.0" + "node": ">=20.18.0" }, "type": "module", "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f699353b8a5..c481eda980ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,14 +11,14 @@ importers: benchmark: devDependencies: '@typescript-eslint/eslint-plugin': - specifier: 8.7.0 - version: 8.7.0(@typescript-eslint/parser@8.3.0(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2))(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2) + specifier: 8.8.0 + version: 8.8.0(@typescript-eslint/parser@8.3.0(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2) dprint: specifier: 0.47.2 version: 0.47.2 eslint: - specifier: 9.11.1 - version: 9.11.1(jiti@1.21.0) + specifier: 9.12.0 + version: 9.12.0(jiti@1.21.0) prettier: specifier: 3.3.3 version: 3.3.3 @@ -72,29 +72,29 @@ importers: packages/@biomejs/biome: optionalDependencies: '@biomejs/cli-darwin-arm64': - specifier: 1.9.2 - version: 1.9.2 + specifier: 1.9.3 + version: 1.9.3 '@biomejs/cli-darwin-x64': - specifier: 1.9.2 - version: 1.9.2 + specifier: 1.9.3 + version: 1.9.3 '@biomejs/cli-linux-arm64': - specifier: 1.9.2 - version: 1.9.2 + specifier: 1.9.3 + version: 1.9.3 '@biomejs/cli-linux-arm64-musl': - specifier: 1.9.2 - version: 1.9.2 + specifier: 1.9.3 + version: 1.9.3 '@biomejs/cli-linux-x64': - specifier: 1.9.2 - version: 1.9.2 + specifier: 1.9.3 + version: 1.9.3 '@biomejs/cli-linux-x64-musl': - specifier: 1.9.2 - version: 1.9.2 + specifier: 1.9.3 + version: 1.9.3 '@biomejs/cli-win32-arm64': - specifier: 1.9.2 - version: 1.9.2 + specifier: 1.9.3 + version: 1.9.3 '@biomejs/cli-win32-x64': - specifier: 1.9.2 - version: 1.9.2 + specifier: 1.9.3 + version: 1.9.3 packages/@biomejs/cli-darwin-arm64: {} @@ -179,96 +179,48 @@ packages: resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} engines: {node: '>=6.9.0'} - '@biomejs/cli-darwin-arm64@1.9.2': - resolution: {integrity: sha512-rbs9uJHFmhqB3Td0Ro+1wmeZOHhAPTL3WHr8NtaVczUmDhXkRDWScaxicG9+vhSLj1iLrW47itiK6xiIJy6vaA==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - '@biomejs/cli-darwin-arm64@1.9.3': resolution: {integrity: sha512-QZzD2XrjJDUyIZK+aR2i5DDxCJfdwiYbUKu9GzkCUJpL78uSelAHAPy7m0GuPMVtF/Uo+OKv97W3P9nuWZangQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@1.9.2': - resolution: {integrity: sha512-BlfULKijNaMigQ9GH9fqJVt+3JTDOSiZeWOQtG/1S1sa8Lp046JHG3wRJVOvekTPL9q/CNFW1NVG8J0JN+L1OA==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - '@biomejs/cli-darwin-x64@1.9.3': resolution: {integrity: sha512-vSCoIBJE0BN3SWDFuAY/tRavpUtNoqiceJ5PrU3xDfsLcm/U6N93JSM0M9OAiC/X7mPPfejtr6Yc9vSgWlEgVw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@1.9.2': - resolution: {integrity: sha512-ZATvbUWhNxegSALUnCKWqetTZqrK72r2RsFD19OK5jXDj/7o1hzI1KzDNG78LloZxftrwr3uI9SqCLh06shSZw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - '@biomejs/cli-linux-arm64-musl@1.9.3': resolution: {integrity: sha512-VBzyhaqqqwP3bAkkBrhVq50i3Uj9+RWuj+pYmXrMDgjS5+SKYGE56BwNw4l8hR3SmYbLSbEo15GcV043CDSk+Q==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@1.9.2': - resolution: {integrity: sha512-T8TJuSxuBDeQCQzxZu2o3OU4eyLumTofhCxxFd3+aH2AEWVMnH7Z/c3QP1lHI5RRMBP9xIJeMORqDQ5j+gVZzw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - '@biomejs/cli-linux-arm64@1.9.3': resolution: {integrity: sha512-vJkAimD2+sVviNTbaWOGqEBy31cW0ZB52KtpVIbkuma7PlfII3tsLhFa+cwbRAcRBkobBBhqZ06hXoZAN8NODQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@1.9.2': - resolution: {integrity: sha512-CjPM6jT1miV5pry9C7qv8YJk0FIZvZd86QRD3atvDgfgeh9WQU0k2Aoo0xUcPdTnoz0WNwRtDicHxwik63MmSg==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - '@biomejs/cli-linux-x64-musl@1.9.3': resolution: {integrity: sha512-TJmnOG2+NOGM72mlczEsNki9UT+XAsMFAOo8J0me/N47EJ/vkLXxf481evfHLlxMejTY6IN8SdRSiPVLv6AHlA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@1.9.2': - resolution: {integrity: sha512-T0cPk3C3Jr2pVlsuQVTBqk2qPjTm8cYcTD9p/wmR9MeVqui1C/xTVfOIwd3miRODFMrJaVQ8MYSXnVIhV9jTjg==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - '@biomejs/cli-linux-x64@1.9.3': resolution: {integrity: sha512-x220V4c+romd26Mu1ptU+EudMXVS4xmzKxPVb9mgnfYlN4Yx9vD5NZraSx/onJnd3Gh/y8iPUdU5CDZJKg9COA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@1.9.2': - resolution: {integrity: sha512-2x7gSty75bNIeD23ZRPXyox6Z/V0M71ObeJtvQBhi1fgrvPdtkEuw7/0wEHg6buNCubzOFuN9WYJm6FKoUHfhg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - '@biomejs/cli-win32-arm64@1.9.3': resolution: {integrity: sha512-lg/yZis2HdQGsycUvHWSzo9kOvnGgvtrYRgoCEwPBwwAL8/6crOp3+f47tPwI/LI1dZrhSji7PNsGKGHbwyAhw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@1.9.2': - resolution: {integrity: sha512-JC3XvdYcjmu1FmAehVwVV0SebLpeNTnO2ZaMdGCSOdS7f8O9Fq14T2P1gTG1Q29Q8Dt1S03hh0IdVpIZykOL8g==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - '@biomejs/cli-win32-x64@1.9.3': resolution: {integrity: sha512-cQMy2zanBkVLpmmxXdK6YePzmZx0s5Z7KEnwmrW54rcXK3myCNbQa09SwGZ8i/8sLw0H9F3X7K4rxVNGU8/D4Q==} engines: {node: '>=14.21.3'} @@ -475,8 +427,8 @@ packages: resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.11.1': - resolution: {integrity: sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==} + '@eslint/js@9.12.0': + resolution: {integrity: sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.4': @@ -487,12 +439,20 @@ packages: resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@humanfs/core@0.19.0': + resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.5': + resolution: {integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==} + engines: {node: '>=18.18.0'} + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.0': - resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} '@isaacs/cliui@8.0.2': @@ -644,8 +604,8 @@ packages: '@types/ws@8.5.10': resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} - '@typescript-eslint/eslint-plugin@8.7.0': - resolution: {integrity: sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==} + '@typescript-eslint/eslint-plugin@8.8.0': + resolution: {integrity: sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -669,12 +629,12 @@ packages: resolution: {integrity: sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.7.0': - resolution: {integrity: sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==} + '@typescript-eslint/scope-manager@8.8.0': + resolution: {integrity: sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.7.0': - resolution: {integrity: sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==} + '@typescript-eslint/type-utils@8.8.0': + resolution: {integrity: sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -686,8 +646,8 @@ packages: resolution: {integrity: sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.7.0': - resolution: {integrity: sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==} + '@typescript-eslint/types@8.8.0': + resolution: {integrity: sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.3.0': @@ -699,8 +659,8 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@8.7.0': - resolution: {integrity: sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==} + '@typescript-eslint/typescript-estree@8.8.0': + resolution: {integrity: sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -708,8 +668,8 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.7.0': - resolution: {integrity: sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==} + '@typescript-eslint/utils@8.8.0': + resolution: {integrity: sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -718,8 +678,8 @@ packages: resolution: {integrity: sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.7.0': - resolution: {integrity: sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==} + '@typescript-eslint/visitor-keys@8.8.0': + resolution: {integrity: sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitest/expect@1.6.0': @@ -933,20 +893,20 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-scope@8.0.2: - resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + eslint-scope@8.1.0: + resolution: {integrity: sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.0.0: - resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + eslint-visitor-keys@4.1.0: + resolution: {integrity: sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.11.1: - resolution: {integrity: sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==} + eslint@9.12.0: + resolution: {integrity: sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -955,8 +915,8 @@ packages: jiti: optional: true - espree@10.1.0: - resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + espree@10.2.0: + resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esquery@1.6.0: @@ -1123,10 +1083,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1736,51 +1692,27 @@ snapshots: chalk: 2.4.2 js-tokens: 4.0.0 - '@biomejs/cli-darwin-arm64@1.9.2': - optional: true - '@biomejs/cli-darwin-arm64@1.9.3': optional: true - '@biomejs/cli-darwin-x64@1.9.2': - optional: true - '@biomejs/cli-darwin-x64@1.9.3': optional: true - '@biomejs/cli-linux-arm64-musl@1.9.2': - optional: true - '@biomejs/cli-linux-arm64-musl@1.9.3': optional: true - '@biomejs/cli-linux-arm64@1.9.2': - optional: true - '@biomejs/cli-linux-arm64@1.9.3': optional: true - '@biomejs/cli-linux-x64-musl@1.9.2': - optional: true - '@biomejs/cli-linux-x64-musl@1.9.3': optional: true - '@biomejs/cli-linux-x64@1.9.2': - optional: true - '@biomejs/cli-linux-x64@1.9.3': optional: true - '@biomejs/cli-win32-arm64@1.9.2': - optional: true - '@biomejs/cli-win32-arm64@1.9.3': optional: true - '@biomejs/cli-win32-x64@1.9.2': - optional: true - '@biomejs/cli-win32-x64@1.9.3': optional: true @@ -1877,9 +1809,9 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@9.11.1(jiti@1.21.0))': + '@eslint-community/eslint-utils@4.4.0(eslint@9.12.0(jiti@1.21.0))': dependencies: - eslint: 9.11.1(jiti@1.21.0) + eslint: 9.12.0(jiti@1.21.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.11.0': {} @@ -1898,7 +1830,7 @@ snapshots: dependencies: ajv: 6.12.6 debug: 4.3.6 - espree: 10.1.0 + espree: 10.2.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.0 @@ -1908,7 +1840,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.11.1': {} + '@eslint/js@9.12.0': {} '@eslint/object-schema@2.1.4': {} @@ -1916,9 +1848,16 @@ snapshots: dependencies: levn: 0.4.1 + '@humanfs/core@0.19.0': {} + + '@humanfs/node@0.16.5': + dependencies: + '@humanfs/core': 0.19.0 + '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.0': {} + '@humanwhocodes/retry@0.3.1': {} '@isaacs/cliui@8.0.2': dependencies: @@ -2039,15 +1978,15 @@ snapshots: dependencies: '@types/node': 20.16.10 - '@typescript-eslint/eslint-plugin@8.7.0(@typescript-eslint/parser@8.3.0(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2))(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2)': + '@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@8.3.0(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2)': dependencies: '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 8.3.0(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2) - '@typescript-eslint/scope-manager': 8.7.0 - '@typescript-eslint/type-utils': 8.7.0(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2) - '@typescript-eslint/utils': 8.7.0(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.7.0 - eslint: 9.11.1(jiti@1.21.0) + '@typescript-eslint/parser': 8.3.0(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2) + '@typescript-eslint/scope-manager': 8.8.0 + '@typescript-eslint/type-utils': 8.8.0(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2) + '@typescript-eslint/utils': 8.8.0(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 8.8.0 + eslint: 9.12.0(jiti@1.21.0) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -2057,14 +1996,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.3.0(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2)': + '@typescript-eslint/parser@8.3.0(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2)': dependencies: '@typescript-eslint/scope-manager': 8.3.0 '@typescript-eslint/types': 8.3.0 '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.6.2) '@typescript-eslint/visitor-keys': 8.3.0 debug: 4.3.6 - eslint: 9.11.1(jiti@1.21.0) + eslint: 9.12.0(jiti@1.21.0) optionalDependencies: typescript: 5.6.2 transitivePeerDependencies: @@ -2075,15 +2014,15 @@ snapshots: '@typescript-eslint/types': 8.3.0 '@typescript-eslint/visitor-keys': 8.3.0 - '@typescript-eslint/scope-manager@8.7.0': + '@typescript-eslint/scope-manager@8.8.0': dependencies: - '@typescript-eslint/types': 8.7.0 - '@typescript-eslint/visitor-keys': 8.7.0 + '@typescript-eslint/types': 8.8.0 + '@typescript-eslint/visitor-keys': 8.8.0 - '@typescript-eslint/type-utils@8.7.0(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2)': + '@typescript-eslint/type-utils@8.8.0(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.7.0(typescript@5.6.2) - '@typescript-eslint/utils': 8.7.0(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2) + '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.6.2) + '@typescript-eslint/utils': 8.8.0(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2) debug: 4.3.6 ts-api-utils: 1.3.0(typescript@5.6.2) optionalDependencies: @@ -2094,7 +2033,7 @@ snapshots: '@typescript-eslint/types@8.3.0': {} - '@typescript-eslint/types@8.7.0': {} + '@typescript-eslint/types@8.8.0': {} '@typescript-eslint/typescript-estree@8.3.0(typescript@5.6.2)': dependencies: @@ -2111,10 +2050,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.7.0(typescript@5.6.2)': + '@typescript-eslint/typescript-estree@8.8.0(typescript@5.6.2)': dependencies: - '@typescript-eslint/types': 8.7.0 - '@typescript-eslint/visitor-keys': 8.7.0 + '@typescript-eslint/types': 8.8.0 + '@typescript-eslint/visitor-keys': 8.8.0 debug: 4.3.6 fast-glob: 3.3.2 is-glob: 4.0.3 @@ -2126,13 +2065,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.7.0(eslint@9.11.1(jiti@1.21.0))(typescript@5.6.2)': + '@typescript-eslint/utils@8.8.0(eslint@9.12.0(jiti@1.21.0))(typescript@5.6.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.1(jiti@1.21.0)) - '@typescript-eslint/scope-manager': 8.7.0 - '@typescript-eslint/types': 8.7.0 - '@typescript-eslint/typescript-estree': 8.7.0(typescript@5.6.2) - eslint: 9.11.1(jiti@1.21.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0(jiti@1.21.0)) + '@typescript-eslint/scope-manager': 8.8.0 + '@typescript-eslint/types': 8.8.0 + '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.6.2) + eslint: 9.12.0(jiti@1.21.0) transitivePeerDependencies: - supports-color - typescript @@ -2142,9 +2081,9 @@ snapshots: '@typescript-eslint/types': 8.3.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.7.0': + '@typescript-eslint/visitor-keys@8.8.0': dependencies: - '@typescript-eslint/types': 8.7.0 + '@typescript-eslint/types': 8.8.0 eslint-visitor-keys: 3.4.3 '@vitest/expect@1.6.0': @@ -2378,27 +2317,27 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-scope@8.0.2: + eslint-scope@8.1.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.0.0: {} + eslint-visitor-keys@4.1.0: {} - eslint@9.11.1(jiti@1.21.0): + eslint@9.12.0(jiti@1.21.0): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.1(jiti@1.21.0)) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0(jiti@1.21.0)) '@eslint-community/regexpp': 4.11.0 '@eslint/config-array': 0.18.0 '@eslint/core': 0.6.0 '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.11.1 + '@eslint/js': 9.12.0 '@eslint/plugin-kit': 0.2.0 + '@humanfs/node': 0.16.5 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.3.0 - '@nodelib/fs.walk': 1.2.8 + '@humanwhocodes/retry': 0.3.1 '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 ajv: 6.12.6 @@ -2406,9 +2345,9 @@ snapshots: cross-spawn: 7.0.3 debug: 4.3.6 escape-string-regexp: 4.0.0 - eslint-scope: 8.0.2 - eslint-visitor-keys: 4.0.0 - espree: 10.1.0 + eslint-scope: 8.1.0 + eslint-visitor-keys: 4.1.0 + espree: 10.2.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -2418,24 +2357,22 @@ snapshots: ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 text-table: 0.2.0 optionalDependencies: jiti: 1.21.0 transitivePeerDependencies: - supports-color - espree@10.1.0: + espree@10.2.0: dependencies: acorn: 8.12.1 acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 4.0.0 + eslint-visitor-keys: 4.1.0 esquery@1.6.0: dependencies: @@ -2588,8 +2525,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-stream@3.0.0: {} isexe@2.0.0: {}