Repository: FHIR/sushi (ts)

Issue:
Annoying white space dependency

THis is OK:   Alias:   $OrgType = http://terminology.hl7.org/CodeSystem/organization-type

This is NOT:  Alias:   $OrgType= http://terminology.hl7.org/CodeSystem/organization-type

Pull request:
Translate some ANTLR errors to more user friendly messages
Provide user friendly messages if we detect the following problems:
* missing space around =
* missing space around ->
* missing space after *

This also addresses one issue that can arise when ANTLR hits a parsing error and tries to keep going -- which is that sometimes we'll process a rule that has missing required tokens.  In this case, we want to be a bit more defensive and due a null check before dereferencing the token.

This also adds a global.d.ts to import jest-extended to allow for jest-extended matchers to work properly in VS Code. This has no effect on the distributed module.

Fixes #318 

Patch:
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000..4e8b709
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1,3 @@
+// Needed to support running tests w/ jest-extended in VS Code
+// See: https://github.com/jest-community/jest-extended#typescript
+import 'jest-extended';
diff --git a/src/import/FSHErrorListener.ts b/src/import/FSHErrorListener.ts
index ef9be2e..fcac02a 100644
--- a/src/import/FSHErrorListener.ts
+++ b/src/import/FSHErrorListener.ts
@@ -1,6 +1,7 @@
-import { ErrorListener } from 'antlr4/error';
+import { TextLocation } from '../fshtypes';
 import { logger } from '../utils/FSHLogger';
 import { Recognizer, Token } from 'antlr4';
