Skip to content

Commit

Permalink
feat(linter/import) check deep namespace in namespace rule
Browse files Browse the repository at this point in the history
  • Loading branch information
Dunqing committed Mar 25, 2024
1 parent 525031b commit 88c17d0
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 38 deletions.
165 changes: 127 additions & 38 deletions crates/oxc_linter/src/rules/import/namespace.rs
@@ -1,19 +1,22 @@
use std::sync::Arc;

use oxc_ast::{ast::BindingPatternKind, AstKind};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_semantic::ModuleRecord;
use oxc_span::{CompactStr, GetSpan, Span};
use oxc_syntax::module_record::ImportImportName;
use oxc_syntax::module_record::{ExportExportName, ExportImportName, ImportImportName};

use crate::{context::LintContext, rule::Rule};

#[derive(Debug, Error, Diagnostic)]
enum NamespaceDiagnostic {
#[error("eslint-plugin-import(namespace): {1:?} not found in imported namespace {2:?}.")]
#[diagnostic(severity(warning))]
NoExport(#[label] Span, CompactStr, CompactStr),
NoExport(#[label] Span, CompactStr, String),
#[error("eslint-plugin-import(namespace): Unable to validate computed reference to imported namespace {1:?}
.")]
#[diagnostic(severity(warning))]
Expand All @@ -37,16 +40,68 @@ declare_oxc_lint!(
nursery
);

// TODO: document here
fn get_source_from_nested_module_namespace(
name: &CompactStr,
module_record: &ModuleRecord,
) -> Option<String> {
if let Some(entry) =
module_record.indirect_export_entries.iter().find(|e| match &e.import_name {
ExportImportName::All => {
if let ExportExportName::Name(name_span) = &e.export_name {
return name_span.name() == name;
}

false
}
ExportImportName::Name(name_span) => name_span.name() == name,
_ => false,
})
{
return entry.module_request.as_ref().map(|name| name.name().to_string());
};

return module_record
.import_entries
.iter()
.find(|entry| entry.local_name.name() == name && entry.import_name.is_namespace_object())
.map(|entry| entry.module_request.name().to_string());
}

impl Rule for Namespace {
fn run_once(&self, ctx: &LintContext<'_>) {
ctx.semantic().module_record().import_entries.iter().for_each(|entry| {
if !matches!(entry.import_name, ImportImportName::NamespaceObject) {
return;
}
let source = entry.module_request.name();
let module_record = ctx.semantic().module_record();
let Some(module) = module_record.loaded_modules.get(source) else {
return;
let module_record = ctx.semantic().module_record();
module_record.import_entries.iter().for_each(|entry| {
let (source, module) = match &entry.import_name {
ImportImportName::NamespaceObject => {
let source = entry.module_request.name();
if let Some(module) = module_record.loaded_modules.get(source) {
(source.to_string(), Arc::clone(module.value()))
} else {
return;
}
}
ImportImportName::Name(name) => {
let Some(loaded_module) =
module_record.loaded_modules.get(entry.module_request.name())
else {
return;
};
let Some(source) =
get_source_from_nested_module_namespace(name.name(), &loaded_module)
else {
return;
};

let Some(loaded_module) =
&loaded_module.loaded_modules.get(&CompactStr::from(source.clone()))
else {
return;
};

(source, Arc::clone(loaded_module.value()))
}
ImportImportName::Default(_) => return,
};

if module.not_esm {
Expand All @@ -59,7 +114,7 @@ impl Rule for Namespace {
return;
};

let check_binding_exported = |name: &str, span| {
let check_binding_exported = |name: &str, span, module: &ModuleRecord| {
if module.exported_bindings.contains_key(name)
|| module
.exported_bindings_from_star_export
Expand Down Expand Up @@ -95,15 +150,49 @@ impl Rule for Namespace {
));
}

if let Some((span, name)) = member.static_property_info() {
check_binding_exported(name, span);
let properties = ctx
.nodes()
.iter_parents(node.id())
.filter_map(|node| {
if let AstKind::MemberExpression(expr) = node.kind() {
expr.static_property_info()
} else {
None
}
})
.collect::<Vec<_>>();

let mut next_module = Arc::clone(&module);
let last_index = properties.len() - 1;
for (index, (span, name)) in properties.into_iter().enumerate() {
if last_index == index {
check_binding_exported(name, span, &next_module);
} else if let Some(source) = get_source_from_nested_module_namespace(
&CompactStr::from(name),
&next_module,
) {
let module = Arc::clone(
next_module
.loaded_modules
.get(&CompactStr::from(source))
.unwrap()
.value(),
);
next_module = module;
} else {
break;
}
}
}
AstKind::JSXMemberExpressionObject(_) => {
if let Some(AstKind::JSXMemberExpression(expr)) =
ctx.nodes().parent_kind(node.id())
{
check_binding_exported(&expr.property.name, expr.property.span);
check_binding_exported(
&expr.property.name,
expr.property.span,
&module,
);
}
}
AstKind::VariableDeclarator(decl) => {
Expand All @@ -112,7 +201,7 @@ impl Rule for Namespace {
};
pattern.properties.iter().for_each(|property| {
if let Some(name) = property.key.name() {
check_binding_exported(&name, property.key.span());
check_binding_exported(&name, property.key.span(), &module);
}
});
}
Expand Down Expand Up @@ -198,18 +287,18 @@ fn test() {
// r#"import * as names from './default-export-string'; console.log(names.default)"#,
// r#"import * as names from './default-export-namespace-string';"#,
// r#"import * as names from './default-export-namespace-string'; console.log(names.default)"#,
// r#"import { "b" as b } from "./deep/a"; console.log(b.c.d.e)"#,
// r#"import { "b" as b } from "./deep/a"; var {c:{d:{e}}} = b"#,
// r#"import * as a from "./deep/a"; console.log(a.b.c.d.e)"#,
// r#"import { b } from "./deep/a"; console.log(b.c.d.e)"#,
// r#"import * as a from "./deep/a"; console.log(a.b.c.d.e.f)"#,
// r#"import * as a from "./deep/a"; var {b:{c:{d:{e}}}} = a"#,
// r#"import { b } from "./deep/a"; var {c:{d:{e}}} = b"#,
// r#"import * as a from "./deep-es7/a"; console.log(a.b.c.d.e)"#,
// r#"import { b } from "./deep-es7/a"; console.log(b.c.d.e)"#,
// r#"import * as a from "./deep-es7/a"; console.log(a.b.c.d.e.f)"#,
// r#"import * as a from "./deep-es7/a"; var {b:{c:{d:{e}}}} = a"#,
// r#"import { b } from "./deep-es7/a"; var {c:{d:{e}}} = b"#,
r#"import { "b" as b } from "./deep/a"; console.log(b.c.d.e)"#,
r#"import { "b" as b } from "./deep/a"; var {c:{d:{e}}} = b"#,
r#"import * as a from "./deep/a"; console.log(a.b.c.d.e)"#,
r#"import { b } from "./deep/a"; console.log(b.c.d.e)"#,
r#"import * as a from "./deep/a"; console.log(a.b.c.d.e.f)"#,
r#"import * as a from "./deep/a"; var {b:{c:{d:{e}}}} = a"#,
r#"import { b } from "./deep/a"; var {c:{d:{e}}} = b"#,
r#"import * as a from "./deep-es7/a"; console.log(a.b.c.d.e)"#,
r#"import { b } from "./deep-es7/a"; console.log(b.c.d.e)"#,
r#"import * as a from "./deep-es7/a"; console.log(a.b.c.d.e.f)"#,
r#"import * as a from "./deep-es7/a"; var {b:{c:{d:{e}}}} = a"#,
r#"import { b } from "./deep-es7/a"; var {c:{d:{e}}} = b"#,
];

let fail = vec![
Expand All @@ -228,18 +317,18 @@ fn test() {
r"function x() { console.log(names.c) } import * as names from './named-exports';",
r#"import * as ree from "./re-export"; console.log(ree.default)"#,
r#"import * as Names from "./named-exports"; const Foo = <Names.e/>"#,
// r#"import { "b" as b } from "./deep/a"; console.log(b.e)"#,
// r#"import { "b" as b } from "./deep/a"; console.log(b.c.e)"#,
// r#"import * as a from "./deep/a"; console.log(a.b.e)"#,
// r#"import { b } from "./deep/a"; console.log(b.e)"#,
// r#"import * as a from "./deep/a"; console.log(a.b.c.e)"#,
// r#"import { b } from "./deep/a"; console.log(b.c.e)"#,
r#"import { "b" as b } from "./deep/a"; console.log(b.e)"#,
r#"import { "b" as b } from "./deep/a"; console.log(b.c.e)"#,
r#"import * as a from "./deep/a"; console.log(a.b.e)"#,
r#"import { b } from "./deep/a"; console.log(b.e)"#,
r#"import * as a from "./deep/a"; console.log(a.b.c.e)"#,
r#"import { b } from "./deep/a"; console.log(b.c.e)"#,
// r#"import * as a from "./deep/a"; var {b:{ e }} = a"#,
// r#"import * as a from "./deep/a"; var {b:{c:{ e }}} = a"#,
// r#"import * as a from "./deep-es7/a"; console.log(a.b.e)"#,
// r#"import { b } from "./deep-es7/a"; console.log(b.e)"#,
// r#"import * as a from "./deep-es7/a"; console.log(a.b.c.e)"#,
// r#"import { b } from "./deep-es7/a"; console.log(b.c.e)"#,
// r#"imprdefttfort * as a from "./deep/a"; var {b:{c:{ e }}} = a"#,
r#"import * as a from "./deep-es7/a"; console.log(a.b.e)"#,
r#"import { b } from "./deep-es7/a"; console.log(b.e)"#,
r#"import * as a from "./deep-es7/a"; console.log(a.b.c.e)"#,
r#"import { b } from "./deep-es7/a"; console.log(b.c.e)"#,
// r#"import * as a from "./deep-es7/a"; var {b:{ e }} = a"#,
// r#"import * as a from "./deep-es7/a"; var {b:{c:{ e }}} = a"#,
];
Expand Down
60 changes: 60 additions & 0 deletions crates/oxc_linter/src/snapshots/namespace.snap
Expand Up @@ -80,3 +80,63 @@ expression: namespace
1import * as Names from "./named-exports"; const Foo = <Names.e/>
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./b".
╭─[index.js:1:52]
1import { "b" as b } from "./deep/a"; console.log(b.e)
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./b".
╭─[index.js:1:54]
1import { "b" as b } from "./deep/a"; console.log(b.c.e)
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./deep/a".
╭─[index.js:1:48]
1import * as a from "./deep/a"; console.log(a.b.e)
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./b".
╭─[index.js:1:45]
1import { b } from "./deep/a"; console.log(b.e)
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./deep/a".
╭─[index.js:1:50]
1import * as a from "./deep/a"; console.log(a.b.c.e)
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./b".
╭─[index.js:1:47]
1import { b } from "./deep/a"; console.log(b.c.e)
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./deep-es7/a".
╭─[index.js:1:52]
1import * as a from "./deep-es7/a"; console.log(a.b.e)
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./b".
╭─[index.js:1:49]
1import { b } from "./deep-es7/a"; console.log(b.e)
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./deep-es7/a".
╭─[index.js:1:54]
1import * as a from "./deep-es7/a"; console.log(a.b.c.e)
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./b".
╭─[index.js:1:51]
1import { b } from "./deep-es7/a"; console.log(b.c.e)
· ─
╰────

0 comments on commit 88c17d0

Please sign in to comment.