-
-
Notifications
You must be signed in to change notification settings - Fork 445
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter): eslint-plugin-import no-named-as-default-member rule
- Loading branch information
Showing
8 changed files
with
234 additions
and
4 deletions.
There are no files selected for viewing
3 changes: 3 additions & 0 deletions
3
crates/oxc_linter/fixtures/import/named-and-default-export.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default {}; | ||
|
||
export const foo = 10 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
175 changes: 175 additions & 0 deletions
175
crates/oxc_linter/src/rules/import/no_named_as_default_member.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} |
50 changes: 50 additions & 0 deletions
50
crates/oxc_linter/src/snapshots/no_named_as_default_member.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
--- | ||
source: crates/oxc_linter/src/tester.rs | ||
assertion_line: 154 | ||
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"` | ||
|
||
|
Submodule typescript
updated
751 files