Repository: MaskingTechnology/jitar (ts)

Issue:
UnexpectedKeyword [Error]: Unexpected keyword 'as' at position 8482
When building a full stack application using Vite, the front end is minimized using rollup. The minified files include the react library, and randomly named functions. The react items that are added contain the keyword `as` in an error string.

```ts
Error("Objects are not valid as a React child (found: " ...
```

Or as the name of a function

```ts
function as(e,n,t){var r,l={},i=null,o=null;t!==void 0&&(i=""+t),n.key!==void 0&&(i=""+n.key),n.ref!==void 0&&(o=n.ref);
```

The parser seems to trip over these values, and throws an error. These generated files prevent jitar to start, and the influence we have on the generated values is minimal.

To get Jitar started, it helps to replace the `as` values in the file to `als`. Then Jitar will simply start and function normally.

The generated file is attached to this ticket below.

[assets.zip](https://github.com/MaskingTechnology/jitar/files/13813282/assets.zip)


Pull request:
fix(reflection): non-reserved keywords as identifiers
Fixes #427 

Changes proposed in this pull request:
- Added support for non-reserved keywords as identifiers

@MaskingTechnology/jitar


Patch:
diff --git a/packages/reflection/src/parser/Parser.ts b/packages/reflection/src/parser/Parser.ts
index b19f111..e0a5fc9 100644
--- a/packages/reflection/src/parser/Parser.ts
+++ b/packages/reflection/src/parser/Parser.ts
@@ -5,7 +5,7 @@ import TokenList from './TokenList.js';
 
 import { Divider, isDivider } from './definitions/Divider.js';
 import { Group } from './definitions/Group.js';
-import { Keyword, isDeclaration, isKeyword } from './definitions/Keyword.js';
+import { Keyword, isDeclaration, isKeyword, isNotReserved } from './definitions/Keyword.js';
 import { List } from './definitions/List.js';
 import { Operator } from './definitions/Operator.js';
 import { Scope } from './definitions/Scope.js';
@@ -184,13 +184,24 @@ export default class Parser
         }
         else if (token.isType(TokenType.KEYWORD))
         {
-            if (token.hasValue(Keyword.ASYNC))
+            if (isNotReserved(token.value))
             {
-                tokenList.step(); // Read away the async keyword
-
-                return this.#parseNext(tokenList, true);
+                const next = tokenList.next;
+                const nextIsFunction = next !== undefined && (next.hasValue(Keyword.FUNCTION) || next.hasValue(Group.OPEN));
+                
+                if (token.hasValue(Keyword.ASYNC) && nextIsFunction)
+                {
+                    tokenList.step(); // Read away the async keyword
+
+                    return this.#parseNext(tokenList, true);
+                }
+                else if (next === undefined || this.#atEndOfStatement(next))
+                {
+                    return this.#parseExpression(tokenList);
+                }
             }
-            else if (token.hasValue(Keyword.RETURN))
+            
+            if (token.hasValue(Keyword.RETURN))
             {
                 return this.#parseExpression(tokenList);
             }
@@ -376,7 +387,7 @@ export default class Parser
             stepSize++;
         }
 
-        const name = token.isType(TokenType.IDENTIFIER) ? token.value : ANONYMOUS_IDENTIFIER;
+        const name = this.#isIdentifier(token) ? token.value : ANONYMOUS_IDENTIFIER;
         const as = isDefault ? DEFAULT_IDENTIFIER : name;
         let from: string | undefined = undefined;
 
@@ -550,7 +561,7 @@ export default class Parser
             token = tokenList.step(); // Read away the generator operator
         }
 
