Repository: FHIR/sushi (ts)

Issue:
Provide better error message for missing snapshot on extension
There are cases where a missing snapshot on an extension may cause a stack trace due to an uncaught exception: 
```
debug TypeError: Cannot read properties of undefined (reading 'element')
    at isModifierExtension (/usr/local/lib/node_modules/fsh-sushi/dist/fhirtypes/common.js:1205:93)
    at /usr/local/lib/node_modules/fsh-sushi/dist/export/StructureDefinitionExporter.js:921:69
    at Array.forEach (<anonymous>)
    at StructureDefinitionExporter.handleExtensionContainsRule (/usr/local/lib/node_modules/fsh-sushi/dist/export/StructureDefinitionExporter.js:883:20)
    at StructureDefinitionExporter.setRules (/usr/local/lib/node_modules/fsh-sushi/dist/export/StructureDefinitionExporter.js:674:34)
    at StructureDefinitionExporter.exportStructDef (/usr/local/lib/node_modules/fsh-sushi/dist/export/StructureDefinitionExporter.js:1111:14)
    at /usr/local/lib/node_modules/fsh-sushi/dist/export/StructureDefinitionExporter.js:1170:22
    at Array.forEach (<anonymous>)
    at StructureDefinitionExporter.export (/usr/local/lib/node_modules/fsh-sushi/dist/export/StructureDefinitionExporter.js:1168:30)
    at FHIRExporter.export (/usr/local/lib/node_modules/fsh-sushi/dist/export/FHIRExporter.js:27:48)
```

When using an extension definition that does not have a snapshot, the normal "missing snapshot" message should be provided.

Originally reported at https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/pre-adopt.20R5.20in.20R4.3B.20pre.20defined.20resources.3F

Pull request description:
Handle missing snapshot when checking `isModifierExtension`
**Description:**

This PR adds better handling for when an extension is missing a snapshot while we check `isModifierExtension`. This doesn't actually provide a better error message for when the snapshot is missing, but it does make it so the `isModifierExtension` function to not throw an unexpected error. If the missing snapshot is an issue later down the line, a clearer message about missing snapshots is issued at that point.

**Testing Instructions:**

The issue and Zulip thread where this was reported doesn't provide a specific example, but I was able to replicate the logged error by providing an Extension resource that had no `snapshot` element in the `input/resources` folder and then used that Extension in a Contains rule.

For example, I used the following FSH and had the attached extension in `input/resources`:

```
Profile: MyObservation
Parent: Observation
* extension contains http://example.org/StructureDefinition/NoSnapshotExtension named nosnapshot 0..1
```
[StructureDefinition-NoSnapshotExtension.json](https://github.com/user-attachments/files/17983410/StructureDefinition-NoSnapshotExtension.json)

You can see a missing snapshot error logged if you then create an Instance of that Profile:

```
Instance: ExampleMyObservation
InstanceOf: MyObservation
* code = #123
* status = #final
* extension[nosnapshot].valueBoolean = true
```

Review the way that I replicated this issue and make sure the fix is sufficient to resolve the reported issue. If I missed anything that the Zulip thread was trying to get at or there should be additional handling for the case when a snapshot is missing, let me know.

**Related Issue:** Fixes #1499 


Patch:
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index 8413eca..d91f740 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -1466,7 +1466,7 @@ export function isExtension(path: string): boolean {
 
 export function isModifierExtension(extension: any): boolean {
   return (
-    extension?.snapshot.element.find((el: ElementDefinition) => el.id === 'Extension')
+    extension?.snapshot?.element.find((el: ElementDefinition) => el.id === 'Extension')
       ?.isModifier === true
   );
 }


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 5f9e123..c3d714a 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -7976,6 +7976,31 @@ describe('StructureDefinitionExporter R4', () => {
         /Cannot slice element 'status'.*File: SingleElement\.fsh.*Line: 6\D*/s
       );
     });
+
+    it('should not report an error for an extension Contains rule with an extension that is missing a snapshot when checking if its a modifierExtension', () => {
+      // Create an extension without a snapshot for testing
+      // Note: SUSHI wouldn't create an extension like this, but it might be provided by a package or custom resource
+      const noSnapshotExtension = cloneDeep(defs.fishForFHIR('familymemberhistory-type'));
+      noSnapshotExtension.id = 'familymemberhistory-type-no-snapshot';
+      noSnapshotExtension.url =
+        'http://hl7.org/fhir/StructureDefinition/familymemberhistory-type-no-snapshot';
+      delete noSnapshotExtension.snapshot;
+      defs.add(noSnapshotExtension);
+
+      const profile = new Profile('Foo');
+      profile.parent = 'Observation';
+      const containsRule = new ContainsRule('extension')
+        .withFile('BadExt.fsh')
+        .withLocation([6, 3, 6, 12]);
+      containsRule.items = [{ name: 'nosnapshot', type: 'familymemberhistory-type-no-snapshot' }];
+      profile.rules.push(containsRule);
+
+      exporter.exportStructDef(profile);
+      const sd = pkg.profiles[0];
+      const noSnapshotSlice = sd.elements.find(e => e.id === 'Observation.extension:nosnapshot');
+      expect(noSnapshotSlice).toBeDefined();
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
   });
 
   describe('#CaretValueRule', () => {