Repository: FHIR/sushi
Pull request #1275 (2023-05-11 15:02:08)

Fish With Version
This PR supports fishing for definitions with a version. This PR adds support for fishing with a specified version in the `FSHTank` and in the `Package`. Support for fishing with a specified version in `FHIRDefinitions` is provided by FPL. There is an FPL PR with this support [here](https://github.com/standardhealth/fhir-package-loader/pull/11).

In addition to supporting fishing with a version, there are a number of places that SUSHI tries to fish for the version specified but is satisfied if any version is found, but logs a warning. To support this, three util functions are included: `fishForFHIRBestVersion`, `fishForMetadataBestVersion`, and `fishInTankBestVersion`. These functions will call either `fishForFHIR`, `fishForMetadata`, or `fish` (in a Tank) first with the item and specified version. If nothing is found, they'll then try again without a version, and if a definition/metadata is found with a differing version, a warning is logged. I replaced the special case handling SUSHI was doing to slice off the version and check for discrepancies between versions with these functions where possible.

To test this, you can use the FPL PR (until a release is ready and available directly in this PR) and link your local FPL to SUSHI. Then you can run the tests and test FSH projects locally.

I ran a full regression and found that two repos had regressions, [HL7/vrdr](https://github.com/HL7/vrdr) and [nightingaleproject/vital_records_fhir_messaging_ig](https://github.com/nightingaleproject/vital_records_fhir_messaging_ig), both with the same issue. Both repos specify a line similar to:

```
* valueDateTime.extension[partialDateTime].extension[time].valueTime.extension[dataabsent].valueCode = http://hl7.org/fhir/ValueSet/data-absent-reason|4.0.1#unknown
```
This line is setting a URL with a version as a system in a FSH code. Before, SUSHI was just fishing for this, didn't find a definition so didn't realize it was a ValueSet, checked that it was a valid URI, and successfully used the code system. In this branch, fishing with the version will work correctly and SUSHI will find a ValueSet definition for this definition, which is a problem, and it will throw a MismatchBindingTypeError and not set the value. I think this regression is actually the correct behavior for SUSHI now.

~_Note: this PR is marked as draft until a new version of FPL is released with support for fishing with versions so that the FHIR Definitions fishing works correctly._~ FPL v.0.3.0 is used now, so this is no longer draft!


Patch:
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index a97f46f..37aa43e 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -14,7 +14,7 @@
         "chalk": "^3.0.0",
         "commander": "^8.2.0",
         "fhir": "^4.9.0",
-        "fhir-package-loader": "^0.2.0",
+        "fhir-package-loader": "^0.3.0",
         "fs-extra": "^8.1.0",
         "html-minifier-terser": "5.1.1",
         "https-proxy-agent": "^5.0.0",
@@ -3536,9 +3536,9 @@
       }
     },
     "node_modules/fhir-package-loader": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-0.2.0.tgz",
-      "integrity": "sha512-jXkZ9m21vOKmQYAwmq1+nXs/PBywnevyBrjupVBpe9ZBkX+3RpWkzxUf8Ur4AnfkGnX2/nQ7XSINjzqEnxthNA==",
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-0.3.0.tgz",
+      "integrity": "sha512-+S1jC8OxFWTeD6Ro+76/29Tk9ffIYIxpOvfEFu1qlnWLNzvTuTI7uM0RjOW79sD1TWv9XWaZRKTIPZooioMUbQ==",
       "dependencies": {
         "axios": "^0.21.1",
         "chalk": "^4.1.2",
@@ -11038,9 +11038,9 @@
       }
     },
     "fhir-package-loader": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-0.2.0.tgz",
-      "integrity": "sha512-jXkZ9m21vOKmQYAwmq1+nXs/PBywnevyBrjupVBpe9ZBkX+3RpWkzxUf8Ur4AnfkGnX2/nQ7XSINjzqEnxthNA==",
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-0.3.0.tgz",
+      "integrity": "sha512-+S1jC8OxFWTeD6Ro+76/29Tk9ffIYIxpOvfEFu1qlnWLNzvTuTI7uM0RjOW79sD1TWv9XWaZRKTIPZooioMUbQ==",
       "requires": {
         "axios": "^0.21.1",
         "chalk": "^4.1.2",
diff --git a/package.json b/package.json
index 6be3f1e..7896e69 100644
--- a/package.json
+++ b/package.json
@@ -86,7 +86,7 @@
     "chalk": "^3.0.0",
     "commander": "^8.2.0",
     "fhir": "^4.9.0",
-    "fhir-package-loader": "^0.2.0",
+    "fhir-package-loader": "^0.3.0",
     "fs-extra": "^8.1.0",
     "html-minifier-terser": "5.1.1",
     "https-proxy-agent": "^5.0.0",
diff --git a/src/export/Package.ts b/src/export/Package.ts
index 5ba8bca..fd69ba6 100644
--- a/src/export/Package.ts
+++ b/src/export/Package.ts
@@ -30,11 +30,19 @@ export class Package implements Fishable {
       ];
     }
 
