diff --git a/Cargo.toml b/Cargo.toml index 1985afb3a61..138a7f74b94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,9 @@ rome_aria_metadata = { path = "./crates/rome_aria_metadata" } rome_cli = { path = "./crates/rome_cli" } rome_console = { version = "0.0.1", path = "./crates/rome_console" } rome_control_flow = { path = "./crates/rome_control_flow" } +rome_css_factory = { path = "./crates/rome_css_factory" } +rome_css_parser = { path = "./crates/rome_css_parser" } +rome_css_syntax = { path = "./crates/rome_css_syntax" } rome_deserialize = { version = "0.0.0", path = "./crates/rome_deserialize" } rome_diagnostics = { version = "0.0.1", path = "./crates/rome_diagnostics" } rome_diagnostics_categories = { version = "0.0.1", path = "./crates/rome_diagnostics_categories" } @@ -51,9 +54,6 @@ rome_json_factory = { version = "0.0.1", path = "./crates/rome_json_fa rome_json_formatter = { path = "./crates/rome_json_formatter" } rome_json_parser = { path = "./crates/rome_json_parser" } rome_json_syntax = { version = "0.0.1", path = "./crates/rome_json_syntax" } -rome_css_factory = { path = "./crates/rome_css_factory" } -rome_css_parser = { path = "./crates/rome_css_parser" } -rome_css_syntax = { path = "./crates/rome_css_syntax" } rome_lsp = { path = "./crates/rome_lsp" } rome_markup = { version = "0.0.1", path = "./crates/rome_markup" } rome_migrate = { path = "./crates/rome_migrate" } @@ -65,24 +65,24 @@ rome_text_size = { version = "0.0.1", path = "./crates/rome_text_si tests_macros = { path = "./crates/tests_macros" } # Crates needed in the workspace +bitflags = "2.3.1" +bpaf = { version = "0.9.2", features = ["derive"] } +countme = "3.0.1" +dashmap = "5.4.0" +indexmap = "1.9.3" +insta = "1.29.0" +lazy_static = "1.4.0" +quickcheck = "1.0.3" quickcheck_macros = "1.0.0" -quickcheck = "1.0.3" -bitflags = "2.3.1" -bpaf = { version = "0.9.2", features = ["derive"] } -countme = "3.0.1" -dashmap = "5.4.0" -indexmap = "1.9.3" -insta = "1.29.0" -lazy_static = "1.4.0" -quote = { version = "1.0.28" } -rustc-hash = "1.1.0" -schemars = { version = "0.8.12" } -serde = { version = "1.0.163", features = ["derive"], default-features = false } -serde_json = "1.0.96" -smallvec = { version = "1.10.0", features = ["union", "const_new"] } -tracing = { version = "0.1.37", default-features = false, features = ["std"] } +quote = { version = "1.0.28" } +rustc-hash = "1.1.0" +schemars = { version = "0.8.12" } +serde = { version = "1.0.163", features = ["derive"], default-features = false } +serde_json = "1.0.96" +smallvec = { version = "1.10.0", features = ["union", "const_new"] } +tracing = { version = "0.1.37", default-features = false, features = ["std"] } # pinning to version 1.18 to avoid multiple versions of windows-sys as dependency -tokio = { version = "~1.18.5" } +tokio = { version = "~1.18.5" } [profile.dev.package.rome_wasm] diff --git a/crates/rome_analyze/CONTRIBUTING.md b/crates/rome_analyze/CONTRIBUTING.md index 7ebb43c76fe..eac2017a15e 100644 --- a/crates/rome_analyze/CONTRIBUTING.md +++ b/crates/rome_analyze/CONTRIBUTING.md @@ -343,45 +343,6 @@ This allows the rule to be configured inside `rome.json` file like: } ``` -In this specific case, we don't want the configuration to replace all the standard React hooks configuration, -so to have more control on the deserialization of options, we can implement the trait `DeserializableRuleOptions`. - -In the example below we also deserialize to a struct with a more user-friendly schema. - -This code run only once when the analyzer is first called. - -```rust,ignore - -impl DeserializableRuleOptions for ReactExtensiveDependenciesOptions { - fn try_from(value: serde_json::Value) -> Result { - #[derive(Debug, Deserialize)] - #[serde(deny_unknown_fields)] - struct Options { - #[serde(default)] - hooks: Vec<(String, usize, usize)>, - #[serde(default)] - stables: HashSet, - } - - let options: Options = serde_json::from_value(value)?; - - let mut default = ReactExtensiveDependenciesOptions::default(); - for (k, closure_index, dependencies_index) in options.hooks.into_iter() { - default.hooks_config.insert( - k, - ReactHookConfiguration { - closure_index, - dependencies_index, - }, - ); - } - default.stable_config.extend(options.stables.into_iter()); - - Ok(default) - } -} -``` - A rule can retrieve its option with: ```rust,ignore diff --git a/crates/rome_css_parser/Cargo.toml b/crates/rome_css_parser/Cargo.toml index f9c2b27c172..44fa3c75c83 100644 --- a/crates/rome_css_parser/Cargo.toml +++ b/crates/rome_css_parser/Cargo.toml @@ -12,9 +12,9 @@ version = "0.0.1" [dependencies] rome_console = { workspace = true } +rome_css_syntax = { workspace = true } rome_diagnostics = { workspace = true } rome_js_unicode_table = { workspace = true } -rome_css_syntax = { workspace = true } rome_parser = { workspace = true } rome_rowan = { workspace = true } tracing = { workspace = true } diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index 7f65283bc09..9b3b5589adf 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -103,6 +103,7 @@ define_categories! { "lint/nursery/useGroupedTypeImport": "https://docs.rome.tools/lint/rules/useGroupedTypeImport", "lint/nursery/useHeadingContent": "https://docs.rome.tools/lint/rules/useHeadingContent", "lint/nursery/useHookAtTopLevel": "https://docs.rome.tools/lint/rules/useHookAtTopLevel", + "lint/nursery/useImportRestrictions": "https://docs.rome.tools/lint/rules/useImportRestrictions", "lint/nursery/useIsArray": "https://docs.rome.tools/lint/rules/useIsArray", "lint/nursery/useIsNan": "https://docs.rome.tools/lint/rules/useIsNan", "lint/nursery/useLiteralEnumMembers": "https://docs.rome.tools/lint/rules/useLiteralEnumMembers", diff --git a/crates/rome_js_analyze/src/analyzers/nursery.rs b/crates/rome_js_analyze/src/analyzers/nursery.rs index 2583f8e5dbd..655790c4cd0 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery.rs @@ -16,6 +16,7 @@ pub(crate) mod no_void; pub(crate) mod use_arrow_function; pub(crate) mod use_grouped_type_import; pub(crate) mod use_heading_content; +pub(crate) mod use_import_restrictions; pub(crate) mod use_literal_enum_members; pub(crate) mod use_literal_keys; pub(crate) mod use_simple_number_keys; @@ -38,6 +39,7 @@ declare_group! { self :: use_arrow_function :: UseArrowFunction , self :: use_grouped_type_import :: UseGroupedTypeImport , self :: use_heading_content :: UseHeadingContent , + self :: use_import_restrictions :: UseImportRestrictions , self :: use_literal_enum_members :: UseLiteralEnumMembers , self :: use_literal_keys :: UseLiteralKeys , self :: use_simple_number_keys :: UseSimpleNumberKeys , diff --git a/crates/rome_js_analyze/src/analyzers/nursery/use_import_restrictions.rs b/crates/rome_js_analyze/src/analyzers/nursery/use_import_restrictions.rs new file mode 100644 index 00000000000..3c7d69d0475 --- /dev/null +++ b/crates/rome_js_analyze/src/analyzers/nursery/use_import_restrictions.rs @@ -0,0 +1,182 @@ +use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic}; +use rome_console::markup; +use rome_js_syntax::JsModuleSource; +use rome_rowan::{AstNode, SyntaxTokenText}; + +const INDEX_BASENAMES: &[&str] = &["index", "mod"]; + +const SOURCE_EXTENSIONS: &[&str] = &["js", "ts", "cjs", "cts", "mjs", "mts", "jsx", "tsx"]; + +declare_rule! { + /// Disallows package private imports. + /// + /// This rules enforces the following restrictions: + /// + /// ## Package private visibility + /// + /// All exported symbols, such as types, functions or other things that may be exported, are + /// considered to be "package private". This means that modules that reside in the same + /// directory, as well as submodules of those "sibling" modules, are allowed to import them, + /// while any other modules that are further away in the file system are restricted from + /// importing them. A symbol's visibility may be extended by re-exporting from an index file. + /// + /// Notes: + /// + /// * This rule only applies to relative imports. External dependencies are exempted. + /// * This rule only applies to imports for JavaScript and TypeScript files. Imports for + /// resources such as images or CSS files are exempted. + /// + /// Source: https://github.com/uhyo/eslint-plugin-import-access + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// // Attempt to import from `foo.js` from outside its `sub` module. + /// import { fooPackageVariable } from "./sub/foo.js"; + /// + /// // Attempt to import from `bar.ts` from outside its `aunt` module. + /// import { barPackageVariable } from "../aunt/bar.ts"; + /// + /// // Assumed to resolve to a JS/TS file. + /// import { fooPackageVariable } from "./sub/foo"; + /// + /// // If the `sub/foo` module is inaccessible, so is its index file. + /// import { fooPackageVariable } from "./sub/foo/index.js"; + /// ``` + /// + /// ### Valid + /// + /// ```js + /// // Imports within the same module are always allowed. + /// import { fooPackageVariable } from "./foo.js"; + /// + /// // Resources (anything other than JS/TS files) are exempt. + /// import { barResource } from "../aunt/bar.png"; + /// + /// // A parent index file is accessible like other modules. + /// import { internal } from "../../index.js"; + /// + /// // If the `sub` module is accessible, so is its index file. + /// import { subPackageVariable } from "./sub/index.js"; + /// + /// // Library imports are exempt. + /// import useAsync from "react-use/lib/useAsync"; + /// ``` + /// + pub(crate) UseImportRestrictions { + version: "next", + name: "useImportRestrictions", + recommended: false, + } +} + +impl Rule for UseImportRestrictions { + type Query = Ast; + type State = ImportRestrictionsState; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let binding = ctx.query(); + let Ok(path) = binding.inner_string_text() else { + return None; + }; + + get_restricted_import(&path) + } + + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + let ImportRestrictionsState { path, suggestion } = state; + + let diagnostic = RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "Importing package private symbols is prohibited from outside the module directory." + }, + ) + .note(markup! { + "Please import from "{suggestion}" instead " + "(you may need to re-export the symbol(s) from "{path}")." + }); + + Some(diagnostic) + } +} + +pub(crate) struct ImportRestrictionsState { + /// The path that is being restricted. + path: String, + + /// Suggestion from which to import instead. + suggestion: String, +} + +fn get_restricted_import(module_path: &SyntaxTokenText) -> Option { + if !module_path.starts_with('.') { + return None; + } + + let mut path_parts: Vec<_> = module_path.text().split('/').collect(); + let mut index_filename = None; + + if let Some(extension) = get_extension(&path_parts) { + if !SOURCE_EXTENSIONS.contains(&extension) { + return None; // Resource files are exempt. + } + + if let Some(basename) = get_basename(&path_parts) { + if INDEX_BASENAMES.contains(&basename) { + // We pop the index file because it shouldn't count as a path, + // component, but we store the file name so we can add it to + // both the reported path and the suggestion. + index_filename = path_parts.last().cloned(); + path_parts.pop(); + } + } + } + + let is_restricted = path_parts + .iter() + .filter(|&&part| part != "." && part != "..") + .count() + > 1; + if !is_restricted { + return None; + } + + let mut suggestion_parts = path_parts[..path_parts.len() - 1].to_vec(); + + // Push the index file if it exists. This makes sure the reported path + // matches the import path exactly. + if let Some(index_filename) = index_filename { + path_parts.push(index_filename); + + // Assumes the user probably wants to use an index file that has the + // same name as the original. + suggestion_parts.push(index_filename); + } + + Some(ImportRestrictionsState { + path: path_parts.join("/"), + suggestion: suggestion_parts.join("/"), + }) +} + +fn get_basename<'a>(path_parts: &'_ [&'a str]) -> Option<&'a str> { + path_parts.last().map(|&part| match part.find('.') { + Some(dot_index) if dot_index > 0 && dot_index < part.len() - 1 => &part[..dot_index], + _ => part, + }) +} + +fn get_extension<'a>(path_parts: &'_ [&'a str]) -> Option<&'a str> { + path_parts.last().and_then(|part| match part.find('.') { + Some(dot_index) if dot_index > 0 && dot_index < part.len() - 1 => { + Some(&part[dot_index + 1..]) + } + _ => None, + }) +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/invalidPackagePrivateImports.js b/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/invalidPackagePrivateImports.js new file mode 100644 index 00000000000..16a7e397bef --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/invalidPackagePrivateImports.js @@ -0,0 +1,11 @@ +// Attempt to import from `foo.js` from outside its `sub` module. +import { fooPackageVariable } from "./sub/foo.js"; + +// Attempt to import from `bar.ts` from outside its `aunt` module. +import { barPackageVariable } from "../aunt/bar.ts"; + +// Assumed to resolve to a JS/TS file. +import { fooPackageVariable } from "./sub/foo"; + +// If the `sub/foo` module is inaccessible, so is its index file. +import { fooPackageVariable } from "./sub/foo/index.js"; diff --git a/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/invalidPackagePrivateImports.js.snap b/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/invalidPackagePrivateImports.js.snap new file mode 100644 index 00000000000..3e74a1033d0 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/invalidPackagePrivateImports.js.snap @@ -0,0 +1,85 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalidPackagePrivateImports.js +--- +# Input +```js +// Attempt to import from `foo.js` from outside its `sub` module. +import { fooPackageVariable } from "./sub/foo.js"; + +// Attempt to import from `bar.ts` from outside its `aunt` module. +import { barPackageVariable } from "../aunt/bar.ts"; + +// Assumed to resolve to a JS/TS file. +import { fooPackageVariable } from "./sub/foo"; + +// If the `sub/foo` module is inaccessible, so is its index file. +import { fooPackageVariable } from "./sub/foo/index.js"; + +``` + +# Diagnostics +``` +invalidPackagePrivateImports.js:2:36 lint/nursery/useImportRestrictions ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Importing package private symbols is prohibited from outside the module directory. + + 1 │ // Attempt to import from `foo.js` from outside its `sub` module. + > 2 │ import { fooPackageVariable } from "./sub/foo.js"; + │ ^^^^^^^^^^^^^^ + 3 │ + 4 │ // Attempt to import from `bar.ts` from outside its `aunt` module. + + i Please import from ./sub instead (you may need to re-export the symbol(s) from ./sub/foo.js). + + +``` + +``` +invalidPackagePrivateImports.js:5:36 lint/nursery/useImportRestrictions ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Importing package private symbols is prohibited from outside the module directory. + + 4 │ // Attempt to import from `bar.ts` from outside its `aunt` module. + > 5 │ import { barPackageVariable } from "../aunt/bar.ts"; + │ ^^^^^^^^^^^^^^^^ + 6 │ + 7 │ // Assumed to resolve to a JS/TS file. + + i Please import from ../aunt instead (you may need to re-export the symbol(s) from ../aunt/bar.ts). + + +``` + +``` +invalidPackagePrivateImports.js:8:36 lint/nursery/useImportRestrictions ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Importing package private symbols is prohibited from outside the module directory. + + 7 │ // Assumed to resolve to a JS/TS file. + > 8 │ import { fooPackageVariable } from "./sub/foo"; + │ ^^^^^^^^^^^ + 9 │ + 10 │ // If the `sub/foo` module is inaccessible, so is its index file. + + i Please import from ./sub instead (you may need to re-export the symbol(s) from ./sub/foo). + + +``` + +``` +invalidPackagePrivateImports.js:11:36 lint/nursery/useImportRestrictions ━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Importing package private symbols is prohibited from outside the module directory. + + 10 │ // If the `sub/foo` module is inaccessible, so is its index file. + > 11 │ import { fooPackageVariable } from "./sub/foo/index.js"; + │ ^^^^^^^^^^^^^^^^^^^^ + 12 │ + + i Please import from ./sub/index.js instead (you may need to re-export the symbol(s) from ./sub/foo/index.js). + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/validPackagePrivateImports.js b/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/validPackagePrivateImports.js new file mode 100644 index 00000000000..a82fd7f72f0 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/validPackagePrivateImports.js @@ -0,0 +1,22 @@ +/* should not generate diagnostics */ + +// Imports within the same module are always allowed. +import { fooPackageVariable } from "./foo.js"; + +// Resources (anything other than JS/TS files) are exempt. +import { barResource } from "../aunt/bar.png"; + +// A parent index file is accessible like other modules. +import { internal } from "../../index.js"; + +// If the `sub` module is accessible, so is its index file. +import { subPackageVariable } from "./sub/index.js"; + +// Library imports are exempt. +import useAsync from "react-use/lib/useAsync"; + +// Including library imports with an extension. +import map from "lodash/map.js"; + +// Scoped packages work too. +import netlify from "@astrojs/netlify/functions.js"; diff --git a/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/validPackagePrivateImports.js.snap b/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/validPackagePrivateImports.js.snap new file mode 100644 index 00000000000..f3a159f401f --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useImportRestrictions/validPackagePrivateImports.js.snap @@ -0,0 +1,32 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: validPackagePrivateImports.js +--- +# Input +```js +/* should not generate diagnostics */ + +// Imports within the same module are always allowed. +import { fooPackageVariable } from "./foo.js"; + +// Resources (anything other than JS/TS files) are exempt. +import { barResource } from "../aunt/bar.png"; + +// A parent index file is accessible like other modules. +import { internal } from "../../index.js"; + +// If the `sub` module is accessible, so is its index file. +import { subPackageVariable } from "./sub/index.js"; + +// Library imports are exempt. +import useAsync from "react-use/lib/useAsync"; + +// Including library imports with an extension. +import map from "lodash/map.js"; + +// Scoped packages work too. +import netlify from "@astrojs/netlify/functions.js"; + +``` + + diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index 90731ccc398..8d30ce78914 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -1959,6 +1959,15 @@ pub struct Nursery { #[bpaf(long("use-hook-at-top-level"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] pub use_hook_at_top_level: Option, + #[doc = "Disallows package private imports."] + #[bpaf( + long("use-import-restrictions"), + argument("on|off|warn"), + optional, + hide + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_import_restrictions: Option, #[doc = "Use Array.isArray() instead of instanceof Array."] #[bpaf(long("use-is-array"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] @@ -1996,7 +2005,7 @@ pub struct Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 34] = [ + pub(crate) const GROUP_RULES: [&'static str; 35] = [ "noAccumulatingSpread", "noAriaUnsupportedElements", "noBannedTypes", @@ -2025,6 +2034,7 @@ impl Nursery { "useGroupedTypeImport", "useHeadingContent", "useHookAtTopLevel", + "useImportRestrictions", "useIsArray", "useIsNan", "useLiteralEnumMembers", @@ -2071,12 +2081,12 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), 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[30]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 34] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 35] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2111,6 +2121,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), 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[34]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended(&self) -> bool { matches!(self.recommended, Some(true)) } @@ -2261,36 +2272,41 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_is_array.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[28])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_is_array.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_literal_enum_members.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_literal_keys.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_literal_keys.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_simple_number_keys.as_ref() { + if let Some(rule) = self.use_naming_convention.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } + if let Some(rule) = self.use_simple_number_keys.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2435,36 +2451,41 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_is_array.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[28])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_is_array.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_literal_enum_members.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_literal_keys.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_literal_keys.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_simple_number_keys.as_ref() { + if let Some(rule) = self.use_naming_convention.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } + if let Some(rule) = self.use_simple_number_keys.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -2476,7 +2497,7 @@ impl Nursery { pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 20] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 34] { Self::ALL_RULES_AS_FILTERS } + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 35] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] pub(crate) fn collect_preset_rules( &self, @@ -2525,6 +2546,7 @@ impl Nursery { "useGroupedTypeImport" => self.use_grouped_type_import.as_ref(), "useHeadingContent" => self.use_heading_content.as_ref(), "useHookAtTopLevel" => self.use_hook_at_top_level.as_ref(), + "useImportRestrictions" => self.use_import_restrictions.as_ref(), "useIsArray" => self.use_is_array.as_ref(), "useIsNan" => self.use_is_nan.as_ref(), "useLiteralEnumMembers" => self.use_literal_enum_members.as_ref(), diff --git a/crates/rome_service/src/configuration/parse/json/rules.rs b/crates/rome_service/src/configuration/parse/json/rules.rs index 15f78ddafdb..a68c3fc11f4 100644 --- a/crates/rome_service/src/configuration/parse/json/rules.rs +++ b/crates/rome_service/src/configuration/parse/json/rules.rs @@ -1672,6 +1672,7 @@ impl VisitNode for Nursery { "useGroupedTypeImport", "useHeadingContent", "useHookAtTopLevel", + "useImportRestrictions", "useIsArray", "useIsNan", "useLiteralEnumMembers", @@ -2341,6 +2342,29 @@ impl VisitNode for Nursery { )); } }, + "useImportRestrictions" => match value { + AnyJsonValue::JsonStringValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_known_string(&value, name_text, &mut configuration, diagnostics)?; + self.use_import_restrictions = Some(configuration); + } + AnyJsonValue::JsonObjectValue(_) => { + let mut rule_configuration = RuleConfiguration::default(); + rule_configuration.map_rule_configuration( + &value, + name_text, + "useImportRestrictions", + diagnostics, + )?; + self.use_import_restrictions = Some(rule_configuration); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + "object or string", + value.range(), + )); + } + }, "useIsArray" => match value { AnyJsonValue::JsonStringValue(_) => { let mut configuration = RuleConfiguration::default(); diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index c9e764d0910..85ec3879115 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -962,6 +962,13 @@ { "type": "null" } ] }, + "useImportRestrictions": { + "description": "Disallows package private imports.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useIsArray": { "description": "Use Array.isArray() instead of instanceof Array.", "anyOf": [ diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index c988b36d882..c51d2ec1272 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -626,6 +626,10 @@ export interface Nursery { * Enforce that all React hooks are being called from the Top Level component functions. */ useHookAtTopLevel?: RuleConfiguration; + /** + * Disallows package private imports. + */ + useImportRestrictions?: RuleConfiguration; /** * Use Array.isArray() instead of instanceof Array. */ @@ -1173,6 +1177,7 @@ export type Category = | "lint/nursery/useGroupedTypeImport" | "lint/nursery/useHeadingContent" | "lint/nursery/useHookAtTopLevel" + | "lint/nursery/useImportRestrictions" | "lint/nursery/useIsArray" | "lint/nursery/useIsNan" | "lint/nursery/useLiteralEnumMembers" diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index c9e764d0910..85ec3879115 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -962,6 +962,13 @@ { "type": "null" } ] }, + "useImportRestrictions": { + "description": "Disallows package private imports.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useIsArray": { "description": "Use Array.isArray() instead of instanceof Array.", "anyOf": [ diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index 8f19d322ea5..ea5c582e0f8 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Rome's linter has a total of 153 rules

\ No newline at end of file +

Rome's linter has a total of 154 rules

\ No newline at end of file diff --git a/website/src/pages/lint/rules/index.mdx b/website/src/pages/lint/rules/index.mdx index 096d9f4b422..b844a5a3727 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -1067,6 +1067,12 @@ Enforce that all React hooks are being called from the Top Level component functions.

+

+ useImportRestrictions +

+Disallows package private imports. +
+

useIsArray

diff --git a/website/src/pages/lint/rules/useImportRestrictions.md b/website/src/pages/lint/rules/useImportRestrictions.md new file mode 100644 index 00000000000..f412b075409 --- /dev/null +++ b/website/src/pages/lint/rules/useImportRestrictions.md @@ -0,0 +1,94 @@ +--- +title: Lint Rule useImportRestrictions +parent: lint/rules/index +--- + +# useImportRestrictions (since vnext) + +Disallows package private imports. + +This rules enforces the following restrictions: + +## Package private visibility + +All exported symbols, such as types, functions or other things that may be exported, are +considered to be "package private". This means that modules that reside in the same +directory, as well as submodules of those "sibling" modules, are allowed to import them, +while any other modules that are further away in the file system are restricted from +importing them. A symbol's visibility may be extended by re-exporting from an index file. + +Notes: + +- This rule only applies to relative imports. External dependencies are exempted. +- This rule only applies to imports for JavaScript and TypeScript files. Imports for +resources such as images or CSS files are exempted. + +Source: https://github.com/uhyo/eslint-plugin-import-access + +## Examples + +### Invalid + +```jsx +// Attempt to import from `foo.js` from outside its `sub` module. +import { fooPackageVariable } from "./sub/foo.js"; + +// Attempt to import from `bar.ts` from outside its `aunt` module. +import { barPackageVariable } from "../aunt/bar.ts"; + +// Assumed to resolve to a JS/TS file. +import { fooPackageVariable } from "./sub/foo"; + +// If the `sub/foo` module is inaccessible, so is its index file. +import { fooPackageVariable } from "./sub/foo/index.js"; +``` + +
nursery/useImportRestrictions.js:2:36 lint/nursery/useImportRestrictions ━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Importing package private symbols is prohibited from outside the module directory.
+  
+    1 │ // Attempt to import from `foo.js` from outside its `sub` module.
+  > 2 │ import { fooPackageVariable } from "./sub/foo.js";
+                                      ^^^^^^^^^^^^^^
+    3 │ 
+    4 │ // Attempt to import from `bar.ts` from outside its `aunt` module.
+  
+   Please import from ./sub instead (you may need to re-export the symbol(s) from ./sub/foo.js).
+  
+nursery/useImportRestrictions.js:5:36 lint/nursery/useImportRestrictions ━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Importing package private symbols is prohibited from outside the module directory.
+  
+    4 │ // Attempt to import from `bar.ts` from outside its `aunt` module.
+  > 5 │ import { barPackageVariable } from "../aunt/bar.ts";
+                                      ^^^^^^^^^^^^^^^^
+    6 │ 
+    7 │ // Assumed to resolve to a JS/TS file.
+  
+   Please import from ../aunt instead (you may need to re-export the symbol(s) from ../aunt/bar.ts).
+  
+
+ +### Valid + +```jsx +// Imports within the same module are always allowed. +import { fooPackageVariable } from "./foo.js"; + +// Resources (anything other than JS/TS files) are exempt. +import { barResource } from "../aunt/bar.png"; + +// A parent index file is accessible like other modules. +import { internal } from "../../index.js"; + +// If the `sub` module is accessible, so is its index file. +import { subPackageVariable } from "./sub/index.js"; + +// Library imports are exempt. +import useAsync from "react-use/lib/useAsync"; +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)