mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
fix(semantic): export default foo should have ExportLocalName::Default(NameSpan) entry (#3823)
This commit is contained in:
parent
7302429b2b
commit
99a40ce6ac
5 changed files with 83 additions and 102 deletions
|
|
@ -43,7 +43,9 @@ impl Rule for NoPageCustomFont {
|
|||
let AstKind::JSXOpeningElement(element) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
if matches!(&element.name, JSXElementName::Identifier(ident) if ident.name != "link") {
|
||||
let JSXElementName::Identifier(ident) = &element.name else { return };
|
||||
|
||||
if ident.name != "link" {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -58,63 +60,12 @@ impl Rule for NoPageCustomFont {
|
|||
return;
|
||||
}
|
||||
|
||||
let mut is_inside_export_default = false;
|
||||
for parent_node in ctx.nodes().iter_parents(node.id()) {
|
||||
// export default function/class
|
||||
let kind = parent_node.kind();
|
||||
if matches!(kind, AstKind::ExportDefaultDeclaration(_)) {
|
||||
is_inside_export_default = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// function variable() {}; export default variable;
|
||||
let id = match kind {
|
||||
AstKind::ArrowFunctionExpression(_) => None,
|
||||
AstKind::Function(Function { id, .. }) | AstKind::Class(Class { id, .. }) => {
|
||||
id.clone()
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let name = id.map_or_else(
|
||||
|| {
|
||||
let parent_parent_kind = ctx.nodes().parent_kind(parent_node.id())?;
|
||||
|
||||
let AstKind::VariableDeclarator(declarator) = parent_parent_kind else {
|
||||
return None;
|
||||
};
|
||||
declarator.id.get_identifier().map(|id| id.to_string())
|
||||
},
|
||||
|id| Some(id.name.to_string()),
|
||||
);
|
||||
let Some(name) = name else {
|
||||
continue;
|
||||
};
|
||||
if let Some(symbol_id) = ctx.scopes().get_root_binding(&name) {
|
||||
if ctx.symbols().get_flag(symbol_id).is_export() {
|
||||
let is_export_default =
|
||||
ctx.symbols().get_resolved_references(symbol_id).any(|reference| {
|
||||
reference.is_read()
|
||||
&& matches!(
|
||||
ctx.nodes().parent_kind(reference.node_id()),
|
||||
Some(AstKind::ExportDefaultDeclaration(_))
|
||||
)
|
||||
});
|
||||
|
||||
if is_export_default {
|
||||
is_inside_export_default = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let in_document = ctx.file_path().file_name().map_or(false, |file_name| {
|
||||
file_name.to_str().map_or(false, |file_name| file_name.starts_with("_document."))
|
||||
});
|
||||
let span = ctx.nodes().parent_kind(node.id()).unwrap().span();
|
||||
let diagnostic = if in_document {
|
||||
if is_inside_export_default {
|
||||
if is_inside_export_default(node, ctx) {
|
||||
return;
|
||||
}
|
||||
link_outside_of_head(span)
|
||||
|
|
@ -125,6 +76,47 @@ impl Rule for NoPageCustomFont {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_inside_export_default(node: &AstNode<'_>, ctx: &LintContext<'_>) -> bool {
|
||||
let mut is_inside_export_default = false;
|
||||
for parent_node in ctx.nodes().iter_parents(node.id()) {
|
||||
// export default function/class
|
||||
let kind = parent_node.kind();
|
||||
if matches!(kind, AstKind::ExportDefaultDeclaration(_)) {
|
||||
is_inside_export_default = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// function variable() {}; export default variable;
|
||||
let id = match kind {
|
||||
AstKind::ArrowFunctionExpression(_) => None,
|
||||
AstKind::Function(Function { id, .. }) | AstKind::Class(Class { id, .. }) => id.clone(),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let name = id.map_or_else(
|
||||
|| {
|
||||
let parent_parent_kind = ctx.nodes().parent_kind(parent_node.id())?;
|
||||
|
||||
let AstKind::VariableDeclarator(declarator) = parent_parent_kind else {
|
||||
return None;
|
||||
};
|
||||
declarator.id.get_identifier().map(|id| id.to_string())
|
||||
},
|
||||
|id| Some(id.name.to_string()),
|
||||
);
|
||||
let Some(name) = name else {
|
||||
continue;
|
||||
};
|
||||
if ctx.module_record().local_export_entries.iter().any(|e| {
|
||||
e.local_name.is_default()
|
||||
&& e.local_name.name().is_some_and(|n| n.as_str() == name.as_str())
|
||||
}) {
|
||||
is_inside_export_default = true;
|
||||
}
|
||||
}
|
||||
is_inside_export_default
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -206,7 +198,7 @@ fn test() {
|
|||
</Html>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default CustomDocument;
|
||||
"#,
|
||||
None,
|
||||
|
|
@ -230,7 +222,7 @@ fn test() {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default MyDocument;"#,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -297,8 +289,8 @@ fn test() {
|
|||
(
|
||||
r#"
|
||||
import Head from 'next/head'
|
||||
|
||||
|
||||
|
||||
|
||||
function Links() {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -313,7 +305,7 @@ fn test() {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ use oxc_cfg::{
|
|||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_span::{CompactStr, SourceType, Span};
|
||||
use oxc_syntax::{
|
||||
identifier::is_identifier_name,
|
||||
module_record::{ExportImportName, ExportLocalName, ModuleRecord},
|
||||
module_record::{ExportImportName, ModuleRecord},
|
||||
operator::AssignmentOperator,
|
||||
};
|
||||
|
||||
|
|
@ -399,23 +398,10 @@ impl<'a> SemanticBuilder<'a> {
|
|||
});
|
||||
|
||||
self.module_record.local_export_entries.iter().for_each(|entry| {
|
||||
match &entry.local_name {
|
||||
ExportLocalName::Name(name_span) => {
|
||||
if let Some(symbol_id) = self.scope.get_root_binding(name_span.name()) {
|
||||
self.symbols.union_flag(symbol_id, SymbolFlags::Export);
|
||||
}
|
||||
if let Some(name) = entry.local_name.name() {
|
||||
if let Some(symbol_id) = self.scope.get_root_binding(name.as_str()) {
|
||||
self.symbols.union_flag(symbol_id, SymbolFlags::Export);
|
||||
}
|
||||
ExportLocalName::Default(_) => {
|
||||
// export default identifier
|
||||
// ^^^^^^^^^^
|
||||
let identifier = entry.span.source_text(self.source_text);
|
||||
if is_identifier_name(identifier) {
|
||||
if let Some(symbol_id) = self.scope.get_root_binding(identifier) {
|
||||
self.symbols.union_flag(symbol_id, SymbolFlags::Export);
|
||||
}
|
||||
}
|
||||
}
|
||||
ExportLocalName::Null => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1726,7 +1712,7 @@ impl<'a> SemanticBuilder<'a> {
|
|||
AstKind::Class(class) => {
|
||||
self.current_node_flags |= NodeFlags::Class;
|
||||
class.bind(self);
|
||||
self.remove_export_flag();
|
||||
self.current_symbol_flags -= SymbolFlags::Export;
|
||||
self.make_all_namespaces_valuelike();
|
||||
}
|
||||
AstKind::ClassBody(body) => {
|
||||
|
|
@ -1748,7 +1734,7 @@ impl<'a> SemanticBuilder<'a> {
|
|||
}
|
||||
AstKind::FormalParameters(_) => {
|
||||
self.current_node_flags |= NodeFlags::Parameter;
|
||||
self.remove_export_flag();
|
||||
self.current_symbol_flags -= SymbolFlags::Export;
|
||||
}
|
||||
AstKind::FormalParameter(param) => {
|
||||
param.bind(self);
|
||||
|
|
@ -1878,10 +1864,6 @@ impl<'a> SemanticBuilder<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn remove_export_flag(&mut self) {
|
||||
self.current_symbol_flags -= SymbolFlags::Export;
|
||||
}
|
||||
|
||||
fn add_current_node_id_to_current_scope(&mut self) {
|
||||
self.scope.add_node_id(self.current_scope_id, self.current_node_id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,22 +243,28 @@ impl ModuleRecordBuilder {
|
|||
return;
|
||||
}
|
||||
let exported_name = &decl.exported;
|
||||
self.add_default_export(exported_name.span());
|
||||
let exported_name_span = decl.exported.span();
|
||||
self.add_default_export(exported_name_span);
|
||||
|
||||
let id = match &decl.declaration {
|
||||
match_expression!(ExportDefaultDeclarationKind) => None,
|
||||
ExportDefaultDeclarationKind::FunctionDeclaration(func) => func.id.as_ref(),
|
||||
ExportDefaultDeclarationKind::ClassDeclaration(class) => class.id.as_ref(),
|
||||
ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => return,
|
||||
let local_name = match &decl.declaration {
|
||||
ExportDefaultDeclarationKind::Identifier(ident) => {
|
||||
ExportLocalName::Default(NameSpan::new(ident.name.to_compact_str(), ident.span))
|
||||
}
|
||||
ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
|
||||
func.id.as_ref().map_or_else(
|
||||
|| ExportLocalName::Null,
|
||||
|id| ExportLocalName::Name(NameSpan::new(id.name.to_compact_str(), id.span)),
|
||||
)
|
||||
}
|
||||
ExportDefaultDeclarationKind::ClassDeclaration(class) => class.id.as_ref().map_or_else(
|
||||
|| ExportLocalName::Null,
|
||||
|id| ExportLocalName::Name(NameSpan::new(id.name.to_compact_str(), id.span)),
|
||||
),
|
||||
_ => ExportLocalName::Null,
|
||||
};
|
||||
let export_entry = ExportEntry {
|
||||
export_name: ExportExportName::Default(exported_name.span()),
|
||||
local_name: id.as_ref().map_or_else(
|
||||
|| ExportLocalName::Default(exported_name.span()),
|
||||
|ident| {
|
||||
ExportLocalName::Name(NameSpan::new(ident.name.to_compact_str(), ident.span))
|
||||
},
|
||||
),
|
||||
local_name,
|
||||
span: decl.declaration.span(),
|
||||
..ExportEntry::default()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ mod module_record_tests {
|
|||
let module_record = build("export default function() {}");
|
||||
let export_entry = ExportEntry {
|
||||
export_name: ExportExportName::Default(Span::new(7, 14)),
|
||||
local_name: ExportLocalName::Default(Span::new(7, 14)),
|
||||
local_name: ExportLocalName::Null,
|
||||
span: Span::new(15, 28),
|
||||
..ExportEntry::default()
|
||||
};
|
||||
|
|
@ -231,7 +231,7 @@ mod module_record_tests {
|
|||
let module_record = build("export default 42");
|
||||
let export_entry = ExportEntry {
|
||||
export_name: ExportExportName::Default(Span::new(7, 14)),
|
||||
local_name: ExportLocalName::Default(Span::new(7, 14)),
|
||||
local_name: ExportLocalName::Null,
|
||||
span: Span::new(15, 17),
|
||||
..ExportEntry::default()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -246,7 +246,8 @@ impl ExportExportName {
|
|||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum ExportLocalName {
|
||||
Name(NameSpan),
|
||||
Default(Span),
|
||||
/// `export default name_span`
|
||||
Default(NameSpan),
|
||||
#[default]
|
||||
Null,
|
||||
}
|
||||
|
|
@ -262,8 +263,8 @@ impl ExportLocalName {
|
|||
|
||||
pub const fn name(&self) -> Option<&CompactStr> {
|
||||
match self {
|
||||
Self::Name(name) => Some(name.name()),
|
||||
Self::Default(_) | Self::Null => None,
|
||||
Self::Name(name) | Self::Default(name) => Some(name.name()),
|
||||
Self::Null => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -332,11 +333,11 @@ mod test {
|
|||
fn export_local_name() {
|
||||
let name = NameSpan::new("name".into(), Span::new(0, 0));
|
||||
assert!(!ExportLocalName::Name(name.clone()).is_default());
|
||||
assert!(ExportLocalName::Default(Span::new(0, 0)).is_default());
|
||||
assert!(ExportLocalName::Default(name.clone()).is_default());
|
||||
assert!(!ExportLocalName::Null.is_default());
|
||||
|
||||
assert!(!ExportLocalName::Name(name).is_null());
|
||||
assert!(!ExportLocalName::Default(Span::new(0, 0)).is_null());
|
||||
assert!(!ExportLocalName::Name(name.clone()).is_null());
|
||||
assert!(!ExportLocalName::Default(name.clone()).is_null());
|
||||
assert!(ExportLocalName::Null.is_null());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue