Repository: MaskingTechnology/jitar (ts)

Issue:
The new reflection parser fails on hooks example
**Describe the bug**
The example _start-hooks_ doesn't work with the new reflection parser.

**To Reproduce**
- Go to examples/2-advanced/1-start-hooks
- `npm install`
- `npm run build`
- `npm run start`

Jitar fails to start.

```ts
import { startClient } from '/jitar/client.js';
const client = await startClient('client');
const { default: sayBoth } = await client.import('./greetings/sayBoth.js');
const message = await sayBoth('John', 'Doe');
alert(message);
```

The TypeScript above doesn't get parsed. It fails on the word _default_.

Pull request description:
fix(reflection): added missing parsing options
Fixes #179 

Changes proposed in this pull request:
- added support for destructuring arrays and objects
- added support for character escaping in strings
- improved support for minified code (no space between keyword and literal)

@MaskingTechnology/jitar


Patch:
diff --git a/packages/jitar-reflection/src/models/ReflectionAlias.ts b/packages/jitar-reflection/src/models/ReflectionAlias.ts
index 8c4be99..fea8495 100644
--- a/packages/jitar-reflection/src/models/ReflectionAlias.ts
+++ b/packages/jitar-reflection/src/models/ReflectionAlias.ts
@@ -13,4 +13,9 @@ export default class ReflectionAlias
     get name(): string { return this.#name; }
 
     get as(): string { return this.#as; }
+
+    toString(): string
+    {
+        return `${this.#name} as ${this.#as}`;
+    }
 }
diff --git a/packages/jitar-reflection/src/models/ReflectionClass.ts b/packages/jitar-reflection/src/models/ReflectionClass.ts
index 3041eed..8910240 100644
--- a/packages/jitar-reflection/src/models/ReflectionClass.ts
+++ b/packages/jitar-reflection/src/models/ReflectionClass.ts
@@ -143,4 +143,9 @@ export default class ReflectionClass extends ReflectionMember
 
         return funktion !== undefined && funktion.isPublic;
     }
+
+    toString(): string
+    {
+        return `class ${this.name}${this.#parentName !== undefined ? ` extends ${this.#parentName}` : ''} { ${this.#scope.toString()} }`;
+    }
 }
diff --git a/packages/jitar-reflection/src/models/ReflectionExport.ts b/packages/jitar-reflection/src/models/ReflectionExport.ts
index e54c634..5ee8798 100644
--- a/packages/jitar-reflection/src/models/ReflectionExport.ts
+++ b/packages/jitar-reflection/src/models/ReflectionExport.ts
@@ -18,4 +18,9 @@ export default class ReflectionExport extends ReflectionMember
     get members() { return this.#members; }
 
     get from() { return this.#from; }
+
+    toString(): string
+    {
+        return `export { ${this.#members.join(', ')} }${this.#from ? ` from '${this.#from}'` : ''}`;
+    }
 }
diff --git a/packages/jitar-reflection/src/models/ReflectionField.ts b/packages/jitar-reflection/src/models/ReflectionField.ts
index e9aa8f8..8f1b39a 100644
--- a/packages/jitar-reflection/src/models/ReflectionField.ts
+++ b/packages/jitar-reflection/src/models/ReflectionField.ts
@@ -14,4 +14,9 @@ export default class ReflectionField extends ReflectionMember
     }
 
     get value() { return this.#value; }
+
+    toString(): string
+    {
+        return `${this.name}${this.value ? ' = ' + this.value.toString() : ''}`;
+    }
 }
