@@ -7,7 +7,12 @@ use oxc_macros::declare_oxc_lint;
77use oxc_span:: Span ;
88use oxc_str:: CompactStr ;
99
10- use crate :: { ModuleRecord , context:: LintContext , rule:: Rule } ;
10+ use crate :: {
11+ ModuleRecord ,
12+ context:: LintContext ,
13+ module_record:: { ExportEntry , ExportExportName } ,
14+ rule:: Rule ,
15+ } ;
1116
1217fn no_named_export ( module_name : & str , span : Span ) -> OxcDiagnostic {
1318 OxcDiagnostic :: warn ( format ! ( "No named exports found in module '{module_name}'" ) )
@@ -56,6 +61,8 @@ impl Rule for Export {
5661 let module_record = ctx. module_record ( ) ;
5762 let named_export = & module_record. exported_bindings ;
5863
64+ diagnose_duplicate_named_exports ( ctx, module_record) ;
65+
5966 let mut all_export_names = FxHashMap :: default ( ) ;
6067 let mut visited = FxHashSet :: default ( ) ;
6168
@@ -104,6 +111,64 @@ impl Rule for Export {
104111 }
105112}
106113
114+ struct ExportNameSpans {
115+ spans : Vec < Span > ,
116+ has_named_specifier : bool ,
117+ }
118+
119+ fn diagnose_duplicate_named_exports ( ctx : & LintContext < ' _ > , module_record : & ModuleRecord ) {
120+ let mut export_names: FxHashMap < ( CompactStr , bool ) , ExportNameSpans > = FxHashMap :: default ( ) ;
121+
122+ module_record
123+ . local_export_entries
124+ . iter ( )
125+ . chain ( & module_record. indirect_export_entries )
126+ . for_each ( |export_entry| {
127+ let Some ( ( name, span) ) = export_name ( export_entry) else {
128+ return ;
129+ } ;
130+
131+ let entry = export_names
132+ . entry ( ( CompactStr :: from ( name) , export_entry. is_type ) )
133+ . or_insert ( ExportNameSpans { spans : Vec :: new ( ) , has_named_specifier : false } ) ;
134+ entry. spans . push ( span) ;
135+ entry. has_named_specifier |= is_named_export_specifier ( ctx, export_entry) ;
136+ } ) ;
137+
138+ for ( ( name, _) , entry) in export_names {
139+ if entry. spans . len ( ) <= 1 || !entry. has_named_specifier {
140+ continue ;
141+ }
142+
143+ let labels = entry. spans . into_iter ( ) . map ( LabeledSpan :: underline) . collect :: < Vec < _ > > ( ) ;
144+ ctx. diagnostic (
145+ OxcDiagnostic :: warn ( format ! ( "Multiple exports of name '{name}'." ) )
146+ . with_help (
147+ "Rename or remove the duplicate export so each name is exported only once." ,
148+ )
149+ . with_labels ( labels) ,
150+ ) ;
151+ }
152+ }
153+
154+ fn export_name ( export_entry : & ExportEntry ) -> Option < ( & str , Span ) > {
155+ match & export_entry. export_name {
156+ ExportExportName :: Name ( name) => Some ( ( name. name ( ) , name. span ) ) ,
157+ ExportExportName :: Default ( span) => Some ( ( "default" , * span) ) ,
158+ ExportExportName :: Null => None ,
159+ }
160+ }
161+
162+ fn is_named_export_specifier ( ctx : & LintContext < ' _ > , export_entry : & ExportEntry ) -> bool {
163+ // Resolved indirect exports use the import statement span, which can appear after the export.
164+ if export_entry. statement_span . start > export_entry. span . start {
165+ return true ;
166+ }
167+
168+ ctx. find_next_token_within ( export_entry. statement_span . start , export_entry. span . start , "{" )
169+ . is_some ( )
170+ }
171+
107172fn walk_exported_recursive (
108173 module_record : & ModuleRecord ,
109174 result : & mut FxHashSet < CompactStr > ,
@@ -265,6 +330,7 @@ fn test() {
265330 export {Bar as default};
266331 "# ) ,
267332 "export type * from './export-props.js'" ,
333+ "export const foo = 1;\n import { foo } from 'mod';" ,
268334 ] ;
269335 let fail = vec ! [
270336 ( r#"let foo; export { foo }; export * from "./export-all""# ) ,
@@ -337,6 +403,9 @@ fn test() {
337403 // export const Foo = 'bar';
338404 // export namespace Foo { }
339405 // "),
406+ ( "export const foo = function () {};\n function bar() {}\n export { bar as foo };" ) ,
407+ ( "export const foo = 1;\n export /* comment */ { foo };" ) ,
408+ ( "export const value = 1;\n export { value };" ) ,
340409 ] ;
341410
342411 Tester :: new ( Export :: NAME , Export :: PLUGIN , pass, fail)
0 commit comments