Repository: FHIR/sushi
Pull request #69 (2019-12-31 20:23:58)

Allow importer to resolve aliases from any input file
Addresses https://standardhealthrecord.atlassian.net/browse/CIMPL-190

This adds support for the importer to resolve an alias from any input file, in any other input file. This was done by updating the `importText.ts` file so that the function takes in all input files, instead of the previous approach of taking in only one at a time. There is still a single importer per input file, though.

An optional `allAliases` argument has been provided to the importers' `visitDoc` method. If it is provided (which it is for our toolchain), then each individual importer will resolve aliases from this `allAliases` map. Otherwise, it will operate based on the aliases within the importer's sole document, as before.

Test cases have been added to test this functionality. Tests have also been updated to reflect the structural change to `importText`.

There's an odd side effect to this. I will explain that in an inline comment in the code.

kjmahalingam: I pushed up a fix that addresses the inline comments. I'll work on the other larger refactor to make one `FSHImporter` later today or tomorrow.
kjmahalingam: @cmoesel , your comments should all be addressed. I also added an `importer.import()` function that takes in all `RawFSHes` in order to run its operations individually on each.

I had to remove the operation (and test) to track whether the `importer.visitDoc()` method has already been used, because that is now irrelevant. I also updated the importer to keep track of its current document and current file, since the importer is no longer associated with a specific document and file at any given time.
kjmahalingam: @cmoesel , your newest comment should be addressed; plus a bonus by resetting after each alias import as well.

Patch:
diff --git a/src/app.ts b/src/app.ts
index 5014221..689dbdb 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -3,7 +3,7 @@
 import path from 'path';
 import fs from 'fs-extra';
 import program from 'commander';
-import { importText, FSHDocument, FSHTank } from './import';
+import { importText, FSHTank, RawFSH } from './import';
 import { exportFHIR } from './export';
 import { IGExporter } from './ig/IGExporter';
 import { logger, stats } from './utils/FSHLogger';
@@ -59,15 +59,15 @@ async function app() {
     );
   }
 
-  const docs: FSHDocument[] = [];
-  for (const file of files) {
-    if (file.endsWith('.fsh')) {
+  const rawFSHes = files
+    .filter(file => file.endsWith('.fsh'))
+    .map(file => {
       const filePath = path.resolve(input, file);
       const fileContent = fs.readFileSync(filePath, 'utf8');
-      const doc = importText(fileContent, filePath);
-      if (doc) docs.push(doc);
-    }
-  }
+      return new RawFSH(fileContent, filePath);
+    });
+
+  const docs = importText(rawFSHes);
 
   const tank = new FSHTank(docs, config);
   await Promise.all(dependencyDefs);
diff --git a/src/import/FSHImporter.ts b/src/import/FSHImporter.ts
index 5abc5f3..ab24c02 100644
--- a/src/import/FSHImporter.ts
+++ b/src/import/FSHImporter.ts
@@ -1,6 +1,10 @@
 import * as pc from './parserContexts';
 import { FSHDocument } from './FSHDocument';
+import { RawFSH } from './RawFSH';
+import { FSHErrorListener } from './FSHErrorListener';
 import { FSHVisitor } from './generated/FSHVisitor';