diff --git a/packages/jitar-reflection/src/models/ReflectionFunction.ts b/packages/jitar-reflection/src/models/ReflectionFunction.ts
index bda49b4..c56d8b9 100644
--- a/packages/jitar-reflection/src/models/ReflectionFunction.ts
+++ b/packages/jitar-reflection/src/models/ReflectionFunction.ts
@@ -22,4 +22,11 @@ export default class ReflectionFunction extends ReflectionMember
     get body() { return this.#body; }
 
     get isAsync() { return this.#isAsync; }
+
+    toString(): string
+    {
+        const parameters = this.parameters.map((parameter) => parameter.toString());
+
+        return `${this.isAsync ? 'async ' : ''}${this.name}(${parameters.join(', ')}) { ${this.body} }`;
+    }
 }
diff --git a/packages/jitar-reflection/src/models/ReflectionGenerator.ts b/packages/jitar-reflection/src/models/ReflectionGenerator.ts
index a2ddfb7..a5fe166 100644
--- a/packages/jitar-reflection/src/models/ReflectionGenerator.ts
+++ b/packages/jitar-reflection/src/models/ReflectionGenerator.ts
@@ -3,5 +3,10 @@ import ReflectionFunction from './ReflectionFunction.js';
 
 export default class ReflectionGenerator extends ReflectionFunction
 {
-    
+    toString(): string
+    {
+        const parameters = this.parameters.map((parameter) => parameter.toString());
+
+        return `${this.isAsync ? 'async ' : ''}${this.name}*(${parameters.join(', ')}) { ${this.body} }`;
+    }
 }
diff --git a/packages/jitar-reflection/src/models/ReflectionGetter.ts b/packages/jitar-reflection/src/models/ReflectionGetter.ts
index af3fad1..052e51f 100644
--- a/packages/jitar-reflection/src/models/ReflectionGetter.ts
+++ b/packages/jitar-reflection/src/models/ReflectionGetter.ts
@@ -3,5 +3,8 @@ import ReflectionFunction from './ReflectionFunction.js';
 
 export default class ReflectionGetter extends ReflectionFunction
 {
-    
+    toString(): string
+    {
+        return `get ${super.toString()}`;
+    }
 }
diff --git a/packages/jitar-reflection/src/models/ReflectionImport.ts b/packages/jitar-reflection/src/models/ReflectionImport.ts
index b09db6c..98d02df 100644
--- a/packages/jitar-reflection/src/models/ReflectionImport.ts
+++ b/packages/jitar-reflection/src/models/ReflectionImport.ts
@@ -18,4 +18,9 @@ export default class ReflectionImport extends ReflectionMember
     get members() { return this.#members; }
 
     get from() { return this.#from; }
+
+    toString(): string
+    {
+        return `import { ${this.#members.map(member => member.toString()).join(', ')} } from '${this.#from}';`;
+    }
 }
diff --git a/packages/jitar-reflection/src/models/ReflectionScope.ts b/packages/jitar-reflection/src/models/ReflectionScope.ts
index e00da8d..22cc1da 100644
--- a/packages/jitar-reflection/src/models/ReflectionScope.ts
+++ b/packages/jitar-reflection/src/models/ReflectionScope.ts
@@ -108,4 +108,9 @@ export default class ReflectionScope
     {
         return this.getClass(name) !== undefined;
     }
+
+    toString(): string
+    {
+        return this.#members.map(member => member.toString()).join('\n');
+    }
 }
diff --git a/packages/jitar-reflection/src/models/ReflectionSetter.ts b/packages/jitar-reflection/src/models/ReflectionSetter.ts
index ed777bb..37f7840 100644
--- a/packages/jitar-reflection/src/models/ReflectionSetter.ts
+++ b/packages/jitar-reflection/src/models/ReflectionSetter.ts
@@ -3,5 +3,8 @@ import ReflectionFunction from './ReflectionFunction.js';
 
 export default class ReflectionSetter extends ReflectionFunction
 {
-    
+    toString(): string
+    {
+        return `set ${super.toString()}`;
+    }
 }
diff --git a/packages/jitar-reflection/src/models/ReflectionValue.ts b/packages/jitar-reflection/src/models/ReflectionValue.ts
index dde397d..1c05408 100644
--- a/packages/jitar-reflection/src/models/ReflectionValue.ts
+++ b/packages/jitar-reflection/src/models/ReflectionValue.ts
@@ -9,4 +9,9 @@ export default class ReflectionValue
     }
 
     get definition() { return this.#definition; }
+
+    toString(): string
+    {
+        return this.#definition;
+    }
 }
diff --git a/packages/jitar-reflection/src/parser/Lexer.ts b/packages/jitar-reflection/src/parser/Lexer.ts
index 22d23d1..3be68ad 100644
--- a/packages/jitar-reflection/src/parser/Lexer.ts
+++ b/packages/jitar-reflection/src/parser/Lexer.ts
@@ -124,6 +124,7 @@ export default class Lexer
         const isOther = isEmpty(char)
                      || isWhitespace(char)
                      || isOperator(char)
+                     || isLiteral(char)
                      || isDivider(char)
                      || isGroup(char)
                      || isScope(char)
