Repository: FHIR/sushi
Pull request #294 (2020-03-24 22:13:59)

Error if Profile defines itself as Parent
This PR covers CIMPL-283: https://standardhealthrecord.atlassian.net/browse/CIMPL-283

I confirmed that we already resolve name/id in the order specified in the issue:
1. Local definitions (in the package and in the tank) are resolved first
2. External references are resolved second

There were actually tests that indicated this was the case, which can be seen in [one test](https://github.com/FHIR/sushi/blob/master/test/utils/MasterFisher.test.ts#L145) and [another test](https://github.com/FHIR/sushi/blob/master/test/utils/MasterFisher.test.ts#L160). (These are just links to our existing code since I haven't changed anything related to them.)

Since this was already covered, I implemented the additional error that was mentioned in the Jira. If a profile declares a Parent with the same name as itself, we now throw an error. This prevents a "Maximum call stack size exceeded" error on master. If the parent name resolves to a FHIR resource, we also include the URL that should be used in place of the resource name in the FHIR message.


Patch:
diff --git a/src/errors/ParentDeclaredAsProfileNameError.ts b/src/errors/ParentDeclaredAsProfileNameError.ts
new file mode 100644
index 0000000..b6da52a
--- /dev/null
+++ b/src/errors/ParentDeclaredAsProfileNameError.ts
@@ -0,0 +1,14 @@
+import { WithSource } from './WithSource';
+import { SourceInfo } from '../fshtypes';
+
+export class ParentDeclaredAsProfileNameError extends Error implements WithSource {
+  constructor(public name: string, public sourceInfo: SourceInfo, public fhirResourceUrl: string) {
+    super(
+      `Profile "${name}" cannot declare itself as a Parent. ${
+        fhirResourceUrl
+          ? `It looks like the parent is an external resource; use its URL (e.g., ${fhirResourceUrl}).`
+          : ''
+      }`
+    );
+  }
+}
diff --git a/src/errors/index.ts b/src/errors/index.ts
index 1ea007a..7611679 100644
--- a/src/errors/index.ts
+++ b/src/errors/index.ts
@@ -36,3 +36,4 @@ export * from './InvalidUnitsError';
 export * from './MultipleStandardsStatusError';
 export * from './InvalidMappingError';
 export * from './InvalidFHIRIdError';
+export * from './ParentDeclaredAsProfileNameError';
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index f2e0b66..c17dfd1 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -6,7 +6,7 @@ import {
 } from '../fhirtypes';
 import { Profile, Extension, Invariant } from '../fshtypes';
 import { FSHTank } from '../import';
-import { ParentNotDefinedError } from '../errors';
+import { ParentNotDefinedError, ParentDeclaredAsProfileNameError } from '../errors';
 import {
   CardRule,
   FixedValueRule,
@@ -397,6 +397,7 @@ export class StructureDefinitionExporter implements Fishable {
    * Exports Profile or Extension to StructureDefinition
    * @param {Profile | Extension} fshDefinition - The Profile or Extension we are exporting
    * @returns {StructureDefinition}
+   * @throws {ParentDeclaredAsProfileNameError} when the Profile declares itself as the parent
    * @throws {ParentNotDefinedError} when the Profile or Extension's parent is not found
    */
   exportStructDef(fshDefinition: Profile | Extension): StructureDefinition {
@@ -408,6 +409,16 @@ export class StructureDefinitionExporter implements Fishable {
     }
 
     const parentName = fshDefinition.parent || 'Resource';
+
+    if (fshDefinition.name === parentName) {
+      const result = this.fishForMetadata(parentName, Type.Resource);
+      throw new ParentDeclaredAsProfileNameError(
+        fshDefinition.name,
+        fshDefinition.sourceInfo,
+        result?.url
+      );
+    }
+
     const json = this.fishForFHIR(parentName);
 
     // If we still don't have a resolution, then it's not defined


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 25b5c13..6f7176b 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -172,6 +172,26 @@ describe('StructureDefinitionExporter', () => {
     }).toThrow('Parent Bar not found for Foo');
   });
 
+  it('should throw ParentDeclaredAsProfileNameError when the profile decares itself as the parent', () => {
+    const profile = new Profile('Foo');
+    profile.parent = 'Foo';
+    doc.profiles.set(profile.name, profile);
+    expect(() => {
+      exporter.exportStructDef(profile);
+    }).toThrow('Profile "Foo" cannot declare itself as a Parent.');
+  });
+
+  it('should throw ParentDeclaredAsProfileNameError and suggest resource URL when the profile decares itself as the parent and it is a FHIR resource', () => {
+    const profile = new Profile('Patient');
+    profile.parent = 'Patient';
+    doc.profiles.set(profile.name, profile);
+    expect(() => {
+      exporter.exportStructDef(profile);
+    }).toThrow(
+      'Profile "Patient" cannot declare itself as a Parent. It looks like the parent is an external resource; use its URL (e.g., http://hl7.org/fhir/StructureDefinition/Patient).'
+    );
+  });
+
   // Extension
   it('should set all user-provided metadata for an extension', () => {
     const extension = new Extension('Foo');