+    // version needs to be checked separately from the base
+    const [base, ...versionParts] = item?.split('|') ?? ['', ''];
+    const version = versionParts.join('|') || null;
+
     for (const type of types) {
       let def;
       switch (type) {
         case Type.Profile:
-          def = this.profiles.find(p => p.id === item || p.name === item || p.url === item);
+          def = this.profiles.find(
+            p =>
+              (p.id === base || p.name === base || p.url === base) &&
+              (version == null || p.version === version)
+          );
           if (!def) {
             def = this.instances.find(
               i =>
@@ -42,12 +50,17 @@ export class Package implements Fishable {
                 i.resourceType === 'StructureDefinition' &&
                 i.derivation === 'constraint' &&
                 i.type !== 'Extension' &&
-                (i.id === item || i._instanceMeta.name === item || i.url === item)
+                (i.id === base || i._instanceMeta.name === base || i.url === base) &&
+                (version == null || i.version === version)
             );
           }
           break;
         case Type.Extension:
-          def = this.extensions.find(e => e.id === item || e.name === item || e.url === item);
+          def = this.extensions.find(
+            e =>
+              (e.id === base || e.name === base || e.url === base) &&
+              (version == null || e.version === version)
+          );
           if (!def) {
             def = this.instances.find(
               i =>
@@ -55,12 +68,17 @@ export class Package implements Fishable {
                 i.resourceType === 'StructureDefinition' &&
                 i.derivation === 'constraint' &&
                 i.type === 'Extension' &&
-                (i.id === item || i._instanceMeta.name === item || i.url === item)
+                (i.id === base || i._instanceMeta.name === base || i.url === base) &&
+                (version == null || i.version === version)
             );
           }
           break;
         case Type.Logical:
-          def = this.logicals.find(e => e.id === item || e.name === item || e.url === item);
+          def = this.logicals.find(
+            l =>
+              (l.id === base || l.name === base || l.url === base) &&
+              (version == null || l.version === version)
+          );
           if (!def) {
             def = this.instances.find(
               i =>
@@ -68,12 +86,17 @@ export class Package implements Fishable {
                 i.resourceType === 'StructureDefinition' &&
                 i.derivation === 'specialization' &&
                 i.kind === 'logical' &&
-                (i.id === item || i._instanceMeta.name === item || i.url === item)
+                (i.id === base || i._instanceMeta.name === base || i.url === base) &&
+                (version == null || i.version === version)
             );
           }
           break;
         case Type.Resource:
-          def = this.resources.find(e => e.id === item || e.name === item || e.url === item);
+          def = this.resources.find(
+            r =>
+              (r.id === base || r.name === base || r.url === base) &&
+              (version == null || r.version === version)
+          );
           if (!def) {
             def = this.instances.find(
               i =>
@@ -81,34 +104,49 @@ export class Package implements Fishable {
                 i.resourceType === 'StructureDefinition' &&
                 i.derivation === 'specialization' &&
                 i.kind === 'resource' &&
-                (i.id === item || i._instanceMeta.name === item || i.url === item)
+                (i.id === base || i._instanceMeta.name === base || i.url === base) &&
+                (version == null || i.version === version)
             );
           }
           break;
         case Type.ValueSet:
-          def = this.valueSets.find(vs => vs.id === item || vs.name === item || vs.url === item);
+          def = this.valueSets.find(
+            vs =>
+              (vs.id === base || vs.name === base || vs.url === base) &&
+              (version == null || vs.version === version)
+          );
           if (!def) {
             def = this.instances.find(
               i =>
                 i._instanceMeta.usage === 'Definition' &&
                 i.resourceType === 'ValueSet' &&
-                (i.id === item || i._instanceMeta.name === item || i.url === item)
+                (i.id === base || i._instanceMeta.name === base || i.url === base) &&
+                (version == null || i.version === version)
             );
           }
           break;
         case Type.CodeSystem:
-          def = this.codeSystems.find(cs => cs.id === item || cs.name === item || cs.url === item);
+          def = this.codeSystems.find(
+            cs =>
+              (cs.id === base || cs.name === base || cs.url === base) &&
+              (version == null || cs.version === version)
+          );
           if (!def) {
             def = this.instances.find(
               i =>
                 i._instanceMeta.usage === 'Definition' &&
                 i.resourceType === 'CodeSystem' &&
-                (i.id === item || i._instanceMeta.name === item || i.url === item)
+                (i.id === base || i._instanceMeta.name === base || i.url === base) &&
+                (version == null || i.version === version)
             );
           }
           break;
         case Type.Instance:
-          def = this.instances.find(i => i.id === item || i._instanceMeta.name === item);
+          def = this.instances.find(
+            i =>
+              (i.id === base || i._instanceMeta.name === base) &&
+              (version == null || i.version === version)
+          );
           break;
         case Type.Type: // Package doesn't currently support types
         default:
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index a8ac885..2daf2f6 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -46,7 +46,15 @@ import {
   ObeysRule,
   OnlyRule
 } from '../fshtypes/rules';
-import { logger, Type, Fishable, Metadata, MasterFisher, resolveSoftIndexing } from '../utils';
+import {
+  logger,
+  Type,
+  Fishable,
+  fishForFHIRBestVersion,
+  Metadata,
+  MasterFisher,
+  resolveSoftIndexing
+} from '../utils';
 import {
   applyInsertRules,
   cleanResource,
@@ -603,7 +611,8 @@ export class StructureDefinitionExporter implements Fishable {
             const target = structDef.getReferenceOrCanonicalName(rule.path, element);
             element.constrainType(rule, this, target);
           } else if (rule instanceof BindingRule) {
-            const vsURI = this.fishForMetadata(rule.valueSet, Type.ValueSet)?.url ?? rule.valueSet;
+            const vsMetadata = this.fishForMetadata(rule.valueSet, Type.ValueSet);
+            const vsURI = rule.valueSet.replace(/^([^|]+)/, vsMetadata?.url ?? '$1');
             const csURI = this.fishForMetadata(rule.valueSet, Type.CodeSystem)?.url;
             if (csURI && !isUri(vsURI)) {
               throw new MismatchedBindingTypeError(rule.valueSet, rule.path, 'ValueSet');
@@ -780,9 +789,9 @@ export class StructureDefinitionExporter implements Fishable {
     }
     rule.items.forEach(item => {
       if (item.type) {
-        // there might be a |version appended to the type, so don't include it while fishing
-        const [typeWithoutVersion, version] = item.type.split('|', 2);
-        const extension = this.fishForFHIR(typeWithoutVersion, Type.Extension);
+        // there might be a |version appended to the type, so try to use the version but fall back
+        // to any version if necessary
+        const extension = fishForFHIRBestVersion(this, item.type, rule.sourceInfo, Type.Extension);
         if (extension == null) {
           logger.error(
             `Cannot create ${item.name} extension; unable to locate extension definition for: ${item.type}.`,
@@ -790,17 +799,8 @@ export class StructureDefinitionExporter implements Fishable {
           );
           return;
         }
-        if (version != null && extension.version != null && version != extension.version) {
-          logger.warn(
-            `The ${typeWithoutVersion} extension was specified with version ${version}, but SUSHI found version ${extension.version}`,
-            rule.sourceInfo
-          );
-        }
         try {
-          let profileUrl = extension.url;
-          if (version) {
-            profileUrl += `|${version}`;
-          }
+          const profileUrl = item.type.replace(/^[^|]+/, extension.url);
           const slice = element.addSlice(item.name);
           if (!slice.type[0].profile) {
             slice.type[0].profile = [];
diff --git a/src/export/ValueSetExporter.ts b/src/export/ValueSetExporter.ts
index 463d35c..3b6980b 100644
--- a/src/export/ValueSetExporter.ts
+++ b/src/export/ValueSetExporter.ts
@@ -56,10 +56,10 @@ export class ValueSetExporter {
         const composeElement: ValueSetComposeIncludeOrExclude = {};
         if (component.from.system) {
           const systemParts = component.from.system.split('|');
-          const foundSystem = (
-            this.fisher.fishForMetadata(systemParts[0], Type.CodeSystem)?.url ??
-            component.from.system
-          ).split('|');
+          const csMetadata = this.fisher.fishForMetadata(component.from.system, Type.CodeSystem);
+          const foundSystem = component.from.system
+            .replace(/^([^|]+)/, csMetadata?.url ?? '$1')
+            .split('|');
           composeElement.system = foundSystem[0];
           // if the rule specified a version, use that version.
           composeElement.version = systemParts.slice(1).join('|') || undefined;
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 4f7a513..ab15abc 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -13,7 +13,15 @@ import { minify } from 'html-minifier-terser';
 import { isUri } from 'valid-url';
 import { StructureDefinition } from './StructureDefinition';
 import { CodeableConcept, Coding, Element, Quantity, Ratio, Reference } from './dataTypes';
-import { FshCanonical, FshCode, FshRatio, FshQuantity, FshReference, Invariant } from '../fshtypes';
+import {
+  FshCanonical,
+  FshCode,
+  FshRatio,
+  FshQuantity,
+  FshReference,
+  Invariant,
+  SourceInfo
+} from '../fshtypes';
 import { AddElementRule, AssignmentValueType, OnlyRule } from '../fshtypes/rules';
 import {
   AssignmentToCodeableReferenceError,
@@ -52,7 +60,14 @@ import {
   isReferenceType,
   isModifierExtension
 } from './common';
-import { Fishable, Type, Metadata, logger } from '../utils';
+import {
+  Fishable,
+  Type,
+  Metadata,
+  logger,
+  fishForMetadataBestVersion,
+  fishForFHIRBestVersion
+} from '../utils';
 import { InstanceDefinition } from './InstanceDefinition';
 import { idRegex } from './primitiveTypes';
 import sax = require('sax');
@@ -1241,14 +1256,19 @@ export class ElementDefinition {
     // Stop when we can't find a definition or the base definition is blank.
     let currentType = type;
     while (currentType != null) {
-      const [name, version] = currentType.split('|', 2);
-      const result = fisher.fishForMetadata(name);
-      if (result) {
-        if (version != null && result.version != null && result.version != version) {
-          logger.error(
+      // fishForMetadataBestVersion is not used here in order to provide additional details in the warning
+      let result = fisher.fishForMetadata(currentType);
+      if (result == null) {
+        const [name, ...versionParts] = currentType.split('|');
+        const version = versionParts.join('|') || null;
+        result = fisher.fishForMetadata(name);
+        if (result && version != null && result.version != null && result.version != version) {
+          logger.warn(
             `${type} is based on ${name} version ${version}, but SUSHI found version ${result.version}`
           );
         }
+      }
+      if (result) {
         results.push(result);
       }
       currentType = result?.parent;
@@ -1643,7 +1663,13 @@ export class ElementDefinition {
           this.parent()?.type?.[0]?.code === 'CodeableReference'
             ? this.parent()
             : this;
-        if (!referenceConstrainingElement.typeSatisfiesTargetProfile(value.sdType, fisher)) {
+        if (
+          !referenceConstrainingElement.typeSatisfiesTargetProfile(
+            value.sdType,
+            value.sourceInfo,
+            fisher
+          )
+        ) {
           throw new InvalidTypeError(
             `Reference(${value.sdType})`,
             referenceConstrainingElement.type
@@ -1666,7 +1692,13 @@ export class ElementDefinition {
           Type.Instance
         );
         let canonicalUrl: string;
-        if (!this.typeSatisfiesTargetProfile(canonicalDefinition?.resourceType, fisher)) {
+        if (
+          !this.typeSatisfiesTargetProfile(
+            canonicalDefinition?.resourceType,
+            value.sourceInfo,
+            fisher
+          )
+        ) {
           throw new InvalidTypeError(`Canonical(${canonicalDefinition.resourceType})`, this.type);
         }
         if (canonicalDefinition?.url) {
@@ -1849,17 +1881,22 @@ export class ElementDefinition {
 
   /**
    * @param sdType - The type to check
+   * @param sourceInfo - Source information for logging purposes
    * @param fisher - A fishable implementation for finding definitions and metadata
    * @returns - False if the type does not satisfy the targetProfile, true otherwise (or if it can't be determined)
    */
-  private typeSatisfiesTargetProfile(sdType: string, fisher: Fishable): boolean {
+  private typeSatisfiesTargetProfile(
+    sdType: string,
+    sourceInfo: SourceInfo,
+    fisher: Fishable
+  ): boolean {
     // If no targetProfile is present, there is nothing to check the value against, so just allow it
     if (sdType && this.type[0].targetProfile) {
       const validTypes: string[] = [];
       this.type[0].targetProfile.forEach(tp => {
-        // target profile may have a version after a | character. don't include it when fishing.
-        const unversionedProfile = tp.split('|', 2)[0];
-        const tpType = fisher.fishForMetadata(unversionedProfile)?.sdType;
+        // target profile may have a version after a | character.
+        // fish for matching version, but fall back to any version if necessary.
+        const tpType = fishForMetadataBestVersion(fisher, tp, sourceInfo)?.sdType;
         if (tpType) {
           validTypes.push(tpType);
         }
@@ -2090,18 +2127,35 @@ export class ElementDefinition {
    * @throws {InvalidUriError} when the system being assigned is not a valid uri
    */
   private assignFshCode(code: FshCode, exactly = false, fisher?: Fishable): void {
+    const type = this.type[0].code;
+
     if (code.system) {
       const csURI = code.system.split('|')[0];
-      const vsURI = fisher?.fishForMetadata(code.system, Type.ValueSet)?.url ?? '';
+      const vsURI =
+        fishForMetadataBestVersion(fisher, code.system, code.sourceInfo, Type.ValueSet)?.url ?? '';
       if (vsURI) {
-        throw new MismatchedBindingTypeError(code.system, this.path, 'CodeSystem');
+        if (type === 'code' || type === 'string' || type === 'uri') {
+          logger.warn(
+            `The fully qualified code ${code.system}#${code.code} is invalid because the specified system is a ValueSet. ` +
+              `Since ${this.path} is a ${type}, the system will not be used, but this issue should be corrected by ` +
+              `updating the system to refer to a proper CodeSystem or by specifying a code only (e.g., #${code.code}).`
+          );
+        } else {
+          throw new MismatchedBindingTypeError(code.system, this.path, 'CodeSystem');
+        }
       } else if (!isUri(csURI)) {
-        throw new InvalidUriError(code.system);
+        if (type === 'code' || type === 'string' || type === 'uri') {
+          logger.warn(
+            `The fully qualified code ${code.system}#${code.code} is invalid because the specified system is not a URI. ` +
+              `Since ${this.path} is a ${type}, the system will not be used, but this issue should be corrected by ` +
+              `updating the system to refer to a proper CodeSystem or by specifying a code only (e.g., #${code.code}).`
+          );
+        } else {
+          throw new InvalidUriError(code.system);
+        }
       }
     }
 
-    const type = this.type[0].code;
-
     if (type === 'code' || type === 'string' || type === 'uri') {
       this.assignFHIRValue(code.toString(), code.code, exactly, type);
     } else if (type === 'CodeableConcept') {
@@ -2315,20 +2369,17 @@ export class ElementDefinition {
       if (newElements.length === 0) {
         // If we have exactly one profile to use, use that, otherwise use the code
         const type = profileToUse ?? this.type[0].code;
-        // There could possibly be a |version appended to the type, so don't include it while fishing
-        const [typeWithoutVersion, version] = type.split('|', 2);
-        let json = fisher.fishForFHIR(
-          typeWithoutVersion,
+        // There could possibly be a |version appended to the type, so try to fish
+        // for that version but fall back to any version if necessary
+        let json = fishForFHIRBestVersion(
+          fisher,
+          type,
+          null, // no source info
           Type.Resource,
           Type.Type,
           Type.Profile,
           Type.Extension
         );
-        if (json && version != null && json.version != null && json.version !== version) {
-          logger.warn(
-            `${type} is based on ${typeWithoutVersion} version ${version}, but SUSHI found version ${json.version}`
-          );
-        }
         if (!json && profileToUse) {
           logger.warn(
             `SUSHI tried to find profile ${type} but could not find it and instead will try to use ${this.type[0].code}`
diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts
index 006c183..00ee7eb 100644
--- a/src/fhirtypes/common.ts
+++ b/src/fhirtypes/common.ts
@@ -34,7 +34,7 @@ import {
 } from '../fshtypes';
 import { FSHTank } from '../import';
 import { Type, Fishable } from '../utils/Fishable';
-import { logger } from '../utils';
+import { fishForMetadataBestVersion, fishInTankBestVersion, logger } from '../utils';
 import { buildSliceTree, calculateSliceTreeCounts } from './sliceTree';
 import { InstanceExporter } from '../export';
 import { MismatchedTypeError } from '../errors';
@@ -855,14 +855,27 @@ export function replaceReferences<T extends AssignmentRule | CaretValueRule>(
       }
     }
   } else if (value instanceof FshCode) {
-    const [system, ...versionParts] = value.system?.split('|') ?? [];
-    const version = versionParts.join('|');
-    const codeSystem = tank.fish(system, Type.CodeSystem);
-    const codeSystemMeta = fisher.fishForMetadata(system, Type.CodeSystem);
+    const codeSystemMeta = fishForMetadataBestVersion(
+      fisher,
+      value.system,
+      rule.sourceInfo,
+      Type.CodeSystem
+    );
     if (codeSystemMeta) {
       clone = cloneDeep(rule);
       const assignedCode = getRuleValue(clone) as FshCode;
-      assignedCode.system = `${codeSystemMeta.url}${version ? `|${version}` : ''}`;
+      assignedCode.system = value.system.replace(/^[^|]+/, codeSystemMeta.url);
+
+      // Find the code system using the returned metadata to avoid duplicate warnings if version mismatches
+      const matchedCanonical = codeSystemMeta.url
+        ? `${codeSystemMeta.url}${codeSystemMeta.version ? `|${codeSystemMeta.version}` : ''}`
+        : value.system;
+      const codeSystem = fishInTankBestVersion(
+        tank,
+        matchedCanonical,
+        rule.sourceInfo,
+        Type.CodeSystem
+      );
       if (codeSystem && (codeSystem instanceof FshCodeSystem || codeSystem instanceof Instance)) {
         // if a local system was used, check to make sure the code is actually in that system
         listUndefinedLocalCodes(codeSystem, [assignedCode.code], tank, rule);
@@ -1209,23 +1222,37 @@ export function isInheritedResource(
 }
 
 /**
- * Determines the formal FHIR URL to use to refer to this entity (for example when fishing).
- * If a caret value rule has been applied to the entity's url, use the value specified in that
- * rule. Otherwise, use the default url based on the configured canonical url.
- *
- * @param fshDefinition - The FSH definition that the returned URL refers to
- * @param canonical - The canonical URL for the FSH project
- * @returns The URL to use to refer to the FHIR entity
+ * A helper function used to determine the value of either an assignment rule or a caret value rule
+ * @param fshDefinition  the FSH definition
+ * @param assignmentRulePath the path of the assignment rule whose value we want
+ * @param caretRulePath the path of the caret value rule whose value we want
+ * @param caretRuleCaretPath the caret path of the caret value rule
+ * @returns the value set by either the assignment rule or the caret value rule or undefined if neither rule is set on the definition
+ * NOTE: this function assumes the value being assigned is a string.
+ * If other assigned types are needed, additional logic is needed before returning the value.
  */
-export function getUrlFromFshDefinition(
-  fshDefinition: Profile | Extension | Logical | Resource | FshValueSet | FshCodeSystem | Instance,
-  canonical: string
-): string {
+function getValueFromFshRules(
+  fshDefinition:
+    | Profile
+    | Extension
+    | Logical
+    | Resource
+    | FshValueSet
+    | FshCodeSystem
+    | Instance
+    | RuleSet
+    | Mapping,
+  assignmentRulePath: string,
+  caretRulePath: string,
+  caretRuleCaretPath: string
+): string | undefined {
   const fshRules: Rule[] = fshDefinition.rules;
   if (fshDefinition instanceof Instance) {
     const assignmentRules = fshRules.filter(
       rule =>
-        rule instanceof AssignmentRule && rule.path === 'url' && typeof rule.value === 'string'
+        rule instanceof AssignmentRule &&
+        rule.path === assignmentRulePath &&
+        typeof rule.value === 'string'
     ) as AssignmentRule[];
     if (assignmentRules.length > 0) {
       const lastAssignmentRule = assignmentRules[assignmentRules.length - 1];
@@ -1233,16 +1260,39 @@ export function getUrlFromFshDefinition(
     }
   } else {
     const caretValueRules = fshRules.filter(
-      rule => rule instanceof CaretValueRule && rule.path === '' && rule.caretPath === 'url'
+      rule =>
+        rule instanceof CaretValueRule &&
+        rule.path === caretRulePath &&
+        rule.caretPath === caretRuleCaretPath
     ) as CaretValueRule[];
     if (caretValueRules.length > 0) {
-      // Select last CaretValueRule with caretPath === 'url' because rules processing
+      // Select last CaretValueRule with caretPath === caretRuleCaretPath because rules processing
       // ends up applying the last rule in the processing order
       const lastCaretValueRule = caretValueRules[caretValueRules.length - 1];
-      // this value should only be a string, but that might change at some point
+      // For now, this value is assumed to be a string, but additional types may be supported in the future.
       return lastCaretValueRule.value.toString();
     }
   }
+  return;
+}
+
+/**
+ * Determines the formal FHIR URL to use to refer to this entity (for example when fishing).
+ * If a caret value rule has been applied to the entity's url, use the value specified in that
+ * rule. Otherwise, use the default url based on the configured canonical url.
+ *
+ * @param fshDefinition - The FSH definition that the returned URL refers to
+ * @param canonical - The canonical URL for the FSH project
+ * @returns The URL to use to refer to the FHIR entity
+ */
+export function getUrlFromFshDefinition(
+  fshDefinition: Profile | Extension | Logical | Resource | FshValueSet | FshCodeSystem | Instance,
+  canonical: string
+): string {
+  const urlFromFshRules = getValueFromFshRules(fshDefinition, 'url', '', 'url');
+  if (urlFromFshRules != null) {
+    return urlFromFshRules;
+  }
 
   let fhirType: string;
   if (fshDefinition instanceof FshValueSet) {
@@ -1255,6 +1305,27 @@ export function getUrlFromFshDefinition(
   return `${canonical}/${fhirType}/${fshDefinition.id}`;
 }
 
+/**
+ * Determines the version of this entity.
+ * If a caret value rule has been applied to the entity's version, use the value specified in that
+ * rule. Otherwise, use the default version based on the configured version from the tank.
+ *
+ * @param fshDefinition - The FSH definition whose version is being determined
+ * @param canonical - The version for the FSH project
+ * @returns The version of the FHIR entity
+ */
+export function getVersionFromFshDefinition(
+  fshDefinition: Profile | Extension | Logical | Resource | FshValueSet | FshCodeSystem | Instance,
+  version: string
+): string {
+  const versionFromFshRules = getValueFromFshRules(fshDefinition, 'version', '', 'version');
+  if (versionFromFshRules != null) {
+    return versionFromFshRules;
+  }
+
+  return version;
+}
+
 /**
  * Determines the formal FHIR type to use to define to this entity for logical models and
  * resources. The type for profiles and extension should not be changed. If a caret value
diff --git a/src/import/FSHTank.ts b/src/import/FSHTank.ts
index 7aa6b34..d465f0e 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 } from '../fhirtypes/common';
+import { getUrlFromFshDefinition, getVersionFromFshDefinition } from '../fhirtypes/common';
 import flatMap from 'lodash/flatMap';
 
 export class FSHTank implements Fishable {
@@ -146,6 +146,10 @@ export class FSHTank implements Fishable {
     // Resolve alias if necessary
     item = this.resolveAlias(item) ?? item;
 
+    // version needs to be checked separately from the base
+    const [base, ...versionParts] = item?.split('|') ?? ['', ''];
+    const version = versionParts.join('|') || null;
+
     // No types passed in means to search ALL supported types
     if (types.length === 0) {
       types = [
@@ -168,18 +172,21 @@ export class FSHTank implements Fishable {
         case Type.Profile:
           result = this.getAllProfiles().find(
             p =>
-              p.name === item ||
-              p.id === item ||
-              getUrlFromFshDefinition(p, this.config.canonical) === item
+              (p.name === base ||
+                p.id === base ||
+                getUrlFromFshDefinition(p, this.config.canonical) === base) &&
+              (version == null || version === getVersionFromFshDefinition(p, this.config.version))
           );
           if (!result) {
             result = this.getAllInstances().find(
               profileInstance =>
                 profileInstance.instanceOf === 'StructureDefinition' &&
                 profileInstance.usage === 'Definition' &&
-                (profileInstance.name === item ||
-                  profileInstance.id === item ||
-                  getUrlFromFshDefinition(profileInstance, this.config.canonical) === item) &&
+                (profileInstance.name === base ||
+                  profileInstance.id === base ||
+                  getUrlFromFshDefinition(profileInstance, this.config.canonical) === base) &&
+                (version == null ||
+                  version === getVersionFromFshDefinition(profileInstance, this.config.version)) &&
                 profileInstance.rules.some(
                   rule =>
                     rule instanceof AssignmentRule &&
@@ -199,9 +206,10 @@ export class FSHTank implements Fishable {
         case Type.Extension:
           result = this.getAllExtensions().find(
             e =>
-              e.name === item ||
-              e.id === item ||
-              getUrlFromFshDefinition(e, this.config.canonical) === item
+              (e.name === base ||
+                e.id === base ||
+                getUrlFromFshDefinition(e, this.config.canonical) === base) &&
+              (version == null || version === getVersionFromFshDefinition(e, this.config.version))
           );
           if (!result) {
             // There may be a matching definitional Instance of StructureDefinition with type Extension
@@ -209,9 +217,12 @@ export class FSHTank implements Fishable {
               extensionInstance =>
                 extensionInstance.instanceOf === 'StructureDefinition' &&
                 extensionInstance.usage === 'Definition' &&
-                (extensionInstance.name === item ||
-                  extensionInstance.id === item ||
-                  getUrlFromFshDefinition(extensionInstance, this.config.canonical) === item) &&
+                (extensionInstance.name === base ||
+                  extensionInstance.id === base ||
+                  getUrlFromFshDefinition(extensionInstance, this.config.canonical) === base) &&
+                (version == null ||
+                  version ===
+                    getVersionFromFshDefinition(extensionInstance, this.config.version)) &&
                 extensionInstance.rules.some(
                   rule =>
                     rule instanceof AssignmentRule &&
@@ -231,18 +242,21 @@ export class FSHTank implements Fishable {
         case Type.Logical:
           result = this.getAllLogicals().find(
             l =>
-              l.name === item ||
-              l.id === item ||
-              getUrlFromFshDefinition(l, this.config.canonical) === item
+              (l.name === base ||
+                l.id === base ||
+                getUrlFromFshDefinition(l, this.config.canonical) === base) &&
+              (version == null || version === getVersionFromFshDefinition(l, this.config.version))
           );
           if (!result) {
             result = this.getAllInstances().find(
               logicalInstance =>
                 logicalInstance.instanceOf === 'StructureDefinition' &&
                 logicalInstance.usage === 'Definition' &&
-                (logicalInstance.name === item ||
-                  logicalInstance.id === item ||
-                  getUrlFromFshDefinition(logicalInstance, this.config.canonical) === item) &&
+                (logicalInstance.name === base ||
+                  logicalInstance.id === base ||
+                  getUrlFromFshDefinition(logicalInstance, this.config.canonical) === base) &&
+                (version == null ||
+                  version === getVersionFromFshDefinition(logicalInstance, this.config.version)) &&
                 logicalInstance.rules.some(
                   rule =>
                     rule instanceof AssignmentRule &&
@@ -263,18 +277,21 @@ export class FSHTank implements Fishable {
         case Type.Resource:
           result = this.getAllResources().find(
             r =>
-              r.name === item ||
-              r.id === item ||
-              getUrlFromFshDefinition(r, this.config.canonical) === item
+              (r.name === base ||
+                r.id === base ||
+                getUrlFromFshDefinition(r, this.config.canonical) === base) &&
+              (version == null || version === getVersionFromFshDefinition(r, this.config.version))
           );
           if (!result) {
             result = this.getAllInstances().find(
               resourceInstance =>
                 resourceInstance.instanceOf === 'StructureDefinition' &&
                 resourceInstance.usage === 'Definition' &&
-                (resourceInstance.name === item ||
-                  resourceInstance.id === item ||
-                  getUrlFromFshDefinition(resourceInstance, this.config.canonical) === item) &&
+                (resourceInstance.name === base ||
+                  resourceInstance.id === base ||
+                  getUrlFromFshDefinition(resourceInstance, this.config.canonical) === base) &&
+                (version == null ||
+                  version === getVersionFromFshDefinition(resourceInstance, this.config.version)) &&
                 resourceInstance.rules.some(
                   rule =>
                     rule instanceof AssignmentRule &&
@@ -295,50 +312,60 @@ export class FSHTank implements Fishable {
         case Type.ValueSet:
           result = this.getAllValueSets().find(
             vs =>
-              vs.name === item ||
-              vs.id === item ||
-              getUrlFromFshDefinition(vs, this.config.canonical) === item
+              (vs.name === base ||
+                vs.id === base ||
+                getUrlFromFshDefinition(vs, this.config.canonical) === base) &&
+              (version == null || version === getVersionFromFshDefinition(vs, this.config.version))
           );
           if (!result) {
             result = this.getAllInstances().find(
               vsInstance =>
                 vsInstance?.instanceOf === 'ValueSet' &&
                 vsInstance?.usage === 'Definition' &&
-                (vsInstance?.name === item ||
-                  vsInstance.id === item ||
-                  getUrlFromFshDefinition(vsInstance, this.config.canonical) === item)
+                (vsInstance?.name === base ||
+                  vsInstance.id === base ||
+                  getUrlFromFshDefinition(vsInstance, this.config.canonical) === base) &&
+                (version == null ||
+                  version === getVersionFromFshDefinition(vsInstance, this.config.version))
             );
           }
           break;
         case Type.CodeSystem:
           result = this.getAllCodeSystems().find(
             cs =>
-              cs.name === item ||
-              cs.id === item ||
-              getUrlFromFshDefinition(cs, this.config.canonical) === item
+              (cs.name === base ||
+                cs.id === base ||
+                getUrlFromFshDefinition(cs, this.config.canonical) === base) &&
+              (version == null || version === getVersionFromFshDefinition(cs, this.config.version))
           );
           if (!result) {
             result = this.getAllInstances().find(
               csInstance =>
                 csInstance?.instanceOf === 'CodeSystem' &&
                 csInstance?.usage === 'Definition' &&
-                (csInstance?.name === item ||
-                  csInstance.id === item ||
-                  getUrlFromFshDefinition(csInstance, this.config.canonical) === item)
+                (csInstance?.name === base ||
+                  csInstance.id === base ||
+                  getUrlFromFshDefinition(csInstance, this.config.canonical) === base) &&
+                (version == null ||
+                  version === getVersionFromFshDefinition(csInstance, this.config.version))
             );
           }
           break;
         case Type.Instance:
-          result = this.getAllInstances().find(i => i.name === item || i.id === item);
+          result = this.getAllInstances().find(
+            i =>
+              (i.name === base || i.id === base) &&
+              (version == null || version === getVersionFromFshDefinition(i, this.config.version))
+          );
           break;
         case Type.Invariant:
-          result = this.getAllInvariants().find(i => i.name === item);
+          result = this.getAllInvariants().find(i => i.name === base);
           break;
         case Type.RuleSet:
-          result = this.getAllRuleSets().find(r => r.name === item);
+          result = this.getAllRuleSets().find(r => r.name === base);
           break;
         case Type.Mapping:
-          result = this.getAllMappings().find(m => m.name === item);
+          result = this.getAllMappings().find(m => m.name === base);
           break;
         case Type.Type:
         default:
diff --git a/src/utils/FishingUtils.ts b/src/utils/FishingUtils.ts
new file mode 100644
index 0000000..4d5deee
--- /dev/null
+++ b/src/utils/FishingUtils.ts
@@ -0,0 +1,109 @@
+import { FSHTank } from '../import';
+import {
+  Profile,
+  Extension,
+  Logical,
+  Resource,
+  Instance,
+  FshValueSet,
+  FshCodeSystem,
+  Invariant,
+  RuleSet,
+  Mapping,
+  SourceInfo
+} from '../fshtypes';
+import { getVersionFromFshDefinition } from '../fhirtypes/common';
+import { logger } from './FSHLogger';
+import { Fishable, Metadata, Type } from './Fishable';
+
+// Use the provided fisher to fish for the item, which might have a |version appended.
+// If found, return it. If not found, try to remove any |version info and fish for any available version.
+export function fishForFHIRBestVersion(
+  fisher: Fishable,
+  item: string,
+  sourceInfo?: SourceInfo,
+  ...types: Type[]
+): any | undefined {
+  let result = fisher.fishForFHIR(item, ...types);
+
+  // If the item was not found, try slicing off a version and searching for any version
+  if (result == null && item?.includes('|')) {
+    const [base, ...versionParts] = item.split('|');
+    const version = versionParts.join('|') || null;
+    result = fisher.fishForFHIR(base, ...types);
+    if (version != null && result?.version != null && version != result.version) {
+      logger.warn(`${item} was requested, but SUSHI found ${base}|${result.version}`, sourceInfo);
+    }
+  }
+
+  return result;
+}
+
+// Use the provided fisher to fish for the item's metadata.
+// If found, return it. If not found, try to remove any |version info and fish for any available version.
+export function fishForMetadataBestVersion(
+  fisher: Fishable,
+  item: string,
+  sourceInfo?: SourceInfo,
+  ...types: Type[]
+): Metadata | undefined {
+  if (fisher == null) {
+    return;
+  }
+
+  let metadata = fisher.fishForMetadata(item, ...types);
+
+  // If the item was not found, try slicing off a version and searching for any version
+  if (metadata == null && item?.includes('|')) {
+    const [base, ...versionParts] = item.split('|');
+    const version = versionParts.join('|') || null;
+    metadata = fisher.fishForMetadata(base, ...types);
+    if (version != null && metadata?.version != null && version != metadata.version) {
+      logger.warn(`${item} was requested, but SUSHI found ${base}|${metadata.version}`, sourceInfo);
+    }
+  }
+
+  return metadata;
+}
+
+// Use the provided tank to fish for the item, which might have a |version appended.
+// If the specified version is found, return it. If not found, try to remove any |version info and fish for any available version.
+export function fishInTankBestVersion(
+  tank: FSHTank,
+  item: string,
+  sourceInfo?: SourceInfo,
+  ...types: Type[]
+):
+  | Profile
+  | Extension
+  | Logical
+  | Resource
+  | FshValueSet
+  | FshCodeSystem
+  | Instance
+  | Invariant
+  | RuleSet
+  | Mapping
+  | undefined {
+  let result = tank.fish(item, ...types);
+
+  // If the item was not found, try slicing off a version and searching for any version
+  if (result == null && item != null && item.includes('|')) {
+    const [base, ...versionParts] = item.split('|');
+    const version = versionParts.join('|') || null;
+    result = tank.fish(base, ...types);
+    if (result != null) {
+      let resultVersion = tank.config.version;
+      if (
+        !(result instanceof Invariant || result instanceof RuleSet || result instanceof Mapping)
+      ) {
+        resultVersion = getVersionFromFshDefinition(result, tank.config.version);
+      }
+      if (version != null && resultVersion != null && version != resultVersion) {
+        logger.warn(`${item} was requested, but SUSHI found ${base}|${resultVersion}`, sourceInfo);
+      }
+    }
+  }
+
+  return result;
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 067061e..524cfb1 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,4 +1,5 @@
 export * from './Fishable';
+export * from './FishingUtils';
 export * from './FSHLogger';
 export * from './MasterFisher';
 export * from './Mixin';


Test patch:
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index 9a81df5..8e142e4 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -5886,6 +5886,33 @@ describe('InstanceExporter', () => {
       ]);
     });
 
+    it('should assign a code with a version to a top level element while replacing the code system name with its url when the correct version is found', () => {
+      const brightInstance = new Instance('BrightObservation');
+      brightInstance.instanceOf = 'Observation';
+      const assignedCodeRule = new AssignmentRule('code');
+      assignedCodeRule.value = new FshCode('bright', 'Visible|1.0.0');
+      brightInstance.rules.push(assignedCodeRule);
+      doc.instances.set(brightInstance.name, brightInstance);
+
+      const visibleSystem = new FshCodeSystem('Visible');
+      const visibleSystemUrl = new CaretValueRule('');
+      visibleSystemUrl.caretPath = 'url';
+      visibleSystemUrl.value = 'http://hl7.org/fhir/us/minimal/CodeSystem/Visible';
+      const visibleSystemVersion = new CaretValueRule('');
+      visibleSystemVersion.caretPath = 'version';
+      visibleSystemVersion.value = '1.0.0';
+      visibleSystem.rules.push(visibleSystemUrl, visibleSystemVersion);
+      doc.codeSystems.set(visibleSystem.name, visibleSystem);
+      const exported = exportInstance(brightInstance);
+      expect(exported.code.coding).toEqual([
+        {
+          code: 'bright',
+          version: '1.0.0',
+          system: 'http://hl7.org/fhir/us/minimal/CodeSystem/Visible'
+        }
+      ]);
+    });
+
     it('should assign a code to a top level element if the code system was defined as an instance of usage definition', () => {
       const visibleSystem = new Instance('Visible');
       visibleSystem.instanceOf = 'CodeSystem';
diff --git a/test/export/Package.test.ts b/test/export/Package.test.ts
index 0da44f7..516c013 100644
--- a/test/export/Package.test.ts
+++ b/test/export/Package.test.ts
@@ -15,6 +15,7 @@ describe('Package', () => {
     profile0.url = 'http://hl7.org/fhir/us/minimal/StructureDefinition/fun-ny';
     profile0.baseDefinition = 'http://hl7.org/fhir/StructureDefinition/Condition';
     profile0.fhirVersion = '4.0.1';
+    profile0.version = '1.0.0';
     pkg.profiles.push(profile0);
     // Profile[1]: Sanguine / san-guine / Condition
     const profile1 = new StructureDefinition();
@@ -42,6 +43,7 @@ describe('Package', () => {
     extension1.url = 'http://hl7.org/fhir/us/minimal/StructureDefinition/poor-taste';
     extension1.baseDefinition = 'http://hl7.org/fhir/StructureDefinition/Extension';
     extension1.fhirVersion = '4.0.1';
+    extension1.version = '1.0.0';
     pkg.extensions.push(extension1);
     // Logical[0]: WheatBeer / wheat-beer / wheat-beer
     const logical0 = new StructureDefinition();
@@ -51,6 +53,7 @@ describe('Package', () => {
     logical0.url = 'http://hl7.org/fhir/us/minimal/StructureDefinition/wheat-beer';
     logical0.baseDefinition = 'http://hl7.org/fhir/StructureDefinition/Element';
     logical0.fhirVersion = '4.0.1';
+    logical0.version = '1.0.0';
     pkg.logicals.push(logical0);
     // Logical[1]: RedWine /red-wine / red-wine
     const logical1 = new StructureDefinition();
@@ -69,6 +72,7 @@ describe('Package', () => {
     resource0.url = 'http://hl7.org/fhir/us/minimal/StructureDefinition/Destination';
     resource0.baseDefinition = 'http://hl7.org/fhir/StructureDefinition/DomainResource';
     resource0.fhirVersion = '4.0.1';
+    resource0.version = '1.0.0';
     pkg.resources.push(resource0);
     // Resource[1]: MovieTheater / MovieTheater / MovieTheater
     const resource1 = new StructureDefinition();
@@ -133,6 +137,7 @@ describe('Package', () => {
     instance2.url = 'http://hl7.org/fhir/us/minimal/StructureDefinition/thrill-ing';
     instance2.baseDefinition = 'http://hl7.org/fhir/StructureDefinition/Condition';
     instance2.fhirVersion = '4.0.1';
+    instance2.version = '1.0.0';
     pkg.instances.push(instance2);
     // Instance[3]: TwoWolves / two-wolves / Extension
     const instance3 = new InstanceDefinition();
@@ -145,6 +150,7 @@ describe('Package', () => {
     instance3.url = 'http://hl7.org/fhir/us/minimal/StructureDefinition/two-wolves';
     instance3.baseDefinition = 'http://hl7.org/fhir/StructureDefinition/Extension';
     instance3.fhirVersion = '4.0.1';
+    instance3.version = '1.0.0';
     pkg.instances.push(instance3);
     // Instance[4]: Spices / spices / CodeSystem
     const instance4 = new InstanceDefinition();
@@ -176,6 +182,7 @@ describe('Package', () => {
     instance6.url = 'http://hl7.org/fhir/us/minimal/StructureDefinition/spider-house';
     instance6.baseDefinition = 'http://hl7.org/fhir/StructureDefinition/Base';
     instance6.fhirVersion = '4.0.1';
+    instance6.version = '1.0.0';
     pkg.instances.push(instance6);
     // Instance[7]: Zone / zone / Resource
     const instance7 = new InstanceDefinition();
@@ -188,6 +195,7 @@ describe('Package', () => {
     instance7.url = 'http://hl7.org/fhir/us/minimal/StructureDefinition/zone';
     instance7.baseDefinition = '';
     instance7.fhirVersion = '4.0.1';
+    instance7.version = '1.0.0';
     pkg.instances.push(instance7);
   });
 
@@ -217,6 +225,25 @@ describe('Package', () => {
       ).toEqual(thrillingProfile);
     });
 
+    it('should find profiles when fishing with a version', () => {
+      const funnyProfile = pkg.fishForFHIR('fun-ny|1.0.0', Type.Profile);
+      expect(funnyProfile.url).toBe('http://hl7.org/fhir/us/minimal/StructureDefinition/fun-ny');
+      expect(funnyProfile.version).toBe('1.0.0');
+    });
+
+    it('should not find a profile when fishing with a version that does not match', () => {
+      const funnyProfile = pkg.fishForFHIR('fun-ny|2.0.0', Type.Profile);
+      expect(funnyProfile).toBeUndefined();
+    });
+
+    it('should find instances of profiles when fishing with a version', () => {
+      const thrillingProfile = pkg.fishForFHIR('thrill-ing|1.0.0', Type.Profile);
+      expect(thrillingProfile.url).toBe(
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/thrill-ing'
+      );
+      expect(thrillingProfile.version).toBe('1.0.0');
+    });
+
     it('should find extensions', () => {
       const poorTasteExtensionByID = pkg.fishForFHIR('poor-taste', Type.Extension);
       expect(poorTasteExtensionByID.url).toBe(
@@ -247,6 +274,27 @@ describe('Package', () => {
       ).toEqual(twoWolvesExtension);
     });
 
+    it('should find extensions when fishing with a version', () => {
+      const poorTasteExtensionByID = pkg.fishForFHIR('poor-taste|1.0.0', Type.Extension);
+      expect(poorTasteExtensionByID.url).toBe(
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/poor-taste'
+      );
+      expect(poorTasteExtensionByID.version).toBe('1.0.0');
+    });
+
+    it('should not find an extension when fishing with a version that does not match', () => {
+      const poorTasteExtensionByID = pkg.fishForFHIR('poor-taste|2.0.0', Type.Extension);
+      expect(poorTasteExtensionByID).toBeUndefined();
+    });
+
+    it('should find instances of extensions when fishing with a version', () => {
+      const twoWolvesExtension = pkg.fishForFHIR('two-wolves|1.0.0', Type.Extension);
+      expect(twoWolvesExtension.url).toBe(
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/two-wolves'
+      );
+      expect(twoWolvesExtension.version).toBe('1.0.0');
+    });
+
     it('should find logicals', () => {
       const beerLogical = pkg.fishForFHIR('wheat-beer', Type.Logical);
       expect(beerLogical.url).toBe('http://hl7.org/fhir/us/minimal/StructureDefinition/wheat-beer');
@@ -275,6 +323,25 @@ describe('Package', () => {
       ).toEqual(spiderHouse);
     });
 
+    it('should find logicals when fishing with a version', () => {
+      const beerLogical = pkg.fishForFHIR('wheat-beer|1.0.0', Type.Logical);
+      expect(beerLogical.url).toBe('http://hl7.org/fhir/us/minimal/StructureDefinition/wheat-beer');
+      expect(beerLogical.version).toBe('1.0.0');
+    });
+
+    it('should not find a logical when fishing with a version that does not match', () => {
+      const beerLogical = pkg.fishForFHIR('wheat-beer|2.0.0', Type.Logical);
+      expect(beerLogical).toBeUndefined();
+    });
+
+    it('should find instances of logicals when fishing with a version', () => {
+      const spiderHouse = pkg.fishForFHIR('spider-house|1.0.0', Type.Logical);
+      expect(spiderHouse.url).toBe(
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/spider-house'
+      );
+      expect(spiderHouse.version).toBe('1.0.0');
+    });
+
     it('should find resources', () => {
       const destination = pkg.fishForFHIR('Destination', Type.Resource);
       expect(destination.url).toBe(
@@ -300,6 +367,25 @@ describe('Package', () => {
       ).toEqual(zoneResource);
     });
 
+    it('should find resources when fishing with a version', () => {
+      const destination = pkg.fishForFHIR('Destination|1.0.0', Type.Resource);
+      expect(destination.url).toBe(
+        'http://hl7.org/fhir/us/minimal/StructureDefinition/Destination'
+      );
+      expect(destination.version).toBe('1.0.0');
+    });
+
+    it('should not find a resource when fishing with a version that does not match', () => {
+      const destination = pkg.fishForFHIR('Destination|2.0.0', Type.Resource);
+      expect(destination).toBeUndefined();
+    });
+
+    it('should find instances of resources when fishing with a version', () => {
+      const zoneResource = pkg.fishForFHIR('zone|1.0.0', Type.Resource);
+      expect(zoneResource.url).toBe('http://hl7.org/fhir/us/minimal/StructureDefinition/zone');
+      expect(zoneResource.version).toBe('1.0.0');
+    });
+
     it('should find value sets', () => {
       const soupsValueSetByID = pkg.fishForFHIR('soup-flavors', Type.ValueSet);
       expect(soupsValueSetByID.url).toBe('http://hl7.org/fhir/us/minimal/ValueSet/soup-flavors');
@@ -324,6 +410,25 @@ describe('Package', () => {
       ).toEqual(cookiesValueSetByID);
     });
 
+    it('should find value sets when fishing with a version', () => {
+      const soupsValueSetByID = pkg.fishForFHIR('soup-flavors|4.0.1', Type.ValueSet);
+      expect(soupsValueSetByID.url).toBe('http://hl7.org/fhir/us/minimal/ValueSet/soup-flavors');
+      expect(soupsValueSetByID.version).toBe('4.0.1');
+    });
+
+    it('should not find a value set when fishing with a version that does not match', () => {
+      const soupsValueSetByID = pkg.fishForFHIR('soup-flavors|1.1.1', Type.ValueSet);
+      expect(soupsValueSetByID).toBeUndefined();
+    });
+
+    it('should find instances of value sets when fishing with a version', () => {
+      const cookiesValueSetByID = pkg.fishForFHIR('cookie-varieties|4.0.1', Type.ValueSet);
+      expect(cookiesValueSetByID.url).toBe(
+        'http://hl7.org/fhir/us/minimal/ValueSet/cookie-varieties'
+      );
+      expect(cookiesValueSetByID.version).toBe('4.0.1');
+    });
+
     it('should find code systems', () => {
       const numericsCodeSystemByID = pkg.fishForFHIR('numerics', Type.CodeSystem);
       expect(numericsCodeSystemByID.url).toBe('http://hl7.org/fhir/us/minimal/CodeSystem/numerics');
@@ -346,6 +451,23 @@ describe('Package', () => {
       ).toEqual(spicesCodeSystemByID);
     });
 
+    it('should find code systems when fishing with a version', () => {
+      const numericsCodeSystemByID = pkg.fishForFHIR('numerics|4.0.1', Type.CodeSystem);
+      expect(numericsCodeSystemByID.url).toBe('http://hl7.org/fhir/us/minimal/CodeSystem/numerics');
+      expect(numericsCodeSystemByID.version).toBe('4.0.1');
+    });
+
+    it('should not find a code system when fishing with a version that does not match', () => {
+      const numericsCodeSystemByID = pkg.fishForFHIR('numerics|1.1.1', Type.CodeSystem);
+      expect(numericsCodeSystemByID).toBeUndefined();
+    });
+
+    it('should find instances of code systems when fishing with a version', () => {
+      const spicesCodeSystemByID = pkg.fishForFHIR('spices|4.0.1', Type.CodeSystem);
+      expect(spicesCodeSystemByID.url).toBe('http://hl7.org/fhir/us/minimal/CodeSystem/spices');
+      expect(spicesCodeSystemByID.version).toBe('4.0.1');
+    });
+
     it('should find instances', () => {
       const drSueInstanceById = pkg.fishForFHIR('dr-sue', Type.Instance);
       expect(drSueInstanceById.id).toBe('dr-sue');
@@ -353,6 +475,17 @@ describe('Package', () => {
       expect(pkg.fishForFHIR('DrSue', Type.Instance)).toEqual(drSueInstanceById);
     });
 
+    it('should find instances that have a version when fishing with a version', () => {
+      const drSueInstanceById = pkg.fishForFHIR('thrill-ing|1.0.0', Type.Instance);
+      expect(drSueInstanceById.id).toBe('thrill-ing');
+      expect(drSueInstanceById.version).toBe('1.0.0');
+    });
+
+    it('should not find an instance that does not have a version when fishing with a version', () => {
+      const drSueInstanceById = pkg.fishForFHIR('dr-sue|1.0.0', Type.Instance);
+      expect(drSueInstanceById).toBeUndefined();
+    });
+
     it('should not find the definition when the type is not requested', () => {
       const funnyProfileByID = pkg.fishForFHIR(
         'fun-ny',
@@ -529,7 +662,8 @@ describe('Package', () => {
         sdType: 'Condition',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/fun-ny',
         parent: 'http://hl7.org/fhir/StructureDefinition/Condition',
-        resourceType: 'StructureDefinition'
+        resourceType: 'StructureDefinition',
+        version: '1.0.0'
       });
       expect(pkg.fishForMetadata('Funny', Type.Profile)).toEqual(funnyProfile);
       expect(
@@ -548,7 +682,8 @@ describe('Package', () => {
         sdType: 'Extension',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/poor-taste',
         parent: 'http://hl7.org/fhir/StructureDefinition/Extension',
-        resourceType: 'StructureDefinition'
+        resourceType: 'StructureDefinition',
+        version: '1.0.0'
       });
       expect(pkg.fishForMetadata('PoorTaste', Type.Extension)).toEqual(poorTasteExtensionByID);
       expect(
@@ -567,7 +702,8 @@ describe('Package', () => {
         sdType: 'wheat-beer',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/wheat-beer',
         parent: 'http://hl7.org/fhir/StructureDefinition/Element',
-        resourceType: 'StructureDefinition'
+        resourceType: 'StructureDefinition',
+        version: '1.0.0'
       });
       expect(pkg.fishForMetadata('WheatBeer', Type.Logical)).toEqual(wheatBeerLogicalByID);
       expect(
@@ -586,7 +722,8 @@ describe('Package', () => {
         sdType: 'Destination',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/Destination',
         parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource',
-        resourceType: 'StructureDefinition'
+        resourceType: 'StructureDefinition',
+        version: '1.0.0'
       });
       expect(pkg.fishForMetadata('Destination', Type.Resource)).toEqual(destinationResourceByID);
       expect(
@@ -731,7 +868,8 @@ describe('Package', () => {
         sdType: 'Condition',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/fun-ny',
         parent: 'http://hl7.org/fhir/StructureDefinition/Condition',
-        resourceType: 'StructureDefinition'
+        resourceType: 'StructureDefinition',
+        version: '1.0.0'
       });
       expect(pkg.fishForMetadata('Funny')).toEqual(funnyProfileByID);
       expect(
@@ -745,7 +883,8 @@ describe('Package', () => {
         sdType: 'Extension',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/poor-taste',
         parent: 'http://hl7.org/fhir/StructureDefinition/Extension',
-        resourceType: 'StructureDefinition'
+        resourceType: 'StructureDefinition',
+        version: '1.0.0'
       });
       expect(pkg.fishForMetadata('PoorTaste')).toEqual(poorTasteExtensionByID);
       expect(
@@ -759,7 +898,8 @@ describe('Package', () => {
         sdType: 'wheat-beer',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/wheat-beer',
         parent: 'http://hl7.org/fhir/StructureDefinition/Element',
-        resourceType: 'StructureDefinition'
+        resourceType: 'StructureDefinition',
+        version: '1.0.0'
       });
       expect(pkg.fishForMetadata('WheatBeer')).toEqual(wheatBeerLogicalByID);
       expect(
@@ -773,7 +913,8 @@ describe('Package', () => {
         sdType: 'Destination',
         url: 'http://hl7.org/fhir/us/minimal/StructureDefinition/Destination',
         parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource',
-        resourceType: 'StructureDefinition'
+        resourceType: 'StructureDefinition',
+        version: '1.0.0'
       });
       expect(pkg.fishForMetadata('Destination')).toEqual(destinationResourceByID);
       expect(
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index 3861ccd..f10439e 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -3306,6 +3306,30 @@ describe('StructureDefinitionExporter R4', () => {
       expect(element.binding.strength).toBe('extensible');
     });
 
+    it('should apply a correct value set rule when the VS specifies a version', () => {
+      const profile = new Profile('Foo');
+      profile.parent = 'Observation';
+
+      const vsRule = new BindingRule('category');
+      vsRule.valueSet = 'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical|4.0.1';
+      vsRule.strength = 'extensible';
+      profile.rules.push(vsRule);
+
+      exporter.exportStructDef(profile);
+      const sd = pkg.profiles[0];
+      const baseStructDef = fisher.fishForStructureDefinition('Observation');
+      const baseElement = baseStructDef.findElement('Observation.category');
+      const changedElement = sd.findElement('Observation.category');
+      expect(baseElement.binding.valueSet).toBe(
+        'http://hl7.org/fhir/ValueSet/observation-category'
+      );
+      expect(baseElement.binding.strength).toBe('preferred');
+      expect(changedElement.binding.valueSet).toBe(
+        'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical|4.0.1'
+      );
+      expect(changedElement.binding.strength).toBe('extensible');
+    });
+
     it('should use the url specified in a CaretValueRule when referencing a named value set', () => {
       const customCategoriesVS = new FshValueSet('CustomCategories');
       customCategoriesVS.id = 'custom-categories';
@@ -5958,7 +5982,9 @@ describe('StructureDefinitionExporter R4', () => {
     it('should apply a ContainsRule of an extension with a versioned URL and log a warning if the version does not match', () => {
       const profile = new Profile('MyFamilyHistory');
       profile.parent = 'FamilyMemberHistory';
-      const containsRule = new ContainsRule('extension');
+      const containsRule = new ContainsRule('extension')
+        .withFile('FSHyFile.fsh')
+        .withLocation([3, 0, 3, 45]);
       containsRule.items = [
         {
           name: 'history',
@@ -5984,7 +6010,7 @@ describe('StructureDefinitionExporter R4', () => {
         )
       ]);
       expect(loggerSpy.getLastMessage('warn')).toMatch(
-        'The http://hl7.org/fhir/StructureDefinition/familymemberhistory-type extension was specified with version 1.2.3, but SUSHI found version 4.0.1'
+        /http:\/\/hl7\.org\/fhir\/StructureDefinition\/familymemberhistory-type\|1\.2\.3 was requested, but SUSHI found http:\/\/hl7\.org\/fhir\/StructureDefinition\/familymemberhistory-type\|4\.0\.1.*File: FSHyFile\.fsh.*Line: 3\D*/s
       );
     });
 
diff --git a/test/fhirtypes/ElementDefinition.assignFshCode.test.ts b/test/fhirtypes/ElementDefinition.assignFshCode.test.ts
index 7499aa0..2401a3c 100644
--- a/test/fhirtypes/ElementDefinition.assignFshCode.test.ts
+++ b/test/fhirtypes/ElementDefinition.assignFshCode.test.ts
@@ -1,7 +1,7 @@
 import path from 'path';
 import cloneDeep from 'lodash/cloneDeep';
 import { loadFromPath } from 'fhir-package-loader';
-import { TestFisher } from '../testhelpers';
+import { TestFisher, loggerSpy } from '../testhelpers';
 import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions';
 import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition';
 import { FshCode } from '../../src/fshtypes/FshCode';
@@ -28,6 +28,7 @@ describe('ElementDefinition', () => {
     barFooCode = new FshCode('foo', 'http://bar.com');
     versionedCode = new FshCode('versioned', 'http://versioned.com|7.6.5');
     codeWithDisplay = new FshCode('bar', 'http://foo.com', 'Foo Bar');
+    loggerSpy.reset();
   });
 
   describe('#assignFshCode()', () => {
@@ -167,7 +168,7 @@ describe('ElementDefinition', () => {
       expect(clone).toEqual(concept);
     });
 
-    it('should throw InvalidUriError when binding with a non-URI value', () => {
+    it('should throw InvalidUriError when assigning a code with a non-URI value', () => {
       const category = observation.elements.find(e => e.id === 'Observation.category');
       const clone = cloneDeep(category);
       expect(() => {
@@ -178,7 +179,30 @@ describe('ElementDefinition', () => {
       }).toThrow(/notAUri/);
     });
 
-    it('should throw MismatchedBindingTypeError when binding with a ValueSet as a system', () => {
+    it('should NOT throw InvalidUriError when assigning a code with a non-URI value to a primitive code', () => {
+      const status = observation.elements.find(e => e.id === 'Observation.status');
+      const clone = cloneDeep(status);
+      clone.assignValue(new FshCode('code', 'notAUri'));
+      expect(loggerSpy.getAllLogs('warn')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /code notAUri#code is invalid.*specified system is not a URI/
+      );
+      expect(loggerSpy.getLastMessage('warn')).toMatch(/Observation.status is a code/);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /specifying a code only \(e\.g\., #code\)\./
+      );
+      clone.assignValue(new FshCode('code', 'notAUri'), true);
+      expect(loggerSpy.getAllLogs('warn')).toHaveLength(2);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /code notAUri#code is invalid.*specified system is not a URI/
+      );
+      expect(loggerSpy.getLastMessage('warn')).toMatch(/Observation.status is a code/);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /specifying a code only \(e\.g\., #code\)\./
+      );
+    });
+
+    it('should throw MismatchedBindingTypeError when assigning a code with a ValueSet as a system', () => {
       const category = observation.elements.find(e => e.id === 'Observation.category');
       const clone = cloneDeep(category);
       expect(() => {
@@ -197,6 +221,56 @@ describe('ElementDefinition', () => {
       }).toThrow(/CodeSystem/);
     });
 
+    it('should throw MismatchedBindingTypeError when assigning a code with a ValueSet as a system when provided with a version', () => {
+      const category = observation.elements.find(e => e.id === 'Observation.category');
+      const clone = cloneDeep(category);
+      expect(() => {
+        clone.assignValue(
+          new FshCode('code', 'http://hl7.org/fhir/ValueSet/observation-category|5.0.0'),
+          false,
+          fisher
+        );
+      }).toThrow(/CodeSystem/);
+      expect(() => {
+        clone.assignValue(
+          new FshCode('code', 'http://hl7.org/fhir/ValueSet/observation-category|5.0.0'),
+          true,
+          fisher
+        );
+      }).toThrow(/CodeSystem/);
+    });
+
+    it('should NOT throw MismatchedBindingTypeError when assigning a code with a ValueSet as a system to a primitive code', () => {
+      const status = observation.elements.find(e => e.id === 'Observation.status');
+      const clone = cloneDeep(status);
+      clone.assignValue(
+        new FshCode('final', 'http://hl7.org/fhir/ValueSet/observation-status'),
+        false,
+        fisher
+      );
+      expect(loggerSpy.getAllLogs('warn')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /code http:\/\/hl7\.org\/fhir\/ValueSet\/observation-status#final is invalid.*specified system is a ValueSet/
+      );
+      expect(loggerSpy.getLastMessage('warn')).toMatch(/Observation.status is a code/);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /specifying a code only \(e\.g\., #final\)\./
+      );
+      clone.assignValue(
+        new FshCode('final', 'http://hl7.org/fhir/ValueSet/observation-status'),
+        true,
+        fisher
+      );
+      expect(loggerSpy.getAllLogs('warn')).toHaveLength(2);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /code http:\/\/hl7\.org\/fhir\/ValueSet\/observation-status#final is invalid.*specified system is a ValueSet/
+      );
+      expect(loggerSpy.getLastMessage('warn')).toMatch(/Observation.status is a code/);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /specifying a code only \(e\.g\., #final\)\./
+      );
+    });
+
     it('should assign a code to a Coding', () => {
       const concept = observation.elements.find(e => e.id === 'Observation.code');
       concept.unfold(fisher);
diff --git a/test/fhirtypes/ElementDefinition.constrainType.test.ts b/test/fhirtypes/ElementDefinition.constrainType.test.ts
index 1634bf1..a614c7f 100644
--- a/test/fhirtypes/ElementDefinition.constrainType.test.ts
+++ b/test/fhirtypes/ElementDefinition.constrainType.test.ts
@@ -657,7 +657,7 @@ describe('ElementDefinition', () => {
           'https://fhir.kbv.de/StructureDefinition/KBV_PR_Base_Observation_Body_Weight-wrong-parent'
         )
       );
-      expect(loggerSpy.getLastMessage('error')).toBe(
+      expect(loggerSpy.getLastMessage('warn')).toBe(
         'https://fhir.kbv.de/StructureDefinition/KBV_PR_Base_Observation_Body_Weight-wrong-parent is based on http://fhir.de/StructureDefinition/observation-de-vitalsign-koerpergewicht version 0.9.34, but SUSHI found version 0.9.13'
       );
     });
diff --git a/test/import/FSHTank.test.ts b/test/import/FSHTank.test.ts
index da4617d..4c20726 100644
--- a/test/import/FSHTank.test.ts
+++ b/test/import/FSHTank.test.ts
@@ -33,7 +33,10 @@ describe('FSHTank', () => {
     const profile3Id = new CaretValueRule('');
     profile3Id.caretPath = 'id';
     profile3Id.value = 'prf3';
-    profile3.rules.push(profile3Id);
+    const profile3Version = new CaretValueRule('');
+    profile3Version.caretPath = 'version';
+    profile3Version.value = 'v-2.0.0';
+    profile3.rules.push(profile3Id, profile3Version);
     doc1.profiles.set(profile3.name, profile3);
     doc1.instances.set('ProfileInstance', new Instance('ProfileInstance'));
     doc1.instances.get('ProfileInstance').instanceOf = 'StructureDefinition';
@@ -41,6 +44,13 @@ describe('FSHTank', () => {
     const profileInstanceDerivation = new AssignmentRule('derivation');
     profileInstanceDerivation.value = new FshCode('constraint');
     doc1.instances.get('ProfileInstance').rules.push(profileInstanceDerivation);
+    const profileInstance2 = new Instance('ProfileInstance2');
+    profileInstance2.instanceOf = 'StructureDefinition';
+    profileInstance2.usage = 'Definition';
+    const profileInstance2Version = new AssignmentRule('version');
+    profileInstance2Version.value = 'v-2.0.0';
+    profileInstance2.rules.push(profileInstanceDerivation, profileInstance2Version);
+    doc1.instances.set(profileInstance2.name, profileInstance2);
     doc1.extensions.set('Extension1', new Extension('Extension1'));
     doc1.extensions.get('Extension1').id = 'ext1';
     doc1.extensions.get('Extension1').parent = 'Extension2';
@@ -52,7 +62,10 @@ describe('FSHTank', () => {
     const ext3Id = new CaretValueRule('');
     ext3Id.caretPath = 'id';
     ext3Id.value = 'ext3';
-    ext3.rules.push(ext3Id);
+    const ext3Version = new CaretValueRule('');
+    ext3Version.caretPath = 'version';
+    ext3Version.value = 'v-2.0.0';
+    ext3.rules.push(ext3Id, ext3Version);
     doc2.extensions.set(ext3.name, ext3);
     doc2.instances.set('ExtensionInstance', new Instance('ExtensionInstance'));
     doc2.instances.get('ExtensionInstance').instanceOf = 'StructureDefinition';
@@ -64,16 +77,41 @@ describe('FSHTank', () => {
     doc2.instances
       .get('ExtensionInstance')
       .rules.push(extensionInstanceDerivation, extensionInstanceType);
+    const extensionInstance2 = new Instance('ExtensionInstance2');
+    extensionInstance2.instanceOf = 'StructureDefinition';
+    extensionInstance2.usage = 'Definition';
+    const extensionInstance2Version = new AssignmentRule('version');
+    extensionInstance2Version.value = 'v-2.0.0';
+    extensionInstance2.rules.push(
+      extensionInstanceDerivation,
+      extensionInstanceType,
+      extensionInstance2Version
+    );
+    doc2.instances.set(extensionInstance2.name, extensionInstance2);
     doc2.valueSets.set('ValueSet1', new FshValueSet('ValueSet1'));
     doc2.valueSets.get('ValueSet1').id = 'vs1';
     doc2.instances.set('ValueSetInstance', new Instance('ValueSetInstance'));
     doc2.instances.get('ValueSetInstance').instanceOf = 'ValueSet';
     doc2.instances.get('ValueSetInstance').usage = 'Definition';
+    const valueSetInstance2 = new Instance('ValueSetInstance2');
+    valueSetInstance2.instanceOf = 'ValueSet';
+    valueSetInstance2.usage = 'Definition';
+    const valueSetInstance2Version = new AssignmentRule('version');
+    valueSetInstance2Version.value = 'v-2.0.0';
+    valueSetInstance2.rules.push(valueSetInstance2Version);
+    doc2.instances.set(valueSetInstance2.name, valueSetInstance2);
     doc2.codeSystems.set('CodeSystem1', new FshCodeSystem('CodeSystem1'));
     doc2.codeSystems.get('CodeSystem1').id = 'cs1';
     doc2.instances.set('CodeSystemInstance', new Instance('CodeSystemInstance'));
     doc2.instances.get('CodeSystemInstance').instanceOf = 'CodeSystem';
     doc2.instances.get('CodeSystemInstance').usage = 'Definition';
+    const codeSystemInstance2 = new Instance('CodeSystemInstance2');
+    codeSystemInstance2.instanceOf = 'CodeSystem';
+    codeSystemInstance2.usage = 'Definition';
+    const codeSystemInstance2Version = new AssignmentRule('version');
+    codeSystemInstance2Version.value = 'v-2.0.0';
+    codeSystemInstance2.rules.push(codeSystemInstance2Version);
+    doc2.instances.set(codeSystemInstance2.name, codeSystemInstance2);
     doc2.logicals.set('Logical1', new Logical('Logical1'));
     doc2.logicals.get('Logical1').id = 'log1';
     doc2.logicals.get('Logical1').parent = 'Element';
@@ -87,6 +125,17 @@ describe('FSHTank', () => {
     doc2.instances
       .get('LogicalInstance')
       .rules.push(logicalInstanceDerivation, logicalInstanceKind);
+    const logicalInstance2 = new Instance('LogicalInstance2');
+    logicalInstance2.instanceOf = 'StructureDefinition';
+    logicalInstance2.usage = 'Definition';
+    const logicalInstance2Version = new AssignmentRule('version');
+    logicalInstance2Version.value = 'v-2.0.0';
+    logicalInstance2.rules.push(
+      logicalInstanceDerivation,
+      logicalInstanceKind,
+      logicalInstance2Version
+    );
+    doc2.instances.set(logicalInstance2.name, logicalInstance2);
     doc2.resources.set('Resource1', new Resource('Resource1'));
     doc2.resources.get('Resource1').id = 'res1';
     const doc3 = new FSHDocument('doc3.fsh');
@@ -105,13 +154,27 @@ describe('FSHTank', () => {
     doc3.instances
       .get('ResourceInstance')
       .rules.push(resourceInstanceDerivation, resourceInstanceKind);
+    const resourceInstance2 = new Instance('ResourceInstance2');
+    resourceInstance2.instanceOf = 'StructureDefinition';
+    resourceInstance2.usage = 'Definition';
+    const resourceInstance2Version = new AssignmentRule('version');
+    resourceInstance2Version.value = 'v-2.0.0';
+    resourceInstance2.rules.push(
+      resourceInstanceDerivation,
+      resourceInstanceKind,
+      resourceInstance2Version
+    );
+    doc3.instances.set(resourceInstance2.name, resourceInstance2);
     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);
+    const vs3Version = new CaretValueRule('');
+    vs3Version.caretPath = 'version';
+    vs3Version.value = 'v-2.0.0';
+    vs3.rules.push(vs3Id, vs3Version);
     doc3.valueSets.set(vs3.name, vs3);
     doc3.codeSystems.set('CodeSystem2', new FshCodeSystem('CodeSystem2'));
     doc3.codeSystems.get('CodeSystem2').id = 'cs2';
@@ -119,19 +182,28 @@ describe('FSHTank', () => {
     const cs3Id = new CaretValueRule('');
     cs3Id.caretPath = 'id';
     cs3Id.value = 'cs3';
-    cs3.rules.push(cs3Id);
+    const cs3Version = new CaretValueRule('');
+    cs3Version.caretPath = 'version';
+    cs3Version.value = 'v-2.0.0';
+    cs3.rules.push(cs3Id, cs3Version);
     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);
+    log3Id.caretPath = 'version';
+    log3Id.value = 'v-2.0.0';
+    const log3Version = new CaretValueRule('');
+    log3Version.caretPath = 'id';
+    log3Version.value = 'log3';
+    log3.rules.push(log3Id, log3Version);
     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);
+    const res3Version = new CaretValueRule('');
+    res3Version.caretPath = 'version';
+    res3Version.value = 'v-2.0.0';
+    res3.rules.push(res3Id, res3Version);
     doc3.resources.set(res3.name, res3);
     doc3.instances.set('Instance1', new Instance('Instance1'));
     doc3.instances.get('Instance1').id = 'inst1';
@@ -140,7 +212,9 @@ describe('FSHTank', () => {
     inst2.instanceOf = 'Condition';
     const inst2Id = new AssignmentRule('id');
     inst2Id.value = 'inst2';
-    inst2.rules.push(inst2Id);
+    const inst2Version = new AssignmentRule('version');
+    inst2Version.value = 'v-2.0.0';
+    inst2.rules.push(inst2Id, inst2Version);
     doc3.instances.set(inst2.name, inst2);
     const idRuleset = new RuleSet('IdRuleset');
     const idAssignment = new AssignmentRule('id');
@@ -256,6 +330,30 @@ describe('FSHTank', () => {
       expect(tank.fish('prf3').name).toBe('Profile3');
     });
 
+    it('should find a profile when fishing with a version', () => {
+      expect(tank.fish('prf1|1.0.0', Type.Profile).name).toBe('Profile1'); // tank.config.version = 1.0.0
+    });
+
+    it('should find a profile when fishing with a version that is set by a caret rule', () => {
+      expect(tank.fish('prf3|v-2.0.0', Type.Profile).name).toBe('Profile3');
+    });
+
+    it('should not find a profile when fishing with a version that does not match', () => {
+      expect(tank.fish('prf1|2.0.0', Type.Profile)).toBeUndefined();
+    });
+
+    it('should find a profile defined as an instance definition when fishing with a version', () => {
+      expect(tank.fish('ProfileInstance|1.0.0', Type.Profile).name).toBe('ProfileInstance');
+    });
+
+    it('should find a profile defined as an instance definition when fishing with a version that is set by an assignment rule', () => {
+      expect(tank.fish('ProfileInstance2|v-2.0.0', Type.Profile).name).toBe('ProfileInstance2');
+    });
+
+    it('should not find a profile defined as an instance definition when fishing with a version that does not match', () => {
+      expect(tank.fish('ProfileInstance|2.0.0', Type.Profile)).toBeUndefined();
+    });
+
     it('should only find extensions when extensions are requested', () => {
       expect(tank.fish('ext1', Type.Extension).name).toBe('Extension1');
       expect(
@@ -321,6 +419,32 @@ describe('FSHTank', () => {
       expect(tank.fish('ext3').name).toBe('Extension3');
     });
 
+    it('should find an extension when fishing with a version', () => {
+      expect(tank.fish('ext1|1.0.0', Type.Extension).name).toBe('Extension1'); // tank.config.version = 1.0.0
+    });
+
+    it('should find an extension when fishing with a version that is set by a caret rule', () => {
+      expect(tank.fish('ext3|v-2.0.0', Type.Extension).name).toBe('Extension3');
+    });
+
+    it('should not find an extension when fishing with a version that does not match', () => {
+      expect(tank.fish('ext1|2.0.0', Type.Extension)).toBeUndefined();
+    });
+
+    it('should find an extension defined as an instance definition when fishing with a version', () => {
+      expect(tank.fish('ExtensionInstance|1.0.0', Type.Extension).name).toBe('ExtensionInstance');
+    });
+
+    it('should find an extension defined as an instance definition when fishing with a version that is set by an assignment rule', () => {
+      expect(tank.fish('ExtensionInstance2|v-2.0.0', Type.Extension).name).toBe(
+        'ExtensionInstance2'
+      );
+    });
+
+    it('should not find an extension defined as an instance definition when fishing with a version that does not match', () => {
+      expect(tank.fish('ExtensionInstance|2.0.0', Type.Extension)).toBeUndefined();
+    });
+
     it('should only find logical models when logical models are requested', () => {
       expect(tank.fish('log1', Type.Logical).name).toBe('Logical1');
       expect(
@@ -362,6 +486,30 @@ describe('FSHTank', () => {
       expect(tank.fish('log3').name).toBe('Logical3');
     });
 
+    it('should find a logical when fishing with a version', () => {
+      expect(tank.fish('log1|1.0.0', Type.Logical).name).toBe('Logical1'); // tank.config.version = 1.0.0
+    });
+
+    it('should find a logical when fishing with a version that is set by a caret rule', () => {
+      expect(tank.fish('log3|v-2.0.0', Type.Logical).name).toBe('Logical3');
+    });
+
+    it('should not find a logical when fishing with a version that does not match', () => {
+      expect(tank.fish('log1|2.0.0', Type.Logical)).toBeUndefined();
+    });
+
+    it('should find a logical defined as an instance definition when fishing with a version', () => {
+      expect(tank.fish('LogicalInstance|1.0.0', Type.Logical).name).toBe('LogicalInstance');
+    });
+
+    it('should find a logical defined as an instance definition when fishing with a version that is set by an assignment rule', () => {
+      expect(tank.fish('LogicalInstance2|v-2.0.0', Type.Logical).name).toBe('LogicalInstance2');
+    });
+
+    it('should not find a logical defined as an instance definition when fishing with a version that does not match', () => {
+      expect(tank.fish('LogicalInstance|2.0.0', Type.Logical)).toBeUndefined();
+    });
+
     it('should only find resources when resources are requested', () => {
       expect(tank.fish('res2', Type.Resource).name).toBe('Resource2');
       expect(
@@ -385,7 +533,7 @@ describe('FSHTank', () => {
       expect(tank.fish('ResourceInstance', Type.Resource).name).toBe('ResourceInstance');
       expect(
         tank.fish(
-          'ResourceInstanceres2',
+          'ResourceInstance',
           Type.Extension,
           Type.Profile,
           Type.ValueSet,
@@ -403,6 +551,30 @@ describe('FSHTank', () => {
       expect(tank.fish('res3').name).toBe('Resource3');
     });
 
+    it('should find a resource when fishing with a version', () => {
+      expect(tank.fish('res1|1.0.0', Type.Resource).name).toBe('Resource1'); // tank.config.version = 1.0.0
+    });
+
+    it('should find a resource when fishing with a version that is set by a caret rule', () => {
+      expect(tank.fish('res3|v-2.0.0', Type.Resource).name).toBe('Resource3');
+    });
+
+    it('should not find a resource when fishing with a version that does not match', () => {
+      expect(tank.fish('res1|2.0.0', Type.Resource)).toBeUndefined();
+    });
+
+    it('should find a resource defined as an instance definition when fishing with a version', () => {
+      expect(tank.fish('ResourceInstance|1.0.0', Type.Resource).name).toBe('ResourceInstance');
+    });
+
+    it('should find a resource defined as an instance definition when fishing with a version that is set by an assignment rule', () => {
+      expect(tank.fish('ResourceInstance2|v-2.0.0', Type.Resource).name).toBe('ResourceInstance2');
+    });
+
+    it('should not find a resource defined as an instance definition when fishing with a version that does not match', () => {
+      expect(tank.fish('ResourceInstance|2.0.0', Type.Resource)).toBeUndefined();
+    });
+
     it('should only find valuesets when valuesets are requested', () => {
       expect(tank.fish('vs1', Type.ValueSet).name).toBe('ValueSet1');
       expect(
@@ -444,6 +616,30 @@ describe('FSHTank', () => {
       expect(tank.fish('vs3').name).toBe('ValueSet3');
     });
 
+    it('should find a valueset when fishing with a version', () => {
+      expect(tank.fish('vs1|1.0.0', Type.ValueSet).name).toBe('ValueSet1'); // tank.config.version = 1.0.0
+    });
+
+    it('should find a valueset when fishing with a version that is set by a caret rule', () => {
+      expect(tank.fish('vs3|v-2.0.0', Type.ValueSet).name).toBe('ValueSet3');
+    });
+
+    it('should not find a valueset when fishing with a version that does not match', () => {
+      expect(tank.fish('vs1|2.0.0', Type.ValueSet)).toBeUndefined();
+    });
+
+    it('should find a valueset defined as an instance definition when fishing with a version', () => {
+      expect(tank.fish('ValueSetInstance|1.0.0', Type.ValueSet).name).toBe('ValueSetInstance');
+    });
+
+    it('should find a valueset defined as an instance definition when fishing with a version that is set by an assignment rule', () => {
+      expect(tank.fish('ValueSetInstance2|v-2.0.0', Type.ValueSet).name).toBe('ValueSetInstance2');
+    });
+
+    it('should not find a valueset defined as an instance definition when fishing with a version that does not match', () => {
+      expect(tank.fish('ValueSetInstance|2.0.0', Type.ValueSet)).toBeUndefined();
+    });
+
     it('should only find codesystems when codesystems are requested', () => {
       expect(tank.fish('cs1', Type.CodeSystem).name).toBe('CodeSystem1');
       expect(
@@ -485,6 +681,34 @@ describe('FSHTank', () => {
       expect(tank.fish('cs3').name).toBe('CodeSystem3');
     });
 
+    it('should find a codesystem when fishing with a version', () => {
+      expect(tank.fish('cs1|1.0.0', Type.CodeSystem).name).toBe('CodeSystem1'); // tank.config.version = 1.0.0
+    });
+
+    it('should find a codesystem when fishing with a version that is set by a caret rule', () => {
+      expect(tank.fish('cs3|v-2.0.0', Type.CodeSystem).name).toBe('CodeSystem3');
+    });
+
+    it('should not find a codesystem when fishing with a version that does not match', () => {
+      expect(tank.fish('cs1|2.0.0', Type.CodeSystem)).toBeUndefined();
+    });
+
+    it('should find a codesystem defined as an instance definition when fishing with a version', () => {
+      expect(tank.fish('CodeSystemInstance|1.0.0', Type.CodeSystem).name).toBe(
+        'CodeSystemInstance'
+      );
+    });
+
+    it('should find a codesystem defined as an instance definition when fishing with a version that is set by an assignment rule', () => {
+      expect(tank.fish('CodeSystemInstance2|v-2.0.0', Type.CodeSystem).name).toBe(
+        'CodeSystemInstance2'
+      );
+    });
+
+    it('should not find a codesystem defined as an instance definition when fishing with a version that does not match', () => {
+      expect(tank.fish('CodeSystemInstance|2.0.0', Type.CodeSystem)).toBeUndefined();
+    });
+
     it('should only find instances when instances are requested', () => {
       expect(tank.fish('inst1', Type.Instance).name).toBe('Instance1');
       expect(
@@ -508,6 +732,18 @@ describe('FSHTank', () => {
       expect(tank.fish('inst2').name).toBe('Instance2');
     });
 
+    it('should find an instance when fishing with a version', () => {
+      expect(tank.fish('inst1|1.0.0', Type.Instance).name).toBe('Instance1'); // tank.config.version = 1.0.0
+    });
+
+    it('should find an instance when fishing with a version that is set by an assignment rule', () => {
+      expect(tank.fish('inst2|v-2.0.0', Type.Instance).name).toBe('Instance2');
+    });
+
+    it('should not find an instance when fishing with a version that does not match', () => {
+      expect(tank.fish('inst1|2.0.0', Type.Instance)).toBeUndefined();
+    });
+
     it('should only find invariants when invariants are requested', () => {
       expect(tank.fish('Invariant1', Type.Invariant).name).toBe('Invariant1');
       expect(
@@ -546,6 +782,11 @@ describe('FSHTank', () => {
       ).toBeUndefined();
     });
 
+    it('should find ruleSets even if fishing with a version', () => {
+      // NOTE: this doesn't make much sense and wouldn't happen in real FSH, but testing for completeness
+      expect(tank.fish('RuleSet1|any', Type.RuleSet).name).toBe('RuleSet1');
+    });
+
     it('should only find Mappings when Mappings are requested', () => {
       expect(tank.fish('Mapping1', Type.Mapping).name).toBe('Mapping1');
       expect(
@@ -564,6 +805,11 @@ describe('FSHTank', () => {
         )
       ).toBeUndefined();
     });
+
+    it('should find Mappings even if fishing with a version', () => {
+      // NOTE: this doesn't make much sense and wouldn't happen in real FSH, but testing for completeness
+      expect(tank.fish('Mapping1|any', Type.Mapping).name).toBe('Mapping1');
+    });
   });
 
   describe('#fishForMetadata', () => {
diff --git a/test/testhelpers/testdefs/r4-definitions/package/ValueSet-observation-status.json b/test/testhelpers/testdefs/r4-definitions/package/ValueSet-observation-status.json
new file mode 100644
index 0000000..d8e6a8f
--- /dev/null
+++ b/test/testhelpers/testdefs/r4-definitions/package/ValueSet-observation-status.json
@@ -0,0 +1,1 @@
+{"resourceType":"ValueSet","id":"observation-status","meta":{"lastUpdated":"2019-11-01T09:29:23.356+11:00","profile":["http://hl7.org/fhir/StructureDefinition/shareablevalueset"]},"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-wg","valueCode":"oo"},{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status","valueCode":"normative"},{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm","valueInteger":5},{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-normative-version","valueCode":"4.0.0"}],"url":"http://hl7.org/fhir/ValueSet/observation-status","identifier":[{"system":"urn:ietf:rfc:3986","value":"urn:oid:2.16.840.1.113883.4.642.3.400"}],"version":"4.0.1","name":"ObservationStatus","title":"ObservationStatus","status":"active","experimental":false,"date":"2019-11-01T09:29:23+11:00","publisher":"HL7 (FHIR Project)","contact":[{"telecom":[{"system":"url","value":"http://hl7.org/fhir"},{"system":"email","value":"fhir@lists.hl7.org"}]}],"description":"Codes providing the status of an observation.","immutable":true,"compose":{"include":[{"system":"http://hl7.org/fhir/observation-status"}]}}
\ No newline at end of file
diff --git a/test/utils/FishingUtils.test.ts b/test/utils/FishingUtils.test.ts
new file mode 100644
index 0000000..a79075e
--- /dev/null
+++ b/test/utils/FishingUtils.test.ts
@@ -0,0 +1,278 @@
+import { TestFisher, loggerSpy } from '../testhelpers';
+import { Package } from '../../src/export';
+import { FSHDocument, FSHTank } from '../../src/import';
+import { FHIRDefinitions } from '../../src/fhirdefs';
+import { Profile } from '../../src/fshtypes';
+import { CaretValueRule } from '../../src/fshtypes/rules';
+import { minimalConfig } from './minimalConfig';
+import {
+  fishForFHIRBestVersion,
+  fishForMetadataBestVersion,
+  fishInTankBestVersion
+} from '../../src/utils';
+
+const someSourceInfo = {
+  file: 'SomeFile.fsh',
+  location: { startLine: 12, startColumn: 0, endLine: 12, endColumn: 35 }
+};
+
+describe('FishingUtils', () => {
+  let fisher: TestFisher;
+  let tank: FSHTank;
+
+  beforeAll(() => {
+    const doc = new FSHDocument('fileName');
+    tank = new FSHTank([doc], minimalConfig);
+    const defs = new FHIRDefinitions();
+    const pkg = new Package(tank.config);
+    fisher = new TestFisher(tank, defs, pkg);
+  });
+
+  beforeEach(() => {
+    loggerSpy.reset();
+  });
+
+  describe('#fishForFHIRBestVersion', () => {
+    let fishForFHIRSpy: jest.SpyInstance;
+    beforeEach(() => {
+      fishForFHIRSpy = jest.spyOn(fisher, 'fishForFHIR');
+      fishForFHIRSpy.mockReset();
+    });
+
+    it('should only fish once if result is found when no version is provided', () => {
+      fishForFHIRSpy.mockReturnValueOnce({ resourceType: 'mock' });
+      fishForFHIRBestVersion(fisher, 'item');
+      expect(fishForFHIRSpy).toHaveBeenCalledTimes(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should only fish once if result is found when version is provided', () => {
+      fishForFHIRSpy.mockReturnValueOnce({ resourceType: 'mock' });
+      fishForFHIRBestVersion(fisher, 'item|1.0');
+      expect(fishForFHIRSpy).toHaveBeenCalledTimes(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should only fish once if result is not found and no version is provided', () => {
+      fishForFHIRSpy.mockReturnValueOnce(undefined);
+      fishForFHIRBestVersion(fisher, 'item');
+      expect(fishForFHIRSpy).toHaveBeenCalledTimes(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should fish twice if result is not found when version is provided and not log a warning when version matches', () => {
+      fishForFHIRSpy.mockReturnValueOnce(undefined);
+      fishForFHIRSpy.mockReturnValueOnce({ resourceType: 'mock', version: '1.0' });
+      fishForFHIRBestVersion(fisher, 'item|1.0');
+      expect(fishForFHIRSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should fish twice if result is not found when version is provided and log a warning if different version is found', () => {
+      fishForFHIRSpy.mockReturnValueOnce(undefined);
+      fishForFHIRSpy.mockReturnValueOnce({ resourceType: 'mock', version: '2.0' });
+      fishForFHIRBestVersion(fisher, 'item|1.0');
+      expect(fishForFHIRSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /item\|1\.0 was requested, but SUSHI found item\|2\.0/
+      );
+    });
+
+    it('should fish twice if result is not found when version is provided and log a warning with source info if different version is found', () => {
+      fishForFHIRSpy.mockReturnValueOnce(undefined);
+      fishForFHIRSpy.mockReturnValueOnce({ resourceType: 'mock', version: '2.0' });
+      fishForFHIRBestVersion(fisher, 'item|1.0', someSourceInfo);
+      expect(fishForFHIRSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /item\|1\.0 was requested, but SUSHI found item\|2\.0.*File: SomeFile\.fsh.*Line: 12\D*/s
+      );
+    });
+
+    it('should fish twice if result is not found when version is provided and not log a warning when provided version is null', () => {
+      fishForFHIRSpy.mockReturnValueOnce(undefined);
+      fishForFHIRSpy.mockReturnValueOnce({ resourceType: 'mock', version: '1.0' });
+      fishForFHIRBestVersion(fisher, 'item|'); // no real version provided
+      expect(fishForFHIRSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should fish twice if result is not found when version is provided and not log a warning when found version is null', () => {
+      fishForFHIRSpy.mockReturnValueOnce(undefined);
+      fishForFHIRSpy.mockReturnValueOnce({ resourceType: 'mock' }); // no version on result
+      fishForFHIRBestVersion(fisher, 'item|1.0');
+      expect(fishForFHIRSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+  });
+
+  describe('#fishForMetadataBestVersion', () => {
+    let fishForMetadataSpy: jest.SpyInstance;
+    beforeEach(() => {
+      fishForMetadataSpy = jest.spyOn(fisher, 'fishForMetadata');
+      fishForMetadataSpy.mockReset();
+    });
+
+    it('should only fishForMetadata once if result is found when no version is provided', () => {
+      fishForMetadataSpy.mockReturnValueOnce({ id: 'mock' });
+      fishForMetadataBestVersion(fisher, 'item');
+      expect(fishForMetadataSpy).toHaveBeenCalledTimes(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should only fishForMetadata once if result is found when version is provided', () => {
+      fishForMetadataSpy.mockReturnValueOnce({ id: 'mock' });
+      fishForMetadataBestVersion(fisher, 'item|1.0');
+      expect(fishForMetadataSpy).toHaveBeenCalledTimes(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should only fishForMetadata once if result is not found and no version is provided', () => {
+      fishForMetadataSpy.mockReturnValueOnce(undefined);
+      fishForMetadataBestVersion(fisher, 'item');
+      expect(fishForMetadataSpy).toHaveBeenCalledTimes(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should fishForMetadata twice if result is not found when version is provided and not log a warning when version matches', () => {
+      fishForMetadataSpy.mockReturnValueOnce(undefined);
+      fishForMetadataSpy.mockReturnValueOnce({ id: 'mock', version: '1.0' });
+      fishForMetadataBestVersion(fisher, 'item|1.0');
+      expect(fishForMetadataSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should fishForMetadata twice if result is not found when version is provided and log a warning if different version is found', () => {
+      fishForMetadataSpy.mockReturnValueOnce(undefined);
+      fishForMetadataSpy.mockReturnValueOnce({ id: 'mock', version: '2.0' });
+      fishForMetadataBestVersion(fisher, 'item|1.0');
+      expect(fishForMetadataSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /item\|1\.0 was requested, but SUSHI found item\|2\.0/
+      );
+    });
+
+    it('should fishForMetadata twice if result is not found when version is provided and log a warning with source info if different version is found', () => {
+      fishForMetadataSpy.mockReturnValueOnce(undefined);
+      fishForMetadataSpy.mockReturnValueOnce({ id: 'mock', version: '2.0' });
+      fishForMetadataBestVersion(fisher, 'item|1.0', someSourceInfo);
+      expect(fishForMetadataSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /item\|1\.0 was requested, but SUSHI found item\|2\.0.*File: SomeFile\.fsh.*Line: 12\D*/s
+      );
+    });
+
+    it('should fish twice if result is not found when version is provided and not log a warning when provided version is null', () => {
+      fishForMetadataSpy.mockReturnValueOnce(undefined);
+      fishForMetadataSpy.mockReturnValueOnce({ resourceType: 'mock', version: '1.0' });
+      fishForMetadataBestVersion(fisher, 'item|'); // no real version provided
+      expect(fishForMetadataSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should fish twice if result is not found when version is provided and not log a warning when found version is null', () => {
+      fishForMetadataSpy.mockReturnValueOnce(undefined);
+      fishForMetadataSpy.mockReturnValueOnce({ resourceType: 'mock' }); // no version on result
+      fishForMetadataBestVersion(fisher, 'item|1.0');
+      expect(fishForMetadataSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+  });
+
+  describe('#fishInTankBestVersion', () => {
+    let fishInTankSpy: jest.SpyInstance;
+    beforeEach(() => {
+      fishInTankSpy = jest.spyOn(tank, 'fish');
+      fishInTankSpy.mockReset();
+    });
+
+    it('should only fish in tank once if result is found when no version is provided', () => {
+      fishInTankSpy.mockReturnValueOnce(new Profile('mock'));
+      fishInTankBestVersion(tank, 'item');
+      expect(fishInTankSpy).toHaveBeenCalledTimes(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should only fish in tank once if result is found when version is provided', () => {
+      fishInTankSpy.mockReturnValueOnce(new Profile('mock'));
+      fishInTankBestVersion(tank, 'item|1.0');
+      expect(fishInTankSpy).toHaveBeenCalledTimes(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should only fish in tank once if result is not found and no version is provided', () => {
+      fishInTankSpy.mockReturnValueOnce(undefined);
+      fishInTankBestVersion(tank, 'item');
+      expect(fishInTankSpy).toHaveBeenCalledTimes(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should fish in tank twice if result is not found when version is provided and not log a warning when version matches', () => {
+      const mockProfile = new Profile('mock');
+      const mockProfileVersion = new CaretValueRule('');
+      mockProfileVersion.caretPath = 'version';
+      mockProfileVersion.value = '1.0';
+      mockProfile.rules.push(mockProfileVersion);
+      fishInTankSpy.mockReturnValueOnce(undefined);
+      fishInTankSpy.mockReturnValueOnce(mockProfile);
+      fishInTankBestVersion(tank, 'item|1.0');
+      expect(fishInTankSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should fish in tank twice if result is not found when version is provided and log a warning if different version is found', () => {
+      const mockProfile = new Profile('mock');
+      const mockProfileVersion = new CaretValueRule('');
+      mockProfileVersion.caretPath = 'version';
+      mockProfileVersion.value = '2.0';
+      mockProfile.rules.push(mockProfileVersion);
+      fishInTankSpy.mockReturnValueOnce(undefined);
+      fishInTankSpy.mockReturnValueOnce(mockProfile);
+      fishInTankBestVersion(tank, 'item|1.0');
+      expect(fishInTankSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /item|1\.0 was requested, but SUSHI found item|2\.0/
+      );
+    });
+
+    it('should fish in tank twice if result is not found when version is provided and log a warning with source info if different version is found', () => {
+      const mockProfile = new Profile('mock');
+      const mockProfileVersion = new CaretValueRule('');
+      mockProfileVersion.caretPath = 'version';
+      mockProfileVersion.value = '2.0';
+      mockProfile.rules.push(mockProfileVersion);
+      fishInTankSpy.mockReturnValueOnce(undefined);
+      fishInTankSpy.mockReturnValueOnce(mockProfile);
+      fishInTankBestVersion(tank, 'item|1.0', someSourceInfo);
+      expect(fishInTankSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /item\|1\.0 was requested, but SUSHI found item\|2\.0.*File: SomeFile\.fsh.*Line: 12\D*/s
+      );
+    });
+
+    it('should fish twice if result is not found when version is provided and not log a warning when provided version is null', () => {
+      fishInTankSpy.mockReturnValueOnce(undefined);
+      fishInTankSpy.mockReturnValueOnce(new Profile('mock'));
+      fishInTankBestVersion(tank, 'item|'); // no version provided
+      expect(fishInTankSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should fish twice if result is not found when version is provided and not log a warning when found version is null', () => {
+      const configWithoutVersion = { ...minimalConfig };
+      delete configWithoutVersion.version;
+      const tankWithoutVersion = new FSHTank([new FSHDocument('no-version')], configWithoutVersion);
+      const fishInTankWithoutVersionSpy = jest.spyOn(tankWithoutVersion, 'fish');
+      fishInTankWithoutVersionSpy.mockReturnValueOnce(undefined);
+      fishInTankWithoutVersionSpy.mockReturnValueOnce(new Profile('mock')); // no version on result (not set with rule or in config)
+      fishInTankBestVersion(tankWithoutVersion, 'item|1.0');
+      expect(fishInTankWithoutVersionSpy).toHaveBeenCalledTimes(2);
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+  });
+});