@@ -176,7 +177,7 @@ export default class Lexer
         {
             const char = charList.current;
 
-            if (isLiteral(char) && char === identifier)
+            if (char === identifier && charList.previous !== '\\')
             {
                 break;
             }
diff --git a/packages/jitar-reflection/src/parser/Parser.ts b/packages/jitar-reflection/src/parser/Parser.ts
index 8a2ec9d..b63ca71 100644
--- a/packages/jitar-reflection/src/parser/Parser.ts
+++ b/packages/jitar-reflection/src/parser/Parser.ts
@@ -3,7 +3,7 @@ import Lexer from './Lexer.js';
 import Token from './Token.js';
 import TokenList from './TokenList.js';
 
-import { Divider } from './definitions/Divider.js';
+import { Divider, isDivider } from './definitions/Divider.js';
 import { Group } from './definitions/Group.js';
 import { Keyword, isDeclaration, isKeyword } from './definitions/Keyword.js';
 import { List } from './definitions/List.js';
@@ -212,8 +212,13 @@ export default class Parser
             // Regular expression or logical not
             return this.#parseExpression(tokenList);
         }
-        else if (token.hasValue(Divider.TERMINATOR) || token.hasValue(Divider.SEPARATOR))
+        else if (isDivider(token.value))
         {
+            // Use cases:
+            // - Terminator as end of statement
+            // - Separator as multi declaration
+            // - Scope as ternary else expression
+
             tokenList.step(); // Read away the divider
 
             return undefined;
@@ -440,11 +445,27 @@ export default class Parser
     #parseDeclaration(tokenList: TokenList, isStatic: boolean): ReflectionMember
     {
         let token = tokenList.current;
+        let name = ANONYMOUS_IDENTIFIER;
+        let isPrivate = false;
 
-        const isPrivate = token.value.startsWith(PRIVATE_INDICATOR);
-        const name = isPrivate ? token.value.substring(1) : token.value;
-
-        token = tokenList.step(); // Read away the field name
+        if (token.hasValue(List.OPEN))
+        {
+            // Array destructuring
+            name = this.#parseBlock(tokenList, List.OPEN, List.CLOSE);
+            token = tokenList.current;
+        }
+        else if (token.hasValue(Scope.OPEN))
+        {
+            // Object destructuring
+            name = this.#parseBlock(tokenList, Scope.OPEN, Scope.CLOSE);
+            token = tokenList.current;
+        }
+        else
+        {
+            isPrivate = token.value.startsWith(PRIVATE_INDICATOR);
+            name = isPrivate ? token.value.substring(1) : token.value;
+            token = tokenList.step(); // Read away the field name
+        }
 
         let value = undefined;
 
@@ -828,7 +849,7 @@ export default class Parser
 
     #atEndOfStatement(token: Token): boolean
     {
-        return [Divider.SEPARATOR, Divider.TERMINATOR].includes(token.value)
+        return [Divider.TERMINATOR, Divider.SEPARATOR].includes(token.value)
             || [List.CLOSE, Group.CLOSE, Scope.CLOSE].includes(token.value)
             || isKeyword(token.value);
     }


Test patch:
diff --git a/packages/jitar-reflection/test/_fixtures/parser/Lexer.fixture.ts b/packages/jitar-reflection/test/_fixtures/parser/Lexer.fixture.ts
index 57f602e..a70d29b 100644
--- a/packages/jitar-reflection/test/_fixtures/parser/Lexer.fixture.ts
+++ b/packages/jitar-reflection/test/_fixtures/parser/Lexer.fixture.ts
@@ -2,11 +2,13 @@
 const CODE =
 {
     OPERATORS: `=====!=/=/!=`,
-    STATEMENT: `const identifier = (12 >= 3) ? { 'foo' } : [ "bar" ];`,
-    WHITESPACE_EXCLUDED: `const identifier="value";`,
-    WHITESPACE_INCLUDED: `const identifier\n=\t"value" ;`,
+    LITERALS: '`foo\\`ter`"bar\\"becue"\'baz\'',
+    KEYWORDS_IDENTIFIERS: 'class Foo function bar',
+    WHITESPACE: `const identifier\n=\t"value" ;`,
     COMMENT_LINE: `const // This is a comment\nidentifier`,
-    COMMENT_BLOCK: `const /* This is a comment */ identifier`
+    COMMENT_BLOCK: `const /* This is a comment */ identifier`,
+    STATEMENT: `const identifier = (12 >= 3) ? { 'foo' } : [ "bar" ];`,
+    MINIFIED: 'return`foo`;identifier1=identifier2'
 }
 
 export { CODE };
diff --git a/packages/jitar-reflection/test/_fixtures/parser/Parser.fixture.ts b/packages/jitar-reflection/test/_fixtures/parser/Parser.fixture.ts
index bab7d05..6ceecf5 100644
--- a/packages/jitar-reflection/test/_fixtures/parser/Parser.fixture.ts
+++ b/packages/jitar-reflection/test/_fixtures/parser/Parser.fixture.ts
@@ -47,6 +47,8 @@ const FIELDS =
     ARRAY: "const array = [ 'value1', 'value2' ];",
     OBJECT: "const object = { key1: 'value1', key2: 'value2' };",
     REGEX: "const regex = /regex/g;",
+    DESTRUCTURING_ARRAY: "const [value1, value2] = array;",
+    DESTRUCTURING_OBJECT: "const {key1, key2} = object;",
 }
 
 const FUNCTIONS =
@@ -123,7 +125,7 @@ const CLASSES =
     async *generator2() { yield 1; }
 
     static async *generator3() { yield 1; }
-}`,
+}`
 }
 
 const MODULES =
