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 244e769f3780..1701be91588f 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 @@ -495,6 +495,13 @@ pub(crate) fn migrate_eslint_any_rule( let rule = group.no_default_export.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "import/no-extraneous-dependencies" => { + let group = rules.correctness.get_or_insert_with(Default::default); + let rule = group + .no_undeclared_dependencies + .get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "import/no-nodejs-modules" => { let group = rules.correctness.get_or_insert_with(Default::default); let rule = group.no_nodejs_modules.get_or_insert(Default::default()); diff --git a/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs b/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs index 89f56b43c6ea..3bb061c5ca57 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs @@ -1,9 +1,15 @@ -use crate::{globals::is_node_builtin_module, services::manifest::Manifest}; -use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic}; +use std::path::Path; + +use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource}; use biome_console::markup; +use biome_deserialize::{Deserializable, DeserializableType}; +use biome_deserialize_macros::Deserializable; use biome_js_syntax::{AnyJsImportClause, AnyJsImportLike}; use biome_rowan::AstNode; +use crate::utils::restricted_glob::{CandidatePath, RestrictedGlob}; +use crate::{globals::is_node_builtin_module, services::manifest::Manifest}; + declare_lint_rule! { /// Disallow the use of dependencies that aren't specified in the `package.json`. /// @@ -34,19 +40,168 @@ declare_lint_rule! { /// ```js,ignore /// import assert from "node:assert"; /// ``` + /// + /// ## Options + /// + /// **Since v2.0.0** + /// + /// This rule supports the following options: + /// - `devDependencies`: If set to `false`, then the rule will show an error when `devDependencies` are imported. Defaults to `true`. + /// - `peerDependencies`: If set to `false`, then the rule will show an error when `peerDependencies` are imported. Defaults to `true`. + /// - `optionalDependencies`: If set to `false`, then the rule will show an error when `optionalDependencies` are imported. Defaults to `true`. + /// + /// You can set the options like this: + /// ```json + /// { + /// "options": { + /// "devDependencies": false, + /// "peerDependencies": false, + /// "optionalDependencies": false + /// } + /// } + /// ``` + /// + /// You can also use an array of globs instead of literal booleans. + /// When using an array of globs, the setting will be set to `true` (no errors reported) + /// if the name of the file being linted (i.e. not the imported file/module) matches a single glob + /// in the array, and `false` otherwise. + /// + /// In the following example, only test files can use dependencies in `devDependencies` section. + /// `dependencies`, `peerDependencies`, and `optionalDependencies` are always available. + /// + /// ```json + /// { + /// "options": { + /// "devDependencies": ["tests/*.test.js", "tests/*.spec.js"] + /// } + /// } + /// ``` pub NoUndeclaredDependencies { version: "1.6.0", name: "noUndeclaredDependencies", language: "js", + sources: &[ + RuleSource::EslintImport("no-extraneous-dependencies"), + ], recommended: false, } } +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +enum DependencyAvailability { + /// Dependencies are always available or unavailable. + Bool(bool), + + /// Dependencies are available in files that matches any of the globs. + Patterns(Box<[RestrictedGlob]>), +} + +impl Default for DependencyAvailability { + fn default() -> Self { + Self::Bool(true) + } +} + +impl Deserializable for DependencyAvailability { + fn deserialize( + value: &impl biome_deserialize::DeserializableValue, + name: &str, + diagnostics: &mut Vec, + ) -> Option { + Some(if value.visitable_type()? == DeserializableType::Bool { + Self::Bool(bool::deserialize(value, name, diagnostics)?) + } else { + Self::Patterns(Deserializable::deserialize(value, name, diagnostics)?) + }) + } +} + +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for DependencyAvailability { + fn schema_name() -> String { + "DependencyAvailability".to_owned() + } + + fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + use schemars::schema::*; + + Schema::Object(SchemaObject { + subschemas: Some(Box::new(SubschemaValidation { + one_of: Some(vec![ + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::Boolean.into()), + metadata: Some(Box::new(Metadata { + description: Some("This type of dependency will be always available or unavailable.".to_owned()), + ..Default::default() + })), + ..Default::default() + }), + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(SingleOrVec::Single(Box::new(Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + ..Default::default() + })))), + min_items: Some(1), + ..Default::default() + })), + metadata: Some(Box::new(Metadata { + description: Some("This type of dependency will be available only if the linted file matches any of the globs.".to_owned()), + ..Default::default() + })), + ..Default::default() + }) + ]), + ..Default::default() + })), + ..Default::default() + }) + } +} + +impl DependencyAvailability { + fn is_available(&self, path: &Path) -> bool { + match self { + Self::Bool(b) => *b, + Self::Patterns(globs) => CandidatePath::new(&path).matches_with_exceptions(globs), + } + } +} + +/// Rule's options +#[derive( + Clone, Debug, Default, Deserializable, Eq, PartialEq, serde::Deserialize, serde::Serialize, +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NoUndeclaredDependenciesOptions { + /// If set to `false`, then the rule will show an error when `devDependencies` are imported. Defaults to `true`. + #[serde(default)] + dev_dependencies: DependencyAvailability, + + /// If set to `false`, then the rule will show an error when `peerDependencies` are imported. Defaults to `true`. + #[serde(default)] + peer_dependencies: DependencyAvailability, + + /// If set to `false`, then the rule will show an error when `optionalDependencies` are imported. Defaults to `true`. + #[serde(default)] + optional_dependencies: DependencyAvailability, +} + +pub struct RuleState { + package_name: String, + is_dev_dependency_available: bool, + is_peer_dependency_available: bool, + is_optional_dependency_available: bool, +} + impl Rule for NoUndeclaredDependencies { type Query = Manifest; - type State = (); + type State = RuleState; type Signals = Option; - type Options = (); + type Options = NoUndeclaredDependenciesOptions; fn run(ctx: &RuleContext) -> Self::Signals { let node = ctx.query(); @@ -54,12 +209,22 @@ impl Rule for NoUndeclaredDependencies { return None; } + let path = ctx.file_path(); + let is_dev_dependency_available = ctx.options().dev_dependencies.is_available(path); + let is_peer_dependency_available = ctx.options().peer_dependencies.is_available(path); + let is_optional_dependency_available = + ctx.options().optional_dependencies.is_available(path); + + let is_available = |package_name| { + ctx.is_dependency(package_name) + || (is_dev_dependency_available && ctx.is_dev_dependency(package_name)) + || (is_peer_dependency_available && ctx.is_peer_dependency(package_name)) + || (is_optional_dependency_available && ctx.is_optional_dependency(package_name)) + }; + let token_text = node.inner_string_text()?; let package_name = parse_package_name(token_text.text())?; - if ctx.is_dependency(package_name) - || ctx.is_dev_dependency(package_name) - || ctx.is_peer_dependency(package_name) - || ctx.is_optional_dependency(package_name) + if is_available(package_name) // Self package imports // TODO: we should also check that an `.` exports exists. // See https://nodejs.org/api/packages.html#self-referencing-a-package-using-its-name @@ -70,42 +235,67 @@ impl Rule for NoUndeclaredDependencies { || package_name == "bun" { return None; - } else if !package_name.starts_with('@') { + } + + if !package_name.starts_with('@') { // Handle DefinitelyTyped imports https://github.com/DefinitelyTyped/DefinitelyTyped - // e.g. `lodash` can import typ[es from `@types/lodash`. + // e.g. `lodash` can import types from `@types/lodash`. if let Some(import_clause) = node.parent::() { if import_clause.type_token().is_some() { let package_name = format!("@types/{package_name}"); - if ctx.is_dependency(&package_name) - || ctx.is_dev_dependency(&package_name) - || ctx.is_peer_dependency(&package_name) - || ctx.is_optional_dependency(&package_name) - { + if is_available(&package_name) { return None; } } } } - Some(()) + Some(RuleState { + package_name: package_name.to_string(), + is_dev_dependency_available, + is_peer_dependency_available, + is_optional_dependency_available, + }) } - fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { - Some( - RuleDiagnostic::new( - rule_category!(), - ctx.query().range(), - markup! { - "The current dependency isn't specified in your package.json." - }, + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + let RuleState { + package_name, + is_dev_dependency_available, + is_peer_dependency_available, + is_optional_dependency_available, + } = state; + + let diag = RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "The current dependency isn't specified in your package.json." + }, + ); + + let available_in = if ctx.is_dev_dependency(package_name) && !is_dev_dependency_available { + Some("devDependencies") + } else if ctx.is_peer_dependency(package_name) && !is_peer_dependency_available { + Some("peerDependencies") + } else if ctx.is_optional_dependency(package_name) && !is_optional_dependency_available { + Some("optionalDependencies") + } else { + None + }; + + if let Some(section) = available_in { + Some(diag.note(markup! { + {package_name}" is part of your "{section}", but it's not intended to be used in this file." + }).note(markup! { + "You may want to consider moving it to the ""dependencies"" section." + })) + } else { + Some( + diag.note(markup! { "This could lead to errors." }) + .note(markup! { "Add the dependency in your manifest." }), ) - .note(markup! { - "This could lead to errors." - }) - .note(markup! { - "Add the dependency in your manifest." - }), - ) + } } } diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js index 22611c83641c..4efce429f13d 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js @@ -1,3 +1,7 @@ import "notInstalled"; import("notInstalled"); require("notInstalled") + +import "@testing-library/react"; +import("@testing-library/react"); +require("@testing-library/react"); diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap index b5907f725c8b..cf9736fddc9f 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap @@ -8,6 +8,10 @@ import "notInstalled"; import("notInstalled"); require("notInstalled") +import "@testing-library/react"; +import("@testing-library/react"); +require("@testing-library/react"); + ``` # Diagnostics @@ -56,6 +60,7 @@ invalid.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━━ > 3 │ require("notInstalled") │ ^^^^^^^^^^^^^^^^^^^^^^^ 4 │ + 5 │ import "@testing-library/react"; i This could lead to errors. @@ -63,3 +68,58 @@ invalid.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━━ ``` + +``` +invalid.js:5:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The current dependency isn't specified in your package.json. + + 3 │ require("notInstalled") + 4 │ + > 5 │ import "@testing-library/react"; + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ import("@testing-library/react"); + 7 │ require("@testing-library/react"); + + i @testing-library/react is part of your devDependencies, but it's not intended to be used in this file. + + i You may want to consider moving it to the dependencies section. + + +``` + +``` +invalid.js:6:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The current dependency isn't specified in your package.json. + + 5 │ import "@testing-library/react"; + > 6 │ import("@testing-library/react"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 7 │ require("@testing-library/react"); + 8 │ + + i @testing-library/react is part of your devDependencies, but it's not intended to be used in this file. + + i You may want to consider moving it to the dependencies section. + + +``` + +``` +invalid.js:7:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The current dependency isn't specified in your package.json. + + 5 │ import "@testing-library/react"; + 6 │ import("@testing-library/react"); + > 7 │ require("@testing-library/react"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + + i @testing-library/react is part of your devDependencies, but it's not intended to be used in this file. + + i You may want to consider moving it to the dependencies section. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.options.json b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.options.json new file mode 100644 index 000000000000..f1506d558807 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "correctness": { + "noUndeclaredDependencies": { + "level": "error", + "options": { + "devDependencies": false + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.package.json b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.package.json index 0e2185e52a36..57f41c027437 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.package.json +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.package.json @@ -1,5 +1,8 @@ { "dependencies": { "react": "1.0.0" - } + }, + "devDependencies": { + "@testing-library/react": "1.0.0" + } } diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js new file mode 100644 index 000000000000..4efce429f13d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js @@ -0,0 +1,7 @@ +import "notInstalled"; +import("notInstalled"); +require("notInstalled") + +import "@testing-library/react"; +import("@testing-library/react"); +require("@testing-library/react"); diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap new file mode 100644 index 000000000000..91a1e1222b2a --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap @@ -0,0 +1,125 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.test.js +--- +# Input +```jsx +import "notInstalled"; +import("notInstalled"); +require("notInstalled") + +import "@testing-library/react"; +import("@testing-library/react"); +require("@testing-library/react"); + +``` + +# Diagnostics +``` +invalid.test.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The current dependency isn't specified in your package.json. + + > 1 │ import "notInstalled"; + │ ^^^^^^^^^^^^^^ + 2 │ import("notInstalled"); + 3 │ require("notInstalled") + + i This could lead to errors. + + i Add the dependency in your manifest. + + +``` + +``` +invalid.test.js:2:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The current dependency isn't specified in your package.json. + + 1 │ import "notInstalled"; + > 2 │ import("notInstalled"); + │ ^^^^^^^^^^^^^^^^^^^^^^ + 3 │ require("notInstalled") + 4 │ + + i This could lead to errors. + + i Add the dependency in your manifest. + + +``` + +``` +invalid.test.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The current dependency isn't specified in your package.json. + + 1 │ import "notInstalled"; + 2 │ import("notInstalled"); + > 3 │ require("notInstalled") + │ ^^^^^^^^^^^^^^^^^^^^^^^ + 4 │ + 5 │ import "@testing-library/react"; + + i This could lead to errors. + + i Add the dependency in your manifest. + + +``` + +``` +invalid.test.js:5:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The current dependency isn't specified in your package.json. + + 3 │ require("notInstalled") + 4 │ + > 5 │ import "@testing-library/react"; + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ import("@testing-library/react"); + 7 │ require("@testing-library/react"); + + i @testing-library/react is part of your devDependencies, but it's not intended to be used in this file. + + i You may want to consider moving it to the dependencies section. + + +``` + +``` +invalid.test.js:6:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The current dependency isn't specified in your package.json. + + 5 │ import "@testing-library/react"; + > 6 │ import("@testing-library/react"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 7 │ require("@testing-library/react"); + 8 │ + + i @testing-library/react is part of your devDependencies, but it's not intended to be used in this file. + + i You may want to consider moving it to the dependencies section. + + +``` + +``` +invalid.test.js:7:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The current dependency isn't specified in your package.json. + + 5 │ import "@testing-library/react"; + 6 │ import("@testing-library/react"); + > 7 │ require("@testing-library/react"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + + i @testing-library/react is part of your devDependencies, but it's not intended to be used in this file. + + i You may want to consider moving it to the dependencies section. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.options.json b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.options.json new file mode 100644 index 000000000000..f1506d558807 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "correctness": { + "noUndeclaredDependencies": { + "level": "error", + "options": { + "devDependencies": false + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.package.json b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.package.json new file mode 100644 index 000000000000..57f41c027437 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "react": "1.0.0" + }, + "devDependencies": { + "@testing-library/react": "1.0.0" + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.js b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.js new file mode 100644 index 000000000000..7937a582a7cf --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.js @@ -0,0 +1,3 @@ +import "@testing-library/react"; +import("@testing-library/react"); +require("@testing-library/react"); diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.js.snap new file mode 100644 index 000000000000..d24278361312 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.js.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.test.js +--- +# Input +```jsx +import "@testing-library/react"; +import("@testing-library/react"); +require("@testing-library/react"); + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.options.json b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.options.json new file mode 100644 index 000000000000..384b59709536 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "correctness": { + "noUndeclaredDependencies": { + "level": "error", + "options": { + "devDependencies": ["**/*.test.js"] + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.package.json b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.package.json new file mode 100644 index 000000000000..57f41c027437 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/valid.test.package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "react": "1.0.0" + }, + "devDependencies": { + "@testing-library/react": "1.0.0" + } +} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 7c531fe9d923..819e60042f2b 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1108,7 +1108,7 @@ export interface Correctness { /** * Disallow the use of dependencies that aren't specified in the package.json. */ - noUndeclaredDependencies?: RuleConfiguration_for_Null; + noUndeclaredDependencies?: RuleConfiguration_for_NoUndeclaredDependenciesOptions; /** * Prevents the usage of variables that haven't been declared inside the document. */ @@ -2058,6 +2058,9 @@ export type RuleFixConfiguration_for_ValidAriaRoleOptions = export type RuleConfiguration_for_ComplexityOptions = | RulePlainConfiguration | RuleWithOptions_for_ComplexityOptions; +export type RuleConfiguration_for_NoUndeclaredDependenciesOptions = + | RulePlainConfiguration + | RuleWithOptions_for_NoUndeclaredDependenciesOptions; export type RuleConfiguration_for_UseExhaustiveDependenciesOptions = | RulePlainConfiguration | RuleWithOptions_for_UseExhaustiveDependenciesOptions; @@ -2203,6 +2206,16 @@ export interface RuleWithOptions_for_ComplexityOptions { */ options: ComplexityOptions; } +export interface RuleWithOptions_for_NoUndeclaredDependenciesOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: NoUndeclaredDependenciesOptions; +} export interface RuleWithOptions_for_UseExhaustiveDependenciesOptions { /** * The severity of the emitted diagnostics by the rule @@ -2446,6 +2459,23 @@ export interface ComplexityOptions { */ maxAllowedComplexity?: number; } +/** + * Rule's options + */ +export interface NoUndeclaredDependenciesOptions { + /** + * If set to `false`, then the rule will show an error when `devDependencies` are imported. Defaults to `true`. + */ + devDependencies?: DependencyAvailability; + /** + * If set to `false`, then the rule will show an error when `optionalDependencies` are imported. Defaults to `true`. + */ + optionalDependencies?: DependencyAvailability; + /** + * If set to `false`, then the rule will show an error when `peerDependencies` are imported. Defaults to `true`. + */ + peerDependencies?: DependencyAvailability; +} /** * Options for the rule `useExhaustiveDependencies` */ @@ -2598,6 +2628,7 @@ If `false`, no such exception will be made. ignoreNull: boolean; } export type ImportGroup = PredefinedImportGroup | Regex; +export type DependencyAvailability = boolean | string[]; export interface Hook { /** * The "position" of the closure function, starting from zero. diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index b198f3614533..eee69adde827 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -910,7 +910,7 @@ "noUndeclaredDependencies": { "description": "Disallow the use of dependencies that aren't specified in the package.json.", "anyOf": [ - { "$ref": "#/definitions/RuleConfiguration" }, + { "$ref": "#/definitions/NoUndeclaredDependenciesConfiguration" }, { "type": "null" } ] }, @@ -1211,6 +1211,20 @@ }, "additionalProperties": false }, + "DependencyAvailability": { + "oneOf": [ + { + "description": "This type of dependency will be always available or unavailable.", + "type": "boolean" + }, + { + "description": "This type of dependency will be available only if the linted file matches any of the globs.", + "type": "array", + "items": { "type": "string" }, + "minItems": 1 + } + ] + }, "DeprecatedHooksConfiguration": { "anyOf": [ { "$ref": "#/definitions/RulePlainConfiguration" }, @@ -2091,6 +2105,34 @@ }, "additionalProperties": false }, + "NoUndeclaredDependenciesConfiguration": { + "anyOf": [ + { "$ref": "#/definitions/RulePlainConfiguration" }, + { "$ref": "#/definitions/RuleWithNoUndeclaredDependenciesOptions" } + ] + }, + "NoUndeclaredDependenciesOptions": { + "description": "Rule's options", + "type": "object", + "properties": { + "devDependencies": { + "description": "If set to `false`, then the rule will show an error when `devDependencies` are imported. Defaults to `true`.", + "default": true, + "allOf": [{ "$ref": "#/definitions/DependencyAvailability" }] + }, + "optionalDependencies": { + "description": "If set to `false`, then the rule will show an error when `optionalDependencies` are imported. Defaults to `true`.", + "default": true, + "allOf": [{ "$ref": "#/definitions/DependencyAvailability" }] + }, + "peerDependencies": { + "description": "If set to `false`, then the rule will show an error when `peerDependencies` are imported. Defaults to `true`.", + "default": true, + "allOf": [{ "$ref": "#/definitions/DependencyAvailability" }] + } + }, + "additionalProperties": false + }, "Nursery": { "description": "A list of rules that belong to this group", "type": "object", @@ -3011,6 +3053,21 @@ }, "additionalProperties": false }, + "RuleWithNoUndeclaredDependenciesOptions": { + "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/NoUndeclaredDependenciesOptions" }] + } + }, + "additionalProperties": false + }, "RuleWithRestrictedGlobalsOptions": { "type": "object", "required": ["level"],