Repository: FHIR/sushi (ts)

Issue:
SUSHI should look at assigned values when resolving References
If an instance sets its `id` via an Assignment Rule, that `id` is what should be referenced from inside a `Reference()` keyword, not the name given to the instance via the `Instance:` keyword. Currently, SUSHI does not properly resolve references that use the id that was assigned from an assignment rule. Neither references to typical example instances or inline instances resolve properly.

An example of both `inline` and `example` instances with this behavior can be seen here: https://fshschool.org/FSHOnline/#/share/3h1fzck.

This was originally reported in [this issue](https://github.com/FSHSchool/FSHOnline/issues/116) on FSH Online.

Pull request:
Find entities by id, even when their id is set by a rule
Fixes #1183 and completes task [CIMPL-1052](https://standardhealthrecord.atlassian.net/browse/CIMPL-1052).

When fishing for most FSH entities, one of the conditions for a match is that the provided string matches the entity's `id`. However, an entity's `id` may be set by one of its rules. To make this more reliable, this PR introduces two main changes:
- All insert rules are applied before any resources are exported.
- When getting the id from a FSH definition, check the rules on the definition for one that sets the `id`.
These two changes together help to make sure that references to resources defined in FSH that have a rule for setting the `id` will resolve correctly.

In addition to the test cases added as part of the PR, the [sample FSH](https://fshschool.org/FSHOnline/#/share/3h1fzck) in the linked issue will now resolve all references correctly.

This change applies only to FSH entities that can be fished up by `id`. This is why `Mapping` is not changed, even though it has an `id`.

Patch:
diff --git a/src/export/CodeSystemExporter.ts b/src/export/CodeSystemExporter.ts
index 2220afd..13f24f2 100644
--- a/src/export/CodeSystemExporter.ts
+++ b/src/export/CodeSystemExporter.ts
@@ -158,14 +158,19 @@ export class CodeSystemExporter {
     }
   }
 
+  applyInsertRules(): void {
+    const codeSystems = this.tank.getAllCodeSystems();
+    for (const cs of codeSystems) {
+      applyInsertRules(cs, this.tank);
+    }
+  }
+
   exportCodeSystem(fshDefinition: FshCodeSystem): CodeSystem {
     if (this.pkg.codeSystems.some(cs => cs.name === fshDefinition.name)) {
       return;
     }
     const codeSystem = new CodeSystem();
     this.setMetadata(codeSystem, fshDefinition);
-    // fshDefinition.rules may include insert rules, which must be expanded before applying other rules
-    applyInsertRules(fshDefinition, this.tank);
     const csStructureDefinition = StructureDefinition.fromJSON(
       this.fisher.fishForFHIR('CodeSystem', Type.Resource)
     );
diff --git a/src/export/FHIRExporter.ts b/src/export/FHIRExporter.ts
index 2d9150d..372d582 100644
--- a/src/export/FHIRExporter.ts
+++ b/src/export/FHIRExporter.ts
@@ -36,6 +36,12 @@ export class FHIRExporter {
   }
 
   export(): Package {
+    this.structureDefinitionExporter.applyInsertRules();
+    this.codeSystemExporter.applyInsertRules();
+    this.valueSetExporter.applyInsertRules();
+    this.instanceExporter.applyInsertRules();
+    this.mappingExporter.applyInsertRules();
+
     this.structureDefinitionExporter.export();
     this.codeSystemExporter.export();
     this.valueSetExporter.export();
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index 34994ad..a411942 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -64,8 +64,6 @@ export class InstanceExporter implements Fishable {
     instanceDef: InstanceDefinition,
     instanceOfStructureDefinition: StructureDefinition
   ): InstanceDefinition {
-    // The fshInstanceDef.rules list may contain insert rules, which will be expanded to AssignmentRules
-    applyInsertRules(fshInstanceDef, this.tank);
     resolveSoftIndexing(fshInstanceDef.rules);
     let rules = fshInstanceDef.rules.map(r => cloneDeep(r)) as AssignmentRule[];
     // Normalize all rules to not use the optional [0] index
@@ -429,8 +427,15 @@ export class InstanceExporter implements Fishable {
     return this.fisher.fishForMetadata(item, Type.Instance);
   }
 
+  applyInsertRules(): void {
+    const instances = this.tank.getAllInstances();
+    for (const instance of instances) {
+      applyInsertRules(instance, this.tank);
+    }
+  }
+
   exportInstance(fshDefinition: Instance): InstanceDefinition {
-    if (this.pkg.instances.some(i => i._instanceMeta.name === fshDefinition.id)) {
+    if (this.pkg.instances.some(i => i._instanceMeta.name === fshDefinition.name)) {
       return;
     }
 
@@ -475,7 +480,7 @@ export class InstanceExporter implements Fishable {
 
     const instanceOfStructureDefinition = StructureDefinition.fromJSON(json);
     let instanceDef = new InstanceDefinition();
-    instanceDef._instanceMeta.name = fshDefinition.id; // This is name of the instance in the FSH
+    instanceDef._instanceMeta.name = fshDefinition.name; // This is name of the instance in the FSH
     if (fshDefinition.title == '') {
       logger.warn(`Instance ${fshDefinition.name} has a title field that should not be empty.`);
     }
diff --git a/src/export/MappingExporter.ts b/src/export/MappingExporter.ts
index 8f435e3..4779cb6 100644
--- a/src/export/MappingExporter.ts
+++ b/src/export/MappingExporter.ts
@@ -42,8 +42,6 @@ export class MappingExporter {
    * @param {Mapping} fshDefinition - The Mapping definition
    */
   private setMappingRules(structDef: StructureDefinition, fshDefinition: Mapping): void {
-    // Before applying mapping rules, applyInsertRules will expand any insert rules into mapping rules
-    applyInsertRules(fshDefinition, this.tank);
     resolveSoftIndexing(fshDefinition.rules);
     for (const rule of fshDefinition.rules as MappingRule[]) {
       const element = structDef.findElementByPath(rule.path, this.fisher);
@@ -62,6 +60,13 @@ export class MappingExporter {
     }
   }
 
+  applyInsertRules(): void {
+    const mappings = this.tank.getAllMappings();
+    for (const mapping of mappings) {
+      applyInsertRules(mapping, this.tank);
+    }
+  }
+
   /**
    * Exports a Mapping by finding the Source StructureDefinition and applying rules
    * @param {Mapping} fshDefinition - The Mapping definition to apply
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index dad2487..1f38a6e 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -864,6 +864,13 @@ export class StructureDefinitionExporter implements Fishable {
     return this.fisher.fishForMetadata(item, ...types);
   }
 
+  applyInsertRules(): void {
+    const structureDefinitions = this.tank.getAllStructureDefinitions();
+    for (const sd of structureDefinitions) {
+      applyInsertRules(sd, this.tank);
+    }
+  }
+
   /**
    * Exports Profile, Extension, Logical model, and custom Resource to StructureDefinition
    * @param {Profile | Extension | Logical | Resource} fshDefinition - The Profile or Extension
@@ -917,9 +924,6 @@ export class StructureDefinitionExporter implements Fishable {
       this.pkg.profiles.push(structDef);
     }
 
-    // fshDefinition.rules may include insert rules, which must be expanded before applying other rules
-    applyInsertRules(fshDefinition, this.tank);
-
     this.preprocessStructureDefinition(fshDefinition, structDef.type === 'Extension');
 
     this.setRules(structDef, fshDefinition);
diff --git a/src/export/ValueSetExporter.ts b/src/export/ValueSetExporter.ts
index 8ed5c7a..0175966 100644
--- a/src/export/ValueSetExporter.ts
+++ b/src/export/ValueSetExporter.ts
@@ -195,6 +195,13 @@ export class ValueSetExporter {
     }
   }
 
+  applyInsertRules(): void {
+    const valueSets = this.tank.getAllValueSets();
+    for (const vs of valueSets) {
+      applyInsertRules(vs, this.tank);
+    }
+  }
+
   export(): Package {
     const valueSets = this.tank.getAllValueSets();
     for (const valueSet of valueSets) {
@@ -216,8 +223,6 @@ export class ValueSetExporter {
     }
     const vs = new ValueSet();
     this.setMetadata(vs, fshDefinition);
-    // fshDefinition.rules may include insert rules, which must be expanded before applying other rules
-    applyInsertRules(fshDefinition, this.tank);
     this.setCaretRules(
       vs,
       fshDefinition.rules.filter(rule => rule instanceof CaretValueRule) as CaretValueRule[]
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index 97a2a9a..93ce445 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -429,14 +429,9 @@ export function replaceReferences<T extends AssignmentRule | CaretValueRule>(
     );
     // If we can't find a matching instance, just leave the reference as is
     if (instance && instanceMeta) {
-      // If the instance has a rule setting id, that overrides instance.id
-      const idRule = instance.rules.find(
-        r => r.path === 'id' && r instanceof AssignmentRule
-      ) as AssignmentRule;
-      const id = idRule?.value ?? instance.id;
       clone = cloneDeep(rule);
       const assignedReference = getRuleValue(clone) as FshReference;
-      assignedReference.reference = `${instanceMeta.sdType}/${id}`;
+      assignedReference.reference = `${instanceMeta.sdType}/${instance.id}`;
       assignedReference.sdType = instanceMeta.sdType;
     }
   } else if (value instanceof FshCode) {
diff --git a/src/fhirtypes/mixins.ts b/src/fhirtypes/mixins.ts
index d9458ae..aaffb7c 100644
--- a/src/fhirtypes/mixins.ts
+++ b/src/fhirtypes/mixins.ts
@@ -1,8 +1,9 @@
 import _ from 'lodash';
-import { CaretValueRule } from '../fshtypes/rules';
+import { CaretValueRule, AssignmentRule } from '../fshtypes/rules';
 import { FshValueSet, FshStructure, FshCodeSystem, Instance } from '../fshtypes';
 import { logger } from '../utils';
 import { FHIRId, idRegex } from './primitiveTypes';
+import { findIdAssignmentRule, findIdCaretRule } from '../fshtypes/common';
 
 const nameRegex = /^[A-Z]([A-Za-z0-9_]){0,254}$/;
 
@@ -61,10 +62,12 @@ export class HasId {
    * @param {FshStructure | FshCodeSystem | FshValueSet} fshDefinition - The entity who's id is being set
    */
   validateId(fshDefinition: FshStructure | FshCodeSystem | FshValueSet | Instance) {
-    const idRule = _.findLast(
-      fshDefinition.rules,
-      rule => rule instanceof CaretValueRule && rule.caretPath === 'id' && rule.path === ''
-    ) as CaretValueRule;
+    let idRule: AssignmentRule | CaretValueRule;
+    if (fshDefinition instanceof Instance) {
+      idRule = findIdAssignmentRule(fshDefinition.rules);
+    } else {
+      idRule = findIdCaretRule(fshDefinition.rules);
+    }
     const idToCheck = idRule ? (idRule.value as string) : this.id;
     let validId = idRegex.test(idToCheck);
     if (!validId && !idRule && nameRegex.test(this.id) && !(fshDefinition instanceof Instance)) {
diff --git a/src/fshtypes/FshCodeSystem.ts b/src/fshtypes/FshCodeSystem.ts
index 56b78d3..b19dcf2 100644
--- a/src/fshtypes/FshCodeSystem.ts
+++ b/src/fshtypes/FshCodeSystem.ts
@@ -3,7 +3,7 @@ import { CodeSystemDuplicateCodeError } from '../errors/CodeSystemDuplicateCodeE
 import { CodeSystemIncorrectHierarchyError } from '../errors/CodeSystemIncorrectHierarchyError';
 import { CaretValueRule, InsertRule, ConceptRule } from './rules';
 import { EOL } from 'os';
-import { fshifyString } from './common';
+import { fshifyString, findIdCaretRule } from './common';
 import isEqual from 'lodash/isEqual';
 
 /**
@@ -11,7 +11,7 @@ import isEqual from 'lodash/isEqual';
  * @see {@link http://hl7.org/fhir/codesystem-definitions.html}
  */
 export class FshCodeSystem extends FshEntity {
-  id: string;
+  private _id: string;
   title?: string;
   description?: string;
   rules: (ConceptRule | CaretValueRule | InsertRule)[];
@@ -26,6 +26,18 @@ export class FshCodeSystem extends FshEntity {
     return 'FshCodeSystem';
   }
 
+  get id() {
+    const idCaretRule = findIdCaretRule(this.rules);
+    if (idCaretRule) {
+      return idCaretRule.value.toString();
+    }
+    return this._id;
+  }
+
+  set id(id: string) {
+    this._id = id;
+  }
+
   addConcept(newConcept: ConceptRule) {
     if (this.checkConcept(newConcept)) {
       this.rules.push(newConcept);
diff --git a/src/fshtypes/FshStructure.ts b/src/fshtypes/FshStructure.ts
index aa908b2..27885ca 100644
--- a/src/fshtypes/FshStructure.ts
+++ b/src/fshtypes/FshStructure.ts
@@ -1,10 +1,10 @@
 import { FshEntity } from './FshEntity';
 import { Rule } from './rules';
 import { EOL } from 'os';
-import { fshifyString } from './common';
+import { fshifyString, findIdCaretRule } from './common';
 
 export abstract class FshStructure extends FshEntity {
-  id: string;
+  private _id: string;
   parent?: string;
   title?: string;
   description?: string;
@@ -20,6 +20,18 @@ export abstract class FshStructure extends FshEntity {
     return 'FshStructure';
   }
 
+  get id() {
+    const idCaretRule = findIdCaretRule(this.rules);
+    if (idCaretRule) {
+      return idCaretRule.value.toString();
+    }
+    return this._id;
+  }
+
+  set id(id: string) {
+    this._id = id;
+  }
+
   metadataToFSH(): string {
     const resultLines: string[] = [];
     resultLines.push(`${this.constructorName}: ${this.name}`);
diff --git a/src/fshtypes/FshValueSet.ts b/src/fshtypes/FshValueSet.ts
index 049e48b..6f3b5d6 100644
--- a/src/fshtypes/FshValueSet.ts
+++ b/src/fshtypes/FshValueSet.ts
@@ -1,14 +1,14 @@
 import { FshEntity } from './FshEntity';
 import { CaretValueRule, InsertRule, ValueSetComponentRule } from './rules';
 import { EOL } from 'os';
-import { fshifyString } from './common';
+import { fshifyString, findIdCaretRule } from './common';
 
 /**
  * For more information about the composition of a ValueSet,
  * @see {@link http://hl7.org/fhir/valueset-definitions.html#ValueSet.compose}
  */
 export class FshValueSet extends FshEntity {
-  id: string;
+  private _id: string;
   title?: string;
   description?: string;
   rules: (ValueSetComponentRule | CaretValueRule | InsertRule)[];
@@ -23,6 +23,18 @@ export class FshValueSet extends FshEntity {
     return 'FshValueSet';
   }
 
+  get id() {
+    const idCaretRule = findIdCaretRule(this.rules);
+    if (idCaretRule) {
+      return idCaretRule.value.toString();
+    }
+    return this._id;
+  }
+
+  set id(id: string) {
+    this._id = id;
+  }
+
   metadataToFSH(): string {
     const resultLines: string[] = [];
     resultLines.push(`ValueSet: ${this.name}`);
diff --git a/src/fshtypes/Instance.ts b/src/fshtypes/Instance.ts
index 854d2a9..69143d2 100644
--- a/src/fshtypes/Instance.ts
+++ b/src/fshtypes/Instance.ts
@@ -1,11 +1,11 @@
 import { AssignmentRule, InsertRule } from './rules';
 import { FshEntity } from './FshEntity';
 import { EOL } from 'os';
-import { fshifyString } from './common';
+import { fshifyString, findIdAssignmentRule } from './common';
 import { InstanceUsage } from './InstanceUsage';
 
 export class Instance extends FshEntity {
-  id: string;
+  private _id: string;
   title?: string;
   instanceOf: string;
   description?: string;
@@ -23,6 +23,18 @@ export class Instance extends FshEntity {
     return 'Instance';
   }
 
+  get id() {
+    const idAssigmentRule = findIdAssignmentRule(this.rules);
+    if (idAssigmentRule) {
+      return idAssigmentRule.value.toString();
+    }
+    return this._id;
+  }
+
+  set id(id: string) {
+    this._id = id;
+  }
+
   metadataToFSH() {
     const resultLines: string[] = [];
     resultLines.push(`Instance: ${this.name}`);
diff --git a/src/fshtypes/common.ts b/src/fshtypes/common.ts
index 99c1ee0..c1345fb 100644
--- a/src/fshtypes/common.ts
+++ b/src/fshtypes/common.ts
@@ -1,4 +1,5 @@
-import { OnlyRuleType } from './rules/OnlyRule';
+import { CaretValueRule, Rule, OnlyRuleType, AssignmentRule } from './rules';
+import { findLast } from 'lodash';
 
 export function typeString(types: OnlyRuleType[]): string {
   const references: OnlyRuleType[] = [];
@@ -32,3 +33,21 @@ export function fshifyString(input: string): string {
     .replace(/\r/g, '\\r')
     .replace(/\t/g, '\\t');
 }
+
+export function findIdCaretRule(rules: Rule[]): CaretValueRule | undefined {
+  return findLast(
+    rules,
+    rule =>
+      rule instanceof CaretValueRule &&
+      rule.path === '' &&
+      rule.caretPath === 'id' &&
+      typeof rule.value === 'string'
+  ) as CaretValueRule;
+}
+
+export function findIdAssignmentRule(rules: Rule[]): AssignmentRule | undefined {
+  return findLast(
+    rules,
+    rule => rule instanceof AssignmentRule && rule.path === 'id' && typeof rule.value === 'string'
+  ) as AssignmentRule;
+}
diff --git a/src/import/FSHTank.ts b/src/import/FSHTank.ts
index ed2b354..7aa6b34 100644
--- a/src/import/FSHTank.ts
+++ b/src/import/FSHTank.ts
@@ -15,7 +15,7 @@ import {
 } from '../fshtypes';
 import { AssignmentRule } from '../fshtypes/rules';
 import { Type, Metadata, Fishable } from '../utils/Fishable';
-import { getUrlFromFshDefinition, applyInsertRules } from '../fhirtypes/common';
+import { getUrlFromFshDefinition } from '../fhirtypes/common';
 import flatMap from 'lodash/flatMap';
 
 export class FSHTank implements Fishable {
@@ -394,8 +394,6 @@ export class FSHTank implements Fishable {
           meta.resourceType = 'CodeSystem';
         }
       } else if (result instanceof Instance) {
-        // the url may be added in a RuleSet, so apply insert rules
-        applyInsertRules(result, this);
         result.rules?.forEach(r => {
           if (r.path === 'url' && r instanceof AssignmentRule && typeof r.value === 'string') {
             meta.url = r.value;


Test patch:
diff --git a/test/export/CodeSystemExporter.test.ts b/test/export/CodeSystemExporter.test.ts
index 87d32f1..01387fe 100644
--- a/test/export/CodeSystemExporter.test.ts
+++ b/test/export/CodeSystemExporter.test.ts
@@ -813,6 +813,7 @@ describe('CodeSystemExporter', () => {
       insertRule.ruleSet = 'Bar';
       cs.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportCodeSystem(cs);
       expect(exported.title).toBe('Wow fancy');
     });
@@ -844,6 +845,7 @@ describe('CodeSystemExporter', () => {
       insertRule.ruleSet = 'Bar';
       cs.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportCodeSystem(cs);
       expect(exported.contact).toEqual([
         {
@@ -878,6 +880,7 @@ describe('CodeSystemExporter', () => {
       insertRule.pathArray = ['bear'];
       cs.rules.push(conceptRule, insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportCodeSystem(cs);
       expect(exported.concept[0]).toEqual({
         code: 'bear',
@@ -907,6 +910,7 @@ describe('CodeSystemExporter', () => {
       insertRule.ruleSet = 'Bar';
       cs.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportCodeSystem(cs);
       expect(exported.concept[0].code).toBe('lion');
       expect(exported.count).toBe(1);
@@ -932,6 +936,7 @@ describe('CodeSystemExporter', () => {
       insertRule.ruleSet = 'Bar';
       cs.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportCodeSystem(cs);
       // CaretRule is still applied
       expect(exported.title).toBe('Wow fancy');
@@ -959,6 +964,7 @@ describe('CodeSystemExporter', () => {
       const alligatorRule = new ConceptRule('alligator');
       cs.rules.push(bearRule, insertRule, alligatorRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportCodeSystem(cs);
       expect(exported.concept[0].code).toBe('bear');
       expect(exported.concept[1].code).toBe('lion');
@@ -988,6 +994,7 @@ describe('CodeSystemExporter', () => {
       const alligatorRule = new ConceptRule('alligator', 'Alligator');
       cs.rules.push(bearRule, insertRule, alligatorRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportCodeSystem(cs);
       expect(exported.concept[0].code).toBe('bear');
       expect(exported.concept[0].display).toBe('Regular Bear');
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index e9023da..8af14e4 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -689,7 +689,9 @@ describe('InstanceExporter', () => {
         .withFile('Some.fsh')
         .withLocation([3, 6, 6, 45]);
       myExamplePatient.instanceOf = 'Patient';
-      const assignedValRule = new AssignmentRule('id');
+      const assignedValRule = new AssignmentRule('id')
+        .withFile('Some.fsh')
+        .withLocation([5, 0, 6, 45]);
       assignedValRule.value = 'Some Patient';
       myExamplePatient.rules.push(assignedValRule);
       const exported = exportInstance(myExamplePatient);
@@ -699,13 +701,15 @@ describe('InstanceExporter', () => {
       };
       expect(exported.toJSON()).toEqual(expectedInstanceJSON);
       expect(loggerSpy.getLastMessage()).toMatch(/does not represent a valid FHIR id/s);
-      expect(loggerSpy.getLastMessage()).toMatch(/File: Some\.fsh.*Line: 3 - 6\D*/s);
+      expect(loggerSpy.getLastMessage()).toMatch(/File: Some\.fsh.*Line: 5 - 6\D*/s);
     });
 
     it('should sanitize the id and log a message when a valid name is used to make an invalid id', () => {
       const instance = new Instance('Foo').withFile('Wrong.fsh').withLocation([2, 8, 5, 18]);
       instance.instanceOf = 'Patient';
-      const assignedValRule = new AssignmentRule('id');
+      const assignedValRule = new AssignmentRule('id')
+        .withFile('Wrong.fsh')
+        .withLocation([5, 0, 5, 18]);
       assignedValRule.value = 'Some_Patient';
       instance.rules.push(assignedValRule);
       const exported = exportInstance(instance);
@@ -717,13 +721,15 @@ describe('InstanceExporter', () => {
       expect(loggerSpy.getLastMessage('error')).toMatch(
         /The string "Some_Patient" does not represent a valid FHIR id\. FHIR ids only allow ASCII letters \(A-Z, a-z\), numbers \(0-9\), hyphens \(-\), and dots \(\.\), with a length limit of 64 characters\. Avoid this warning by changing the Instance declaration to follow the FHIR id requirements\./s
       );
-      expect(loggerSpy.getLastMessage('error')).toMatch(/File: Wrong\.fsh.*Line: 2 - 5\D*/s);
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: Wrong\.fsh.*Line: 5\D*/s);
     });
 
     it('should log a message when a long valid name is used to make an invalid id', () => {
       const instance = new Instance('Foo').withFile('Wrong.fsh').withLocation([2, 8, 5, 18]);
       instance.instanceOf = 'Patient';
-      const assignedValRule = new AssignmentRule('id');
+      const assignedValRule = new AssignmentRule('id')
+        .withFile('Wrong.fsh')
+        .withLocation([4, 0, 5, 18]);
       let longId = 'Toolong';
       while (longId.length < 65) longId += 'longer';
       assignedValRule.value = longId;
@@ -737,7 +743,7 @@ describe('InstanceExporter', () => {
       expect(loggerSpy.getLastMessage('error')).toMatch(
         /The string "Toolong(longer)+" does not represent a valid FHIR id\. FHIR ids only allow ASCII letters \(A-Z, a-z\), numbers \(0-9\), hyphens \(-\), and dots \(\.\), with a length limit of 64 characters\. Avoid this warning by changing the Instance declaration to follow the FHIR id requirements\./s
       );
-      expect(loggerSpy.getLastMessage('error')).toMatch(/File: Wrong\.fsh.*Line: 2 - 5\D*/s);
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: Wrong\.fsh.*Line: 4 - 5\D*/s);
     });
 
     it('should log an error when multiple instances of the same type have the same id', () => {
@@ -2588,6 +2594,7 @@ describe('InstanceExporter', () => {
       libraryInstance.rules.push(libraryInsert, libraryStatus, libraryType);
       doc.instances.set(libraryInstance.name, libraryInstance);
 
+      exporter.applyInsertRules();
       const instances = exporter.export().instances;
       const exportedActivity = instances.find(
         instanceDefinition => instanceDefinition.id === 'MyActivity'
@@ -5174,6 +5181,7 @@ describe('InstanceExporter', () => {
       insertRule.ruleSet = 'Bar';
       instance.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportInstance(instance);
       expect(exported.id).toBe('my-id');
     });
@@ -5195,6 +5203,7 @@ describe('InstanceExporter', () => {
       const insertRule = new InsertRule('');
       insertRule.ruleSet = 'Bar';
       patientInstance.rules.push(insertRule);
+      exporter.applyInsertRules();
       const exported = exporter.exportInstance(patientInstance);
       expect(exported.name).toEqual([
         {
@@ -5227,6 +5236,7 @@ describe('InstanceExporter', () => {
       insertRule.ruleSet = 'Bar';
       instance.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportInstance(instance);
       // valueRule is still applied
       expect(exported.id).toBe('my-id');
diff --git a/test/export/MappingExporter.test.ts b/test/export/MappingExporter.test.ts
index 0c9b559..908b501 100644
--- a/test/export/MappingExporter.test.ts
+++ b/test/export/MappingExporter.test.ts
@@ -487,6 +487,7 @@ describe('MappingExporter', () => {
       insertRule.ruleSet = 'Bar';
       mapping.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       exporter.export();
       const status = observation.elements.find(e => e.id === 'Observation.status');
       const exported = status.mapping.slice(-1)[0];
@@ -514,6 +515,7 @@ describe('MappingExporter', () => {
       insertRule.ruleSet = 'Bar';
       mapping.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       exporter.export();
       // mapping rule is still applied
       const status = observation.elements.find(e => e.id === 'Observation.status');
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 3243b8f..6d6e012 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -1116,7 +1116,7 @@ describe('StructureDefinitionExporter R4', () => {
     it('should properly set/clear all metadata properties for an extension', () => {
       const extension = new Extension('Foo');
       extension.parent = 'patient-mothersMaidenName';
-      doc.profiles.set(extension.name, extension);
+      doc.extensions.set(extension.name, extension);
       exporter.exportStructDef(extension);
       const exported = pkg.extensions[0];
 
@@ -7912,6 +7912,7 @@ describe('StructureDefinitionExporter R4', () => {
       insertRule.ruleSet = 'Bar';
       profile.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportStructDef(profile);
       expect(exported.title).toBe('Wow fancy');
     });
@@ -7934,6 +7935,7 @@ describe('StructureDefinitionExporter R4', () => {
       insertRule.ruleSet = 'Bar';
       profile.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportStructDef(profile);
       // CaretRule is still applied
       expect(exported.title).toBe('Wow fancy');
diff --git a/test/export/ValueSetExporter.test.ts b/test/export/ValueSetExporter.test.ts
index 2e54922..aca36b6 100644
--- a/test/export/ValueSetExporter.test.ts
+++ b/test/export/ValueSetExporter.test.ts
@@ -1747,6 +1747,7 @@ describe('ValueSetExporter', () => {
       insertRule.ruleSet = 'Bar';
       vs.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportValueSet(vs);
       expect(exported.title).toBe('Wow fancy');
     });
@@ -1769,6 +1770,7 @@ describe('ValueSetExporter', () => {
       insertRule.ruleSet = 'Bar';
       vs.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportValueSet(vs);
       expect(exported.contact).toEqual([
         {
@@ -1803,6 +1805,7 @@ describe('ValueSetExporter', () => {
       insertRule.ruleSet = 'Bar';
       vs.rules.push(insertRule);
 
+      exporter.applyInsertRules();
       const exported = exporter.exportValueSet(vs);
       // CaretRule is still applied
       expect(exported.title).toBe('Wow fancy');
diff --git a/test/import/FSHTank.test.ts b/test/import/FSHTank.test.ts
index 7a0ea7b..da4617d 100644
--- a/test/import/FSHTank.test.ts
+++ b/test/import/FSHTank.test.ts
@@ -15,7 +15,7 @@ import {
 } from '../../src/fshtypes';
 import { Metadata, Type } from '../../src/utils/Fishable';
 import { minimalConfig } from '../utils/minimalConfig';
-import { AssignmentRule } from '../../src/fshtypes/rules';
+import { AssignmentRule, CaretValueRule } from '../../src/fshtypes/rules';
 
 describe('FSHTank', () => {
   let tank: FSHTank;
@@ -28,6 +28,13 @@ describe('FSHTank', () => {
     doc1.profiles.set('Profile2', new Profile('Profile2'));
     doc1.profiles.get('Profile2').id = 'prf2';
     doc1.profiles.get('Profile2').parent = 'Observation';
+    const profile3 = new Profile('Profile3');
+    profile3.parent = 'Observation';
+    const profile3Id = new CaretValueRule('');
+    profile3Id.caretPath = 'id';
+    profile3Id.value = 'prf3';
+    profile3.rules.push(profile3Id);
+    doc1.profiles.set(profile3.name, profile3);
     doc1.instances.set('ProfileInstance', new Instance('ProfileInstance'));
     doc1.instances.get('ProfileInstance').instanceOf = 'StructureDefinition';
     doc1.instances.get('ProfileInstance').usage = 'Definition';
@@ -41,6 +48,12 @@ describe('FSHTank', () => {
     doc2.aliases.set('BAR', 'http://bar.com');
     doc2.extensions.set('Extension2', new Extension('Extension2'));
     doc2.extensions.get('Extension2').id = 'ext2';
+    const ext3 = new Extension('Extension3');
+    const ext3Id = new CaretValueRule('');
+    ext3Id.caretPath = 'id';
+    ext3Id.value = 'ext3';
+    ext3.rules.push(ext3Id);
+    doc2.extensions.set(ext3.name, ext3);
     doc2.instances.set('ExtensionInstance', new Instance('ExtensionInstance'));
     doc2.instances.get('ExtensionInstance').instanceOf = 'StructureDefinition';
     doc2.instances.get('ExtensionInstance').usage = 'Definition';
@@ -94,11 +107,47 @@ describe('FSHTank', () => {
       .rules.push(resourceInstanceDerivation, resourceInstanceKind);
     doc3.valueSets.set('ValueSet2', new FshValueSet('ValueSet2'));
     doc3.valueSets.get('ValueSet2').id = 'vs2';
+    const vs3 = new FshValueSet('ValueSet3');
+    const vs3Id = new CaretValueRule('');
+    vs3Id.caretPath = 'id';
+    vs3Id.value = 'vs3';
+    vs3.rules.push(vs3Id);
+    doc3.valueSets.set(vs3.name, vs3);
     doc3.codeSystems.set('CodeSystem2', new FshCodeSystem('CodeSystem2'));
     doc3.codeSystems.get('CodeSystem2').id = 'cs2';
+    const cs3 = new FshCodeSystem('CodeSystem3');
+    const cs3Id = new CaretValueRule('');
+    cs3Id.caretPath = 'id';
+    cs3Id.value = 'cs3';
+    cs3.rules.push(cs3Id);
+    doc3.codeSystems.set(cs3.name, cs3);
+    const log3 = new Logical('Logical3');
+    const log3Id = new CaretValueRule('');
+    log3Id.caretPath = 'id';
+    log3Id.value = 'log3';
+    log3.rules.push(log3Id);
+    doc3.logicals.set(log3.name, log3);
+    const res3 = new Resource('Resource3');
+    const res3Id = new CaretValueRule('');
+    res3Id.caretPath = 'id';
+    res3Id.value = 'res3';
+    res3.rules.push(res3Id);
+    doc3.resources.set(res3.name, res3);
     doc3.instances.set('Instance1', new Instance('Instance1'));
     doc3.instances.get('Instance1').id = 'inst1';
     doc3.instances.get('Instance1').instanceOf = 'Condition';
+    const inst2 = new Instance('Instance2');
+    inst2.instanceOf = 'Condition';
+    const inst2Id = new AssignmentRule('id');
+    inst2Id.value = 'inst2';
+    inst2.rules.push(inst2Id);
+    doc3.instances.set(inst2.name, inst2);
+    const idRuleset = new RuleSet('IdRuleset');
+    const idAssignment = new AssignmentRule('id');
+    idAssignment.value = 'inst3';
+    idRuleset.rules.push(idAssignment);
+    doc3.ruleSets.set(idRuleset.name, idRuleset);
+
     doc3.invariants.set('Invariant1', new Invariant('Invariant1'));
     doc3.invariants.get('Invariant1').description = 'first invariant';
     doc3.invariants.get('Invariant1').severity = new FshCode('error');
@@ -153,7 +202,7 @@ describe('FSHTank', () => {
     });
 
     it('should not find fish when fishing by invalid name/id/url', () => {
-      expect(tank.fish('Profile3')).toBeUndefined();
+      expect(tank.fish('ProfileFake')).toBeUndefined();
     });
 
     it('should only find profiles when profiles are requested', () => {
@@ -203,6 +252,10 @@ describe('FSHTank', () => {
       expect(tank.fish('ProfileInstance', Type.Profile)).toBeUndefined();
     });
 
+    it('should find a profile when fishing by id when the profile id is set by a caret rule', () => {
+      expect(tank.fish('prf3').name).toBe('Profile3');
+    });
+
     it('should only find extensions when extensions are requested', () => {
       expect(tank.fish('ext1', Type.Extension).name).toBe('Extension1');
       expect(
@@ -264,6 +317,10 @@ describe('FSHTank', () => {
       expect(tank.fish('ExtensionInstance', Type.Extension)).toBeUndefined();
     });
 
+    it('should find an extension when fishing by id when the extension id is set by a caret rule', () => {
+      expect(tank.fish('ext3').name).toBe('Extension3');
+    });
+
     it('should only find logical models when logical models are requested', () => {
       expect(tank.fish('log1', Type.Logical).name).toBe('Logical1');
       expect(
@@ -301,6 +358,10 @@ describe('FSHTank', () => {
       ).toBeUndefined();
     });
 
+    it('should find a logical model when fishing by id when the logical model id is set by a caret rule', () => {
+      expect(tank.fish('log3').name).toBe('Logical3');
+    });
+
     it('should only find resources when resources are requested', () => {
       expect(tank.fish('res2', Type.Resource).name).toBe('Resource2');
       expect(
@@ -338,6 +399,10 @@ describe('FSHTank', () => {
       ).toBeUndefined();
     });
 
+    it('should find a resource when fishing by id when the resource id is set by a caret rule', () => {
+      expect(tank.fish('res3').name).toBe('Resource3');
+    });
+
     it('should only find valuesets when valuesets are requested', () => {
       expect(tank.fish('vs1', Type.ValueSet).name).toBe('ValueSet1');
       expect(
@@ -375,6 +440,10 @@ describe('FSHTank', () => {
       ).toBeUndefined();
     });
 
+    it('should find a valueset when fishing by id when the valueset id is set by a caret rule', () => {
+      expect(tank.fish('vs3').name).toBe('ValueSet3');
+    });
+
     it('should only find codesystems when codesystems are requested', () => {
       expect(tank.fish('cs1', Type.CodeSystem).name).toBe('CodeSystem1');
       expect(
@@ -412,6 +481,10 @@ describe('FSHTank', () => {
       ).toBeUndefined();
     });
 
+    it('should find a codesystem when fishing by id when the codesystem id is set by a caret rule', () => {
+      expect(tank.fish('cs3').name).toBe('CodeSystem3');
+    });
+
     it('should only find instances when instances are requested', () => {
       expect(tank.fish('inst1', Type.Instance).name).toBe('Instance1');
       expect(
@@ -431,6 +504,10 @@ describe('FSHTank', () => {
       ).toBeUndefined();
     });
 
+    it('should find an instance when fishing by id when the instance id is set by an assignment rule', () => {
+      expect(tank.fish('inst2').name).toBe('Instance2');
+    });
+
     it('should only find invariants when invariants are requested', () => {
       expect(tank.fish('Invariant1', Type.Invariant).name).toBe('Invariant1');
       expect(
@@ -592,7 +669,7 @@ describe('FSHTank', () => {
     });
 
     it('should not find fish when fishing by invalid name/id/url', () => {
-      expect(tank.fishForMetadata('Profile3')).toBeUndefined();
+      expect(tank.fishForMetadata('ProfileFake')).toBeUndefined();
     });
 
     it('should only find profiles when profiles are requested', () => {