+import { FSHLexer } from './generated/FSHLexer';
+import { FSHParser } from './generated/FSHParser';
 import {
   Profile,
   Extension,
@@ -29,7 +33,7 @@ import {
   ContainsRule,
   CaretValueRule
 } from '../fshtypes/rules';
-import { ParserRuleContext } from 'antlr4';
+import { ParserRuleContext, InputStream, CommonTokenStream } from 'antlr4';
 import { logger } from '../utils/FSHLogger';
 import { TerminalNode } from 'antlr4/tree/Tree';
 import {
@@ -39,6 +43,7 @@ import {
   ValueSetFilterValueTypeError,
   ValueSetFilterMissingValueError
 } from '../errors';
+import flatMap from 'lodash/flatMap';
 
 enum SdMetadataKey {
   Id = 'Id',
@@ -76,41 +81,65 @@ enum Flag {
  * we must call the explicit visitX functions.
  */
 export class FSHImporter extends FSHVisitor {
-  private used = false;
-  private readonly doc: FSHDocument;
+  private currentFile: string;
+  private currentDoc: FSHDocument;
+  private allAliases: Map<string, string>;
 
-  constructor(public readonly file: string = '') {
+  constructor() {
     super();
-    this.doc = new FSHDocument(file);
   }
 
-  visitDoc(ctx: pc.DocContext): FSHDocument {
-    if (this.used) {
-      logger.error('FSHImporter cannot be re-used. Construct a new instance.');
-      return;
-    }
-    this.used = true;
+  import(rawFSHes: RawFSH[]): FSHDocument[] {
+    const docs: FSHDocument[] = [];
+    const contexts: pc.DocContext[] = [];
+    rawFSHes.forEach(rawFSH => {
+      docs.push(new FSHDocument(rawFSH.path));
+      contexts.push(this.parseDoc(rawFSH.content, rawFSH.path));
+    });
+
+    // Import all aliases first
+    this.allAliases = new Map(
+      flatMap(
+        contexts.map((context, index) => {
+          this.currentDoc = docs[index];
+          this.currentFile = this.currentDoc.file ?? '';
+          const currentAliases = this.getAliases(context);
+          this.currentDoc = null;
+          this.currentFile = null;
+          return currentAliases;
+        }),
+        aliases => Array.from(aliases)
+      )
+    );
+
+    contexts.forEach((context, index) => {
+      this.currentDoc = docs[index];
+      this.currentFile = this.currentDoc.file ?? '';
+      this.visitDoc(context);
+      this.currentDoc = null;
+      this.currentFile = null;
+    });
 
-    // First collect the aliases
+    return docs;
+  }
+
+  getAliases(ctx: pc.DocContext): Map<string, string> {
     ctx.entity().forEach(e => {
       if (e.alias()) {
         this.visitAlias(e.alias());
       }
     });
 
-    // Now process the rest of the document
+    return this.currentDoc.aliases;
+  }
+
+  visitDoc(ctx: pc.DocContext): void {
     ctx.entity().forEach(e => {
       this.visitEntity(e);
     });
-
-    return this.doc;
   }
 
   visitEntity(ctx: pc.EntityContext): void {
-    if (ctx.alias()) {
-      this.visitAlias(ctx.alias());
-    }
-
     if (ctx.profile()) {
       this.visitProfile(ctx.profile());
     }
@@ -129,23 +158,23 @@ export class FSHImporter extends FSHVisitor {
   }
 
   visitAlias(ctx: pc.AliasContext): void {
-    this.doc.aliases.set(ctx.SEQUENCE()[0].getText(), ctx.SEQUENCE()[1].getText());
+    this.currentDoc.aliases.set(ctx.SEQUENCE()[0].getText(), ctx.SEQUENCE()[1].getText());
   }
 
   visitProfile(ctx: pc.ProfileContext) {
     const profile = new Profile(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     this.parseProfileOrExtension(profile, ctx.sdMetadata(), ctx.sdRule());
-    this.doc.profiles.set(profile.name, profile);
+    this.currentDoc.profiles.set(profile.name, profile);
   }
 
   visitExtension(ctx: pc.ExtensionContext) {
     const extension = new Extension(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     this.parseProfileOrExtension(extension, ctx.sdMetadata(), ctx.sdRule());
-    this.doc.extensions.set(extension.name, extension);
+    this.currentDoc.extensions.set(extension.name, extension);
   }
 
   private parseProfileOrExtension(
@@ -162,7 +191,7 @@ export class FSHImporter extends FSHVisitor {
             `Metadata field '${pair.key}' already declared with value '${seenPairs.get(
               pair.key
             )}'.`,
-            { file: this.file, location: this.extractStartStop(pair.context) }
+            { file: this.currentFile, location: this.extractStartStop(pair.context) }
           );
           return;
         }
@@ -185,10 +214,10 @@ export class FSHImporter extends FSHVisitor {
   visitInstance(ctx: pc.InstanceContext) {
     const instance = new Instance(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     try {
       this.parseInstance(instance, ctx.instanceMetadata(), ctx.fixedValueRule());
-      this.doc.instances.set(instance.name, instance);
+      this.currentDoc.instances.set(instance.name, instance);
     } catch (e) {
       logger.error(e.message, instance.sourceInfo);
     }
@@ -211,7 +240,7 @@ export class FSHImporter extends FSHVisitor {
             `Metadata field '${pair.key}' already declared with value '${seenPairs.get(
               pair.key
             )}'.`,
-            { file: this.file, location: this.extractStartStop(pair.context) }
+            { file: this.currentFile, location: this.extractStartStop(pair.context) }
           );
           return;
         }
@@ -233,9 +262,9 @@ export class FSHImporter extends FSHVisitor {
   visitValueSet(ctx: pc.ValueSetContext) {
     const valueSet = new ValueSet(ctx.SEQUENCE().getText())
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     this.parseValueSet(valueSet, ctx.vsMetadata(), ctx.vsComponent());
-    this.doc.valueSets.set(valueSet.name, valueSet);
+    this.currentDoc.valueSets.set(valueSet.name, valueSet);
   }
 
   private parseValueSet(
@@ -255,7 +284,7 @@ export class FSHImporter extends FSHVisitor {
             `Metadata field '${pair.key}' already declared with value '${seenPairs.get(
               pair.key
             )}'.`,
-            { file: this.file, location: this.extractStartStop(pair.context) }
+            { file: this.currentFile, location: this.extractStartStop(pair.context) }
           );
           return;
         }
@@ -352,7 +381,7 @@ export class FSHImporter extends FSHVisitor {
       return [this.visitCaretValueRule(ctx.caretValueRule())];
     }
     logger.warn(`Unsupported rule: ${ctx.getText()}`, {
-      file: this.file,
+      file: this.currentFile,
       location: this.extractStartStop(ctx)
     });
     return [];
@@ -378,7 +407,7 @@ export class FSHImporter extends FSHVisitor {
 
     const cardRule = new CardRule(this.visitPath(ctx.path()))
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     const card = this.parseCard(ctx.CARD().getText());
     cardRule.min = card.min;
     cardRule.max = card.max;
@@ -387,7 +416,7 @@ export class FSHImporter extends FSHVisitor {
     if (ctx.flag() && ctx.flag().length > 0) {
       const flagRule = new FlagRule(cardRule.path)
         .withLocation(this.extractStartStop(ctx))
-        .withFile(this.file);
+        .withFile(this.currentFile);
       this.parseFlags(flagRule, ctx.flag());
       rules.push(flagRule);
     }
@@ -413,7 +442,7 @@ export class FSHImporter extends FSHVisitor {
     return paths.map(path => {
       const flagRule = new FlagRule(path)
         .withLocation(this.extractStartStop(ctx))
-        .withFile(this.file);
+        .withFile(this.currentFile);
       this.parseFlags(flagRule, ctx.flag());
       return flagRule;
     });
@@ -446,7 +475,7 @@ export class FSHImporter extends FSHVisitor {
   visitValueSetRule(ctx: pc.ValueSetRuleContext): ValueSetRule {
     const vsRule = new ValueSetRule(this.visitPath(ctx.path()))
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     vsRule.valueSet = this.aliasAwareValue(ctx.SEQUENCE().getText());
     vsRule.strength = ctx.strength() ? this.visitStrength(ctx.strength()) : 'required';
     return vsRule;
@@ -466,7 +495,7 @@ export class FSHImporter extends FSHVisitor {
   visitFixedValueRule(ctx: pc.FixedValueRuleContext): FixedValueRule {
     const fixedValueRule = new FixedValueRule(this.visitPath(ctx.path()))
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     fixedValueRule.fixedValue = this.visitValue(ctx.value());
     return fixedValueRule;
   }
@@ -524,7 +553,9 @@ export class FSHImporter extends FSHVisitor {
         .replace(/\\\\/g, '\\')
         .replace(/\\"/g, '"');
     }
-    const concept = new FshCode(code).withLocation(this.extractStartStop(ctx)).withFile(this.file);
+    const concept = new FshCode(code)
+      .withLocation(this.extractStartStop(ctx))
+      .withFile(this.currentFile);
     if (system && system.length > 0) {
       concept.system = this.aliasAwareValue(system);
     }
@@ -540,10 +571,10 @@ export class FSHImporter extends FSHVisitor {
     // the literal version of quantity always assumes UCUM code system
     const unit = new FshCode(delimitedUnit.slice(1, -1), 'http://unitsofmeasure.org')
       .withLocation(this.extractStartStop(ctx.UNIT()))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     const quantity = new FshQuantity(value, unit)
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     return quantity;
   }
 
@@ -553,7 +584,7 @@ export class FSHImporter extends FSHVisitor {
       this.visitRatioPart(ctx.ratioPart()[1])
     )
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     return ratio;
   }
 
@@ -561,7 +592,7 @@ export class FSHImporter extends FSHVisitor {
     if (ctx.NUMBER()) {
       const quantity = new FshQuantity(parseFloat(ctx.NUMBER().getText()))
         .withLocation(this.extractStartStop(ctx.NUMBER()))
-        .withFile(this.file);
+        .withFile(this.currentFile);
       return quantity;
     }
     return this.visitQuantity(ctx.quantity());
@@ -574,7 +605,7 @@ export class FSHImporter extends FSHVisitor {
   visitOnlyRule(ctx: pc.OnlyRuleContext): OnlyRule {
     const onlyRule = new OnlyRule(this.visitPath(ctx.path()))
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
     ctx.targetType().forEach(t => {
       if (t.REFERENCE()) {
         const text = t.REFERENCE().getText();
@@ -593,7 +624,7 @@ export class FSHImporter extends FSHVisitor {
     const rules: (ContainsRule | CardRule | FlagRule)[] = [];
     const containsRule = new ContainsRule(this.visitPath(ctx.path()))
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
 
     rules.push(containsRule);
     ctx.item().forEach(i => {
@@ -602,7 +633,7 @@ export class FSHImporter extends FSHVisitor {
 
       const cardRule = new CardRule(`${containsRule.path}[${item}]`)
         .withLocation(this.extractStartStop(i))
-        .withFile(this.file);
+        .withFile(this.currentFile);
       const card = this.parseCard(i.CARD().getText());
       cardRule.min = card.min;
       cardRule.max = card.max;
@@ -611,7 +642,7 @@ export class FSHImporter extends FSHVisitor {
       if (i.flag() && i.flag().length > 0) {
         const flagRule = new FlagRule(`${containsRule.path}[${item}]`)
           .withLocation(this.extractStartStop(i))
-          .withFile(this.file);
+          .withFile(this.currentFile);
         this.parseFlags(flagRule, i.flag());
         rules.push(flagRule);
       }
@@ -623,7 +654,7 @@ export class FSHImporter extends FSHVisitor {
     const path = ctx.path() ? this.visitPath(ctx.path()) : '';
     const caretValueRule = new CaretValueRule(path)
       .withLocation(this.extractStartStop(ctx))
-      .withFile(this.file);
+      .withFile(this.currentFile);
 
     // Get the caret path, but slice off the starting ^
     caretValueRule.caretPath = this.visitCaretPath(ctx.caretPath()).slice(1);
@@ -657,7 +688,7 @@ export class FSHImporter extends FSHVisitor {
       const singleCode = this.visitCode(ctx.code());
       if (singleCode.system && from.system) {
         logger.error(`Concept ${singleCode.code} specifies system multiple times`, {
-          file: this.file,
+          file: this.currentFile,
           location: this.extractStartStop(ctx)
         });
       } else if (singleCode.system) {
@@ -670,7 +701,7 @@ export class FSHImporter extends FSHVisitor {
         logger.error(
           `Concept ${singleCode.code} must include system as "SYSTEM#CONCEPT" or "#CONCEPT from system SYSTEM"`,
           {
-            file: this.file,
+            file: this.currentFile,
             location: this.extractStartStop(ctx)
           }
         );
@@ -705,7 +736,9 @@ export class FSHImporter extends FSHVisitor {
           }
         }
         concepts.push(
-          new FshCode(codePart, from.system, description).withLocation(location).withFile(this.file)
+          new FshCode(codePart, from.system, description)
+            .withLocation(location)
+            .withFile(this.currentFile)
         );
       });
     }
@@ -729,7 +762,7 @@ export class FSHImporter extends FSHVisitor {
           } catch (e) {
             logger.error(e, {
               location: this.extractStartStop(filterDefinition),
-              file: this.file
+              file: this.currentFile
             });
           }
         });
@@ -843,7 +876,7 @@ export class FSHImporter extends FSHVisitor {
   }
 
   private aliasAwareValue(value: string): string {
-    return this.doc.aliases.has(value) ? this.doc.aliases.get(value) : value;
+    return this.allAliases.has(value) ? this.allAliases.get(value) : value;
   }
 
   private extractString(stringCtx: ParserRuleContext): string {
@@ -905,4 +938,26 @@ export class FSHImporter extends FSHVisitor {
       };
     }
   }
+
+  // NOTE: Since the ANTLR parser/lexer is JS (not typescript), we need to use some ts-ignore here.
+  private parseDoc(input: string, file?: string): pc.DocContext {
+    const chars = new InputStream(input);
+    const lexer = new FSHLexer(chars);
+    const listener = new FSHErrorListener(file);
+    // @ts-ignore
+    lexer.removeErrorListeners();
+    // @ts-ignore
+    lexer.addErrorListener(listener);
+    // @ts-ignore
+    const tokens = new CommonTokenStream(lexer);
+    const parser = new FSHParser(tokens);
+    // @ts-ignore
+    parser.removeErrorListeners();
+    // @ts-ignore
+    parser.addErrorListener(listener);
+    // @ts-ignore
+    parser.buildParseTrees = true;
+    // @ts-ignore
+    return parser.doc() as DocContext;
+  }
 }
diff --git a/src/import/RawFSH.ts b/src/import/RawFSH.ts
new file mode 100644
index 0000000..5d4fe35
--- /dev/null
+++ b/src/import/RawFSH.ts
@@ -0,0 +1,6 @@
+export class RawFSH {
+  public readonly path: string;
+  constructor(public readonly content: string, path?: string) {
+    this.path = path ?? '';
+  }
+}
diff --git a/src/import/importText.ts b/src/import/importText.ts
index 384c344..c4d38aa 100644
--- a/src/import/importText.ts
+++ b/src/import/importText.ts
@@ -1,40 +1,14 @@
-import { InputStream, CommonTokenStream } from 'antlr4/index';
-import { FSHLexer } from './generated/FSHLexer';
-import { FSHParser } from './generated/FSHParser';
-import { DocContext } from './parserContexts';
 import { FSHImporter } from './FSHImporter';
 import { FSHDocument } from './FSHDocument';
-import { FSHErrorListener } from './FSHErrorListener';
+import { RawFSH } from './RawFSH';
 
 /**
- * Parses a text string as a FSHDocument.
- * @param {string} text - the FSH text to parse
- * @param {string} file - the file name to record as the text source
- * @returns {FSHDocument} - the FSH document representing the parsed text
+ * Parses various text strings into individual FSHDocuments.
+ * @param {RawFSH[]} rawFSHes - the list of RawFSH to parse into FSHDocuments
+ * @returns {FSHDocument[]} - the FSH documents representing each parsed text
  */
-export function importText(text: string, file?: string): FSHDocument {
-  const importer = new FSHImporter(file);
-  return importer.visitDoc(parseDoc(text, file));
-}
+export function importText(rawFSHes: RawFSH[]): FSHDocument[] {
+  const importer = new FSHImporter();
 
-// NOTE: Since the ANTLR parser/lexer is JS (not typescript), we need to use some ts-ignore here.
-function parseDoc(input: string, file?: string): DocContext {
-  const chars = new InputStream(input);
-  const lexer = new FSHLexer(chars);
-  const listener = new FSHErrorListener(file);
-  // @ts-ignore
-  lexer.removeErrorListeners();
-  // @ts-ignore
-  lexer.addErrorListener(listener);
-  // @ts-ignore
-  const tokens = new CommonTokenStream(lexer);
-  const parser = new FSHParser(tokens);
-  // @ts-ignore
-  parser.removeErrorListeners();
-  // @ts-ignore
-  parser.addErrorListener(listener);
-  // @ts-ignore
-  parser.buildParseTrees = true;
-  // @ts-ignore
-  return parser.doc() as DocContext;
+  return importer.import(rawFSHes);
 }
diff --git a/src/import/index.ts b/src/import/index.ts
index 7b52fdb..104f0a6 100644
--- a/src/import/index.ts
+++ b/src/import/index.ts
@@ -3,3 +3,4 @@ export * from './FSHTank';
 export * from './FSHImporter';
 export * from './importText';
 export * from './parserContexts';
+export * from './RawFSH';


Test patch:
diff --git a/test/import/FSHImporter.Alias.test.ts b/test/import/FSHImporter.Alias.test.ts
index 4700d82..a357ffb 100644
--- a/test/import/FSHImporter.Alias.test.ts
+++ b/test/import/FSHImporter.Alias.test.ts
@@ -1,5 +1,6 @@
-import { importText } from '../../src/import';
+import { importText, RawFSH } from '../../src/import';
 import { ValueSetRule } from '../../src/fshtypes/rules';
+import { importSingleText } from '../testhelpers/importSingleText';
 
 // Aliases are tested as part of the other entity tests where aliases are allowed
 // but these tests ensure that aliases work generally and can be in any order
@@ -22,7 +23,7 @@ describe('FSHImporter', () => {
       Alias: UCUM = http://unitsofmeasure.org
       `;
 
-      const result = importText(input);
+      const result = importSingleText(input);
       expect(result.aliases.size).toBe(4);
       expect(result.aliases.get('LOINC')).toBe('http://loinc.org');
       expect(result.aliases.get('SCT')).toBe('http://snomed.info/sct');
@@ -39,7 +40,7 @@ describe('FSHImporter', () => {
       * code from LOINC
       `;
 
-      const result = importText(input);
+      const result = importSingleText(input);
       const rule = result.profiles.get('ObservationProfile').rules[0] as ValueSetRule;
       expect(rule.valueSet).toBe('http://loinc.org');
     });
@@ -53,7 +54,7 @@ describe('FSHImporter', () => {
       Alias: LOINC = http://loinc.org
       `;
 
-      const result = importText(input);
+      const result = importSingleText(input);
       const rule = result.profiles.get('ObservationProfile').rules[0] as ValueSetRule;
       expect(rule.valueSet).toBe('http://loinc.org');
     });
@@ -67,9 +68,25 @@ describe('FSHImporter', () => {
       * code from LAINC
       `;
 
-      const result = importText(input);
+      const result = importSingleText(input);
       const rule = result.profiles.get('ObservationProfile').rules[0] as ValueSetRule;
       expect(rule.valueSet).toBe('LAINC');
     });
+
+    it('should translate an alias from any input file', () => {
+      const input = `
+      Profile: ObservationProfile
+      Parent: Observation
+      * code from LOINC
+      `;
+      const input2 = `
+      Alias: LOINC = http://loinc.org
+      `;
+
+      const results = importText([new RawFSH(input), new RawFSH(input2)]);
+      expect(results.length).toBe(2);
+      const rule = results[0].profiles.get('ObservationProfile').rules[0] as ValueSetRule;
+      expect(rule.valueSet).toBe('http://loinc.org');
+    });
   });
 });
diff --git a/test/import/FSHImporter.Extension.test.ts b/test/import/FSHImporter.Extension.test.ts
index 6d51142..008f743 100644
--- a/test/import/FSHImporter.Extension.test.ts
+++ b/test/import/FSHImporter.Extension.test.ts
@@ -5,8 +5,8 @@ import {
   assertValueSetRule,
   assertCaretValueRule
 } from '../testhelpers/asserts';
-import { importText } from '../../src/import';
 import { loggerSpy } from '../testhelpers/loggerSpy';
+import { importSingleText } from '../testhelpers/importSingleText';
 
 describe('FSHImporter', () => {
   describe('Extension', () => {
@@ -16,7 +16,7 @@ describe('FSHImporter', () => {
         Extension: SomeExtension
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.extensions.size).toBe(1);
         const extension = result.extensions.get('SomeExtension');
         expect(extension.name).toBe('SomeExtension');
@@ -41,7 +41,7 @@ describe('FSHImporter', () => {
         Description: "An extension on something"
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.extensions.size).toBe(1);
         const extension = result.extensions.get('SomeExtension');
         expect(extension.name).toBe('SomeExtension');
@@ -70,7 +70,7 @@ describe('FSHImporter', () => {
         Description: "A duplicated extension on something"
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.extensions.size).toBe(1);
         const extension = result.extensions.get('SomeExtension');
         expect(extension.name).toBe('SomeExtension');
@@ -91,7 +91,7 @@ describe('FSHImporter', () => {
         Description: "A duplicated extension on something"
         `;
 
-        importText(input, 'Dupe.fsh');
+        importSingleText(input, 'Dupe.fsh');
         expect(loggerSpy.getMessageAtIndex(-2)).toMatch(/File: Dupe\.fsh.*Line: 7\D/s);
         expect(loggerSpy.getLastMessage()).toMatch(/File: Dupe\.fsh.*Line: 8\D/s);
       });
@@ -106,7 +106,7 @@ describe('FSHImporter', () => {
         * value[x] 1..1
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const extension = result.extensions.get('SomeExtension');
         expect(extension.rules).toHaveLength(2);
         assertCardRule(extension.rules[0], 'extension', 0, 0);
@@ -120,7 +120,7 @@ describe('FSHImporter', () => {
         * value[x] 1..1 MS
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const extension = result.extensions.get('SomeExtension');
         expect(extension.rules).toHaveLength(3);
         assertCardRule(extension.rules[0], 'extension', 0, 0);
@@ -136,7 +136,7 @@ describe('FSHImporter', () => {
         * extension MS
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const extension = result.extensions.get('SomeExtension');
         expect(extension.rules).toHaveLength(1);
         assertFlagRule(extension.rules[0], 'extension', true, undefined, undefined);
@@ -151,7 +151,7 @@ describe('FSHImporter', () => {
         * valueCodeableConcept from ExtensionValueSet (extensible)
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const extension = result.extensions.get('SomeExtension');
         expect(extension.rules).toHaveLength(1);
         assertValueSetRule(
@@ -170,7 +170,7 @@ describe('FSHImporter', () => {
         * value[x] only Quantity
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const extension = result.extensions.get('SomeExtension');
         expect(extension.rules).toHaveLength(1);
         assertOnlyRule(extension.rules[0], 'value[x]', { type: 'Quantity' });
@@ -184,7 +184,7 @@ describe('FSHImporter', () => {
         * id ^short = "foo"
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const extension = result.extensions.get('SomeExtension');
         expect(extension.rules).toHaveLength(1);
         assertCaretValueRule(extension.rules[0], 'id', 'short', 'foo');
diff --git a/test/import/FSHImporter.Instance.test.ts b/test/import/FSHImporter.Instance.test.ts
index 30f124e..b164916 100644
--- a/test/import/FSHImporter.Instance.test.ts
+++ b/test/import/FSHImporter.Instance.test.ts
@@ -1,7 +1,7 @@
-import { importText } from '../../src/import';
 import { assertFixedValueRule } from '../testhelpers/asserts';
 import { FshCode } from '../../src/fshtypes';
 import { loggerSpy } from '../testhelpers/loggerSpy';
+import { importSingleText } from '../testhelpers/importSingleText';
 
 describe('FSHImporter', () => {
   describe('Instance', () => {
@@ -12,7 +12,7 @@ describe('FSHImporter', () => {
         InstanceOf: Observation
         `;
 
-        const result = importText(input, 'SimpleInstance.fsh');
+        const result = importSingleText(input, 'SimpleInstance.fsh');
         expect(result.instances.size).toBe(1);
         const instance = result.instances.get('MyObservation');
         expect(instance.name).toBe('MyObservation');
@@ -35,7 +35,7 @@ describe('FSHImporter', () => {
         InstanceOf: obs
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.instances.size).toBe(1);
         const instance = result.instances.get('MyObservation');
         expect(instance.instanceOf).toBe('Observation');
@@ -47,7 +47,7 @@ describe('FSHImporter', () => {
         Title: "My Important Observation"
         `;
 
-        const result = importText(input, 'Missing.fsh');
+        const result = importSingleText(input, 'Missing.fsh');
         expect(result.instances.size).toBe(0);
         expect(loggerSpy.getLastMessage()).toMatch(/File: Missing\.fsh.*Line: 2 - 3\D/s);
       });
@@ -61,7 +61,7 @@ describe('FSHImporter', () => {
         Title: "My Important Observation"
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.instances.size).toBe(1);
         const instance = result.instances.get('MyObservation');
         expect(instance.name).toBe('MyObservation');
@@ -81,7 +81,7 @@ describe('FSHImporter', () => {
         * gender = #other
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.instances.size).toBe(1);
         const instance = result.instances.get('SamplePatient');
         expect(instance.rules.length).toBe(3);
@@ -105,7 +105,7 @@ describe('FSHImporter', () => {
         Title: "My Duplicate Observation"
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.instances.size).toBe(1);
         const instance = result.instances.get('MyObservation');
         expect(instance.name).toBe('MyObservation');
@@ -122,7 +122,7 @@ describe('FSHImporter', () => {
         Title: "My Duplicate Observation"
         `;
 
-        importText(input, 'Dupe.fsh');
+        importSingleText(input, 'Dupe.fsh');
         expect(loggerSpy.getMessageAtIndex(-2)).toMatch(/File: Dupe\.fsh.*Line: 5\D/s);
         expect(loggerSpy.getLastMessage()).toMatch(/File: Dupe\.fsh.*Line: 6\D/s);
       });
diff --git a/test/import/FSHImporter.Profile.test.ts b/test/import/FSHImporter.Profile.test.ts
index 355a815..58d3c1e 100644
--- a/test/import/FSHImporter.Profile.test.ts
+++ b/test/import/FSHImporter.Profile.test.ts
@@ -7,9 +7,9 @@ import {
   assertContainsRule,
   assertCaretValueRule
 } from '../testhelpers/asserts';
-import { importText } from '../../src/import';
 import { FshCode, FshQuantity, FshRatio } from '../../src/fshtypes';
 import { loggerSpy } from '../testhelpers/loggerSpy';
+import { importSingleText } from '../testhelpers/importSingleText';
 
 describe('FSHImporter', () => {
   describe('Profile', () => {
@@ -20,7 +20,7 @@ describe('FSHImporter', () => {
         Parent: Observation
         `;
 
-        const result = importText(input, 'Simple.fsh');
+        const result = importSingleText(input, 'Simple.fsh');
         expect(result.profiles.size).toBe(1);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.name).toBe('ObservationProfile');
@@ -45,7 +45,7 @@ describe('FSHImporter', () => {
         Description: "A profile on Observation"
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.profiles.size).toBe(1);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.name).toBe('ObservationProfile');
@@ -81,7 +81,7 @@ describe('FSHImporter', () => {
           """
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.profiles.size).toBe(1);
         const profile = result.profiles.get('ObservationProfile');
         const expectedDescriptionLines = [
@@ -108,7 +108,7 @@ describe('FSHImporter', () => {
         Parent: OBS
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.profiles.size).toBe(1);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.name).toBe('ObservationProfile');
@@ -128,7 +128,7 @@ describe('FSHImporter', () => {
         Description: "A duplicated profile on Observation"
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         expect(result.profiles.size).toBe(1);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.name).toBe('ObservationProfile');
@@ -148,7 +148,7 @@ describe('FSHImporter', () => {
         Description: "A duplicated profile on Observation"
         `;
 
-        importText(input, 'Dupe.fsh');
+        importSingleText(input, 'Dupe.fsh');
         expect(loggerSpy.getMessageAtIndex(-2)).toMatch(/File: Dupe\.fsh.*Line: 7\D/s);
         expect(loggerSpy.getLastMessage()).toMatch(/File: Dupe\.fsh.*Line: 8\D/s);
       });
@@ -164,7 +164,7 @@ describe('FSHImporter', () => {
         * component 2..*
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(3);
         assertCardRule(profile.rules[0], 'category', 1, 5);
@@ -181,7 +181,7 @@ describe('FSHImporter', () => {
         * component 2..* SU
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(6);
         assertCardRule(profile.rules[0], 'category', 1, 5);
@@ -201,7 +201,7 @@ describe('FSHImporter', () => {
         * component 2..* SU MS
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(6);
         assertCardRule(profile.rules[0], 'category', 1, 5);
@@ -223,7 +223,7 @@ describe('FSHImporter', () => {
         * component SU
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(3);
         assertFlagRule(profile.rules[0], 'category', true, undefined, undefined);
@@ -240,7 +240,7 @@ describe('FSHImporter', () => {
         * component MS SU ?!
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(3);
         assertFlagRule(profile.rules[0], 'category', true, undefined, true);
@@ -256,7 +256,7 @@ describe('FSHImporter', () => {
         * subject, focus ?!
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(5);
         assertFlagRule(profile.rules[0], 'category', true, undefined, undefined);
@@ -274,7 +274,7 @@ describe('FSHImporter', () => {
         * subject, focus ?! SU
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(5);
         assertFlagRule(profile.rules[0], 'category', true, true, undefined);
@@ -296,7 +296,7 @@ describe('FSHImporter', () => {
         * component.code from ComponentCodeValueSet (example)
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(4);
         assertValueSetRule(profile.rules[0], 'category', 'CategoryValueSet', 'required');
@@ -315,7 +315,7 @@ describe('FSHImporter', () => {
         * component.code from http://example.org/fhir/ValueSet/ComponentCodeValueSet (example)
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(4);
         assertValueSetRule(
@@ -359,7 +359,7 @@ describe('FSHImporter', () => {
         * component.code from COMP (example)
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(4);
         assertValueSetRule(
@@ -396,7 +396,7 @@ describe('FSHImporter', () => {
         * code from http://example.org/fhir/ValueSet/CodeValueSet
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(2);
         assertValueSetRule(profile.rules[0], 'category', 'CategoryValueSet', 'required');
@@ -417,7 +417,7 @@ describe('FSHImporter', () => {
         * valueBoolean = true
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         assertFixedValueRule(profile.rules[0], 'valueBoolean', true);
@@ -430,7 +430,7 @@ describe('FSHImporter', () => {
         * valueDecimal = 1.23
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         assertFixedValueRule(profile.rules[0], 'valueDecimal', 1.23);
@@ -443,7 +443,7 @@ describe('FSHImporter', () => {
         * valueString = "hello world"
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         assertFixedValueRule(profile.rules[0], 'valueString', 'hello world');
@@ -459,7 +459,7 @@ describe('FSHImporter', () => {
             """
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         assertFixedValueRule(profile.rules[0], 'valueString', 'hello\nworld');
@@ -472,7 +472,7 @@ describe('FSHImporter', () => {
         * valueDateTime = 2019-11-01T12:30:01.999Z
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         // For now, treating dates like strings
@@ -486,7 +486,7 @@ describe('FSHImporter', () => {
         * valueTime = 12:30:01.999-05:00
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         // For now, treating dates like strings
@@ -502,7 +502,7 @@ describe('FSHImporter', () => {
         * status = #final
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         const expectedCode = new FshCode('final').withLocation([6, 20, 6, 25]).withFile('');
@@ -518,7 +518,7 @@ describe('FSHImporter', () => {
         * valueCodeableConcept = LOINC#718-7 "Hemoglobin [Mass/volume] in Blood"
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         const expectedCode = new FshCode(
@@ -539,7 +539,7 @@ describe('FSHImporter', () => {
         * valueQuantity = 1.5 'mm'
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         const expectedQuantity = new FshQuantity(
@@ -559,7 +559,7 @@ describe('FSHImporter', () => {
         * valueRatio = 130 'mg' : 1 'dL'
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         const expectedRatio = new FshRatio(
@@ -589,7 +589,7 @@ describe('FSHImporter', () => {
         * valueRatio = 130 : 1 'dL'
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         const expectedRatio = new FshRatio(
@@ -614,7 +614,7 @@ describe('FSHImporter', () => {
         * valueRatio = 130 'mg' : 1
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         const expectedRatio = new FshRatio(
@@ -639,7 +639,7 @@ describe('FSHImporter', () => {
         * valueRatio = 130 : 1
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         const expectedRatio = new FshRatio(
@@ -660,7 +660,7 @@ describe('FSHImporter', () => {
         * value[x] only Quantity
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         assertOnlyRule(profile.rules[0], 'value[x]', { type: 'Quantity' });
@@ -673,7 +673,7 @@ describe('FSHImporter', () => {
         * value[x] only Quantity or CodeableConcept or string
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         assertOnlyRule(
@@ -692,7 +692,7 @@ describe('FSHImporter', () => {
         * performer only Reference(Practitioner)
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         assertOnlyRule(profile.rules[0], 'performer', { type: 'Practitioner', isReference: true });
@@ -705,7 +705,7 @@ describe('FSHImporter', () => {
         * performer only Reference(Organization | CareTeam)
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         assertOnlyRule(
@@ -726,7 +726,7 @@ describe('FSHImporter', () => {
         * value[x] only CodeableConcept or CODING or string or QUANTITY
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(1);
         assertOnlyRule(
@@ -748,7 +748,7 @@ describe('FSHImporter', () => {
         * component contains SystolicBP 1..1
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(2);
         assertContainsRule(profile.rules[0], 'component', 'SystolicBP');
@@ -762,7 +762,7 @@ describe('FSHImporter', () => {
         * component contains SystolicBP 1..1 and DiastolicBP 2..*
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(3);
         assertContainsRule(profile.rules[0], 'component', 'SystolicBP', 'DiastolicBP');
@@ -777,7 +777,7 @@ describe('FSHImporter', () => {
         * component contains SystolicBP 1..1 MS and DiastolicBP 2..* MS SU
         `;
 
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(5);
         assertContainsRule(profile.rules[0], 'component', 'SystolicBP', 'DiastolicBP');
@@ -797,7 +797,7 @@ describe('FSHImporter', () => {
         * ^experimental = false
         * ^keyword[0] = foo#bar "baz"
         `;
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         assertCaretValueRule(profile.rules[0], '', 'description', 'foo');
         assertCaretValueRule(profile.rules[1], '', 'experimental', false);
@@ -817,7 +817,7 @@ describe('FSHImporter', () => {
         * status ^sliceIsConstraining = false
         * status ^code[0] = foo#bar "baz"
         `;
-        const result = importText(input);
+        const result = importSingleText(input);
         const profile = result.profiles.get('ObservationProfile');
         assertCaretValueRule(profile.rules[0], 'status', 'short', 'foo');
         assertCaretValueRule(profile.rules[1], 'status', 'sliceIsConstraining', false);
@@ -839,7 +839,7 @@ describe('FSHImporter', () => {
         Parent: Observation
         * category obeys SomeInvariant
         `;
-        const result = importText(input, 'Obeys.fsh');
+        const result = importSingleText(input, 'Obeys.fsh');
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(0);
         expect(loggerSpy.getLastMessage()).toMatch(/File: Obeys\.fsh.*Line: 4\D/s);
diff --git a/test/import/FSHImporter.ValueSet.test.ts b/test/import/FSHImporter.ValueSet.test.ts
index 5b70878..9cd9d2a 100644
--- a/test/import/FSHImporter.ValueSet.test.ts
+++ b/test/import/FSHImporter.ValueSet.test.ts
@@ -1,10 +1,10 @@
-import { importText } from '../../src/import';
 import {
   assertValueSetConceptComponent,
   assertValueSetFilterComponent
 } from '../testhelpers/asserts';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { FshCode, VsProperty, VsOperator } from '../../src/fshtypes';
+import { importSingleText } from '../testhelpers/importSingleText';
 
 describe('FSHImporter', () => {
   describe('ValueSet', () => {
@@ -16,7 +16,7 @@ describe('FSHImporter', () => {
         Title: "Simple Value Set"
         Description: "A simple value set for testing metadata"
         `;
-        const result = importText(input, 'Simple.fsh');
+        const result = importSingleText(input, 'Simple.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('SimpleVS');
         expect(valueSet.name).toBe('SimpleVS');
@@ -42,7 +42,7 @@ describe('FSHImporter', () => {
         Title: "Duplicate Value Set"
         Description: "A duplicate value set for testing metadata"
         `;
-        const result = importText(input, 'Dupe.fsh');
+        const result = importSingleText(input, 'Dupe.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('SimpleVS');
         expect(valueSet.name).toBe('SimpleVS');
@@ -68,7 +68,7 @@ describe('FSHImporter', () => {
         Title: "Duplicate Value Set"
         Description: "A duplicate value set for testing metadata"
         `;
-        importText(input, 'Dupe.fsh');
+        importSingleText(input, 'Dupe.fsh');
         expect(loggerSpy.getMessageAtIndex(-3)).toMatch(/File: Dupe\.fsh.*Line: 6\D/s);
         expect(loggerSpy.getMessageAtIndex(-2)).toMatch(/File: Dupe\.fsh.*Line: 7\D/s);
         expect(loggerSpy.getLastMessage()).toMatch(/File: Dupe\.fsh.*Line: 8\D/s);
@@ -81,7 +81,7 @@ describe('FSHImporter', () => {
         ValueSet: SimpleVS
         * ZOO#bear
         `;
-        const result = importText(input, 'Simple.fsh');
+        const result = importSingleText(input, 'Simple.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('SimpleVS');
         expect(valueSet.name).toBe('SimpleVS');
@@ -106,7 +106,7 @@ describe('FSHImporter', () => {
         * #hippo "Hippopotamus" from system ZOO
         `;
 
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -130,7 +130,7 @@ describe('FSHImporter', () => {
         * #hippo "Hippopotamus", #crocodile "Crocodile" from system ZOO
         `;
 
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -156,7 +156,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooVS
         * #hippo
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -168,7 +168,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooVS
         * ZOO#hippo from system ZOO
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -182,7 +182,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooVS
         * codes from system ZOO
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -202,7 +202,7 @@ describe('FSHImporter', () => {
         * codes from valueset FirstZooVS
         * codes from valueset SecondZooVS, ThirdZooVS
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(2);
@@ -227,7 +227,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooVS
         * codes from system ZOO and valueset NorthZooVS, SouthZooVS
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -251,7 +251,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooVS
         * codes from system ZOO where version = "2.0"
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -269,7 +269,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooVS
         * codes from system ZOO where version = /[1-9].*/
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -283,7 +283,7 @@ describe('FSHImporter', () => {
         ValueSet: AllUrsinesVS
         * codes from system ZOO where code is-a #bear "Bear"
         `;
-        const result = importText(input, 'Ursines.fsh');
+        const result = importSingleText(input, 'Ursines.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('AllUrsinesVS');
         expect(valueSet.components.length).toBe(1);
@@ -303,7 +303,7 @@ describe('FSHImporter', () => {
         ValueSet: AllUrsinesVS
         * codes from system ZOO where code is-a "Bear"
         `;
-        const result = importText(input, 'Ursines.fsh');
+        const result = importSingleText(input, 'Ursines.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('AllUrsinesVS');
         expect(valueSet.components.length).toBe(1);
@@ -317,7 +317,7 @@ describe('FSHImporter', () => {
         ValueSet: AllFelinesVS
         * codes from valueset ZooVS where code descendent-of ZOO#cat
         `;
-        const result = importText(input, 'Felines.fsh');
+        const result = importSingleText(input, 'Felines.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('AllFelinesVS');
         expect(valueSet.components.length).toBe(1);
@@ -342,7 +342,7 @@ describe('FSHImporter', () => {
         ValueSet: AllFelinesVS
         * codes from valueset ZooVS where code descendant-of ZOO#cat
         `;
-        const result = importText(input, 'Felines.fsh');
+        const result = importSingleText(input, 'Felines.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('AllFelinesVS');
         expect(valueSet.components.length).toBe(1);
@@ -367,7 +367,7 @@ describe('FSHImporter', () => {
         ValueSet: AllFelinesVS
         * codes from valueset ZooVS where code descendent-of "Cat"
         `;
-        const result = importText(input, 'Felines.fsh');
+        const result = importSingleText(input, 'Felines.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('AllFelinesVS');
         expect(valueSet.components.length).toBe(1);
@@ -381,7 +381,7 @@ describe('FSHImporter', () => {
         ValueSet: NonCanineVS
         * codes from system ZOO where code is-not-a #dog
         `;
-        const result = importText(input, 'NonCanine.fsh');
+        const result = importSingleText(input, 'NonCanine.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('NonCanineVS');
         expect(valueSet.components.length).toBe(1);
@@ -401,7 +401,7 @@ describe('FSHImporter', () => {
         ValueSet: NonCanineVS
         * codes from system ZOO where code is-not-a "dog"
         `;
-        const result = importText(input, 'NonCanine.fsh');
+        const result = importSingleText(input, 'NonCanine.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('NonCanineVS');
         expect(valueSet.components.length).toBe(1);
@@ -415,7 +415,7 @@ describe('FSHImporter', () => {
         ValueSet: ProbablyDogsVS
         * codes from system ZOO where display regex /([Dd]og)|([Cc]anine)/
         `;
-        const result = importText(input, 'MostlyDogs.fsh');
+        const result = importSingleText(input, 'MostlyDogs.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ProbablyDogsVS');
         expect(valueSet.components.length).toBe(1);
@@ -433,7 +433,7 @@ describe('FSHImporter', () => {
         ValueSet: ProbablyDogsVS
         * codes from system ZOO where display regex "Dog|Canine"
         `;
-        const result = importText(input, 'MostlyDogs.fsh');
+        const result = importSingleText(input, 'MostlyDogs.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ProbablyDogsVS');
         expect(valueSet.components.length).toBe(1);
@@ -447,7 +447,7 @@ describe('FSHImporter', () => {
         ValueSet: CatAndDogVS
         * codes from system ZOO where code in "#cat, #dog"
         `;
-        const result = importText(input, 'CatDog.fsh');
+        const result = importSingleText(input, 'CatDog.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('CatAndDogVS');
         expect(valueSet.components.length).toBe(1);
@@ -465,7 +465,7 @@ describe('FSHImporter', () => {
         ValueSet: CatAndDogVS
         * codes from system ZOO where code in ZOO#cat
         `;
-        const result = importText(input, 'CatDog.fsh');
+        const result = importSingleText(input, 'CatDog.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('CatAndDogVS');
         expect(valueSet.components.length).toBe(1);
@@ -479,7 +479,7 @@ describe('FSHImporter', () => {
         ValueSet: NoGooseVS
         * codes from system ZOO where code not-in "#goose"
         `;
-        const result = importText(input, 'NoGoose.fsh');
+        const result = importSingleText(input, 'NoGoose.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('NoGooseVS');
         expect(valueSet.components.length).toBe(1);
@@ -497,7 +497,7 @@ describe('FSHImporter', () => {
         ValueSet: NoGooseVS
         * codes from system ZOO where code not-in /duck|goose/
         `;
-        const result = importText(input, 'NoGoose.fsh');
+        const result = importSingleText(input, 'NoGoose.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('NoGooseVS');
         expect(valueSet.components.length).toBe(1);
@@ -511,7 +511,7 @@ describe('FSHImporter', () => {
         ValueSet: MustelidVS
         * codes from system ZOO where code generalizes #mustela-nivalis "least weasel"
         `;
-        const result = importText(input, 'Mustelids.fsh');
+        const result = importSingleText(input, 'Mustelids.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('MustelidVS');
         expect(valueSet.components.length).toBe(1);
@@ -531,7 +531,7 @@ describe('FSHImporter', () => {
         ValueSet: MustelidVS
         * codes from system ZOO where code generalizes "least weasel"
         `;
-        const result = importText(input, 'Mustelids.fsh');
+        const result = importSingleText(input, 'Mustelids.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('MustelidVS');
         expect(valueSet.components.length).toBe(1);
@@ -546,7 +546,7 @@ describe('FSHImporter', () => {
         * codes from system ZOO where display exists true
         * codes from system ZOO where version exists
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(2);
@@ -571,7 +571,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooVS
         * codes from system ZOO where display exists "display"
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -585,7 +585,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooTwoVS
         * codes from system ZOO where version regex /2\\./ and display exists
         `;
-        const result = importText(input, 'ZooTwo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooTwoVS');
         expect(valueSet.components.length).toBe(1);
@@ -609,7 +609,7 @@ describe('FSHImporter', () => {
         * codes from system ZOO
         * exclude codes from valueset UnavailableAnimalVS
         `;
-        const result = importText(input, 'Available.fsh');
+        const result = importSingleText(input, 'Available.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('AvailableVS');
         expect(valueSet.components.length).toBe(2);
@@ -628,7 +628,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooVS
         * codes from system ZOO where animal exists true
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -641,7 +641,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooVS
         * codes from system ZOO where display resembles "cat"
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
@@ -654,7 +654,7 @@ describe('FSHImporter', () => {
         ValueSet: ZooVS
         * codes from system ZOO where display regex
         `;
-        const result = importText(input, 'Zoo.fsh');
+        const result = importSingleText(input, 'Zoo.fsh');
         expect(result.valueSets.size).toBe(1);
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
diff --git a/test/import/FSHImporter.test.ts b/test/import/FSHImporter.test.ts
index 1c5cd54..c5b18ba 100644
--- a/test/import/FSHImporter.test.ts
+++ b/test/import/FSHImporter.test.ts
@@ -1,48 +1,36 @@
-import { importText, FSHImporter, FSHDocument } from '../../src/import';
+import { importText, RawFSH } from '../../src/import';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { FshCode } from '../../src/fshtypes';
 import { assertFixedValueRule } from '../testhelpers/asserts';
+import { importSingleText } from '../testhelpers/importSingleText';
 
 describe('FSHImporter', () => {
   it('should default filename to blank string', () => {
     const input = '';
-    const result = importText(input);
+    const result = importSingleText(input);
     expect(result.file).toBe('');
   });
 
   it('should retain the passed in file name', () => {
     const input = '';
-    const result = importText(input, 'FSHImporter.test.ts');
+    const result = importSingleText(input, 'FSHImporter.test.ts');
     expect(result.file).toBe('FSHImporter.test.ts');
   });
 
   it('should allow a blank FSH document', () => {
     const input = '';
-    const result = importText(input);
+    const result = importSingleText(input);
     expect(result.aliases.size).toBe(0);
     expect(result.profiles.size).toBe(0);
     expect(result.extensions.size).toBe(0);
   });
 
-  it('should not allow the same visitor instance to be invoked twice', () => {
-    const visitor = new FSHImporter();
-    // First time should work (ts-ignore is to get around typing of DocContext for now)
-    // @ts-ignore
-    const result = visitor.visitDoc({ entity: () => [] });
-    expect(result).toBeInstanceOf(FSHDocument);
-
-    // Second time should not work
-    // @ts-ignore
-    const result2 = visitor.visitDoc({ entity: () => [] });
-    expect(result2).toBeUndefined();
-  });
-
   it('should report mismatched input errors from antlr', () => {
     const input = `
     Profile: MismatchedPizza
     Pizza: Large
     `;
-    importText(input, 'Pizza.fsh');
+    importSingleText(input, 'Pizza.fsh');
     expect(loggerSpy.getLastMessage()).toMatch(/File: Pizza\.fsh.*Line: 3\D/s);
   });
 
@@ -51,7 +39,7 @@ describe('FSHImporter', () => {
     Profile: Something Spaced
     Parent: Spacious
     `;
-    importText(input, 'Space.fsh');
+    importSingleText(input, 'Space.fsh');
     expect(loggerSpy.getLastMessage()).toMatch(/File: Space\.fsh.*Line: 2\D/s);
   });
 
@@ -62,7 +50,7 @@ describe('FSHImporter', () => {
     Title: "Can \\"you\\" escape \\\\ this \\\\ string?"
     * code = #"This \\\\ code \\"is quite\\" challenging \\\\to escape"
     `;
-    const result = importText(input, 'Escape.fsh');
+    const result = importSingleText(input, 'Escape.fsh');
     const profile = result.profiles.get('Escape');
     expect(profile.title).toBe('Can "you" escape \\ this \\ string?');
     const expectedCode = new FshCode('This \\ code "is quite" challenging \\to escape')
@@ -78,7 +66,7 @@ describe('FSHImporter', () => {
     Parent: Observation
     *123 code = #"This rule is identified"
     `;
-    const result = importText(input, 'IdentifyingInteger.fsh');
+    const result = importSingleText(input, 'IdentifyingInteger.fsh');
     const profile = result.profiles.get('IdentifyingInteger');
     const expectedCode = new FshCode('This rule is identified')
       .withLocation([4, 17, 4, 42])
@@ -86,4 +74,30 @@ describe('FSHImporter', () => {
     expect(profile.rules).toHaveLength(1);
     assertFixedValueRule(profile.rules[0], 'code', expectedCode);
   });
+
+  it('should allow two FSH documents', () => {
+    const input = '';
+    const input2 = '';
+    const results = importText([new RawFSH(input), new RawFSH(input2)]);
+    expect(results.length).toBe(2);
+    expect(results[0].aliases.size).toBe(0);
+    expect(results[0].profiles.size).toBe(0);
+    expect(results[0].extensions.size).toBe(0);
+    expect(results[1].aliases.size).toBe(0);
+    expect(results[1].profiles.size).toBe(0);
+    expect(results[1].extensions.size).toBe(0);
+  });
+
+  it('should allow two FSH documents even when first is invalid', () => {
+    const input = 'invalid FSH string';
+    const input2 = '';
+    const results = importText([new RawFSH(input, 'Invalid.fsh'), new RawFSH(input2, 'Blank.fsh')]);
+    expect(results.length).toBe(2);
+    expect(results[0].aliases.size).toBe(0);
+    expect(results[0].profiles.size).toBe(0);
+    expect(results[0].extensions.size).toBe(0);
+    expect(results[1].aliases.size).toBe(0);
+    expect(results[1].profiles.size).toBe(0);
+    expect(results[1].extensions.size).toBe(0);
+  });
 });
diff --git a/test/testhelpers/importSingleText.ts b/test/testhelpers/importSingleText.ts
new file mode 100644
index 0000000..cb72255
--- /dev/null
+++ b/test/testhelpers/importSingleText.ts
@@ -0,0 +1,5 @@
+import { importText, FSHDocument, RawFSH } from '../../src/import';
+
+export function importSingleText(content: string, path?: string): FSHDocument {
+  return importText([new RawFSH(content, path)])[0];
+}