Repository: FHIR/sushi (ts)

Issue:
Warn when instances look like examples but omit "Usage: #example"?
It seems somewhat common for developers to create example resources without adding

    Usage: #example 

There may be some heuristics that could help here, if the title/description says "example" but the usage does not.

Pull request description:
Log a warning when conformance and terminology instances default to Usage: #example
Fixes #1303

This PR adds a warning when instances of conformance or terminology resources will use the default `#example` usage. Because we default the usage within the importer, I added the warning within the importer so we can distinguish between a usage that was set in FSH and a usage set by the default value.

Any feedback on the wording of the warning is appreciated.

Patch:
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index 83b61fc..339220d 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -40,6 +40,28 @@ import { buildSliceTree, calculateSliceTreeCounts } from './sliceTree';
 import { InstanceExporter } from '../export';
 import { MismatchedTypeError } from '../errors';
 
+// List of Conformance and Terminology resources from http://hl7.org/fhir/R4/resourcelist.html
+// and https://hl7.org/fhir/R5/resourcelist.html
+export const CONFORMANCE_AND_TERMINOLOGY_RESOURCES = new Set([
+  'CapabilityStatement',
+  'CapabilityStatement2', // pre-release R5
+  'StructureDefinition',
+  'ImplementationGuide',
+  'SearchParameter',
+  'MessageDefinition',
+  'OperationDefinition',
+  'CompartmentDefinition',
+  'StructureMap',
+  'GraphDefinition',
+  'ExampleScenario',
+  'CodeSystem',
+  'ValueSet',
+  'ConceptMap',
+  'ConceptMap2', // pre-release R5
+  'NamingSystem',
+  'TerminologyCapabilities'
+]);
+
 // characteristics are set using the structuredefinition-type-characteristics extension
 export const TYPE_CHARACTERISTICS_EXTENSION =
   'http://hl7.org/fhir/StructureDefinition/structuredefinition-type-characteristics';
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index b545f55..729783f 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -26,6 +26,7 @@ import {
   ImplementationGuideDefinitionPage,
   ImplementationGuideDependsOn
 } from '../fhirtypes';
+import { CONFORMANCE_AND_TERMINOLOGY_RESOURCES } from '../fhirtypes/common';
 import { ConfigurationMenuItem, ConfigurationResource } from '../fshtypes';
 import { logger, Type, getFilesRecursive } from '../utils';
 import { FHIRDefinitions } from '../fhirdefs';
@@ -42,28 +43,6 @@ function isR4(fhirVersion: string[]) {
   return containsR4Version;
 }
 