diff --git a/packages/jitar-reflection/test/parser/Lexer.spec.ts b/packages/jitar-reflection/test/parser/Lexer.spec.ts
index c603bcc..c7237d3 100644
--- a/packages/jitar-reflection/test/parser/Lexer.spec.ts
+++ b/packages/jitar-reflection/test/parser/Lexer.spec.ts
@@ -42,9 +42,42 @@ describe('parser/Lexer', () =>
             expect(tokens.get(5).value).toBe(Operator.NOT_EQUAL);
         });
 
+        it('should separate keywords from literals', () =>
+        {
+            const tokens = lexer.tokenize(CODE.LITERALS);
+            expect(tokens.size).toBe(3);
+
+            expect(tokens.get(0).type).toBe(TokenType.LITERAL);
+            expect(tokens.get(0).value).toBe('`foo\\`ter`');
+
+            expect(tokens.get(1).type).toBe(TokenType.LITERAL);
+            expect(tokens.get(1).value).toBe('"bar\\"becue"');
+
+            expect(tokens.get(2).type).toBe(TokenType.LITERAL);
+            expect(tokens.get(2).value).toBe("'baz'");
+        });
+
+        it('should separate keywords from identifiers', () =>
+        {
+            const tokens = lexer.tokenize(CODE.KEYWORDS_IDENTIFIERS);
+            expect(tokens.size).toBe(4);
+
+            expect(tokens.get(0).type).toBe(TokenType.KEYWORD);
+            expect(tokens.get(0).value).toBe('class');
+
+            expect(tokens.get(1).type).toBe(TokenType.IDENTIFIER);
+            expect(tokens.get(1).value).toBe('Foo');
+
+            expect(tokens.get(2).type).toBe(TokenType.KEYWORD);
+            expect(tokens.get(2).value).toBe('function');
+
+            expect(tokens.get(3).type).toBe(TokenType.IDENTIFIER);
+            expect(tokens.get(3).value).toBe('bar');
+        });
+
         it('should include whitespace when requested', () =>
         {
-            const tokens = lexer.tokenize(CODE.WHITESPACE_INCLUDED, false);
+            const tokens = lexer.tokenize(CODE.WHITESPACE, false);
             expect(tokens.size).toBe(9);
 
             expect(tokens.get(0).type).toBe(TokenType.KEYWORD);
@@ -77,28 +110,7 @@ describe('parser/Lexer', () =>
 
         it('should omit whitespace when requested', () =>
         {
-            const tokens = lexer.tokenize(CODE.WHITESPACE_INCLUDED, true);
-            expect(tokens.size).toBe(5);
-
-            expect(tokens.get(0).type).toBe(TokenType.KEYWORD);
-            expect(tokens.get(0).value).toBe('const');
-
-            expect(tokens.get(1).type).toBe(TokenType.IDENTIFIER);
-            expect(tokens.get(1).value).toBe('identifier');
-
-            expect(tokens.get(2).type).toBe(TokenType.OPERATOR);
-            expect(tokens.get(2).value).toBe(Operator.ASSIGN);
-
-            expect(tokens.get(3).type).toBe(TokenType.LITERAL);
-            expect(tokens.get(3).value).toBe('"value"');
-
-            expect(tokens.get(4).type).toBe(TokenType.DIVIDER);
-            expect(tokens.get(4).value).toBe(Divider.TERMINATOR);
-        });
-
-        it('should tokenize when whitespace is excluded', () =>
-        {
-            const tokens = lexer.tokenize(CODE.WHITESPACE_EXCLUDED, true);
+            const tokens = lexer.tokenize(CODE.WHITESPACE, true);
             expect(tokens.size).toBe(5);
 
             expect(tokens.get(0).type).toBe(TokenType.KEYWORD);
@@ -227,5 +239,29 @@ describe('parser/Lexer', () =>
             expect(tokens.get(16).type).toBe(TokenType.DIVIDER);
             expect(tokens.get(16).value).toBe(Divider.TERMINATOR);
         });
+
+        it('should tokenize minified code', () =>
+        {
+            const tokens = lexer.tokenize(CODE.MINIFIED);
+            expect(tokens.size).toBe(6);
+
+            expect(tokens.get(0).type).toBe(TokenType.IDENTIFIER);
+            expect(tokens.get(0).value).toBe('return');
+
+            expect(tokens.get(1).type).toBe(TokenType.LITERAL);
+            expect(tokens.get(1).value).toBe('`foo`');
+
+            expect(tokens.get(2).type).toBe(TokenType.DIVIDER);
+            expect(tokens.get(2).value).toBe(Divider.TERMINATOR);
+
+            expect(tokens.get(3).type).toBe(TokenType.IDENTIFIER);
+            expect(tokens.get(3).value).toBe('identifier1');
+
+            expect(tokens.get(4).type).toBe(TokenType.OPERATOR);
+            expect(tokens.get(4).value).toBe(Operator.ASSIGN);
+
+            expect(tokens.get(5).type).toBe(TokenType.IDENTIFIER);
+            expect(tokens.get(5).value).toBe('identifier2');
+        });
     });
 });
