diff --git a/packages/jitar-reflection/src/models/ReflectionAlias.ts b/packages/jitar-reflection/src/models/ReflectionAlias.ts index 8c4be994..fea8495c 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 3041eeda..89102405 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 e54c6343..5ee8798c 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 e9aa8f89..8f1b39a7 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 bda49b42..c56d8b97 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 a2ddfb7f..a5fe166e 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 af3fad1e..052e51fa 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 b09db6ca..98d02dfc 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 e00da8d0..22cc1da1 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 ed777bbb..37f78409 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 dde397dd..1c054086 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 22d23d18..3be68ad0 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 8a2ec9db..b63ca713 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); } diff --git a/packages/jitar-reflection/test/_fixtures/parser/Lexer.fixture.ts b/packages/jitar-reflection/test/_fixtures/parser/Lexer.fixture.ts index 57f602e3..a70d29b5 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 bab7d05a..6ceecf52 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 c603bcc4..c7237d34 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 25dc1fb3..b4e41890 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)', () =>