-        if (token.isType(TokenType.IDENTIFIER))
+        if (this.#isIdentifier(token))
         {
             isPrivate = token.value.startsWith(PRIVATE_INDICATOR);
             name = isPrivate ? token.value.substring(1) : token.value;
@@ -669,7 +680,7 @@ export default class Parser
         let name = ANONYMOUS_IDENTIFIER;
         let parent: string | undefined = undefined;
 
-        if (token.isType(TokenType.IDENTIFIER))
+        if (this.#isIdentifier(token))
         {
             name = token.value;
 
@@ -910,4 +921,10 @@ export default class Parser
             || [List.CLOSE, Group.CLOSE, Scope.CLOSE].includes(token.value)
             || isKeyword(token.value);
     }
+
+    #isIdentifier(token: Token): boolean
+    {
+        return token.isType(TokenType.IDENTIFIER)
+            || (token.isType(TokenType.KEYWORD) && isNotReserved(token.value));
+    }
 }
diff --git a/packages/reflection/src/parser/definitions/Keyword.ts b/packages/reflection/src/parser/definitions/Keyword.ts
index 10f09a5..1f02cbf 100644
--- a/packages/reflection/src/parser/definitions/Keyword.ts
+++ b/packages/reflection/src/parser/definitions/Keyword.ts
@@ -37,4 +37,13 @@ function isDeclaration(value: string): boolean
         || value === Keyword.VAR;
 }
 
-export { Keyword, isKeyword, isDeclaration };
+function isNotReserved(value: string): boolean
+{
+    return value === Keyword.AS
+        || value === Keyword.ASYNC
+        || value === Keyword.FROM
+        || value === Keyword.GET
+        || value === Keyword.SET;
+}
+
+export { Keyword, isKeyword, isDeclaration, isNotReserved };


Test patch:
diff --git a/packages/reflection/test/_fixtures/parser/Parser.fixture.ts b/packages/reflection/test/_fixtures/parser/Parser.fixture.ts
index fd7c433..86a928e 100644
--- a/packages/reflection/test/_fixtures/parser/Parser.fixture.ts
+++ b/packages/reflection/test/_fixtures/parser/Parser.fixture.ts
@@ -49,7 +49,9 @@ const DECLARATIONS =
     OBJECT: "const object = { key1: 'value1', key2: 'value2' };",
     REGEX: "const regex = /regex/g;",
     DESTRUCTURING_ARRAY: "const [value1, value2 = true] = array;",
-    DESTRUCTURING_OBJECT: "const {key1, key2 = false} = object;"
+    DESTRUCTURING_OBJECT: "const {key1, key2 = false} = object;",
+    KEYWORD_AS_NAME: "const as = 'value';",
+    KEYWORD_AS_VALUE: "const alias = as;"
 };
 
 const FUNCTIONS =
@@ -74,6 +76,7 @@ const FUNCTIONS =
     DESTRUCTURING_REST_PARAMETERS: "function name({ param1, param2 }, [ param3, ...param4 ]) {}",
     SIMPLE_BODY: "function name() { return 'value'; }",
     BLOCK_BODY: "function name() { if (true) { return 'value'; } }",
+    KEYWORD_AS_NAME: "function as() {}"
 };
 
 const CLASSES =
@@ -158,6 +161,13 @@ export class Person
 
 const peter = new Person(name, 42);
 
+async function async() { }
+const a = async;
+const b = async () => {};
+
+const as = 12;
+export { as as hi };
+
 export { name, peter };
 `,
     UNTERMINATED:
diff --git a/packages/reflection/test/parser/Parser.spec.ts b/packages/reflection/test/parser/Parser.spec.ts
index 8a7d28b..45c3d2c 100644
--- a/packages/reflection/test/parser/Parser.spec.ts
+++ b/packages/reflection/test/parser/Parser.spec.ts
@@ -414,6 +414,24 @@ describe('parser/Parser', () =>
             expect(secondMember.value).toBeInstanceOf(ReflectionExpression);
             expect(secondMember.value?.definition).toBe('false');
         });
+
+        it('should parse a declaration with a non reserved keyword as name', () =>
+        {
+            const declaration = parser.parseDeclaration(DECLARATIONS.KEYWORD_AS_NAME);
+
+            expect(declaration.name).toBe('as');
+            expect(declaration.value).toBeInstanceOf(ReflectionExpression);
+            expect(declaration.value?.definition).toBe("'value'");
+        });
+
+        it('should parse a declaration that refers to a non reserved keyword as value', () =>
+        {
+            const declaration = parser.parseDeclaration(DECLARATIONS.KEYWORD_AS_VALUE);
+
+            expect(declaration.name).toBe('alias');
+            expect(declaration.value).toBeInstanceOf(ReflectionExpression);
+            expect(declaration.value?.definition).toBe('as');
+        });
     });
 
     describe('.parseFunction(code)', () =>
@@ -738,6 +756,17 @@ describe('parser/Parser', () =>
             const parameters = funktion.parameters;
             expect(parameters.length).toBe(0);
         });
+
+        it('should parse function with a non reserved keyword as name', () =>
+        {
+            const funktion = parser.parseFunction(FUNCTIONS.KEYWORD_AS_NAME);
+            expect(funktion.name).toBe('as');
+            expect(funktion.isAsync).toBe(false);
+            expect(funktion.body).toBe("{ }");
+
+            const parameters = funktion.parameters;
+            expect(parameters.length).toBe(0);
+        });
     });
 
     describe('.parseClass(code)', () =>
@@ -918,19 +947,19 @@ describe('parser/Parser', () =>
             const module = parser.parse(MODULES.TERMINATED);
             
             const members = module.members;
-            expect(members.length).toBe(9);
+            expect(members.length).toBe(14);
 
             const imports = module.imports;
             expect(imports.length).toBe(2);
 
             const exports = module.exports;
-            expect(exports.length).toBe(3);
+            expect(exports.length).toBe(4);
 
             const declarations = module.declarations;
-            expect(declarations.length).toBe(2);
+            expect(declarations.length).toBe(4);
 
             const functions = module.functions;
-            expect(functions.length).toBe(1);
+            expect(functions.length).toBe(3);
 
             const classes = module.classes;
             expect(classes.length).toBe(1);