Repository: FHIR/sushi
Pull request #900 (2021-08-06 19:38:02)

Detect duplicates across files
Fixes https://github.com/FHIR/sushi/issues/865.

It seems that our current duplicate detection for all entities only worked within the same file. This updates that so that now duplicate detection is done globally across all files, for all entities.

The bug is specific to instances, but this should fix it for all types of entities.

To test, just make a FSH project with two files, and two entities of the same name, one in each file. On this branch, you should get an error alerting you that there are two entities of the same name, and so one is being ignored. On master, no such error will be logged, even though the second defined entity is ignored.

ngfreiter: > This looks good! Could you add a test case showing that no error is produced when two entities of different types have the same name?

Good call! Done in https://github.com/FHIR/sushi/pull/900/commits/8dd8f2654bd9c75c88a07468a074422ce69c4d0e

Patch:
diff --git a/src/import/FSHImporter.ts b/src/import/FSHImporter.ts
index e03768b..d612897 100644
--- a/src/import/FSHImporter.ts
+++ b/src/import/FSHImporter.ts
@@ -139,6 +139,7 @@ const DEFAULT_START_COLUMN = 1;
  * we must call the explicit visitX functions.
  */
 export class FSHImporter extends FSHVisitor {
+  private docs: FSHDocument[] = [];
   private currentFile: string;
   private currentDoc: FSHDocument;
   private allAliases: Map<string, string>;
@@ -154,14 +155,13 @@ export class FSHImporter extends FSHVisitor {
 
   import(rawFSHes: RawFSH[]): FSHDocument[] {
     this.allAliases = new Map();
-    const docs: FSHDocument[] = [];
     const contexts: pc.DocContext[] = [];
 
     // Preprocess the FSH files
     rawFSHes.forEach(rawFSH => {
       // Create and store doc for main import process
       const doc = new FSHDocument(rawFSH.path);
-      docs.push(doc);
+      this.docs.push(doc);
       this.currentDoc = doc;
       this.currentFile = this.currentDoc.file ?? '';
 
@@ -212,11 +212,11 @@ export class FSHImporter extends FSHVisitor {
       this.currentDoc = null;
       this.currentFile = null;
     });
-    logger.info(`Preprocessed ${docs.length} documents with ${this.allAliases.size} aliases.`);
+    logger.info(`Preprocessed ${this.docs.length} documents with ${this.allAliases.size} aliases.`);
 
     // Now do the main import
     contexts.forEach((context, index) => {
-      this.currentDoc = docs[index];
+      this.currentDoc = this.docs[index];
       this.currentFile = this.currentDoc.file ?? '';
       this.visitDoc(context);
       this.currentDoc = null;
@@ -224,14 +224,14 @@ export class FSHImporter extends FSHVisitor {
     });
 
     let [definitions, instances] = [0, 0];
-    docs.forEach(doc => {
+    this.docs.forEach(doc => {
       definitions +=
         doc.codeSystems.size + doc.extensions.size + doc.profiles.size + doc.valueSets.size;
       instances += doc.instances.size;
     });
     logger.info(`Imported ${definitions} definitions and ${instances} instances.`);
 
-    return docs;
+    return this.docs;
   }
 
   visitDoc(ctx: pc.DocContext): void {
@@ -276,7 +276,7 @@ export class FSHImporter extends FSHVisitor {
     const profile = new Profile(ctx.name().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    if (this.currentDoc.profiles.has(profile.name)) {
+    if (this.docs.some(doc => doc.profiles.has(profile.name))) {
       logger.error(`Skipping Profile: a Profile named ${profile.name} already exists.`, {
         file: this.currentFile,
         location: this.extractStartStop(ctx)
@@ -291,7 +291,7 @@ export class FSHImporter extends FSHVisitor {
     const extension = new Extension(ctx.name().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    if (this.currentDoc.extensions.has(extension.name)) {
+    if (this.docs.some(doc => doc.extensions.has(extension.name))) {
       logger.error(`Skipping Extension: an Extension named ${extension.name} already exists.`, {
         file: this.currentFile,
         location: this.extractStartStop(ctx)
@@ -340,7 +340,7 @@ export class FSHImporter extends FSHVisitor {
     const resource = new Resource(ctx.name().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    if (this.currentDoc.resources.has(resource.name)) {
+    if (this.docs.some(doc => doc.resources.has(resource.name))) {
       logger.error(`Skipping Resource: a Resource named ${resource.name} already exists.`, {
         file: this.currentFile,
         location: this.extractStartStop(ctx)
@@ -355,7 +355,7 @@ export class FSHImporter extends FSHVisitor {
     const logical = new Logical(ctx.name().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    if (this.currentDoc.logicals.has(logical.name)) {
+    if (this.docs.some(doc => doc.logicals.has(logical.name))) {
       logger.error(
         `Skipping Logical Model: a Logical Model named ${logical.name} already exists.`,
         {
@@ -407,7 +407,7 @@ export class FSHImporter extends FSHVisitor {
     const instance = new Instance(ctx.name().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    if (this.currentDoc.instances.has(instance.name)) {
+    if (this.docs.some(doc => doc.instances.has(instance.name))) {
       logger.error(`Skipping Instance: an Instance named ${instance.name} already exists.`, {
         file: this.currentFile,
         location: this.extractStartStop(ctx)
@@ -469,7 +469,7 @@ export class FSHImporter extends FSHVisitor {
     const valueSet = new FshValueSet(ctx.name().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    if (this.currentDoc.valueSets.has(valueSet.name)) {
+    if (this.docs.some(doc => doc.valueSets.has(valueSet.name))) {
       logger.error(`Skipping ValueSet: a ValueSet named ${valueSet.name} already exists.`, {
         file: this.currentFile,
         location: this.extractStartStop(ctx)
@@ -538,7 +538,7 @@ export class FSHImporter extends FSHVisitor {
     const codeSystem = new FshCodeSystem(ctx.name().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    if (this.currentDoc.codeSystems.has(codeSystem.name)) {
+    if (this.docs.some(doc => doc.codeSystems.has(codeSystem.name))) {
       logger.error(`Skipping code system: a code system named ${codeSystem.name} already exists.`, {
         file: this.currentFile,
         location: this.extractStartStop(ctx)
@@ -597,7 +597,7 @@ export class FSHImporter extends FSHVisitor {
     const invariant = new Invariant(ctx.name().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    if (this.currentDoc.invariants.has(invariant.name)) {
+    if (this.docs.some(doc => doc.invariants.has(invariant.name))) {
       logger.error(`Skipping Invariant: an Invariant named ${invariant.name} already exists.`, {
         file: this.currentFile,
         location: this.extractStartStop(ctx)
@@ -648,7 +648,7 @@ export class FSHImporter extends FSHVisitor {
     const ruleSet = new RuleSet(ctx.RULESET_REFERENCE().getText().trim())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    if (this.currentDoc.ruleSets.has(ruleSet.name)) {
+    if (this.docs.some(doc => doc.ruleSets.has(ruleSet.name))) {
       logger.error(`Skipping RuleSet: a RuleSet named ${ruleSet.name} already exists.`, {
         file: this.currentFile,
         location: this.extractStartStop(ctx)
@@ -716,7 +716,7 @@ export class FSHImporter extends FSHVisitor {
     const mapping = new Mapping(ctx.name().getText())
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-    if (this.currentDoc.mappings.has(mapping.name)) {
+    if (this.docs.some(doc => doc.mappings.has(mapping.name))) {
       logger.error(`Skipping Mapping: a Mapping named ${mapping.name} already exists.`, {
         file: this.currentFile,
         location: this.extractStartStop(ctx)


Test patch:
diff --git a/test/import/FSHImporter.CodeSystem.test.ts b/test/import/FSHImporter.CodeSystem.test.ts
index d2e9b81..042794f 100644
--- a/test/import/FSHImporter.CodeSystem.test.ts
+++ b/test/import/FSHImporter.CodeSystem.test.ts
@@ -3,6 +3,7 @@ import { assertConceptRule, assertInsertRule, assertCaretValueRule } from '../te
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { Rule, CaretValueRule, InsertRule, ConceptRule } from '../../src/fshtypes/rules';
 import { leftAlign } from '../utils/leftAlign';
+import { importText, RawFSH } from '../../src/import';
 
 describe('FSHImporter', () => {
   describe('CodeSystem', () => {
@@ -163,6 +164,30 @@ describe('FSHImporter', () => {
         );
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: Bread\.fsh.*Line: 5 - 6\D*/s);
       });
+
+      it('should log an error and skip the code system when encountering an code system with a name used by another code system in another file', () => {
+        const input1 = `
+          CodeSystem: SomeCodeSystem
+          Title: "This CodeSystem"
+        `;
+
+        const input2 = `
+          CodeSystem: SomeCodeSystem
+          Title: "That CodeSystem"
+        `;
+
+        const result = importText([
+          new RawFSH(input1, 'File1.fsh'),
+          new RawFSH(input2, 'File2.fsh')
+        ]);
+        expect(result.reduce((sum, d2) => sum + d2.codeSystems.size, 0)).toBe(1);
+        const codesystem = result[0].codeSystems.get('SomeCodeSystem');
+        expect(codesystem.title).toBe('This CodeSystem');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /code system named SomeCodeSystem already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: File2\.fsh.*Line: 2 - 3\D*/s);
+      });
     });
 
     describe('#concept', () => {
diff --git a/test/import/FSHImporter.Extension.test.ts b/test/import/FSHImporter.Extension.test.ts
index e723088..7ce2ab0 100644
--- a/test/import/FSHImporter.Extension.test.ts
+++ b/test/import/FSHImporter.Extension.test.ts
@@ -11,6 +11,7 @@ import {
 } from '../testhelpers/asserts';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { importSingleText } from '../testhelpers/importSingleText';
+import { importText, RawFSH } from '../../src/import';
 
 describe('FSHImporter', () => {
   describe('Extension', () => {
@@ -135,6 +136,30 @@ describe('FSHImporter', () => {
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
       });
 
+      it('should log an error and skip the extension when encountering an extension with a name used by another extension in another file', () => {
+        const input1 = `
+          Extension: SomeExtension
+          Title: "This Extension"
+        `;
+
+        const input2 = `
+          Extension: SomeExtension
+          Title: "That Extension"
+        `;
+
+        const result = importText([
+          new RawFSH(input1, 'File1.fsh'),
+          new RawFSH(input2, 'File2.fsh')
+        ]);
+        expect(result.reduce((sum, d2) => sum + d2.extensions.size, 0)).toBe(1);
+        const extension = result[0].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: File2\.fsh.*Line: 2 - 3\D*/s);
+      });
+
       it('should log an error when the deprecated Mixins keyword is used', () => {
         const input = `
         Extension: SomeExtension
diff --git a/test/import/FSHImporter.Instance.test.ts b/test/import/FSHImporter.Instance.test.ts
index 25634e8..fff1950 100644
--- a/test/import/FSHImporter.Instance.test.ts
+++ b/test/import/FSHImporter.Instance.test.ts
@@ -2,6 +2,7 @@ import { assertAssignmentRule, assertInsertRule } from '../testhelpers/asserts';
 import { FshCode } from '../../src/fshtypes';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { importSingleText } from '../testhelpers/importSingleText';
+import { importText, RawFSH } from '../../src/import';
 
 describe('FSHImporter', () => {
   describe('Instance', () => {
@@ -357,6 +358,32 @@ describe('FSHImporter', () => {
         );
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
       });
+
+      it('should log an error and skip the instance when encountering an instance with a name used by another instance in another file', () => {
+        const input1 = `
+          Instance: SomeInstance
+          InstanceOf: Patient
+          Title: "Instance 1"
+        `;
+
+        const input2 = `
+          Instance: SomeInstance
+          InstanceOf: Patient
+          Title: "Instance 2"
+        `;
+
+        const result = importText([
+          new RawFSH(input1, 'File1.fsh'),
+          new RawFSH(input2, 'File2.fsh')
+        ]);
+        expect(result.reduce((sum, d2) => sum + d2.instances.size, 0)).toBe(1);
+        const instance = result[0].instances.get('SomeInstance');
+        expect(instance.title).toBe('Instance 1');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /Instance named SomeInstance already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: File2\.fsh.*Line: 2 - 4\D*/s);
+      });
     });
   });
 });
diff --git a/test/import/FSHImporter.Invariant.test.ts b/test/import/FSHImporter.Invariant.test.ts
index bf0b0f3..10ffd3e 100644
--- a/test/import/FSHImporter.Invariant.test.ts
+++ b/test/import/FSHImporter.Invariant.test.ts
@@ -1,6 +1,7 @@
 import { importSingleText } from '../testhelpers/importSingleText';
 import { FshCode } from '../../src/fshtypes';
 import { loggerSpy } from '../testhelpers/loggerSpy';
+import { importText, RawFSH } from '../../src/import';
 
 describe('FSHImporter', () => {
   describe('Invariant', () => {
@@ -177,6 +178,30 @@ describe('FSHImporter', () => {
         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);
       });
+
+      it('should log an error and skip the invariant when encountering an invariant with a name used by another invariant in another file', () => {
+        const input1 = `
+          Invariant: same-1
+          Severity: #error
+          Description: "First description."
+        `;
+
+        const input2 = `
+          Invariant: same-1
+          Severity: #error
+          Description: "Second description."
+        `;
+
+        const result = importText([
+          new RawFSH(input1, 'File1.fsh'),
+          new RawFSH(input2, 'File2.fsh')
+        ]);
+        expect(result.reduce((sum, d2) => sum + d2.invariants.size, 0)).toBe(1);
+        const invariant = result[0].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: File2\.fsh.*Line: 2 - 4\D*/s);
+      });
     });
   });
 });
diff --git a/test/import/FSHImporter.Logical.test.ts b/test/import/FSHImporter.Logical.test.ts
index e7d6a2f..aa4e2c7 100644
--- a/test/import/FSHImporter.Logical.test.ts
+++ b/test/import/FSHImporter.Logical.test.ts
@@ -14,6 +14,7 @@ import {
 } from '../testhelpers';
 import { FshCode } from '../../src/fshtypes';
 import { leftAlign } from '../utils/leftAlign';
+import { importText, RawFSH } from '../../src/import';
 
 describe('FSHImporter', () => {
   beforeEach(() => loggerSpy.reset());
@@ -241,6 +242,32 @@ describe('FSHImporter', () => {
         );
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 6 - 8\D*/s);
       });
+
+      it('should log an error and skip the logical when encountering an logical with a name used by another logical in another file', () => {
+        const input1 = `
+          Logical: MyObservationModel
+          Parent: BaseObservationModel
+          Title: "An Observation-based Logical Model"
+        `;
+
+        const input2 = `
+          Logical: MyObservationModel
+          Parent: BaseObservationModel
+          Title: "Second Observation-based Logical Model"
+        `;
+
+        const result = importText([
+          new RawFSH(input1, 'File1.fsh'),
+          new RawFSH(input2, 'File2.fsh')
+        ]);
+        expect(result.reduce((sum, d2) => sum + d2.logicals.size, 0)).toBe(1);
+        const l = result[0].logicals.get('MyObservationModel');
+        expect(l.title).toBe('An Observation-based Logical Model');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /Logical Model named MyObservationModel already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: File2\.fsh.*Line: 2 - 4\D*/s);
+      });
     });
 
     // Tests for all supported rules are in FSHImporter.SDRules.test.ts
diff --git a/test/import/FSHImporter.Mapping.test.ts b/test/import/FSHImporter.Mapping.test.ts
index 6871cff..be44a62 100644
--- a/test/import/FSHImporter.Mapping.test.ts
+++ b/test/import/FSHImporter.Mapping.test.ts
@@ -2,6 +2,7 @@ import { importSingleText } from '../testhelpers/importSingleText';
 import { assertMappingRule, assertInsertRule } from '../testhelpers/asserts';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { FshCode } from '../../src/fshtypes';
+import { importText, RawFSH } from '../../src/import';
 
 describe('FSHImporter', () => {
   beforeAll(() => {
@@ -135,6 +136,27 @@ describe('FSHImporter', () => {
       expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
     });
 
+    it('should log an error and skip the mapping when encountering an mapping with a name used by another mapping in another file', () => {
+      const input1 = `
+        Mapping: SameMapping
+        Title: "First Mapping"
+      `;
+
+      const input2 = `
+        Mapping: SameMapping
+        Title: "Second Mapping"
+      `;
+
+      const result = importText([new RawFSH(input1, 'File1.fsh'), new RawFSH(input2, 'File2.fsh')]);
+      expect(result.reduce((sum, d2) => sum + d2.mappings.size, 0)).toBe(1);
+      const m = result[0].mappings.get('SameMapping');
+      expect(m.title).toBe('First Mapping');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Mapping named SameMapping already exists/s
+      );
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: File2\.fsh.*Line: 2 - 3\D*/s);
+    });
+
     describe('#mappingRule', () => {
       it('should parse a simple mapping rule', () => {
         const input = `
diff --git a/test/import/FSHImporter.ParamRuleSet.test.ts b/test/import/FSHImporter.ParamRuleSet.test.ts
index 95bad99..15edbfe 100644
--- a/test/import/FSHImporter.ParamRuleSet.test.ts
+++ b/test/import/FSHImporter.ParamRuleSet.test.ts
@@ -120,6 +120,36 @@ describe('FSHImporter', () => {
       expect(loggerSpy.getLastMessage('error')).toMatch(/File: Params\.fsh.*Line: 6 - 7/s);
     });
 
+    it('should log an error and skip the ParamRuleSet when encountering an ParamRuleSet with a name used by another ParamRuleSet in another file', () => {
+      const importer = new FSHImporter();
+      const input1 = `
+        RuleSet: MyRuleSet (system, strength)
+        * code from http://example.org/{system}/info.html {strength}
+        * pig from egg
+      `;
+
+      const input2 = `
+        RuleSet: MyRuleSet (someName)
+        * name = "Dr. {someName}"
+      `;
+
+      importer.import([new RawFSH(input1, 'File1.fsh'), new RawFSH(input2, 'File2.fsh')]);
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
+      expect(importer.paramRuleSets.size).toBe(1);
+      expect(importer.paramRuleSets.size).toBe(1);
+      expect(importer.paramRuleSets.has('MyRuleSet')).toBe(true);
+      const result = importer.paramRuleSets.get('MyRuleSet');
+      expect(result.name).toBe('MyRuleSet');
+      expect(result.parameters).toEqual(['system', 'strength']);
+      const expectedContents = `
+        * code from http://example.org/{system}/info.html {strength}
+        * pig from egg`;
+      expect(result.contents).toBe(expectedContents);
+      expect(loggerSpy.getLastMessage('error')).toMatch(/RuleSet named MyRuleSet already exists/s);
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: File2\.fsh.*Line: 2 - 3/s);
+    });
+
     it('should log a warning when a ParamRuleSet has parameters that are not used in the contents', () => {
       const importer = new FSHImporter();
       const input = `
diff --git a/test/import/FSHImporter.Profile.test.ts b/test/import/FSHImporter.Profile.test.ts
index 3ec92bf..0cf3033 100644
--- a/test/import/FSHImporter.Profile.test.ts
+++ b/test/import/FSHImporter.Profile.test.ts
@@ -20,7 +20,7 @@ import {
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { stats } from '../../src/utils/FSHLogger';
 import { importSingleText } from '../testhelpers/importSingleText';
-import { FSHImporter, RawFSH } from '../../src/import';
+import { FSHImporter, importText, RawFSH } from '../../src/import';
 import { EOL } from 'os';
 import { leftAlign } from '../utils/leftAlign';
 
@@ -233,6 +233,32 @@ describe('FSHImporter', () => {
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 6 - 8\D*/s);
       });
 
+      it('should log an error and skip the profile when encountering an profile with a name used by another profile in another file', () => {
+        const input1 = `
+        Profile: SameProfile
+        Title: "First Profile"
+        Parent: Observation
+      `;
+
+        const input2 = `
+        Profile: SameProfile
+        Title: "Second Profile"
+        Parent: Observation
+      `;
+
+        const result = importText([
+          new RawFSH(input1, 'File1.fsh'),
+          new RawFSH(input2, 'File2.fsh')
+        ]);
+        expect(result.reduce((sum, d2) => sum + d2.profiles.size, 0)).toBe(1);
+        const p = result[0].profiles.get('SameProfile');
+        expect(p.title).toBe('First Profile');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /Profile named SameProfile already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: File2\.fsh.*Line: 2 - 4\D*/s);
+      });
+
       it('should log an error when the deprecated Mixins keyword is used', () => {
         const input = leftAlign(`
         Profile: SomeProfile
diff --git a/test/import/FSHImporter.Resource.test.ts b/test/import/FSHImporter.Resource.test.ts
index 9355de5..70b6700 100644
--- a/test/import/FSHImporter.Resource.test.ts
+++ b/test/import/FSHImporter.Resource.test.ts
@@ -14,6 +14,7 @@ import {
 } from '../testhelpers';
 import { FshCode } from '../../src/fshtypes';
 import { leftAlign } from '../utils/leftAlign';
+import { importText, RawFSH } from '../../src/import';
 
 describe('FSHImporter', () => {
   describe('Resource', () => {
@@ -210,6 +211,30 @@ describe('FSHImporter', () => {
         );
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
       });
+
+      it('should log an error and skip the resource when encountering an resource with a name used by another resource in another file', () => {
+        const input1 = `
+        Resource: SameResource
+        Title: "First Resource"
+      `;
+
+        const input2 = `
+        Resource: SameResource
+        Title: "Second Resource"
+      `;
+
+        const result = importText([
+          new RawFSH(input1, 'File1.fsh'),
+          new RawFSH(input2, 'File2.fsh')
+        ]);
+        expect(result.reduce((sum, d2) => sum + d2.resources.size, 0)).toBe(1);
+        const r = result[0].resources.get('SameResource');
+        expect(r.title).toBe('First Resource');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /Resource named SameResource already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: File2\.fsh.*Line: 2 - 3\D*/s);
+      });
     });
 
     // Tests for all supported rules are in FSHImporter.SD-Rules.test.ts
diff --git a/test/import/FSHImporter.RuleSet.test.ts b/test/import/FSHImporter.RuleSet.test.ts
index df97d2f..f847aea 100644
--- a/test/import/FSHImporter.RuleSet.test.ts
+++ b/test/import/FSHImporter.RuleSet.test.ts
@@ -14,6 +14,7 @@ import { loggerSpy } from '../testhelpers/loggerSpy';
 import { Rule, ConceptRule } from '../../src/fshtypes/rules';
 import { FshCode } from '../../src/fshtypes';
 import { leftAlign } from '../utils/leftAlign';
+import { importText, RawFSH } from '../../src/import';
 
 describe('FSHImporter', () => {
   beforeEach(() => {
@@ -253,6 +254,27 @@ describe('FSHImporter', () => {
       expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
     });
 
+    it('should log an error and skip the ruleset when encountering an ruleset with a name used by another ruleset in another file', () => {
+      const input1 = `
+        RuleSet: SameRuleSet
+        * gender 0..0
+      `;
+
+      const input2 = `
+        RuleSet: SameRuleSet
+        * active = true
+      `;
+
+      const result = importText([new RawFSH(input1, 'File1.fsh'), new RawFSH(input2, 'File2.fsh')]);
+      expect(result.reduce((sum, d2) => sum + d2.ruleSets.size, 0)).toBe(1);
+      const r = result[0].ruleSets.get('SameRuleSet');
+      assertCardRule(r.rules[0] as Rule, 'gender', 0, '0');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /RuleSet named SameRuleSet already exists/s
+      );
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: File2\.fsh.*Line: 2 - 3\D*/s);
+    });
+
     it('should not log an error when the ConceptRule has one code with a system, no definition, and no hierarchy', () => {
       const input = leftAlign(`
       RuleSet: VSRuleSet
diff --git a/test/import/FSHImporter.ValueSet.test.ts b/test/import/FSHImporter.ValueSet.test.ts
index 08986f6..46a1070 100644
--- a/test/import/FSHImporter.ValueSet.test.ts
+++ b/test/import/FSHImporter.ValueSet.test.ts
@@ -9,6 +9,7 @@ import { FshCode, VsOperator } from '../../src/fshtypes';
 import { importSingleText } from '../testhelpers/importSingleText';
 import { Rule } from '../../src/fshtypes/rules';
 import { leftAlign } from '../utils/leftAlign';
+import { importText, RawFSH } from '../../src/import';
 
 describe('FSHImporter', () => {
   afterEach(() => loggerSpy.reset());
@@ -108,6 +109,30 @@ describe('FSHImporter', () => {
         );
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: SameName\.fsh.*Line: 5 - 6\D*/s);
       });
+
+      it('should log an error and skip the value set when encountering an value set with a name used by another value set in another file', () => {
+        const input1 = `
+          ValueSet: SimpleVS
+          Title: "First Value Set"
+        `;
+
+        const input2 = `
+          ValueSet: SimpleVS
+          Title: "Second Value Set"
+        `;
+
+        const result = importText([
+          new RawFSH(input1, 'File1.fsh'),
+          new RawFSH(input2, 'File2.fsh')
+        ]);
+        expect(result.reduce((sum, d2) => sum + d2.valueSets.size, 0)).toBe(1);
+        const v = result[0].valueSets.get('SimpleVS');
+        expect(v.title).toBe('First Value Set');
+        expect(loggerSpy.getLastMessage('error')).toMatch(
+          /ValueSet named SimpleVS already exists/s
+        );
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: File2\.fsh.*Line: 2 - 3\D*/s);
+      });
     });
 
     describe('#ValueSetConceptComponent', () => {
diff --git a/test/import/FSHImporter.test.ts b/test/import/FSHImporter.test.ts
index 7bb1eee..c135cea 100644
--- a/test/import/FSHImporter.test.ts
+++ b/test/import/FSHImporter.test.ts
@@ -495,4 +495,23 @@ Long statement:
     );
     expect(result).toBeDefined();
   });
+
+  it('should log no error when two entities of different types have the same name', () => {
+    const input = `
+      CodeSystem: Foo
+      Title: "This"
+
+      ValueSet: Foo
+      Title: "That"
+    `;
+
+    const result = importSingleText(input, 'RepeatedName.fsh');
+    expect(result.codeSystems.size).toBe(1);
+    expect(result.valueSets.size).toBe(1);
+    const codesystem = result.codeSystems.get('Foo');
+    expect(codesystem.title).toBe('This');
+    const valueSet = result.valueSets.get('Foo');
+    expect(valueSet.title).toBe('That');
+    expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+  });
 });