Skip to content

Commit

Permalink
feat(linter/import): partial support namespace check (#2538)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dunqing authored Feb 29, 2024
1 parent 5a13714 commit fe777f3
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 17 deletions.
115 changes: 98 additions & 17 deletions crates/oxc_linter/src/rules/import/namespace.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,111 @@
use oxc_ast::{ast::JSXElementName, AstKind};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::{CompactString, Span};
use oxc_span::{CompactString, GetSpan, Span};
use oxc_syntax::module_record::ImportImportName;

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

#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-import(namespace): ")]
#[diagnostic(severity(warning), help(""))]
struct NamespaceDiagnostic(CompactString, #[label] pub Span);
enum NamespaceDiagnostic {
#[error("eslint-plugin-import(namespace): {1:?} not found in imported namespace {2:?}.")]
#[diagnostic(severity(warning))]
NoExport(#[label] Span, CompactString, CompactString),
#[error("eslint-plugin-import(namespace): Unable to validate computed reference to imported namespace {1:?}
.")]
#[diagnostic(severity(warning))]
ComputedReference(#[label] Span, CompactString),
#[error("eslint-plugin-import(namespace): Assignment to member of namespace {1:?}.'")]
#[diagnostic(severity(warning))]
Assignment(#[label] Span, CompactString),
}

/// <https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/namespace.md>
#[derive(Debug, Default, Clone)]
pub struct Namespace;

declare_oxc_lint!(
/// ### What it does
/// TODO
/// Enforces names exist at the time they are dereferenced, when imported as a full namespace (i.e. import * as foo from './foo'; foo.bar(); will report if bar is not exported by ./foo.).
/// Will report at the import declaration if there are no exported names found.
/// Also, will report for computed references (i.e. foo["bar"]()).
/// Reports on assignment to a member of an imported namespace.
Namespace,
nursery
);

impl Rule for Namespace {
fn run_once(&self, _ctx: &LintContext<'_>) {}
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;
};

if module.not_esm {
return;
}

let Some(symbol_id) =
ctx.semantic().symbols().get_symbol_id_from_span(&entry.local_name.span())
else {
return;
};

let check_binding_exported = |name: &str, span| {
if module.exported_bindings.get(name).is_some() {
return;
}
ctx.diagnostic(NamespaceDiagnostic::NoExport(span, name.into(), source.clone()));
};

ctx.symbols().get_resolved_references(symbol_id).for_each(|reference| {
if let Some(node) = ctx.nodes().parent_node(reference.node_id()) {
let name = entry.local_name.name();

match node.kind() {
AstKind::MemberExpression(member) => {
if matches!(
ctx.nodes().parent_kind(node.id()),
Some(AstKind::SimpleAssignmentTarget(_))
) {
ctx.diagnostic(NamespaceDiagnostic::Assignment(
member.span(),
name.clone(),
));
};

// TODO: Support allow_computed option
if member.is_computed() {
return ctx.diagnostic(NamespaceDiagnostic::ComputedReference(
member.span(),
name.clone(),
));
}

if let Some((span, name)) = member.static_property_info() {
check_binding_exported(name, span);
}
}

AstKind::JSXOpeningElement(element) => {
if let JSXElementName::MemberExpression(expr) = &element.name {
check_binding_exported(&expr.property.name, expr.property.span);
}
}
_ => {}
}
}
});
});
}
}

#[test]
Expand All @@ -33,11 +114,11 @@ fn test() {

let pass = vec![
r#"import "./malformed.js""#,
// r#"import * as foo from './empty-folder';"#,
// r#"import * as names from "./named-exports"; console.log((names.b).c);"#,
// r#"import * as names from "./named-exports"; console.log(names.a);"#,
r"import * as foo from './empty-folder';",
r#"import * as names from "./named-exports"; console.log((names.b).c);"#,
r#"import * as names from "./named-exports"; console.log(names.a);"#,
// r#"import * as names from "./re-export-names"; console.log(names.foo);"#,
// r#"import * as elements from './jsx';"#,
r"import * as elements from './jsx';",
// r#"import * as foo from "./jsx/re-export.js";
// console.log(foo.jsxFoo);"#,
// r#"import * as foo from "./jsx/bar/index.js";
Expand Down Expand Up @@ -118,21 +199,21 @@ fn test() {
];

let fail = vec![
// r#"import * as names from './named-exports'; console.log(names.c)"#,
// r#"import * as names from './named-exports'; console.log(names['a']);"#,
// r#"import * as foo from './bar'; foo.foo = 'y';"#,
// r#"import * as foo from './bar'; foo.x = 'y';"#,
r"import * as names from './named-exports'; console.log(names.c)",
r"import * as names from './named-exports'; console.log(names['a']);",
r"import * as foo from './bar'; foo.foo = 'y';",
r"import * as foo from './bar'; foo.x = 'y';",
// r#"import * as names from "./named-exports"; const { c } = names"#,
// r#"import * as names from "./named-exports"; function b() { const { c } = names }"#,
// r#"import * as names from "./named-exports"; const { c: d } = names"#,
// r#"import * as names from "./named-exports"; const { c: { d } } = names"#,
// r#"import * as Endpoints from "./issue-195/Endpoints"; console.log(Endpoints.Foo)"#,
// r#"import * as namespace from './malformed.js';"#,
// r#"import b from './deep/default'; console.log(b.e)"#,
// r#"console.log(names.c); import * as names from './named-exports';"#,
// r#"function x() { console.log(names.c) } import * as names from './named-exports';"#,
r"console.log(names.c); import * as names from './named-exports';",
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 * 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)"#,
Expand Down
47 changes: 47 additions & 0 deletions crates/oxc_linter/src/snapshots/namespace.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,51 @@
source: crates/oxc_linter/src/tester.rs
expression: namespace
---
eslint-plugin-import(namespace): "c" not found in imported namespace "./named-exports".
╭─[index.js:1:61]
1import * as names from './named-exports'; console.log(names.c)
· ─
╰────

eslint-plugin-import(namespace): Unable to validate computed reference to imported namespace "names"
│ .
╭─[index.js:1:55]
1import * as names from './named-exports'; console.log(names['a']);
· ──────────
╰────

eslint-plugin-import(namespace): Assignment to member of namespace "foo".'
╭─[index.js:1:31]
1import * as foo from './bar'; foo.foo = 'y';
· ───────
╰────

eslint-plugin-import(namespace): Assignment to member of namespace "foo".'
╭─[index.js:1:31]
1import * as foo from './bar'; foo.x = 'y';
· ─────
╰────

eslint-plugin-import(namespace): "x" not found in imported namespace "./bar".
╭─[index.js:1:35]
1import * as foo from './bar'; foo.x = 'y';
· ─
╰────

eslint-plugin-import(namespace): "c" not found in imported namespace "./named-exports".
╭─[index.js:1:19]
1console.log(names.c); import * as names from './named-exports';
· ─
╰────

eslint-plugin-import(namespace): "c" not found in imported namespace "./named-exports".
╭─[index.js:1:34]
1function x() { console.log(names.c) } import * as names from './named-exports';
· ─
╰────

eslint-plugin-import(namespace): "e" not found in imported namespace "./named-exports".
╭─[index.js:1:62]
1import * as Names from "./named-exports"; const Foo = <Names.e/>
· ─
╰────

0 comments on commit fe777f3

Please sign in to comment.