-// List of Conformance and Terminology resources from http://hl7.org/fhir/R4/resourcelist.html
-// and http://hl7.org/fhir/5.0.0-snapshot1/resourcelist.html
-const CONFORMANCE_AND_TERMINOLOGY_RESOURCES = new Set([
-  'CapabilityStatement',
-  'CapabilityStatement2', // pre-release R5
-  'StructureDefinition',
-  'ImplementationGuide',
-  'SearchParameter',
-  'MessageDefinition',
-  'OperationDefinition',
-  'CompartmentDefinition',
-  'StructureMap',
-  'GraphDefinition',
-  'ExampleScenario',
-  'CodeSystem',
-  'ValueSet',
-  'ConceptMap',
-  'ConceptMap2', // pre-release R5
-  'NamingSystem',
-  'TerminologyCapabilities'
-]);
-
 /**
  * The IG Exporter exports the FSH artifacts into a file structure supported by the IG Publisher.
  * This allows a FSH Tank to be built as a FHIR IG.  Currently, template-based IG publishing is
diff --git a/src/import/FSHImporter.ts b/src/import/FSHImporter.ts
index da607ad..34c3419 100644
--- a/src/import/FSHImporter.ts
+++ b/src/import/FSHImporter.ts
@@ -54,7 +54,7 @@ import {
   OnlyRuleType,
   PathRule
 } from '../fshtypes/rules';
-import { splitOnPathPeriods } from '../fhirtypes/common';
+import { CONFORMANCE_AND_TERMINOLOGY_RESOURCES, splitOnPathPeriods } from '../fhirtypes/common';
 import { ParserRuleContext, InputStream, CommonTokenStream, TerminalNode } from 'antlr4';
 import { logger, switchToSecretLogger, LoggerData, restoreMainLogger } from '../utils/FSHLogger';
 import {
@@ -430,17 +430,18 @@ export class FSHImporter extends FSHVisitor {
   }
 
   visitInstance(ctx: pc.InstanceContext) {
+    const location = this.extractStartStop(ctx);
     const instance = new Instance(ctx.name().getText())
-      .withLocation(this.extractStartStop(ctx))
+      .withLocation(location)
       .withFile(this.currentFile);
     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)
+        location
       });
     } else {
       try {
-        this.parseInstance(instance, ctx.instanceMetadata(), ctx.instanceRule());
+        this.parseInstance(instance, location, ctx.instanceMetadata(), ctx.instanceRule());
         this.currentDoc.instances.set(instance.name, instance);
       } catch (e) {
         logger.error(e.message, instance.sourceInfo);
@@ -450,6 +451,7 @@ export class FSHImporter extends FSHVisitor {
 
   private parseInstance(
     instance: Instance,
+    location: TextLocation,
     metaCtx: pc.InstanceMetadataContext[] = [],
     ruleCtx: pc.InstanceRuleContext[] = []
   ): void {
@@ -483,6 +485,18 @@ export class FSHImporter extends FSHVisitor {
     if (!instance.instanceOf) {
       throw new RequiredMetadataError('InstanceOf', 'Instance', instance.name);
     }
+    if (
+      CONFORMANCE_AND_TERMINOLOGY_RESOURCES.has(instance.instanceOf) &&
+      !metaCtx.some(ctx => ctx.usage())
+    ) {
+      logger.warn(
+        `No usage was specified on ${instance.name}. The default #example usage will be applied, but ${instance.instanceOf} Instances are typically definitions. Specify a usage to remove this warning.`,
+        {
+          file: this.currentFile,
+          location
+        }
+      );
+    }
     ruleCtx.forEach(instanceRule => {
       const rule = this.visitInstanceRule(instanceRule);
       if (rule) {


Test patch:
diff --git a/test/import/FSHImporter.Instance.test.ts b/test/import/FSHImporter.Instance.test.ts
index 89d9b55..d830bfe 100644
--- a/test/import/FSHImporter.Instance.test.ts
+++ b/test/import/FSHImporter.Instance.test.ts
@@ -5,6 +5,10 @@ import { importSingleText } from '../testhelpers/importSingleText';
 import { importText, RawFSH } from '../../src/import';
 
 describe('FSHImporter', () => {
+  beforeEach(() => {
+    loggerSpy.reset();
+  });
+
   describe('Instance', () => {
     describe('#instanceOf', () => {
       it('should parse the simplest possible instance', () => {
@@ -156,6 +160,27 @@ describe('FSHImporter', () => {
         );
         expect(loggerSpy.getLastMessage('warn')).toMatch(/File: Bad\.fsh.*Line: 4\D*/s);
       });
+
+      it('should log a warning if a conformance or terminology resource does not have a usage (and therefore will default to example)', () => {
+        // No usage specified for a CapabilityStatement instance
+        const input = `
+        Instance: MyCapabilityStatementDefinition
+        InstanceOf: CapabilityStatement
+        `;
+
+        const result = importSingleText(input, 'CS.fsh');
+        expect(result.instances.size).toBe(1);
+        const instance = result.instances.get('MyCapabilityStatementDefinition');
+        expect(instance.name).toBe('MyCapabilityStatementDefinition');
+        expect(instance.instanceOf).toBe('CapabilityStatement');
+        expect(instance.usage).toBe('Example'); // default is still used but warning is logged
+        expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+        expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
+        expect(loggerSpy.getLastMessage('warn')).toMatch(
+          /No usage was specified on MyCapabilityStatementDefinition\. The default #example usage will be applied, but CapabilityStatement Instances are typically definitions/s
+        );
+        expect(loggerSpy.getLastMessage('warn')).toMatch(/File: CS\.fsh.*Line: 2 - 3\D*/s);
+      });
     });
 
     describe('#mixins', () => {