Repository: FHIR/sushi (ts)

Issue:
SUSHI should validate that names and IDs are unique
Within an IG / FSH Tank, names and ids should be unique.  Technically I think that uniqueness is only required per type by FHIR (e.g., you *could* have a profile and extension w/ the same name) --- but for now I think we should be more stringent in FSH.  I'm not sure SUSHI would handle duplicate names/ids well, even if they were for different types.

So, SUSHI should issue an error when encountering definitions that introduce a name or id that has already been used in the FSHTank.

Pull request description:
Enforce uniqueness for FSH names and FHIR ids
Fixes #152 and completes [CIMPL-375](https://standardhealthrecord.atlassian.net/browse/CIMPL-375).

FSH entities are tracked by name, so a reused name for the same entity type is an error. FHIR resources must have [unique ids](https://www.hl7.org/fhir/resource.html#id), so a reused id for the same FHIR type is an error. Mappings are not resources, but multiple mappings with the same id on the same StructureDefinition is confusing, so that is also an error.

The checks for imported FSH entities occur during import, and entities that reuse a name are skipped. This is different than the previous behavior, where the new entity would overwrite the previous entity. Because parsing is skipped for the new entity, errors that would have occurred while parsing it are never actually encountered.

The checks for exported FHIR resources are mostly similar to one another, but there are small differences based on when exactly the resource is added to the package.

Patch:
diff --git a/src/export/CodeSystemExporter.ts b/src/export/CodeSystemExporter.ts
index dd63247..fb7ebb4 100644
--- a/src/export/CodeSystemExporter.ts
+++ b/src/export/CodeSystemExporter.ts
@@ -86,6 +86,16 @@ export class CodeSystemExporter {
     this.setMetadata(codeSystem, fshDefinition);
     this.setCaretRules(codeSystem, fshDefinition.rules);
     this.setConcepts(codeSystem, fshDefinition);
+
+    // check for another code system with the same id
+    // see https://www.hl7.org/fhir/resource.html#id
+    if (this.pkg.codeSystems.some(cs => codeSystem.id === cs.id)) {
+      logger.error(
+        `Multiple code systems with id ${codeSystem.id}. Each code system must have a unique id.`,
+        fshDefinition.sourceInfo
+      );
+    }
+
     this.updateCount(codeSystem, fshDefinition);
     this.pkg.codeSystems.push(codeSystem);
     return codeSystem;
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index 7c9f413..d6a36d8 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -281,6 +281,27 @@ export class InstanceExporter implements Fishable {
       fshDefinition
     );
     cleanResource(instanceDef);
+
+    // check for another instance of the same type with the same id
+    // see https://www.hl7.org/fhir/resource.html#id
+    // instanceDef has already been added to the package, so it's fine if it matches itself
+    // inline instances are not written to their own files, so those can be skipped in this
+    if (
+      instanceDef._instanceMeta.usage !== 'Inline' &&
+      this.pkg.instances.some(
+        instance =>
+          instance._instanceMeta.usage !== 'Inline' &&
+          instanceDef.resourceType === instance.resourceType &&
+          instanceDef.id === instance.id &&
+          instanceDef !== instance
+      )
+    ) {
+      logger.error(
+        `Multiple instances of type ${instanceDef.resourceType} with id ${instanceDef.id}. Each non-inline instance of a given type must have a unique id.`,
+        fshDefinition.sourceInfo
+      );
+    }
+
     return instanceDef;
   }
 
diff --git a/src/export/MappingExporter.ts b/src/export/MappingExporter.ts
index 8b744ee..4fd19f7 100644
--- a/src/export/MappingExporter.ts
+++ b/src/export/MappingExporter.ts
@@ -4,6 +4,7 @@ import { logger, Type, MasterFisher } from '../utils';
 import { Mapping } from '../fshtypes';
 import { StructureDefinition, idRegex } from '../fhirtypes';
 import { InvalidFHIRIdError } from '../errors';
+import { groupBy, pickBy } from 'lodash';
 
 export class MappingExporter {
   constructor(
@@ -83,5 +84,16 @@ export class MappingExporter {
         logger.error(e.message, mapping.sourceInfo);
       }
     }
+    // The mappings on each Structure Definition should have a unique id
+    const groupedMappings = groupBy(mappings, 'source');
+    for (const sd in groupedMappings) {
+      const duplicateMappings = pickBy(groupBy(groupedMappings[sd], 'id'), m => m.length > 1);
+      for (const duplicateId in duplicateMappings) {
+        // show error for each time the id was used after the first
+        duplicateMappings[duplicateId].slice(1).forEach(m => {
+          logger.error(`Multiple mappings on ${sd} found with id ${m.id}`, m.sourceInfo);
+        });
+      }
+    }
   }
 }
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index 27efd06..54fc7e5 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -433,6 +433,21 @@ export class StructureDefinitionExporter implements Fishable {
     cleanResource(structDef, (prop: string) => prop == 'elements' || prop.indexOf('_') == 0);
     structDef.inProgress = false;
 
+    // check for another structure definition with the same id
+    // see https://www.hl7.org/fhir/resource.html#id
+    // the structure definition has already been added to the package, so it's fine if it matches itself
+    if (
+      this.pkg.profiles.some(profile => structDef.id === profile.id && structDef !== profile) ||
+      this.pkg.extensions.some(
+        extension => structDef.id === extension.id && structDef !== extension
+      )
+    ) {
+      logger.error(
+        `Multiple structure definitions with id ${structDef.id}. Each structure definition must have a unique id.`,
+        fshDefinition.sourceInfo
+      );
+    }
+
     return structDef;
   }
 
