Repository: FHIR/sushi (ts)

Issue:
SUSHI should emit error if "inline" extension is declared on a profile
Inline extensions should only be supported within complex extensions.  If an author attempts to create an inline extension in a profile, SUSHI should emit an error.

See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/Extension.2Eurl.20must.20be.20an.20absolute.20URL

Pull request description:
Inline extensions are not allowed on profiles
Completes task [CIMPL-592](https://standardhealthrecord.atlassian.net/browse/CIMPL-592) and fixes #591 .

Log an error when an inline extension would be used on a profile. Many existing test cases were using inline extensions on profiles, so those tests are updated such that they still test the same code. Inline extensions are still allowed on extensions.

Patch:
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index db91321..2a97663 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -322,6 +322,10 @@ export class StructureDefinitionExporter implements Fishable {
           this
         );
         urlElement.assignValue(slice.sliceName, true);
+        // Inline extensions should not be used on profiles
+        if (fshDefinition instanceof Profile) {
+          logger.error('Inline extensions should not be used on profiles.', rule.sourceInfo);
+        }
       }
     });
   }


Test patch:
diff --git a/test/export/StructureDefinition.ExtensionExporter.test.ts b/test/export/StructureDefinition.ExtensionExporter.test.ts
index d787387..1ab11a8 100644
--- a/test/export/StructureDefinition.ExtensionExporter.test.ts
+++ b/test/export/StructureDefinition.ExtensionExporter.test.ts
@@ -6,6 +6,7 @@ import { loggerSpy } from '../testhelpers/loggerSpy';
 import { TestFisher } from '../testhelpers';
 import path from 'path';
 import { minimalConfig } from '../utils/minimalConfig';
+import { ContainsRule } from '../../src/fshtypes/rules';
 
 describe('ExtensionExporter', () => {
   let defs: FHIRDefinitions;
@@ -145,4 +146,20 @@ describe('ExtensionExporter', () => {
     expect(exported[1].baseDefinition === exported[0].url);
     expect(exported[2].baseDefinition === exported[1].url);
   });
+
+  it('should not log an error when an inline extension is used', () => {
+    loggerSpy.reset();
+    const extension = new Extension('MyExtension');
+    const containsRule = new ContainsRule('extension')
+      .withFile('MyExtension.fsh')
+      .withLocation([3, 8, 3, 25]);
+    containsRule.items.push({
+      name: 'SomeExtension'
+    });
+    extension.rules.push(containsRule);
+    doc.extensions.set(extension.name, extension);
+    exporter.export();
+
+    expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
+  });
 });
diff --git a/test/export/StructureDefinition.ProfileExporter.test.ts b/test/export/StructureDefinition.ProfileExporter.test.ts
index c7d3de9..8ee047e 100644
--- a/test/export/StructureDefinition.ProfileExporter.test.ts
+++ b/test/export/StructureDefinition.ProfileExporter.test.ts
@@ -6,7 +6,7 @@ import { Profile, Instance } from '../../src/fshtypes';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { TestFisher } from '../testhelpers';
 import { minimalConfig } from '../utils/minimalConfig';
