mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter/no_restricted_imports): add the no_restricted_imports rules (#7629)
add first test cases related to the 'paths' config Note that the test cases and configuration format is not the same as the original ESLint rule. What is the oxc team strategy to develop such a rule? Is it ok to adapt the config format ? --- I started a discussion here : https://github.com/oxc-project/oxc/discussions/7534#discussion-7574282 I copy/paste the content here. Maybe it is more relevant? I am working to implement [this no-restricted-imports rule](https://eslint.org/docs/latest/rules/no-restricted-imports). I have several problems: How to handle multiple format configuration in rust? The eslint config can be: "fs", ["fs"], or {paths: [{name: "fs"}]}. But Rust needs only one type. I don't know how to do this in rust. Is it ok to cover only the {paths: [{name: "fs"}]} case ? How to parse this config with the from_configuration method? Here is what I have done: ``` fn from_configuration(value: serde_json::Value) -> Self { let mut paths = Vec::new(); let mut patterns = Vec::new(); if let Some(obj) = value.as_object() { // Handle paths array if let Some(paths_value) = obj.get("paths") { if let Some(paths_array) = paths_value.as_array() { for path_value in paths_array { if let Ok(path) = serde_json::from_value(path_value.clone()) { paths.push(path); } } } } // Handle patterns array if let Some(patterns_value) = obj.get("patterns") { if let Some(patterns_array) = patterns_value.as_array() { for pattern_value in patterns_array { if let Ok(pattern) = serde_json::from_value(pattern_value.clone()) { patterns.push(pattern); } } } } } Self { paths, patterns } } ```` But here is my result: ``` [RestrictedPath { name: "foo", import_names: None, message: None }] -------- rule config -------- { "paths": [ { "name": "foo", "importNames": [ "AllowedObject" ] } ] } ``` Note the "None" values
This commit is contained in:
parent
a222f2b055
commit
3d5f0a1a0c
3 changed files with 385 additions and 0 deletions
|
|
@ -104,6 +104,7 @@ mod eslint {
|
|||
pub mod no_redeclare;
|
||||
pub mod no_regex_spaces;
|
||||
pub mod no_restricted_globals;
|
||||
pub mod no_restricted_imports;
|
||||
pub mod no_return_assign;
|
||||
pub mod no_script_url;
|
||||
pub mod no_self_assign;
|
||||
|
|
@ -536,6 +537,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
eslint::max_classes_per_file,
|
||||
eslint::max_lines,
|
||||
eslint::max_params,
|
||||
eslint::no_restricted_imports,
|
||||
eslint::no_object_constructor,
|
||||
eslint::no_duplicate_imports,
|
||||
eslint::no_alert,
|
||||
|
|
|
|||
338
crates/oxc_linter/src/rules/eslint/no_restricted_imports.rs
Normal file
338
crates/oxc_linter/src/rules/eslint/no_restricted_imports.rs
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{CompactStr, Span};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{context::LintContext, module_record::ImportImportName, rule::Rule};
|
||||
|
||||
fn no_restricted_imports_diagnostic(
|
||||
ctx: &LintContext,
|
||||
span: Span,
|
||||
message: Option<CompactStr>,
|
||||
source: &str,
|
||||
) {
|
||||
let msg = message.unwrap_or_else(|| {
|
||||
CompactStr::new(&format!("'{source}' import is restricted from being used."))
|
||||
});
|
||||
ctx.diagnostic(
|
||||
OxcDiagnostic::warn(msg).with_help("Remove the import statement.").with_label(span),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NoRestrictedImports {
|
||||
paths: Box<NoRestrictedImportsConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct NoRestrictedImportsConfig {
|
||||
paths: Box<[RestrictedPath]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct RestrictedPath {
|
||||
name: CompactStr,
|
||||
#[serde(rename = "importNames")]
|
||||
import_names: Option<Box<[CompactStr]>>,
|
||||
message: Option<CompactStr>,
|
||||
}
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
/// This rule allows you to specify imports that you don’t want to use in your application.
|
||||
/// It applies to static imports only, not dynamic ones.
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///Some imports might not make sense in a particular environment. For example, Node.js’ fs module would not make sense in an environment that didn’t have a file system.
|
||||
///
|
||||
/// Some modules provide similar or identical functionality, think lodash and underscore. Your project may have standardized on a module. You want to make sure that the other alternatives are not being used as this would unnecessarily bloat the project and provide a higher maintenance cost of two dependencies when one would suffice.
|
||||
///
|
||||
/// ### Examples
|
||||
///
|
||||
/// Examples of **incorrect** code for this rule:
|
||||
/// ```js
|
||||
/// /*eslint no-restricted-imports: ["error", {
|
||||
/// "name": "disallowed-import",
|
||||
/// "message": "Please use 'allowed-import' instead"
|
||||
/// }]*/
|
||||
///
|
||||
/// import foo from 'disallowed-import';
|
||||
/// ```
|
||||
///
|
||||
/// Examples of **correct** code for this rule:
|
||||
/// ```js
|
||||
/// /*eslint no-restricted-imports: ["error", {"name": "fs"}]*/
|
||||
///
|
||||
/// import crypto from 'crypto';
|
||||
/// export { foo } from "bar";
|
||||
/// ```
|
||||
NoRestrictedImports,
|
||||
style,
|
||||
);
|
||||
|
||||
impl Rule for NoRestrictedImports {
|
||||
fn from_configuration(value: serde_json::Value) -> Self {
|
||||
let mut paths = Vec::new();
|
||||
match value {
|
||||
Value::Array(module_names) => {
|
||||
for module_name in module_names {
|
||||
if let Some(module_name) = module_name.as_str() {
|
||||
paths.push(RestrictedPath {
|
||||
name: CompactStr::new(module_name),
|
||||
import_names: None,
|
||||
message: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::String(module_name) => {
|
||||
paths.push(RestrictedPath {
|
||||
name: CompactStr::new(module_name.as_str()),
|
||||
import_names: None,
|
||||
message: None,
|
||||
});
|
||||
}
|
||||
Value::Object(obj) => {
|
||||
if let Some(paths_value) = obj.get("paths") {
|
||||
if let Some(paths_array) = paths_value.as_array() {
|
||||
for path_value in paths_array {
|
||||
if let Ok(mut path) =
|
||||
serde_json::from_value::<RestrictedPath>(path_value.clone())
|
||||
{
|
||||
if let Some(import_names) = path.import_names {
|
||||
path.import_names = Some(
|
||||
import_names
|
||||
.iter()
|
||||
.map(|s| CompactStr::new(s))
|
||||
.collect::<Vec<_>>()
|
||||
.into_boxed_slice(),
|
||||
);
|
||||
}
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Self { paths: Box::new(NoRestrictedImportsConfig { paths: paths.into_boxed_slice() }) }
|
||||
}
|
||||
|
||||
fn run_once(&self, ctx: &LintContext<'_>) {
|
||||
let module_record = ctx.module_record();
|
||||
let mut side_effect_import_map: FxHashMap<&CompactStr, Vec<Span>> = FxHashMap::default();
|
||||
|
||||
for path in &self.paths.paths {
|
||||
for entry in &module_record.import_entries {
|
||||
let source = entry.module_request.name();
|
||||
let span = entry.module_request.span();
|
||||
|
||||
if source == path.name.as_str() {
|
||||
if let Some(import_names) = &path.import_names {
|
||||
match &entry.import_name {
|
||||
ImportImportName::Name(import) => {
|
||||
let name = CompactStr::new(import.name());
|
||||
|
||||
if !import_names.contains(&name) {
|
||||
no_restricted_imports_diagnostic(
|
||||
ctx,
|
||||
span,
|
||||
path.message.clone(),
|
||||
source,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ImportImportName::Default(_) | ImportImportName::NamespaceObject => {
|
||||
let name = CompactStr::new(entry.local_name.name());
|
||||
if !import_names.contains(&name) {
|
||||
no_restricted_imports_diagnostic(
|
||||
ctx,
|
||||
span,
|
||||
path.message.clone(),
|
||||
source,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
no_restricted_imports_diagnostic(ctx, span, path.message.clone(), source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (source, requests) in &module_record.requested_modules {
|
||||
for request in requests {
|
||||
if request.is_import && module_record.import_entries.is_empty() {
|
||||
side_effect_import_map.entry(source).or_default().push(request.span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (source, spans) in &side_effect_import_map {
|
||||
if source.as_str() == path.name.as_str() {
|
||||
if let Some(span) = spans.iter().next() {
|
||||
no_restricted_imports_diagnostic(ctx, *span, path.message.clone(), source);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for entry in &module_record.local_export_entries {
|
||||
if let Some(module_request) = &entry.module_request {
|
||||
let source = module_request.name();
|
||||
let span = entry.span;
|
||||
|
||||
if source == path.name.as_str() {
|
||||
no_restricted_imports_diagnostic(ctx, span, path.message.clone(), source);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for entry in &module_record.indirect_export_entries {
|
||||
if let Some(module_request) = &entry.module_request {
|
||||
let source = module_request.name();
|
||||
let span = entry.span;
|
||||
|
||||
if source == path.name.as_str() {
|
||||
no_restricted_imports_diagnostic(ctx, span, path.message.clone(), source);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// Basic cases - no matches
|
||||
(
|
||||
r#"import os from "os";"#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [{ "name": "fs" }]
|
||||
})),
|
||||
),
|
||||
(
|
||||
r#"import fs from "fs";"#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [{ "name": "crypto" }]
|
||||
})),
|
||||
),
|
||||
(
|
||||
r#"import path from "path";"#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [
|
||||
{ "name": "crypto" },
|
||||
{ "name": "stream" },
|
||||
{ "name": "os" }
|
||||
]
|
||||
})),
|
||||
),
|
||||
// Testing with import names
|
||||
(
|
||||
r#"import AllowedObject from "foo";"#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [{
|
||||
"name": "foo",
|
||||
"importNames": ["AllowedObject"]
|
||||
}]
|
||||
})),
|
||||
),
|
||||
// Testing relative paths
|
||||
(
|
||||
"import relative from '../foo';",
|
||||
Some(serde_json::json!({
|
||||
"paths": [{ "name": "../notFoo" }]
|
||||
})),
|
||||
),
|
||||
// Multiple restricted imports
|
||||
(
|
||||
r#"import { DisallowedObjectOne, DisallowedObjectTwo } from "foo";"#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [{
|
||||
"name": "foo",
|
||||
"importNames": ["DisallowedObjectOne", "DisallowedObjectTwo"],
|
||||
}]
|
||||
})),
|
||||
),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// Basic restrictions
|
||||
(
|
||||
r#"import "fs""#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [{ "name": "fs" }]
|
||||
})),
|
||||
),
|
||||
// With custom message
|
||||
(
|
||||
r#"import withGitignores from "foo";"#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [{
|
||||
"name": "foo",
|
||||
"message": "Please import from 'bar' instead."
|
||||
}]
|
||||
})),
|
||||
),
|
||||
// Restricting default import
|
||||
(
|
||||
r#"import DisallowedObject from "foo";"#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [{
|
||||
"name": "foo",
|
||||
"importNames": ["default"],
|
||||
"message": "Please import the default import of 'foo' from /bar/ instead."
|
||||
}]
|
||||
})),
|
||||
),
|
||||
// Namespace imports
|
||||
(
|
||||
r#"import * as All from "foo";"#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [{
|
||||
"name": "foo",
|
||||
"importNames": ["DisallowedObject"],
|
||||
"message": "Please import 'DisallowedObject' from /bar/ instead."
|
||||
}]
|
||||
})),
|
||||
),
|
||||
// Export restrictions
|
||||
(
|
||||
r#"export { something } from "fs";"#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [{ "name": "fs" }]
|
||||
})),
|
||||
),
|
||||
// Complex case with multiple restrictions
|
||||
(
|
||||
r#"import { foo, bar, baz } from "mod""#,
|
||||
Some(serde_json::json!({
|
||||
"paths": [
|
||||
{
|
||||
"name": "mod",
|
||||
"importNames": ["foo"],
|
||||
"message": "Import foo from qux instead."
|
||||
},
|
||||
{
|
||||
"name": "mod",
|
||||
"importNames": ["baz"],
|
||||
"message": "Import baz from qux instead."
|
||||
}
|
||||
]
|
||||
})),
|
||||
),
|
||||
];
|
||||
|
||||
Tester::new(NoRestrictedImports::NAME, NoRestrictedImports::CATEGORY, pass, fail)
|
||||
.test_and_snapshot();
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
⚠ eslint(no-restricted-imports): 'fs' import is restricted from being used.
|
||||
╭─[no_restricted_imports.tsx:1:8]
|
||||
1 │ import "fs"
|
||||
· ────
|
||||
╰────
|
||||
help: Remove the import statement.
|
||||
|
||||
⚠ eslint(no-restricted-imports): Please import from 'bar' instead.
|
||||
╭─[no_restricted_imports.tsx:1:28]
|
||||
1 │ import withGitignores from "foo";
|
||||
· ─────
|
||||
╰────
|
||||
help: Remove the import statement.
|
||||
|
||||
⚠ eslint(no-restricted-imports): Please import the default import of 'foo' from /bar/ instead.
|
||||
╭─[no_restricted_imports.tsx:1:30]
|
||||
1 │ import DisallowedObject from "foo";
|
||||
· ─────
|
||||
╰────
|
||||
help: Remove the import statement.
|
||||
|
||||
⚠ eslint(no-restricted-imports): Please import 'DisallowedObject' from /bar/ instead.
|
||||
╭─[no_restricted_imports.tsx:1:22]
|
||||
1 │ import * as All from "foo";
|
||||
· ─────
|
||||
╰────
|
||||
help: Remove the import statement.
|
||||
|
||||
⚠ eslint(no-restricted-imports): 'fs' import is restricted from being used.
|
||||
╭─[no_restricted_imports.tsx:1:10]
|
||||
1 │ export { something } from "fs";
|
||||
· ─────────
|
||||
╰────
|
||||
help: Remove the import statement.
|
||||
|
||||
⚠ eslint(no-restricted-imports): Import foo from qux instead.
|
||||
╭─[no_restricted_imports.tsx:1:31]
|
||||
1 │ import { foo, bar, baz } from "mod"
|
||||
· ─────
|
||||
╰────
|
||||
help: Remove the import statement.
|
||||
Loading…
Reference in a new issue