+import { ErrorListener } from 'antlr4/error';
 
 export class FSHErrorListener extends ErrorListener {
   constructor(readonly file: string) {
@@ -14,14 +15,175 @@ export class FSHErrorListener extends ErrorListener {
     column: number,
     msg: string
   ): void {
-    logger.error(msg, {
-      file: this.file,
-      location: {
-        startLine: line,
-        startColumn: column + 1,
-        endLine: line,
-        endColumn: column + offendingSymbol.text.length
-      }
-    });
+    let message = msg;
+    let location: TextLocation = {
+      startLine: line,
+      startColumn: column + 1,
+      endLine: line,
+      endColumn: column + offendingSymbol.text.length
+    };
+
+    // Now attempt to detect known patterns and improve the error messages
+    const oneTokenBack = getPreviousNonWsToken(recognizer, offendingSymbol);
+    const twoTokensBack = getPreviousNonWsToken(recognizer, oneTokenBack);
+
+    // ########################################################################
+    // # Missing space around =                                               #
+    // ########################################################################
+
+    // Alias: MyAlias =http://myserver.com/
+    // > missing '=' at '=http://myserver.com/'
+    if (
+      /^missing '='/.test(msg) &&
+      /^=/.test(offendingSymbol.text) &&
+      !/^\^/.test(oneTokenBack?.text)
+    ) {
+      message =
+        "Alias declarations must include at least one space both before and after the '=' sign";
+    }
+
+    // Alias: MyAlias= http://myserver.com/
+    // > missing '=' at 'http://myserver.com/'
+    else if (/^missing '='/.test(msg) && /^[^\^].*=$/.test(oneTokenBack?.text)) {
+      message =
+        "Alias declarations must include at least one space both before and after the '=' sign";
+      // Need to adjust the location to match the previous token (where '=' is)
+      location = getTokenLocation(oneTokenBack);
+    }
+
+    // Alias: MyAlias=http://myserver.com/
+    // > mismatched input '<EOF>' expecting '='
+    else if (
+      /^mismatched input .+ expecting '='$/.test(msg) &&
+      /^[^\^].*=/.test(oneTokenBack?.text)
+    ) {
+      message =
+        "Alias declarations must include at least one space both before and after the '=' sign";
+      // Need to adjust the location to match the previous token (where '=' is)
+      location = getTokenLocation(oneTokenBack);
+    }
+
+    // * active= true
+    // > no viable alternative at input '* active= true'
+    // * active =true
+    // > no viable alternative at input '* active =true'
+    // * active=true
+    // > no viable alternative at input '* active=true'
+    else if (/^no viable alternative at input '.*((\S=)|(=\S))/.test(msg)) {
+      message =
+        "Assignment rules must include at least one space both before and after the '=' sign";
+    }
+
+    // * component ^short ="Component1"
+    // > 'missing '=' at '="Component1"''
+    else if (
+      /^missing '='/.test(msg) &&
+      /^=/.test(offendingSymbol.text) &&
+      /^\^/.test(oneTokenBack?.text)
+    ) {
+      message =
+        "Assignment rules must include at least one space both before and after the '=' sign";
+    }
+
+    // * component ^short ="A component"
+    // > extraneous input 'component\"' expecting {<EOF>, KW_ALIAS, KW_PROFILE, KW_EXTENSION, KW_INSTANCE, KW_INVARIANT, KW_VALUESET, KW_CODESYSTEM, KW_RULESET, KW_MAPPING}
+    else if (
+      /^extraneous input/.test(msg) &&
+      /^\^/.test(twoTokensBack?.text) &&
+      /^=/.test(oneTokenBack?.text)
+    ) {
+      message =
+        "Assignment rules must include at least one space both before and after the '=' sign";
+      // Need to adjust the location to match the previous token (where '=' is)
+      location = getTokenLocation(oneTokenBack);
+    }
+
+    // * component ^short= "Component1"
+    // > 'missing '=' at '"Component1"''
+    // * component ^short= "A component"
+    // > missing '=' at '"A component"'
+    // * component ^short="A component"
+    // > missing '=' at 'component"'
+    else if (/^missing '='/.test(msg) && /^\^.+=/.test(oneTokenBack?.text)) {
+      message =
+        "Assignment rules must include at least one space both before and after the '=' sign";
+      // Need to adjust the location to match the previous token (where '=' is)
+      location = getTokenLocation(oneTokenBack);
+    }
+
+    // * component ^short="Component1"
+    // > 'mismatched input '<EOF>' expecting '=''
+    else if (/^mismatched input .+ expecting '='$/.test(msg) && /^\^.+=/.test(oneTokenBack?.text)) {
+      message =
+        "Assignment rules must include at least one space both before and after the '=' sign";
+      // Need to adjust the location to match the previous token (where '=' is)
+      location = getTokenLocation(oneTokenBack);
+    }
+
+    // ########################################################################
+    // # Missing spaces around ->                                             #
+    // ########################################################################
+
+    // * identifier ->"Patient.identifier"
+    // > mismatched input '->\"Patient.identifier\"' expecting '->'
+    // * identifier->"Patient.identifier"
+    // > mismatched input '<EOF>' expecting '->'
+    else if (/^mismatched input .+ expecting '->'$/.test(msg)) {
+      message =
+        "Mapping rules must include at least one space both before and after the '->' operator";
+      // Need to adjust the location to match the previous token (where '=' is)
+      location = getTokenLocation(oneTokenBack);
+    }
+
+    // * identifier-> "Patient.identifier"
+    // > missing '->' at '"Patient.identifier"'
+    else if (/^missing '->'/.test(msg) && /->$/.test(oneTokenBack?.text)) {
+      message =
+        "Mapping rules must include at least one space both before and after the '->' operator";
+      // Need to adjust the location to match the previous token (where '=' is)
+      location = getTokenLocation(oneTokenBack);
+    }
+
+    // ########################################################################
+    // # Missing space after *                                                #
+    // ########################################################################
+
+    // *active = true
+    // > extraneous input '*component' expecting {<EOF>, KW_ALIAS, KW_PROFILE, KW_EXTENSION,
+    // > KW_INSTANCE, KW_INVARIANT, KW_VALUESET, KW_CODESYSTEM, KW_RULESET, KW_MAPPING}
+    else if (/^extraneous input '\*\S/.test(msg)) {
+      message = "Rules must start with a '*' symbol followed by at least one space";
+    }
+
+    logger.error(message, { file: this.file, location });
+  }
+}
+
+/**
+ * Gets the previous non-whitespace token, which may be needed to help interpret the error
+ * @param recognizer - the Recognizer instance provided by ANTLR
+ * @param token - the token ahead of the token we want returned
+ * @returns the previous non-WS token or undefined if there is no previous non-WS token
+ */
+function getPreviousNonWsToken(recognizer: Recognizer, token: Token): Token | undefined {
+  if (token == null) {
+    return;
+  }
+
+  for (let i = token.tokenIndex - 1; i >= 0; i--) {
+    // @ts-ignore _input is private, but we need it
+    const previousToken = recognizer._input.tokens[i];
+    if (/\S/.test(previousToken.text)) {
+      return previousToken;
+    }
   }
 }
+
+function getTokenLocation(token: Token): TextLocation {
+  return {
+    startLine: token.line,
+    startColumn: token.column + 1,
+    endLine: token.line,
+    endColumn: token.column + token.text.length
+  };
+}
diff --git a/src/import/FSHImporter.ts b/src/import/FSHImporter.ts
index ccfbcbe..4368e4d 100644
--- a/src/import/FSHImporter.ts
+++ b/src/import/FSHImporter.ts
@@ -1090,6 +1090,11 @@ export class FSHImporter extends FSHVisitor {
   }
 
   visitValue(ctx: pc.ValueContext): FixedValueType {
+    // In cases where the parser encounters an error, ctx might be null
+    if (ctx == null) {
+      return;
+    }
+
     if (ctx.SEQUENCE()) {
       return this.aliasAwareValue(ctx, ctx.SEQUENCE().getText());
     }
@@ -1357,7 +1362,7 @@ export class FSHImporter extends FSHVisitor {
     caretValueRule.caretPath = this.visitCaretPath(ctx.caretPath()).slice(1);
     caretValueRule.value = this.visitValue(ctx.value());
     caretValueRule.isInstance =
-      ctx.value().SEQUENCE() != null && !this.allAliases.has(ctx.value().SEQUENCE().getText());
+      ctx.value()?.SEQUENCE() != null && !this.allAliases.has(ctx.value().SEQUENCE().getText());
     return caretValueRule;
   }
 
@@ -1655,7 +1660,7 @@ export class FSHImporter extends FSHVisitor {
   }
 
   private extractString(stringCtx: ParserRuleContext): string {
-    const str = stringCtx.getText();
+    const str = stringCtx?.getText() ?? '""'; // default to empty string if stringCtx is null
     const strNoQuotes = str.slice(1, str.length - 1); // Strip surrounding quotes
 
     // Replace escaped characters


Test patch:
diff --git a/test/import/FSHErrorListener.test.ts b/test/import/FSHErrorListener.test.ts
new file mode 100644
index 0000000..0299208
--- /dev/null
+++ b/test/import/FSHErrorListener.test.ts
@@ -0,0 +1,322 @@
+import { loggerSpy } from '../testhelpers/loggerSpy';
+import { importSingleText } from '../testhelpers/importSingleText';
+
+describe('FSHErrorListener', () => {
+  beforeEach(() => loggerSpy.reset());
+
+  it('should not log any errors for valid FSH', () => {
+    const input = `
+    Profile: MyPatient
+    Parent: Patient
+    * active = true
+    `;
+    importSingleText(input, 'MyPatient.fsh');
+    expect(loggerSpy.getAllMessages('error')).toBeEmpty();
+  });
+
+  it('should report mismatched input errors from antlr', () => {
+    const input = `
+    Profile: MismatchedPizza
+    Pizza: Large
+    `;
+    importSingleText(input, 'Pizza.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Pizza\.fsh.*Line: 3\D*/s);
+  });
+
+  it('should report extraneous input errors from antlr', () => {
+    const input = `
+    Profile: Something Spaced
+    Parent: Spacious
+    `;
+    importSingleText(input, 'Space.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Space\.fsh.*Line: 2\D*/s);
+  });
+
+  // ########################################################################
+  // # Missing space around =                                               #
+  // ########################################################################
+
+  it('should make sense of errors due to no space before = in an Alias definition', () => {
+    const input = `
+    Alias: $OrgType= http://terminology.hl7.org/CodeSystem/organization-type
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Alias declarations must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 2\D*/s
+    );
+  });
+
+  it('should make sense of errors due to no space after = in an Alias definition', () => {
+    const input = `
+    Alias: $OrgType =http://terminology.hl7.org/CodeSystem/organization-type
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Alias declarations must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 2\D*/s
+    );
+  });
+
+  it('should make sense of errors due to no space around = in an Alias definition', () => {
+    const input = `
+    Alias: $OrgType=http://terminology.hl7.org/CodeSystem/organization-type
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Alias declarations must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 2\D*/s
+    );
+  });
+
+  it('should make sense of errors due to no space before = in an assignment rule', () => {
+    const input = `
+    Profile: SampleObservation
+    Parent: Observation
+    * component= FOO#bar
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+
+    // Test with a multi-word string as well since sometimes that results in a different error
+    loggerSpy.reset();
+    const input2 = `
+    Profile: SampleObservation
+    Parent: Observation
+    * valueString= "My Value"
+    `;
+    importSingleText(input2, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+  });
+
+  it('should make sense of errors due to no space after = in an assignment rule', () => {
+    const input = `
+    Profile: SampleObservation
+    Parent: Observation
+    * component =FOO#bar
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+
+    // Test with a multi-word string as well since sometimes that results in a different error
+    loggerSpy.reset();
+    const input2 = `
+    Profile: SampleObservation
+    Parent: Observation
+    * valueString ="My Value"
+    `;
+    importSingleText(input2, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+  });
+
+  it('should make sense of errors due to no space around = in an assignment rule', () => {
+    const input = `
+    Profile: SampleObservation
+    Parent: Observation
+    * component=FOO#bar
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+
+    // Test with a multi-word string as well since sometimes that results in a different error
+    loggerSpy.reset();
+    const input2 = `
+    Profile: SampleObservation
+    Parent: Observation
+    * valueString="My Value"
+    `;
+    importSingleText(input2, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+  });
+
+  it('should make sense of errors due to no space before = in a caret rule', () => {
+    const input = `
+    Profile: SampleObservation
+    Parent: Observation
+    * component ^short= "Component1"
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+
+    // Test with a multi-word string as well since sometimes that results in a different error
+    loggerSpy.reset();
+    const input2 = `
+    Profile: SampleObservation
+    Parent: Observation
+    * component ^short= "A component"
+    `;
+    importSingleText(input2, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+  });
+
+  it('should make sense of errors due to no space after = in a caret rule', () => {
+    const input = `
+    Profile: SampleObservation
+    Parent: Observation
+    * component ^short ="Component1"
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+
+    // Test with a multi-word string as well since sometimes that results in a different error
+    loggerSpy.reset();
+    const input2 = `
+    Profile: SampleObservation
+    Parent: Observation
+    * component ^short ="A component"
+    `;
+    importSingleText(input2, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+  });
+
+  it('should make sense of errors due to no space around = in a caret rule', () => {
+    const input = `
+    Profile: SampleObservation
+    Parent: Observation
+    * component ^short="Component1"
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+
+    // Test with a multi-word string as well since sometimes that results in a different error
+    loggerSpy.reset();
+    const input2 = `
+    Profile: SampleObservation
+    Parent: Observation
+    * component ^short="A component"
+    `;
+    importSingleText(input2, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Assignment rules must include at least one space both before and after the '=' sign.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+  });
+
+  // ########################################################################
+  // # Missing spaces around ->                                             #
+  // ########################################################################
+
+  it('should make sense of errors due to no space after -> in a mapping rule', () => {
+    const input = `
+    Mapping:  USCorePatientToArgonaut
+    Source:   USCorePatient
+    Target:   "http://unknown.org/Argonaut-DQ-DSTU2"
+    Title:    "Argonaut DSTU2"
+    Id:       argonaut-dq-dstu2
+    * identifier ->"Patient.identifier"
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Mapping rules must include at least one space both before and after the '->' operator.*File: Invalid\.fsh.*Line: 7\D*/s
+    );
+
+    // Test with a multi-word string as well since sometimes that results in a different error
+    loggerSpy.reset();
+    const input2 = `
+    Mapping:  USCorePatientToArgonaut
+    Source:   USCorePatient
+    Target:   "http://unknown.org/Argonaut-DQ-DSTU2"
+    Title:    "Argonaut DSTU2"
+    Id:       argonaut-dq-dstu2
+    * identifier ->"Patient identifier"
+    `;
+    importSingleText(input2, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Mapping rules must include at least one space both before and after the '->' operator.*File: Invalid\.fsh.*Line: 7\D*/s
+    );
+  });
+
+  it('should make sense of errors due to no space before -> in a mapping rule', () => {
+    const input = `
+    Mapping:  USCorePatientToArgonaut
+    Source:   USCorePatient
+    Target:   "http://unknown.org/Argonaut-DQ-DSTU2"
+    Title:    "Argonaut DSTU2"
+    Id:       argonaut-dq-dstu2
+    * identifier-> "Patient.identifier"
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Mapping rules must include at least one space both before and after the '->' operator.*File: Invalid\.fsh.*Line: 7\D*/s
+    );
+
+    // Test with a multi-word string as well since sometimes that results in a different error
+    loggerSpy.reset();
+    const input2 = `
+    Mapping:  USCorePatientToArgonaut
+    Source:   USCorePatient
+    Target:   "http://unknown.org/Argonaut-DQ-DSTU2"
+    Title:    "Argonaut DSTU2"
+    Id:       argonaut-dq-dstu2
+    * identifier-> "Patient identifier"
+    `;
+    importSingleText(input2, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Mapping rules must include at least one space both before and after the '->' operator.*File: Invalid\.fsh.*Line: 7\D*/s
+    );
+  });
+
+  it('should make sense of errors due to no space around -> in a mapping rule', () => {
+    const input = `
+    Mapping:  USCorePatientToArgonaut
+    Source:   USCorePatient
+    Target:   "http://unknown.org/Argonaut-DQ-DSTU2"
+    Title:    "Argonaut DSTU2"
+    Id:       argonaut-dq-dstu2
+    * identifier->"Patient.identifier"
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Mapping rules must include at least one space both before and after the '->' operator.*File: Invalid\.fsh.*Line: 7\D*/s
+    );
+
+    // Test with a multi-word string as well since sometimes that results in a different error
+    loggerSpy.reset();
+    const input2 = `
+    Mapping:  USCorePatientToArgonaut
+    Source:   USCorePatient
+    Target:   "http://unknown.org/Argonaut-DQ-DSTU2"
+    Title:    "Argonaut DSTU2"
+    Id:       argonaut-dq-dstu2
+    * identifier->"Patient identifier"
+    `;
+    importSingleText(input2, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Mapping rules must include at least one space both before and after the '->' operator.*File: Invalid\.fsh.*Line: 7\D*/s
+    );
+  });
+
+  // ########################################################################
+  // # Missing space after *                                                #
+  // ########################################################################
+
+  it('should make sense of errors due to no space after * in a rule', () => {
+    const input = `
+    Profile: SampleObservation
+    Parent: Observation
+    *component = FOO#bar
+    `;
+    importSingleText(input, 'Invalid.fsh');
+    expect(loggerSpy.getLastMessage('error')).toMatch(
+      /Rules must start with a '\*' symbol followed by at least one space.*File: Invalid\.fsh.*Line: 4\D*/s
+    );
+  });
+});
diff --git a/test/import/FSHImporter.test.ts b/test/import/FSHImporter.test.ts
index 35971d6..bfe8281 100644
--- a/test/import/FSHImporter.test.ts
+++ b/test/import/FSHImporter.test.ts
@@ -27,24 +27,6 @@ describe('FSHImporter', () => {
     expect(result.extensions.size).toBe(0);
   });
 
-  it('should report mismatched input errors from antlr', () => {
-    const input = `
-    Profile: MismatchedPizza
-    Pizza: Large
-    `;
-    importSingleText(input, 'Pizza.fsh');
-    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Pizza\.fsh.*Line: 3\D*/s);
-  });
-
-  it('should report extraneous input errors from antlr', () => {
-    const input = `
-    Profile: Something Spaced
-    Parent: Spacious
-    `;
-    importSingleText(input, 'Space.fsh');
-    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Space\.fsh.*Line: 2\D*/s);
-  });
-
   it('should recover from extraneous input errors from antlr', () => {
     const input = `
     OOPS!
@@ -337,10 +319,10 @@ describe('FSHImporter', () => {
     Description: """
     This
       Description
-      
+
         Looks
           Like
-          
+
             A
               Staircase
                 """`;
@@ -463,7 +445,7 @@ describe('FSHImporter', () => {
   it('should avoid crashing and log error messages because of mismatched input', () => {
     const input = `
     Profile: "BadProfile"
-  
+
     Profile: BetterProfile
     Id: "BadId"
     Description: BadDescription