-import { CaretValueRule } from '../../src/fshtypes/rules';
+import { CaretValueRule, ContainsRule } from '../../src/fshtypes/rules';
 
 describe('ProfileExporter', () => {
   let defs: FHIRDefinitions;
@@ -157,4 +157,23 @@ describe('ProfileExporter', () => {
     expect(exporter.deferredRules.get(exported[0]).length).toBe(1);
     expect(exporter.deferredRules.get(exported[0])).toContain(caretValueRule);
   });
+
+  it('should log an error when an inline extension is used', () => {
+    const profile = new Profile('MyObservation');
+    profile.parent = 'Observation';
+    const containsRule = new ContainsRule('extension')
+      .withFile('MyObservation.fsh')
+      .withLocation([3, 8, 3, 25]);
+    containsRule.items.push({
+      name: 'SomeExtension'
+    });
+    profile.rules.push(containsRule);
+    doc.profiles.set(profile.name, profile);
+    exporter.export();
+
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: MyObservation\.fsh.*Line: 3\D*/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Inline extensions should not be used on profiles/s
+    );
+  });
 });
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index d459e12..3d22ea9 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -2136,13 +2136,11 @@ describe('StructureDefinitionExporter', () => {
   });
 
   it('should apply a ContainsRule on an extension with defined slicing', () => {
-    // Profile: Foo
-    // Parent: resprate
+    // Extension: Foo
     // * extension contains bar
     // * extension[bar] ^slicing.discriminator.type = #pattern
     // * extension[bar] contains barReslice
-    const profile = new Profile('Foo');
-    profile.parent = 'resprate';
+    const extension = new Extension('Foo');
 
     const sliceRule = new ContainsRule('extension');
     sliceRule.items = [{ name: 'bar' }];
@@ -2151,13 +2149,13 @@ describe('StructureDefinitionExporter', () => {
     caretRule.value = new FshCode('pattern');
     const resliceRule = new ContainsRule('extension[bar]');
     resliceRule.items = [{ name: 'barReslice' }];
-    profile.rules.push(sliceRule, caretRule, resliceRule);
+    extension.rules.push(sliceRule, caretRule, resliceRule);
 
-    exporter.exportStructDef(profile);
-    const sd = pkg.profiles[0];
-    const baseStructDef = fisher.fishForStructureDefinition('resprate');
+    exporter.exportStructDef(extension);
+    const sd = pkg.extensions[0];
+    const baseStructDef = fisher.fishForStructureDefinition('Extension');
 
-    const barReslice = sd.elements.find(e => e.id === 'Observation.extension:bar/barReslice');
+    const barReslice = sd.elements.find(e => e.id === 'Extension.extension:bar/barReslice');
 
     expect(sd.elements.length).toBe(baseStructDef.elements.length + 6);
     expect(barReslice).toBeDefined();
@@ -2322,32 +2320,31 @@ describe('StructureDefinitionExporter', () => {
     // for the code type.  This test initially failed and was assigned by changing the code that handles extension
     // slices to only look for Extension resolutions (as opposed to all types).
     // NOTE: This test is mainly irrelevant now that we switched to a syntax that distinguishes slice name from type.
-    const profile = new Profile('Foo');
-    profile.parent = 'Observation';
+    const extension = new Extension('Foo');
 
     const containsRule = new ContainsRule('extension');
     containsRule.items = [{ name: 'code' }];
-    profile.rules.push(containsRule);
+    extension.rules.push(containsRule);
 
     const onlyRule = new OnlyRule('extension[code].value[x]');
     onlyRule.types = [{ type: 'Quantity' }];
-    profile.rules.push(onlyRule);
+    extension.rules.push(onlyRule);
 
-    exporter.exportStructDef(profile);
-    const sd = pkg.profiles[0];
+    exporter.exportStructDef(extension);
+    const sd = pkg.extensions[0];
 
     expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
 
-    const extension = sd.elements.find(e => e.id === 'Observation.extension');
-    const extensionSlice = sd.elements.find(e => e.id === 'Observation.extension:code');
-    const extensionSliceUrl = sd.elements.find(e => e.id === 'Observation.extension:code.url');
+    const innerExtension = sd.elements.find(e => e.id === 'Extension.extension');
+    const extensionSlice = sd.elements.find(e => e.id === 'Extension.extension:code');
+    const extensionSliceUrl = sd.elements.find(e => e.id === 'Extension.extension:code.url');
     const extensionSliceValueX = sd.elements.find(
-      e => e.id === 'Observation.extension:code.value[x]'
+      e => e.id === 'Extension.extension:code.value[x]'
     );
 
-    expect(extension.slicing).toBeDefined();
-    expect(extension.slicing.discriminator.length).toBe(1);
-    expect(extension.slicing.discriminator[0]).toEqual({ type: 'value', path: 'url' });
+    expect(innerExtension.slicing).toBeDefined();
+    expect(innerExtension.slicing.discriminator.length).toBe(1);
+    expect(innerExtension.slicing.discriminator[0]).toEqual({ type: 'value', path: 'url' });
     expect(extensionSlice).toBeDefined();
     expect(extensionSliceUrl).toBeDefined();
     expect(extensionSliceUrl.fixedUri).toBe('code');
@@ -3469,12 +3466,15 @@ describe('StructureDefinitionExporter', () => {
     patientProfile.parent = 'Patient';
 
     const containsRule = new ContainsRule('maritalStatus.extension');
-    containsRule.items = [{ name: 'maritalSlice' }];
+    containsRule.items = [{ name: 'maritalSlice', type: 'MaritalExtension' }];
     const sliceCardRule = new CardRule('maritalStatus.extension[maritalSlice].extension');
     sliceCardRule.min = 1;
     sliceCardRule.max = '2';
     patientProfile.rules.push(containsRule, sliceCardRule);
 
+    const extension = new Extension('MaritalExtension');
+    doc.extensions.set(extension.name, extension);
+
     exporter.exportStructDef(patientProfile);
     const sd = pkg.profiles[0];
     const valueElement = sd.findElement('Patient.maritalStatus.extension:maritalSlice.value[x]');
@@ -3489,11 +3489,14 @@ describe('StructureDefinitionExporter', () => {
     profile.parent = 'Observation';
 
     const containsRule = new ContainsRule('extension');
-    containsRule.items = [{ name: 'EvidenceType' }];
+    containsRule.items = [{ name: 'EvidenceType', type: 'EvidenceExtension' }];
     const onlyRule = new OnlyRule('value[x]');
     onlyRule.types = [{ type: 'string' }];
     profile.rules.push(containsRule, onlyRule);
 
+    const extension = new Extension('EvidenceExtension');
+    doc.extensions.set(extension.name, extension);
+
     exporter.exportStructDef(profile);
     const sd = pkg.profiles[0];
 
@@ -3512,12 +3515,15 @@ describe('StructureDefinitionExporter', () => {
     profile.parent = 'Patient';
 
     const containsRule = new ContainsRule('extension');
-    containsRule.items = [{ name: 'PatientNote' }];
+    containsRule.items = [{ name: 'PatientNote', type: 'NoteExtension' }];
     const cardRule = new CardRule('extension[PatientNote].extension');
     cardRule.min = 1;
     cardRule.max = '1';
     profile.rules.push(containsRule, cardRule);
 
+    const noteExtension = new Extension('NoteExtension');
+    doc.extensions.set(noteExtension.name, noteExtension);
+
     exporter.exportStructDef(profile);
     const sd = pkg.profiles[0];