diff --git a/packages/jitar-reflection/test/parser/Parser.spec.ts b/packages/jitar-reflection/test/parser/Parser.spec.ts
index 25dc1fb..b4e4189 100644
--- a/packages/jitar-reflection/test/parser/Parser.spec.ts
+++ b/packages/jitar-reflection/test/parser/Parser.spec.ts
@@ -1,14 +1,14 @@
 
 import { describe, expect, it } from 'vitest';
 
-import ReflectionExpression from '../../src/models/ReflectionExpression';
 import ReflectionArray from '../../src/models/ReflectionArray';
+import ReflectionExpression from '../../src/models/ReflectionExpression';
+import ReflectionField from '../../src/models/ReflectionField';
+import ReflectionGenerator from '../../src/models/ReflectionGenerator';
 import ReflectionObject from '../../src/models/ReflectionObject';
 import Parser from '../../src/parser/Parser';
 
 import { VALUES, IMPORTS, EXPORTS, FIELDS, FUNCTIONS, CLASSES, MODULES } from '../_fixtures/parser/Parser.fixture';
-import ReflectionField from '../../src/models/ReflectionField';
-import ReflectionGenerator from '../../src/models/ReflectionGenerator';
 
 const parser = new Parser();
 
@@ -359,6 +359,24 @@ describe('parser/Parser', () =>
             expect(field.value).toBeInstanceOf(ReflectionExpression);
             expect(field.value?.definition).toBe("/ regex / g");
         });
+
+        it('should parse a field that is destructuring an array', () =>
+        {
+            const field = parser.parseField(FIELDS.DESTRUCTURING_ARRAY);
+
+            expect(field.name).toBe('[ value1 , value2 ]');
+            expect(field.value).toBeInstanceOf(ReflectionExpression);
+            expect(field.value?.definition).toBe("array");
+        });
+
+        it('should parse a field that is destructuring an object', () =>
+        {
+            const field = parser.parseField(FIELDS.DESTRUCTURING_OBJECT);
+
+            expect(field.name).toBe('{ key1 , key2 }');
+            expect(field.value).toBeInstanceOf(ReflectionExpression);
+            expect(field.value?.definition).toBe("object");
+        });
     });
 
     describe('.parseFunction(code)', () =>