diff --git a/src/export/ValueSetExporter.ts b/src/export/ValueSetExporter.ts
index 9958180..de80d6e 100644
--- a/src/export/ValueSetExporter.ts
+++ b/src/export/ValueSetExporter.ts
@@ -155,6 +155,16 @@ export class ValueSetExporter {
     if (vs.compose && vs.compose.include.length == 0) {
       throw new ValueSetComposeError(fshDefinition.name);
     }
+
+    // check for another value set with the same id
+    // see https://www.hl7.org/fhir/resource.html#id
+    if (this.pkg.valueSets.some(valueSet => vs.id === valueSet.id)) {
+      logger.error(
+        `Multiple value sets with id ${vs.id}. Each value set must have a unique id.`,
+        fshDefinition.sourceInfo
+      );
+    }
+
     this.pkg.valueSets.push(vs);
     return vs;
   }
diff --git a/src/import/FSHImporter.ts b/src/import/FSHImporter.ts
index d800269..d7907b2 100644
--- a/src/import/FSHImporter.ts
+++ b/src/import/FSHImporter.ts
@@ -246,16 +246,30 @@ export class FSHImporter extends FSHVisitor {
     const profile = new Profile(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    this.parseProfileOrExtension(profile, ctx.sdMetadata(), ctx.sdRule());
-    this.currentDoc.profiles.set(profile.name, profile);
+    if (this.currentDoc.profiles.has(profile.name)) {
+      logger.error(`Skipping Profile: a Profile named ${profile.name} already exists.`, {
+        file: this.currentFile,
+        location: this.extractStartStop(ctx)
+      });
+    } else {
+      this.parseProfileOrExtension(profile, ctx.sdMetadata(), ctx.sdRule());
+      this.currentDoc.profiles.set(profile.name, profile);
+    }
   }
 
   visitExtension(ctx: pc.ExtensionContext) {
     const extension = new Extension(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    this.parseProfileOrExtension(extension, ctx.sdMetadata(), ctx.sdRule());
-    this.currentDoc.extensions.set(extension.name, extension);
+    if (this.currentDoc.extensions.has(extension.name)) {
+      logger.error(`Skipping Extension: an Extension named ${extension.name} already exists.`, {
+        file: this.currentFile,
+        location: this.extractStartStop(ctx)
+      });
+    } else {
+      this.parseProfileOrExtension(extension, ctx.sdMetadata(), ctx.sdRule());
+      this.currentDoc.extensions.set(extension.name, extension);
+    }
   }
 
   private parseProfileOrExtension(
@@ -298,11 +312,18 @@ export class FSHImporter extends FSHVisitor {
     const instance = new Instance(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    try {
-      this.parseInstance(instance, ctx.instanceMetadata(), ctx.fixedValueRule());
-      this.currentDoc.instances.set(instance.name, instance);
-    } catch (e) {
-      logger.error(e.message, instance.sourceInfo);
+    if (this.currentDoc.instances.has(instance.name)) {
+      logger.error(`Skipping Instance: an Instance named ${instance.name} already exists.`, {
+        file: this.currentFile,
+        location: this.extractStartStop(ctx)
+      });
+    } else {
+      try {
+        this.parseInstance(instance, ctx.instanceMetadata(), ctx.fixedValueRule());
+        this.currentDoc.instances.set(instance.name, instance);
+      } catch (e) {
+        logger.error(e.message, instance.sourceInfo);
+      }
     }
   }
 
@@ -352,8 +373,15 @@ export class FSHImporter extends FSHVisitor {
     const valueSet = new FshValueSet(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    this.parseValueSet(valueSet, ctx.vsMetadata(), ctx.vsComponent(), ctx.caretValueRule());
-    this.currentDoc.valueSets.set(valueSet.name, valueSet);
+    if (this.currentDoc.valueSets.has(valueSet.name)) {
+      logger.error(`Skipping ValueSet: a ValueSet named ${valueSet.name} already exists.`, {
+        file: this.currentFile,
+        location: this.extractStartStop(ctx)
+      });
+    } else {
+      this.parseValueSet(valueSet, ctx.vsMetadata(), ctx.vsComponent(), ctx.caretValueRule());
+      this.currentDoc.valueSets.set(valueSet.name, valueSet);
+    }
   }
 
   private parseValueSet(
@@ -427,8 +455,15 @@ export class FSHImporter extends FSHVisitor {
     const codeSystem = new FshCodeSystem(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    this.parseCodeSystem(codeSystem, ctx.csMetadata(), ctx.concept(), ctx.caretValueRule());
-    this.currentDoc.codeSystems.set(codeSystem.name, codeSystem);
+    if (this.currentDoc.codeSystems.has(codeSystem.name)) {
+      logger.error(`Skipping code system: a code system named ${codeSystem.name} already exists.`, {
+        file: this.currentFile,
+        location: this.extractStartStop(ctx)
+      });
+    } else {
+      this.parseCodeSystem(codeSystem, ctx.csMetadata(), ctx.concept(), ctx.caretValueRule());
+      this.currentDoc.codeSystems.set(codeSystem.name, codeSystem);
+    }
   }
 
   private parseCodeSystem(
@@ -487,14 +522,21 @@ export class FSHImporter extends FSHVisitor {
     const invariant = new Invariant(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    this.parseInvariant(invariant, ctx.invariantMetadata());
-    if (invariant.description == null) {
-      logger.error(`Invariant ${invariant.name} must have a Description.`, invariant.sourceInfo);
-    }
-    if (invariant.severity == null) {
-      logger.error(`Invariant ${invariant.name} must have a Severity.`, invariant.sourceInfo);
+    if (this.currentDoc.invariants.has(invariant.name)) {
+      logger.error(`Skipping Invariant: an Invariant named ${invariant.name} already exists.`, {
+        file: this.currentFile,
+        location: this.extractStartStop(ctx)
+      });
+    } else {
+      this.parseInvariant(invariant, ctx.invariantMetadata());
+      if (invariant.description == null) {
+        logger.error(`Invariant ${invariant.name} must have a Description.`, invariant.sourceInfo);
+      }
+      if (invariant.severity == null) {
+        logger.error(`Invariant ${invariant.name} must have a Severity.`, invariant.sourceInfo);
+      }
+      this.currentDoc.invariants.set(invariant.name, invariant);
     }
-    this.currentDoc.invariants.set(invariant.name, invariant);
   }
 
   private parseInvariant(invariant: Invariant, metaCtx: pc.InvariantMetadataContext[] = []) {
@@ -531,8 +573,15 @@ export class FSHImporter extends FSHVisitor {
     const ruleSet = new RuleSet(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    this.parseRuleSet(ruleSet, ctx.sdRule());
-    this.currentDoc.ruleSets.set(ruleSet.name, ruleSet);
+    if (this.currentDoc.ruleSets.has(ruleSet.name)) {
+      logger.error(`Skipping RuleSet: a RuleSet named ${ruleSet.name} already exists.`, {
+        file: this.currentFile,
+        location: this.extractStartStop(ctx)
+      });
+    } else {
+      this.parseRuleSet(ruleSet, ctx.sdRule());
+      this.currentDoc.ruleSets.set(ruleSet.name, ruleSet);
+    }
   }
 
   parseRuleSet(ruleSet: RuleSet, rules: pc.SdRuleContext[]) {
@@ -545,8 +594,15 @@ export class FSHImporter extends FSHVisitor {
     const mapping = new Mapping(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    this.parseMapping(mapping, ctx.mappingMetadata(), ctx.mappingRule());
-    this.currentDoc.mappings.set(mapping.name, mapping);
+    if (this.currentDoc.mappings.has(mapping.name)) {
+      logger.error(`Skipping Mapping: a Mapping named ${mapping.name} already exists.`, {
+        file: this.currentFile,
+        location: this.extractStartStop(ctx)
+      });
+    } else {
+      this.parseMapping(mapping, ctx.mappingMetadata(), ctx.mappingRule());
+      this.currentDoc.mappings.set(mapping.name, mapping);
+    }
   }
 
   parseMapping(


Test patch:
diff --git a/test/export/CodeSystemExporter.test.ts b/test/export/CodeSystemExporter.test.ts
index 5d0d7f7..72687ef 100644
--- a/test/export/CodeSystemExporter.test.ts
+++ b/test/export/CodeSystemExporter.test.ts
@@ -198,6 +198,25 @@ describe('CodeSystemExporter', () => {
     expect(loggerSpy.getLastMessage('warn')).toMatch(warning);
     expect(loggerSpy.getLastMessage('warn')).toMatch(/File: Wrong\.fsh.*Line: 2 - 5\D*/s);
   });
+  it('should log an error when multiple code systems have the same id', () => {
+    const firstCodeSystem = new FshCodeSystem('FirstCodeSystem')
+      .withFile('CodeSystems.fsh')
+      .withLocation([2, 8, 6, 15]);
+    firstCodeSystem.id = 'my-code-system';
+    const secondCodeSystem = new FshCodeSystem('SecondCodeSystem')
+      .withFile('CodeSystems.fsh')
+      .withLocation([8, 8, 15, 19]);
+    secondCodeSystem.id = 'my-code-system';
+    doc.codeSystems.set(firstCodeSystem.name, firstCodeSystem);
+    doc.codeSystems.set(secondCodeSystem.name, secondCodeSystem);
+
+    const exported = exporter.export().codeSystems;
+    expect(exported.length).toBe(2);
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Multiple code systems with id my-code-system/s
+    );
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: CodeSystems\.fsh.*Line: 8 - 15\D*/s);
+  });
 
   // CaretValueRules
   it('should apply a CaretValueRule', () => {
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 09acdf7..7d4bd73 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -273,6 +273,97 @@ describe('InstanceExporter', () => {
       expect(loggerSpy.getLastMessage('warn')).toMatch(warning);
       expect(loggerSpy.getLastMessage('warn')).toMatch(/File: Wrong\.fsh.*Line: 2 - 5\D*/s);
     });
+    it('should log an error when multiple instances of the same type have the same id', () => {
+      const firstExample = new Instance('FirstExample')
+        .withFile('Repeat.fsh')
+        .withLocation([3, 8, 11, 25]);
+      firstExample.instanceOf = 'Patient';
+      const firstId = new FixedValueRule('id');
+      firstId.fixedValue = 'repeated-id';
+      firstExample.rules.push(firstId);
+      doc.instances.set(firstExample.name, firstExample);
+
+      const secondExample = new Instance('SecondExample')
+        .withFile('Repeat.fsh')
+        .withLocation([13, 8, 20, 22]);
+      secondExample.instanceOf = 'Patient';
+      const secondId = new FixedValueRule('id');
+      secondId.fixedValue = 'repeated-id';
+      secondExample.rules.push(secondId);
+      doc.instances.set(secondExample.name, secondExample);
+
+      exporter.exportInstance(firstExample);
+      exporter.exportInstance(secondExample);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Multiple instances of type Patient with id repeated-id/s
+      );
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: Repeat\.fsh.*Line: 13 - 20\D*/s);
+    });
+
+    it('should not log an error when multiple instances of different types have the same id', () => {
+      const firstExample = new Instance('FirstExample')
+        .withFile('Repeat.fsh')
+        .withLocation([3, 8, 11, 25]);
+      firstExample.instanceOf = 'Patient';
+      const firstId = new FixedValueRule('id');
+      firstId.fixedValue = 'repeated-id';
+      firstExample.rules.push(firstId);
+      doc.instances.set(firstExample.name, firstExample);
+
+      const secondExample = new Instance('SecondExample')
+        .withFile('Repeat.fsh')
+        .withLocation([13, 8, 20, 22]);
+      secondExample.instanceOf = 'Practitioner';
+      const secondId = new FixedValueRule('id');
+      secondId.fixedValue = 'repeated-id';
+      secondExample.rules.push(secondId);
+      doc.instances.set(secondExample.name, secondExample);
+
+      exporter.exportInstance(firstExample);
+      exporter.exportInstance(secondExample);
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
+    it('should not log an error when multiple inline instances of the same type have the same id', () => {
+      // Inline instances will typically not have an id assigned to them
+      const firstQuantity = new Instance('FirstQuantity');
+      firstQuantity.instanceOf = 'Quantity';
+      firstQuantity.usage = 'Inline';
+      doc.instances.set(firstQuantity.name, firstQuantity);
+
+      const secondQuantity = new Instance('SecondQuantity');
+      secondQuantity.instanceOf = 'Quantity';
+      secondQuantity.usage = 'Inline';
+      doc.instances.set(secondQuantity.name, secondQuantity);
+
+      const firstInstance = exporter.exportInstance(firstQuantity);
+      const secondInstance = exporter.exportInstance(secondQuantity);
+      expect(firstInstance.id).toBe(secondInstance.id);
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
+
+    it('should not log an error when an inline instance and a non-inline instance of the same type have the same id', () => {
+      const inlineQuantity = new Instance('FirstQuantity');
+      inlineQuantity.instanceOf = 'Quantity';
+      inlineQuantity.usage = 'Inline';
+      const inlineId = new FixedValueRule('id');
+      inlineId.fixedValue = 'my-quantity';
+      inlineQuantity.rules.push(inlineId);
+      doc.instances.set(inlineQuantity.name, inlineQuantity);
+
+      const exampleQuantity = new Instance('SecondQuantity');
+      exampleQuantity.instanceOf = 'Quantity';
+      exampleQuantity.usage = 'Example';
+      const exampleId = new FixedValueRule('id');
+      exampleId.fixedValue = 'my-quantity';
+      exampleQuantity.rules.push(exampleId);
+      doc.instances.set(exampleQuantity.name, exampleQuantity);
+
+      const inlineInstance = exporter.exportInstance(inlineQuantity);
+      const exampleInstance = exporter.exportInstance(exampleQuantity);
+      expect(inlineInstance.id).toBe(exampleInstance.id);
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
 
     // Fixing top level elements
     it('should fix top level elements that are fixed by pattern[x] on the Structure Definition', () => {
diff --git a/test/export/MappingExporter.test.ts b/test/export/MappingExporter.test.ts
index 156b922..adf8927 100644
--- a/test/export/MappingExporter.test.ts
+++ b/test/export/MappingExporter.test.ts
@@ -13,6 +13,7 @@ describe('MappingExporter', () => {
   let doc: FSHDocument;
   let exporter: MappingExporter;
   let observation: StructureDefinition;
+  let practitioner: StructureDefinition;
 
   beforeAll(() => {
     defs = new FHIRDefinitions();
@@ -24,6 +25,7 @@ describe('MappingExporter', () => {
   });
 
   beforeEach(() => {
+    loggerSpy.reset();
     doc = new FSHDocument('fileName');
     const input = new FSHTank([doc], {
       name: 'test',
@@ -36,6 +38,9 @@ describe('MappingExporter', () => {
     observation = fisher.fishForStructureDefinition('Observation');
     observation.id = 'MyObservation';
     pkg.profiles.push(observation);
+    practitioner = fisher.fishForStructureDefinition('Practitioner');
+    practitioner.id = 'MyPractitioner';
+    pkg.profiles.push(practitioner);
   });
 
   it('should log an error when the mapping source does not exist', () => {
@@ -135,6 +140,63 @@ describe('MappingExporter', () => {
       );
       expect(loggerSpy.getLastMessage('error')).toMatch(/File: BadMapping\.fsh.*Line: 1 - 3\D*/s);
     });
+
+    it('should log an error when multiple mappings have the same source and the same id', () => {
+      /**
+       * Mapping: FirstMapping
+       * Source: MyObservation
+       * Id: reused-id
+       *
+       * Mapping: SecondMapping
+       * Source: MyObservation
+       * Id: reused-id
+       */
+      const firstMapping = new Mapping('FirstMapping')
+        .withFile('Mappings.fsh')
+        .withLocation([3, 8, 7, 18]);
+      firstMapping.source = 'MyObservation';
+      firstMapping.id = 'reused-id';
+      doc.mappings.set(firstMapping.name, firstMapping);
+      const secondMapping = new Mapping('SecondMapping')
+        .withFile('Mappings.fsh')
+        .withLocation([8, 8, 11, 19]);
+      secondMapping.source = 'MyObservation';
+      secondMapping.id = 'reused-id';
+      doc.mappings.set(secondMapping.name, secondMapping);
+
+      exporter.export();
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Multiple mappings on MyObservation found with id reused-id/
+      );
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: Mappings\.fsh.*Line: 8 - 11\D*/s);
+    });
+
+    it('should not log an error when multiple mappings have different sources and the same id', () => {
+      /**
+       * Mapping: FirstMapping
+       * Source: MyObservation
+       * Id: reused-id
+       *
+       * Mapping: SecondMapping
+       * Source: MyPractitioner
+       * Id: reused-id
+       */
+      const firstMapping = new Mapping('FirstMapping')
+        .withFile('Mappings.fsh')
+        .withLocation([3, 8, 7, 18]);
+      firstMapping.source = 'MyObservation';
+      firstMapping.id = 'reused-id';
+      doc.mappings.set(firstMapping.name, firstMapping);
+      const secondMapping = new Mapping('SecondMapping')
+        .withFile('Mappings.fsh')
+        .withLocation([8, 8, 11, 19]);
+      secondMapping.source = 'MyPractitioner';
+      secondMapping.id = 'reused-id';
+      doc.mappings.set(secondMapping.name, secondMapping);
+
+      exporter.export();
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
   });
 
   describe('#setMappingRules', () => {
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 1cb5868..c89301f 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -424,6 +424,64 @@ describe('StructureDefinitionExporter', () => {
     expect(loggerSpy.getLastMessage('warn')).toMatch(warning);
     expect(loggerSpy.getLastMessage('warn')).toMatch(/File: Wrong\.fsh.*Line: 2 - 5\D*/s);
   });
+  it('should log an error when multiple profiles have the same id', () => {
+    const firstProfile = new Profile('FirstProfile')
+      .withFile('Profiles.fsh')
+      .withLocation([2, 8, 6, 25]);
+    firstProfile.id = 'my-profile';
+    const secondProfile = new Profile('SecondProfile')
+      .withFile('Profiles.fsh')
+      .withLocation([8, 8, 11, 25]);
+    secondProfile.id = 'my-profile';
+    doc.profiles.set(firstProfile.name, firstProfile);
+    doc.profiles.set(secondProfile.name, secondProfile);
+
+    exporter.export();
+    expect(pkg.profiles).toHaveLength(2);
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Multiple structure definitions with id my-profile/s
+    );
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Profiles\.fsh.*Line: 8 - 11\D*/s);
+  });
+
+  it('should log an error when multiple extensions have the same id', () => {
+    const firstExtension = new Extension('FirstExtension')
+      .withFile('Extensions.fsh')
+      .withLocation([3, 8, 7, 22]);
+    firstExtension.id = 'my-extension';
+    const secondExtension = new Extension('SecondExtension')
+      .withFile('Extensions.fsh')
+      .withLocation([11, 8, 15, 29]);
+    secondExtension.id = 'my-extension';
+    doc.extensions.set(firstExtension.name, firstExtension);
+    doc.extensions.set(secondExtension.name, secondExtension);
+
+    exporter.export();
+    expect(pkg.extensions).toHaveLength(2);
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Multiple structure definitions with id my-extension/s
+    );
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Extensions\.fsh.*Line: 11 - 15\D*/s);
+  });
+
+  it('should log an error when a profile and an extension have the same id', () => {
+    const profile = new Profile('MyProfile').withFile('Profiles.fsh').withLocation([2, 8, 5, 15]);
+    profile.id = 'custom-definition';
+    const extension = new Extension('MyExtension')
+      .withFile('Extensions.fsh')
+      .withLocation([3, 8, 5, 19]);
+    extension.id = 'custom-definition';
+    doc.profiles.set(profile.name, profile);
+    doc.extensions.set(extension.name, extension);
+
+    exporter.export();
+    expect(pkg.profiles).toHaveLength(1);
+    expect(pkg.extensions).toHaveLength(1);
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Multiple structure definitions with id custom-definition/s
+    );
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Extensions\.fsh.*Line: 3 - 5\D*/s);
+  });
 
   // Rules
   it('should emit an error and continue when the path is not found', () => {
diff --git a/test/export/ValueSetExporter.test.ts b/test/export/ValueSetExporter.test.ts
index 0aef934..1317c64 100644
--- a/test/export/ValueSetExporter.test.ts
+++ b/test/export/ValueSetExporter.test.ts
@@ -144,6 +144,23 @@ describe('ValueSetExporter', () => {
     expect(loggerSpy.getLastMessage('warn')).toMatch(warning);
     expect(loggerSpy.getLastMessage('warn')).toMatch(/File: Wrong\.fsh.*Line: 2 - 5\D*/s);
   });
+  it('should log an error when multiple value sets have the same id', () => {
+    const firstValueSet = new FshValueSet('FirstVS')
+      .withFile('ValueSets.fsh')
+      .withLocation([2, 8, 5, 15]);
+    firstValueSet.id = 'my-value-set';
+    const secondValueSet = new FshValueSet('SecondVS')
+      .withFile('ValueSets.fsh')
+      .withLocation([7, 8, 9, 19]);
+    secondValueSet.id = 'my-value-set';
+    doc.valueSets.set(firstValueSet.name, firstValueSet);
+    doc.valueSets.set(secondValueSet.name, secondValueSet);
+
+    const exported = exporter.export().valueSets;
+    expect(exported.length).toBe(2);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/Multiple value sets with id my-value-set/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: ValueSets\.fsh.*Line: 7 - 9\D*/s);
+  });
 
   it('should export each value set once, even if export is called more than once', () => {
     const breakfast = new FshValueSet('BreakfastVS');
diff --git a/test/import/FSHImporter.CodeSystem.test.ts b/test/import/FSHImporter.CodeSystem.test.ts
index ec7dd50..645a16d 100644
--- a/test/import/FSHImporter.CodeSystem.test.ts
+++ b/test/import/FSHImporter.CodeSystem.test.ts
@@ -126,7 +126,26 @@ describe('FSHImporter', () => {
         expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Zoo\.fsh.*Line: 7\D*/s);
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 8\D*/s);
       });
+
+      it('should log an error and skip the code system when encountering a code system with a name used by another code system', () => {
+        const input = `
+        CodeSystem: BREAD
+        Title: "Known Bread"
+        
+        CodeSystem: BREAD
+        Title: "Unknown Bread"
+        `;
+        const result = importSingleText(input, 'Bread.fsh');
+        expect(result.codeSystems.size).toBe(1);
+        const codeSystem = result.codeSystems.get('BREAD');
+        expect(codeSystem.title).toBe('Known Bread');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /code system named BREAD already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Bread\.fsh.*Line: 5 - 6\D*/s);
+      });
     });
+
     describe('#concept', () => {
       it('should parse a code system with one concept', () => {
         const input = `
diff --git a/test/import/FSHImporter.Extension.test.ts b/test/import/FSHImporter.Extension.test.ts
index fb467b5..3c45e25 100644
--- a/test/import/FSHImporter.Extension.test.ts
+++ b/test/import/FSHImporter.Extension.test.ts
@@ -103,6 +103,25 @@ describe('FSHImporter', () => {
         expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Dupe\.fsh.*Line: 7\D*/s);
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: Dupe\.fsh.*Line: 8\D*/s);
       });
+
+      it('should log an error and skip the extension when encountering an extension with a name used by another extension', () => {
+        const input = `
+        Extension: SomeExtension
+        Title: "This Extension"
+        
+        Extension: SomeExtension
+        Title: "That Extension"
+        `;
+
+        const result = importSingleText(input, 'SameName.fsh');
+        expect(result.extensions.size).toBe(1);
+        const extension = result.extensions.get('SomeExtension');
+        expect(extension.title).toBe('This Extension');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /Extension named SomeExtension already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
+      });
     });
 
     // Since Extensions use the same rule parsing code as Profiles, only do minimal tests of rules
diff --git a/test/import/FSHImporter.Instance.test.ts b/test/import/FSHImporter.Instance.test.ts
index b86c288..f5e526c 100644
--- a/test/import/FSHImporter.Instance.test.ts
+++ b/test/import/FSHImporter.Instance.test.ts
@@ -272,6 +272,25 @@ describe('FSHImporter', () => {
         expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Dupe\.fsh.*Line: 9\D*/s);
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: Dupe\.fsh.*Line: 10\D*/s);
       });
+
+      it('should log an error and skip the instance when encountering an instance with a name used by another instance', () => {
+        const input = `
+        Instance: MyInstance
+        InstanceOf: Observation
+        
+        Instance: MyInstance
+        InstanceOf: Patient
+        `;
+
+        const result = importSingleText(input, 'SameName.fsh');
+        expect(result.instances.size).toBe(1);
+        const instance = result.instances.get('MyInstance');
+        expect(instance.instanceOf).toBe('Observation');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /Instance named MyInstance already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
+      });
     });
   });
 });
diff --git a/test/import/FSHImporter.Invariant.test.ts b/test/import/FSHImporter.Invariant.test.ts
index e329190..61790a4 100644
--- a/test/import/FSHImporter.Invariant.test.ts
+++ b/test/import/FSHImporter.Invariant.test.ts
@@ -146,6 +146,24 @@ describe('FSHImporter', () => {
         expect(invariant.severity).toEqual(severityCode);
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: Nope\.fsh.*Line: 3\D*/s);
       });
+
+      it('should log an error and skip the invariant when encountering a invariant with a name used by another invariant', () => {
+        const input = `
+        Invariant: same-1
+        Severity: #error
+        Description: "First description."
+
+        Invariant: same-1
+        Severity: #error
+        Description: "Second description."
+        `;
+        const result = importSingleText(input, 'SameName.fsh');
+        expect(result.invariants.size).toBe(1);
+        const invariant = result.invariants.get('same-1');
+        expect(invariant.description).toBe('First description.');
+        expect(loggerSpy.getLastMessage('error')).toMatch(/Invariant named same-1 already exists/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 6 - 8\D*/s);
+      });
     });
   });
 });
diff --git a/test/import/FSHImporter.Mapping.test.ts b/test/import/FSHImporter.Mapping.test.ts
index d76165d..6d0762c 100644
--- a/test/import/FSHImporter.Mapping.test.ts
+++ b/test/import/FSHImporter.Mapping.test.ts
@@ -102,6 +102,24 @@ describe('FSHImporter', () => {
       expect(mapping.sourceInfo.file).toBe('Mapping.fsh');
     });
 
+    it('should log an error and skip the mapping when encountering a mapping with a name used by another mapping', () => {
+      const input = `
+      Mapping: SameMapping
+      Title: "First Mapping"
+      
+      Mapping: SameMapping
+      Title: "Second Mapping"
+      `;
+      const result = importSingleText(input, 'SameName.fsh');
+      expect(result.mappings.size).toBe(1);
+      const mapping = result.mappings.get('SameMapping');
+      expect(mapping.title).toBe('First Mapping');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Mapping named SameMapping already exists/s
+      );
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
+    });
+
     describe('#mappingRule', () => {
       it('should parse a simple mapping rule', () => {
         const input = `
diff --git a/test/import/FSHImporter.Profile.test.ts b/test/import/FSHImporter.Profile.test.ts
index 5894aa5..a1f3daa 100644
--- a/test/import/FSHImporter.Profile.test.ts
+++ b/test/import/FSHImporter.Profile.test.ts
@@ -173,6 +173,26 @@ describe('FSHImporter', () => {
         expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Dupe\.fsh.*Line: 7\D*/s);
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: Dupe\.fsh.*Line: 8\D*/s);
       });
+
+      it('should log an error and skip the profile when encountering a profile with a name used by another profile', () => {
+        const input = `
+        Profile: ObservationProfile
+        Parent: Observation
+        Title: "First Observation Profile"
+
+        Profile: ObservationProfile
+        Parent: Observation
+        Title: "Second Observation Profile"
+        `;
+        const result = importSingleText(input, 'SameName.fsh');
+        expect(result.profiles.size).toBe(1);
+        const profile = result.profiles.get('ObservationProfile');
+        expect(profile.title).toBe('First Observation Profile');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /Profile named ObservationProfile already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 6 - 8\D*/s);
+      });
     });
 
     describe('#cardRule', () => {
diff --git a/test/import/FSHImporter.RuleSet.test.ts b/test/import/FSHImporter.RuleSet.test.ts
index d1f9a58..35e449a 100644
--- a/test/import/FSHImporter.RuleSet.test.ts
+++ b/test/import/FSHImporter.RuleSet.test.ts
@@ -70,5 +70,24 @@ describe('FSHImporter', () => {
       });
       expect(loggerSpy.getLastMessage('error')).toMatch(/File: Empty\.fsh.*Line: 4\D*/s);
     });
+
+    it('should log an error and skip the RuleSet when encountering a RuleSet with a name used by another RuleSet', () => {
+      const input = `
+      RuleSet: SameRuleSet
+      * gender 0..0
+
+      RuleSet: SameRuleSet
+      * active = true
+      `;
+      const result = importSingleText(input, 'SameName.fsh');
+      expect(result.ruleSets.size).toBe(1);
+      const ruleSet = result.ruleSets.get('SameRuleSet');
+      expect(ruleSet.rules.length).toBe(1);
+      assertCardRule(ruleSet.rules[0], 'gender', 0, '0');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /RuleSet named SameRuleSet already exists/s
+      );
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
+    });
   });
 });
diff --git a/test/import/FSHImporter.ValueSet.test.ts b/test/import/FSHImporter.ValueSet.test.ts
index 7b5eaa1..85228c8 100644
--- a/test/import/FSHImporter.ValueSet.test.ts
+++ b/test/import/FSHImporter.ValueSet.test.ts
@@ -74,6 +74,24 @@ describe('FSHImporter', () => {
         expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Dupe\.fsh.*Line: 7\D*/s);
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: Dupe\.fsh.*Line: 8\D*/s);
       });
+
+      it('should log an error and skip the value set when encountering a value set with a name used by another value set', () => {
+        const input = `
+        ValueSet: SimpleVS
+        Title: "First Value Set"
+
+        ValueSet: SimpleVS
+        Title: "Second Value Set"
+        `;
+        const result = importSingleText(input, 'SameName.fsh');
+        expect(result.valueSets.size).toBe(1);
+        const valueSet = result.valueSets.get('SimpleVS');
+        expect(valueSet.title).toBe('First Value Set');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /ValueSet named SimpleVS already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
+      });
     });
 
     describe('#ValueSetConceptComponent', () => {