mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(linter): eslint-plugin-import no-named-as-default-member rule (#1988)
- Docs: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-named-as-default-member.md - Code: https://github.com/import-js/eslint-plugin-import/blob/main/src/rules/no-named-as-default-member.js - Tests: https://github.com/import-js/eslint-plugin-import/blob/main/tests/src/rules/no-named-as-default-member.js
This commit is contained in:
parent
21909fa962
commit
c60c31521e
8 changed files with 233 additions and 4 deletions
|
|
@ -0,0 +1,3 @@
|
|||
export default {};
|
||||
|
||||
export const foo = 10
|
||||
|
|
@ -12,6 +12,7 @@ mod import {
|
|||
pub mod named;
|
||||
pub mod no_amd;
|
||||
pub mod no_cycle;
|
||||
pub mod no_named_as_default_member;
|
||||
pub mod no_self_import;
|
||||
}
|
||||
|
||||
|
|
@ -504,6 +505,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
react::no_unknown_property,
|
||||
react::require_render_return,
|
||||
import::default,
|
||||
import::no_named_as_default_member,
|
||||
import::named,
|
||||
import::no_cycle,
|
||||
import::no_self_import,
|
||||
|
|
|
|||
175
crates/oxc_linter/src/rules/import/no_named_as_default_member.rs
Normal file
175
crates/oxc_linter/src/rules/import/no_named_as_default_member.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
#![allow(clippy::significant_drop_tightening)]
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dashmap::mapref::one::Ref;
|
||||
use oxc_ast::{
|
||||
ast::{BindingPatternKind, Expression, MemberExpression},
|
||||
AstKind,
|
||||
};
|
||||
use oxc_diagnostics::{
|
||||
miette::{self, Diagnostic},
|
||||
thiserror::Error,
|
||||
};
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{Atom, Span};
|
||||
use oxc_syntax::module_record::ImportImportName;
|
||||
|
||||
use crate::{context::LintContext, rule::Rule};
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("eslint-plugin-import(no-named-as-default-member): {1:?} also has a named export {2:?}")]
|
||||
#[diagnostic(severity(warning), help("Check if you meant to write `import {{{2:}}} from {3:?}`"))]
|
||||
struct NoNamedAsDefaultMemberDignostic(#[label] pub Span, String, String, String);
|
||||
|
||||
/// <https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-named-as-default-member.md>
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NoNamedAsDefaultMember;
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// Reports use of an exported name as a property on the default export.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```javascript
|
||||
/// // ./bar.js
|
||||
/// export function bar() { return null }
|
||||
/// export default () => { return 1 }
|
||||
///
|
||||
/// // ./foo.js
|
||||
/// import bar from './bar'
|
||||
/// const bar = foo.bar // trying to access named export via default
|
||||
/// ```
|
||||
NoNamedAsDefaultMember,
|
||||
nursery
|
||||
);
|
||||
|
||||
impl Rule for NoNamedAsDefaultMember {
|
||||
fn run_once(&self, ctx: &LintContext<'_>) {
|
||||
let module_record = ctx.semantic().module_record();
|
||||
|
||||
let mut has_members_map: HashMap<&Atom, (Ref<'_, Atom, _, _>, Atom)> = HashMap::default();
|
||||
for import_entry in &module_record.import_entries {
|
||||
let ImportImportName::Default(_) = import_entry.import_name else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let specifier = import_entry.module_request.name();
|
||||
let Some(remote_module_record_ref) = module_record.loaded_modules.get(specifier) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !remote_module_record_ref.exported_bindings.is_empty() {
|
||||
has_members_map.insert(
|
||||
import_entry.local_name.name(),
|
||||
(remote_module_record_ref, import_entry.module_request.name().to_owned()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if has_members_map.is_empty() {
|
||||
return;
|
||||
};
|
||||
let get_external_module_name_if_has_entry = |module_name: &Atom, entry_name: &Atom| {
|
||||
has_members_map.get(&module_name).and_then(|it| {
|
||||
if it.0.exported_bindings.contains_key(entry_name) {
|
||||
Some(it.1.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let process_member_expr = |member_expr: &MemberExpression| {
|
||||
let Expression::Identifier(ident) = member_expr.object() else {
|
||||
return;
|
||||
};
|
||||
let Some(prop_str) = member_expr.static_property_name() else {
|
||||
return;
|
||||
};
|
||||
if let Some(module_name) =
|
||||
get_external_module_name_if_has_entry(&ident.name, &Atom::new_inline(prop_str))
|
||||
{
|
||||
ctx.diagnostic(NoNamedAsDefaultMemberDignostic(
|
||||
match member_expr {
|
||||
MemberExpression::ComputedMemberExpression(it) => it.span,
|
||||
MemberExpression::StaticMemberExpression(it) => it.span,
|
||||
MemberExpression::PrivateFieldExpression(it) => it.span,
|
||||
},
|
||||
ident.name.to_string(),
|
||||
prop_str.to_string(),
|
||||
module_name,
|
||||
));
|
||||
};
|
||||
};
|
||||
|
||||
for item in ctx.semantic().nodes().iter() {
|
||||
match item.kind() {
|
||||
AstKind::MemberExpression(member_expr) => process_member_expr(member_expr),
|
||||
AstKind::VariableDeclarator(decl) => {
|
||||
if let Some(Expression::MemberExpression(member_expr)) = &decl.init {
|
||||
process_member_expr(member_expr);
|
||||
return;
|
||||
}
|
||||
let Some(Expression::Identifier(ident)) = &decl.init else {
|
||||
return;
|
||||
};
|
||||
let BindingPatternKind::ObjectPattern(object_pattern) = &decl.id.kind else {
|
||||
return;
|
||||
};
|
||||
|
||||
for prop in &*object_pattern.properties {
|
||||
let Some(name) = prop.key.static_name() else { return };
|
||||
if let Some(module_name) =
|
||||
get_external_module_name_if_has_entry(&ident.name, &name)
|
||||
{
|
||||
ctx.diagnostic(NoNamedAsDefaultMemberDignostic(
|
||||
decl.span,
|
||||
ident.name.to_string(),
|
||||
name.to_string(),
|
||||
module_name,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
r#"import baz, {a} from "./named-exports""#,
|
||||
r#"import baz from "./named-exports"; const jjj = bar.jjj"#,
|
||||
r#"import {a} from "./named-exports"; const baz = a.baz"#,
|
||||
r#"import baz from "./default_export_default_property"; const d = baz.default;"#,
|
||||
r#"import baz, {foo} from "./named-and-default-export"; const d = baz.default;"#,
|
||||
r"import baz from './named-exports';
|
||||
{
|
||||
const baz = {};
|
||||
const a = baz.a;
|
||||
}",
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
r#"import baz from "./named-exports"; const a = baz.a;"#,
|
||||
r#"import baz from "./named-exports"; const a = baz["a"];"#,
|
||||
r#"import baz from "./named-exports"; baz.a();"#,
|
||||
r"import baz from './named-exports';
|
||||
{
|
||||
const a = baz.a;
|
||||
}",
|
||||
r#"import baz, { bar } from "./named-exports"; const {a} = baz"#,
|
||||
r#"import baz from "./named-and-default-export"; const {foo: _foo} = baz"#,
|
||||
];
|
||||
|
||||
Tester::new(NoNamedAsDefaultMember::NAME, pass, fail)
|
||||
.change_rule_path("index.js")
|
||||
.with_import_plugin(true)
|
||||
.test_and_snapshot();
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
expression: no_named_as_default_member
|
||||
---
|
||||
⚠ eslint-plugin-import(no-named-as-default-member): "baz" also has a named export "a"
|
||||
╭─[index.js:1:1]
|
||||
1 │ import baz from "./named-exports"; const a = baz.a;
|
||||
· ─────
|
||||
╰────
|
||||
help: Check if you meant to write `import {a} from "./named-exports"`
|
||||
|
||||
⚠ eslint-plugin-import(no-named-as-default-member): "baz" also has a named export "a"
|
||||
╭─[index.js:1:1]
|
||||
1 │ import baz from "./named-exports"; const a = baz["a"];
|
||||
· ────────
|
||||
╰────
|
||||
help: Check if you meant to write `import {a} from "./named-exports"`
|
||||
|
||||
⚠ eslint-plugin-import(no-named-as-default-member): "baz" also has a named export "a"
|
||||
╭─[index.js:1:1]
|
||||
1 │ import baz from "./named-exports"; baz.a();
|
||||
· ─────
|
||||
╰────
|
||||
help: Check if you meant to write `import {a} from "./named-exports"`
|
||||
|
||||
⚠ eslint-plugin-import(no-named-as-default-member): "baz" also has a named export "a"
|
||||
╭─[index.js:2:1]
|
||||
2 │ {
|
||||
3 │ const a = baz.a;
|
||||
· ─────
|
||||
4 │ }
|
||||
╰────
|
||||
help: Check if you meant to write `import {a} from "./named-exports"`
|
||||
|
||||
⚠ eslint-plugin-import(no-named-as-default-member): "baz" also has a named export "a"
|
||||
╭─[index.js:1:1]
|
||||
1 │ import baz, { bar } from "./named-exports"; const {a} = baz
|
||||
· ─────────
|
||||
╰────
|
||||
help: Check if you meant to write `import {a} from "./named-exports"`
|
||||
|
||||
⚠ eslint-plugin-import(no-named-as-default-member): "baz" also has a named export "foo"
|
||||
╭─[index.js:1:1]
|
||||
1 │ import baz from "./named-and-default-export"; const {foo: _foo} = baz
|
||||
· ─────────────────
|
||||
╰────
|
||||
help: Check if you meant to write `import {foo} from "./named-and-default-export"`
|
||||
|
||||
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 7c29fbc4db7809d24789235b0597e7e8dd61c4ae
|
||||
Subproject commit 98c08853e3518c8e311a0d45f73f5ef4efae5d77
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 467a0fe68f633fabde77df94d993c45e840627a0
|
||||
Subproject commit a1ba783ca340e4bf3d80b5f5e11fa54f2ee5f1ef
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit b6121e400cf8636760aa8a7da6b7fac14e2e70c7
|
||||
Subproject commit cf33fd0cde22905effce371bb02484a9f2009023
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 8d64505bd47bddb5f559f4a1eead64a158d81c5a
|
||||
Subproject commit ff83d55d05e92ceef10ec0cb1c0272ab894a00a0
|
||||
Loading…
Reference in a new issue