Repository: import-js/eslint-plugin-import (JavaScript)

Issue:
BUG? Typescript export namespace nested `Multiple exports of name`
`import/export`
```ts
export namespace API {
    //Multiple exports of name 'User' 👇
    export namespace User {
        export const A = 0;
    }

    export namespace M {
        //Multiple exports of name 'User' 👇
        export const User = {};
    }
}
```
But they are not the same `User`


Discussion:
I'm not familiar with how namespaces work (since that's not a concept that exists in javascript) but clearly it seems like one is a sibling of M, and one is nested under M.

Do you think the rule should understand namespaces, or do you think it should ignore them?
it will be translated to 
```
exports.API = {
  User:{
    A:0
  },
  M:{
    User:{}
  }
}
```

* the first User's full name is `API.User`
* the second  User's full name is `API.M.User`

If namespaces can be parsed as above it will be resolved.




In that case, there's only one export - API - and all the others are just object properties.

So, it seems like this plugin should ignore everything but the top-level one.
yes, it's ok.
@ljharb I'm experiencing this and happy to try and submit a PR to fix it. I'll start poking around the codebase anyway, but if you had any suggestions of what would need to change to enable this then that would help. Thanks!
I guess we could add a check to [`addNamed`](https://github.com/benmosher/eslint-plugin-import/blob/206d67653c0d6cd087a51233f81d911fd347ca8e/src/rules/export.js#L15) in the export rule. 

Would something like this be a terrible idea?

```javascript
function addNamed(name, node) {
  if (node.parent.type !== 'Program') return

  ... // rest of the code
}
```
I'm not very knowledgeable on this part of the code; but it doesn't seem like skipping any top-level named export would be ideal :-)
@ljharb that example would do the opposite, but I'm sure it'll break something else instead. Was just hoping for some pointers of how this sort of thing would usually be approached. I'll see if I can take a proper look sometime in the next week 😀



Patch:
diff --git a/src/rules/export.js b/src/rules/export.js
index a4691bbe97..caa28e119f 100644
--- a/src/rules/export.js
+++ b/src/rules/export.js
@@ -2,6 +2,28 @@ import ExportMap, { recursivePatternCapture } from '../ExportMap'
 import docsUrl from '../docsUrl'
 import includes from 'array-includes'
 
+/*
+Notes on Typescript namespaces aka TSModuleDeclaration:
+
+There are two forms:
+- active namespaces: namespace Foo {} / module Foo {}
+- ambient modules; declare module "eslint-plugin-import" {}
+
+active namespaces:
+- cannot contain a default export
+- cannot contain an export all
+- cannot contain a multi name export (export { a, b })
+- can have active namespaces nested within them
+
+ambient namespaces:
+- can only be defined in .d.ts files
+- cannot be nested within active namespaces
+- have no other restrictions
+*/
+
+const rootProgram = 'root'
+const tsTypePrefix = 'type:'
+
 module.exports = {
   meta: {
     type: 'problem',
@@ -11,10 +33,15 @@ module.exports = {
   },
 
   create: function (context) {
-    const named = new Map()
+    const namespace = new Map([[rootProgram, new Map()]])
+
+    function addNamed(name, node, parent, isType) {
+      if (!namespace.has(parent)) {
+        namespace.set(parent, new Map())
+      }
+      const named = namespace.get(parent)
 
-    function addNamed(name, node, type) {
-      const key = type ? `${type}:${name}` : name
+      const key = isType ? `${tsTypePrefix}${name}` : name
       let nodes = named.get(key)
 
       if (nodes == null) {
@@ -25,30 +52,43 @@ module.exports = {
       nodes.add(node)
     }
 
+    function getParent(node) {
+      if (node.parent && node.parent.type === 'TSModuleBlock') {
+        return node.parent.parent
+      }
+
+      // just in case somehow a non-ts namespace export declaration isn't directly
+      // parented to the root Program node
+      return rootProgram
+    }
+
     return {
-      'ExportDefaultDeclaration': (node) => addNamed('default', node),
+      'ExportDefaultDeclaration': (node) => addNamed('default', node, getParent(node)),
 
-      'ExportSpecifier': function (node) {
-        addNamed(node.exported.name, node.exported)
-      },
+      'ExportSpecifier': (node) => addNamed(node.exported.name, node.exported, getParent(node)),
 
       'ExportNamedDeclaration': function (node) {
         if (node.declaration == null) return
 
+        const parent = getParent(node)
+        // support for old typescript versions
+        const isTypeVariableDecl = node.declaration.kind === 'type'
+
         if (node.declaration.id != null) {
           if (includes([
             'TSTypeAliasDeclaration',
             'TSInterfaceDeclaration',
           ], node.declaration.type)) {
-            addNamed(node.declaration.id.name, node.declaration.id, 'type')
+            addNamed(node.declaration.id.name, node.declaration.id, parent, true)
           } else {
-            addNamed(node.declaration.id.name, node.declaration.id)
+            addNamed(node.declaration.id.name, node.declaration.id, parent, isTypeVariableDecl)
           }
         }
 
         if (node.declaration.declarations != null) {
           for (let declaration of node.declaration.declarations) {
-            recursivePatternCapture(declaration.id, v => addNamed(v.name, v))
+            recursivePatternCapture(declaration.id, v =>
+              addNamed(v.name, v, parent, isTypeVariableDecl))
           }
         }
       },
@@ -63,11 +103,14 @@ module.exports = {
           remoteExports.reportErrors(context, node)
           return
         }
+
+        const parent = getParent(node)
+
         let any = false
         remoteExports.forEach((v, name) =>
           name !== 'default' &&
           (any = true) && // poor man's filter
-          addNamed(name, node))
+          addNamed(name, node, parent))
 
         if (!any) {
           context.report(node.source,
@@ -76,13 +119,20 @@ module.exports = {
       },
 
       'Program:exit': function () {
-        for (let [name, nodes] of named) {
-          if (nodes.size <= 1) continue
-
-          for (let node of nodes) {
-            if (name === 'default') {
-              context.report(node, 'Multiple default exports.')
-            } else context.report(node, `Multiple exports of name '${name}'.`)
+        for (let [, named] of namespace) {
+          for (let [name, nodes] of named) {
+            if (nodes.size <= 1) continue
+
+            for (let node of nodes) {
+              if (name === 'default') {
+                context.report(node, 'Multiple default exports.')
+              } else {
+                context.report(
+                  node,
+                  `Multiple exports of name '${name.replace(tsTypePrefix, '')}'.`
+                )
+              }
+            }
           }
         }
       },


Test patch:
diff --git a/tests/src/rules/export.js b/tests/src/rules/export.js
index a7a9e81922..3aad5241e9 100644
--- a/tests/src/rules/export.js
+++ b/tests/src/rules/export.js
@@ -126,26 +126,154 @@ context('Typescript', function () {
       },
     }
 
-    const isLT4 = process.env.ESLINT_VERSION === '3' || process.env.ESLINT_VERSION === '2';
-    const valid = [
-      test(Object.assign({
-        code: `
-          export const Foo = 1;
-          export interface Foo {}
-        `,
-      }, parserConfig)),
-    ]
-    if (!isLT4) {
-      valid.unshift(test(Object.assign({
-        code: `
-          export const Foo = 1;
-          export type Foo = number;
-        `,
-      }, parserConfig)))
-    }
     ruleTester.run('export', rule, {
-      valid: valid,
-      invalid: [],
+      valid: [
+        // type/value name clash
+        test(Object.assign({
+          code: `
+            export const Foo = 1;
+            export type Foo = number;
+          `,
+        }, parserConfig)),
+        test(Object.assign({
+          code: `
+            export const Foo = 1;
+            export interface Foo {}
+          `,
+        }, parserConfig)),
+
+        // namespace
+        test(Object.assign({
+          code: `
+            export const Bar = 1;
+            export namespace Foo {
+              export const Bar = 1;
+            }
+          `,
+        }, parserConfig)),
+        test(Object.assign({
+          code: `
+            export type Bar = string;
+            export namespace Foo {
+              export type Bar = string;
+            }
+          `,
+        }, parserConfig)),
+        test(Object.assign({
+          code: `
+            export const Bar = 1;
+            export type Bar = string;
+            export namespace Foo {
+              export const Bar = 1;
+              export type Bar = string;
+            }
+          `,
+        }, parserConfig)),
+        test(Object.assign({
+          code: `
+            export namespace Foo {
+              export const Foo = 1;
+              export namespace Bar {
+                export const Foo = 2;
+              }
+              export namespace Baz {
+                export const Foo = 3;
+              }
+            }
+          `,
+        }, parserConfig)),
+      ],
+      invalid: [
+        // type/value name clash
+        test(Object.assign({
+          code: `
+            export type Foo = string;
+            export type Foo = number;
+          `,
+          errors: [
+            {
+              message: `Multiple exports of name 'Foo'.`,
+              line: 2,
+            },
+            {
+              message: `Multiple exports of name 'Foo'.`,
+              line: 3,
+            },
+          ],
+        }, parserConfig)),
+
+        // namespace
+        test(Object.assign({
+          code: `
+            export const a = 1
+            export namespace Foo {
+              export const a = 2;
+              export const a = 3;
+            }
+          `,
+          errors: [
+            {
+              message: `Multiple exports of name 'a'.`,
+              line: 4,
+            },
+            {
+              message: `Multiple exports of name 'a'.`,
+              line: 5,
+            },
+          ],
+        }, parserConfig)),
+        test(Object.assign({
+          code: `
+            declare module 'foo' {
+              const Foo = 1;
+              export default Foo;
+              export default Foo;
+            }
+          `,
+          errors: [
+            {
+              message: 'Multiple default exports.',
+              line: 4,
+            },
+            {
+              message: 'Multiple default exports.',
+              line: 5,
+            },
+          ],
+        }, parserConfig)),
+        test(Object.assign({
+          code: `
+            export namespace Foo {
+              export namespace Bar {
+                export const Foo = 1;
+                export const Foo = 2;
+              }
+              export namespace Baz {
+                export const Bar = 3;
+                export const Bar = 4;
+              }
+            }
+          `,
+          errors: [
+            {
+              message: `Multiple exports of name 'Foo'.`,
+              line: 4,
+            },
+            {
+              message: `Multiple exports of name 'Foo'.`,
+              line: 5,
+            },
+            {
+              message: `Multiple exports of name 'Bar'.`,
+              line: 8,
+            },
+            {
+              message: `Multiple exports of name 'Bar'.`,
+              line: 9,
+            },
+          ],
+        }, parserConfig)),
+      ],
     })
   })
 })