diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 969ade3f..c3d28467 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -12,9 +12,9 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: webfactory/ssh-agent@v0.8.0 - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + # - uses: webfactory/ssh-agent@v0.8.0 + # with: + # ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: Read .nvmrc run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" id: nvm diff --git a/.vscode/settings.json b/.vscode/settings.json index f7da2b98..226b47df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,4 +7,6 @@ "typescript.tsdk": "node_modules/typescript/lib", "typescript.preferences.quoteStyle": "single", "typescript.format.semicolons": "remove", + "editor.tabSize": 2, + "editor.insertSpaces": true } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b60f7e7..bf8c02d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## v4.1.9 +**Wollok Version: `3.2.7`** +- πŸ‘‰ Fix report current line on the frame for stack traces +- ‴️ Fix using `super` inside a closure +- πŸ“ Fix imports for chained files +- πŸ”„ Fix #cyclic const instance initialization +- β›΅ Moving function utils for navigation +- ❌ Error handling enhancements + - Better error messages from `lang` (and `native` methods) + - Add return of `void` value + - Sanitize stack trace + +## v4.1.8 +**Wollok Version: `3.2.6`** +- ❌ Hot fix for validation messages + +## v4.1.7 +**Wollok Version: `3.2.6`** +- 🌎 Migrate validation messages + ## v4.1.6 **Wollok Version: `3.2.5`** - πŸ‘Ύ Fix REPL constants in sub-folder files for dynamic diagram diff --git a/README.md b/README.md index 7d027a6f..5a2cf3f4 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,20 @@ -# Wollok-TS [![npm version](https://badge.fury.io/js/wollok-ts.svg)](https://badge.fury.io/js/wollok-ts) [![codecov](https://codecov.io/gh/uqbar-project/wollok-ts/graph/badge.svg?token=4U99G67xRT)](https://codecov.io/gh/uqbar-project/wollok-ts) +# Wollok-TS + +[![npm version](https://badge.fury.io/js/wollok-ts.svg)](https://badge.fury.io/js/wollok-ts) [![Node.js CI](https://github.com/uqbar-project/wollok-ts/actions/workflows/node.js.yml/badge.svg)](https://github.com/uqbar-project/wollok-ts/actions/workflows/node.js.yml) [![codecov](https://codecov.io/gh/uqbar-project/wollok-ts/graph/badge.svg?token=4U99G67xRT)](https://codecov.io/gh/uqbar-project/wollok-ts) ![GitHub License](https://img.shields.io/github/license/uqbar-project/wollok-ts) + TypeScript based Wollok language implementation -## Usage +## πŸ“– Usage For an in-dept explanation of the API and how to use it please refer to [the documentation page](https://uqbar-project.github.io/wollok-ts/). -## Contributing +## πŸ‘©β€πŸ’» Contributing + +All contributions are welcome! Feel free to report issues on [the project's issue tracker](https://github.com/uqbar-project/wollok-ts/issues), or fork the project and [create a *Pull Request*](https://help.github.com/articles/creating-a-pull-request-from-a-fork/). If you've never collaborated with an open source project before, you might want to read [this guide](https://akrabat.com/the-beginners-guide-to-contributing-to-a-github-project/). -All contributions are welcome! Feel free to report issues on [the project's issue tracker](https://github.com/uqbar-project/wollok-ts/issues), or fork the project and [create a *Pull Request*](https://help.github.com/articles/creating-a-pull-request-from-a-fork/). If you've never collaborated with an open source project before, you might want to read [this guide](https://akrabat.com/the-beginners-guide-to-contributing-to-a-github-project/). +If you plan to contribute with code, please refer to [this page](https://uqbar-project.github.io/wollok-ts/pages/How-To-Contribute/). There is a list of [good first issues](https://github.com/uqbar-project/wollok-ts/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22), if you have any question please reach out us [in our Discord channel](https://discord.gg/ZstgCPKEaa). -If you plan to contribute with code, please refer to [this page](https://uqbar-project.github.io/wollok-ts/pages/How-To-Contribute/). +**Powered by [Uqbar](https://uqbar.org/)** diff --git a/docs/_includes/navbar.md b/docs/_includes/navbar.md index 1b6c2d97..47f05963 100644 --- a/docs/_includes/navbar.md +++ b/docs/_includes/navbar.md @@ -9,4 +9,5 @@ - [How To Contribute](/wollok-ts/pages/How-To-Contribute) - [General Design](/wollok-ts/pages/How-To-Contribute/General-Design-and-Main-Concerns) - [Developer Environment](/wollok-ts/pages/How-To-Contribute/Developer-environment) + - [Developer Tools](/wollok-ts/pages/How-To-Contribute/Developer-tools) - [Publish instructions](/wollok-ts/pages/Publish-Instructions) diff --git a/docs/pages/How-To-Contribute/Developer-environment.md b/docs/pages/How-To-Contribute/Developer-environment.md index 544f49b6..be0e61fe 100644 --- a/docs/pages/How-To-Contribute/Developer-environment.md +++ b/docs/pages/How-To-Contribute/Developer-environment.md @@ -27,9 +27,12 @@ There are also other plugins that some people on the team find interesting and y - [TODO Highlight](https://marketplace.visualstudio.com/items?itemName=wayou.vscode-todo-highlight) - [Test Explorer UI](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer) - [Mocha Test Explorer](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-mocha-test-adapter) +- [Editor Config for VS Code - Editor Config](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig), only if you use the editor config settings ### Node +#### For Linux and Mac + You need to install [NodeJS](https://nodejs.org/es/), which provides VM environment, and [NVM - Node Version Manager](https://github.com/nvm-sh/nvm). Make sure you are using the right version of node by running this command: @@ -60,6 +63,20 @@ then you need to install it nvm install lts/hydrogen # or the version you get on the previous input ``` +#### For Windows + +You need to install the [NVM - for Windows](https://github.com/coreybutler/nvm-windows). + +Run the installer `nvm-setup.exe` as Administrator. + +Open an elevated Command Prompt or Git Bash in the project folder (with Administrator privileges) and run: + +```bash +nvm install <> +nvm use <> +# The version number is in the .nvmrc file (do not use codename version e.g. lts/gallium, in Windows you have to use the equivalent version number e.g. 16.15.0) +``` + ### NPM You will also need to install [NPM](https://www.npmjs.com/). If you are not familiar with *dependency manager tools*, you can think of this program as the entry point for all the important tasks development-related tasks, like installing dependencies and running tests. After installing the client, go to the project root folder and run: @@ -84,63 +101,6 @@ We use [ESLint](https://eslint.org/) to make sure our code complies with our cod ![settingTSversion](https://user-images.githubusercontent.com/4549002/71355632-68957400-255e-11ea-808b-39ec97abff5c.gif) -## Testing - -We use [BDD chai unit testing style](https://www.chaijs.com/api/bdd/), in particular - -- [should](http://shouldjs.github.io/) -- expect - -They are located in `test` folder. - -You can run all the project tests from the console by executing: - -```bash -npm test -``` - -We also have specific tests for each component of Wollok-TS: - -```bash -npm run test:dynamicDiagram -npm run test:validations -... -``` - -Please refer to the `package.json` file or just run `npm run` command to see a list of alternatives. - -## Debugging - -The folder `.vscode` has a `launch.json` file which configures everything for running tests in an embedded VSCode environment. You can set a breakpoint and run the tests: - -![ezgif com-video-to-gif](https://user-images.githubusercontent.com/4549002/71355164-00925e00-255d-11ea-9a83-c37f420d4e61.gif) - -More on debugging: - -- [Debugging in Visual Studio Code](https://code.visualstudio.com/docs/editor/debugging) -- [Debugging Typescript in VS Code](https://code.visualstudio.com/docs/typescript/typescript-debugging) -- [How to debug Typescript in VS Code](https://medium.com/@PhilippKief/how-to-debug-typescript-with-vs-code-9cec93b4ae56) - -### Debugging a single test - -You can use **Test Explorer with Mocha**, if you follow current instructions and install plugins Test Explorer and Mocha Test Explorer. Then, you can go to the Test Explorer tab and run/debug a single test from the left sidebar: - -![debuggingWollokTs2](https://user-images.githubusercontent.com/4549002/71355441-cd040380-255d-11ea-82b6-1cb7c19c1c7a.gif) - -Or, if you prefer using the console: - -```bash -npm run test:unit -- -f -``` - -### Building it locally - -If you are developing a dependency of Wollok-TS (for instance Wollok-TS CLI or Wollok Web Tools), you might need to run a local build. To do so, just run: - -```bash -npm run build -``` - -### Deploying / Publishing +## Moving on -If you need to deploy or publish a new version, please refer to [this page](../Publish-Instructions.md) \ No newline at end of file +You can check Wollok-TS tools in [the specific tools page](./Developer-tools.md) \ No newline at end of file diff --git a/docs/pages/How-To-Contribute/Developer-tools.md b/docs/pages/How-To-Contribute/Developer-tools.md new file mode 100644 index 00000000..45b8b8af --- /dev/null +++ b/docs/pages/How-To-Contribute/Developer-tools.md @@ -0,0 +1,70 @@ + +## Base environment + +Please take a look at [the developer environment page](./Developer-environment.md) in order to install all required components. + +## Testing + +We use [BDD chai unit testing style](https://www.chaijs.com/api/bdd/), in particular + +- [should](http://shouldjs.github.io/) +- expect + +They are located in `test` folder. + +You can run all the project tests from the console by executing: + +```bash +npm test +``` + +We also have specific tests for each component of Wollok-TS: + +```bash +npm run test:dynamicDiagram +npm run test:validations +... +``` + +Please refer to the `package.json` file or just run `npm run` command to see a list of alternatives. + +## Debugging + +The folder `.vscode` has a `launch.json` file which configures everything for running tests in an embedded VSCode environment. You can set a breakpoint and run the tests: + +![ezgif com-video-to-gif](https://user-images.githubusercontent.com/4549002/71355164-00925e00-255d-11ea-9a83-c37f420d4e61.gif) + +More on debugging: + +- [Debugging in Visual Studio Code](https://code.visualstudio.com/docs/editor/debugging) +- [Debugging Typescript in VS Code](https://code.visualstudio.com/docs/typescript/typescript-debugging) +- [How to debug Typescript in VS Code](https://medium.com/@PhilippKief/how-to-debug-typescript-with-vs-code-9cec93b4ae56) + +### Debugging a single test + +You can use **Test Explorer with Mocha**, if you follow current instructions and install plugins Test Explorer and Mocha Test Explorer. Then, you can go to the Test Explorer tab and run/debug a single test from the left sidebar: + +![debuggingWollokTs2](https://user-images.githubusercontent.com/4549002/71355441-cd040380-255d-11ea-82b6-1cb7c19c1c7a.gif) + +Or, if you prefer using the console: + +```bash +npm run test:unit -- -f +``` + +### Building it locally + +If you are developing a dependency of Wollok-TS (for instance Wollok-TS CLI or Wollok Web Tools), you might need to run a local build. To do so, just run: + +```bash +npm run build +``` + +### We, the People + +If you need some human interaction, you're always welcome at [our Discord channel](https://discord.gg/ZstgCPKEaa). We also have [a list of first good issues](https://github.com/uqbar-project/wollok-ts/labels/good%20first%20issue) you can take a look and ask for help to get more information. + +### Deploying / Publishing + +If you need to deploy or publish a new version, please refer to [this page](../Publish-Instructions.md) + diff --git a/package-lock.json b/package-lock.json index a5fe5156..15a1b5f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wollok-ts", - "version": "4.1.6", + "version": "4.1.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wollok-ts", - "version": "4.1.6", + "version": "4.1.10", "license": "MIT", "dependencies": { "@types/parsimmon": "^1.10.8", diff --git a/package.json b/package.json index bf07aae0..9cd6a93c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wollok-ts", - "version": "4.1.7", + "version": "4.1.10", "wollokVersion": ":master", "description": "TypeScript based Wollok language implementation", "repository": "https://github.com/uqbar-project/wollok-ts", @@ -12,17 +12,20 @@ "scripts": { "build": "rm -rf dist && tsc && cp -r dist/temp/src/* dist && rm -r dist/temp && cp -r language/src/* dist/wre/", "buildWRE": "ts-node scripts/buildWRE.ts", - "prepare": "ts-node scripts/fetchLanguage.ts && npm run buildWRE", + "prepare": "ts-node scripts/fetchLanguage.ts && npm run buildWRE && npm run copy:translations", + "copy:translations": "cp ./language/src/resources/validationMessages/*.json ./src/validator", "diagnostic": "tsc --noEmit --diagnostics --extendedDiagnostics", "test": "npm run test:lint && npm run test:unit && npm run test:sanity && npm run test:examples && npm run test:validations && npm run test:typeSystem && npm run test:printer", "test:lint": "eslint .", "test:coverage": "nyc --reporter=lcov npm run test", "test:unit": "mocha --parallel -r ts-node/register/transpile-only test/**/*.test.ts", "test:examples": "npm run test:wtest -- --root language/test/examples", + "test:game": "mocha --parallel -r ts-node/register/transpile-only test/**/game.test.ts", "test:dynamicDiagram": "mocha --parallel -r ts-node/register/transpile-only test/dynamicDiagram.test.ts", "test:helpers": "mocha --parallel -r ts-node/register/transpile-only test/helpers.test.ts", - "test:interpreter": "mocha --parallel -r ts-node/register/transpile-only test/interpreter.test.ts", + "test:interpreter": "mocha -r ts-node/register/transpile-only test/interpreter.test.ts", "test:linker": "mocha --parallel -r ts-node/register/transpile-only test/linker.test.ts", + "test:messageReporter": "mocha --parallel -r ts-node/register/transpile-only test/messageReporter.test.ts", "test:model": "mocha --parallel -r ts-node/register/transpile-only test/model.test.ts", "test:sanity": "npm run test:wtest -- --root language/test/sanity", "test:validations": "mocha --parallel -r ts-node/register/transpile-only test/validator.test.ts", diff --git a/scripts/fetchLanguage.ts b/scripts/fetchLanguage.ts index f4cc70c5..a5815592 100644 --- a/scripts/fetchLanguage.ts +++ b/scripts/fetchLanguage.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync } from 'fs' import gitClient from 'simple-git' import { wollokVersion } from '../package.json' -const WOLLOK_LANGUAGE_REPO = 'git@github.com:uqbar-project/wollok-language.git' +const WOLLOK_LANGUAGE_REPO = 'https://github.com/uqbar-project/wollok-language.git' const WOLLOK_LANGUAGE_TAG = wollokVersion.includes(':') ? wollokVersion.split(':')[1] : `v${wollokVersion}` const WOLLOK_LANGUAGE_FOLDER = 'language' diff --git a/src/constants.ts b/src/constants.ts index 9dd66460..c119d0f1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -22,6 +22,7 @@ export const DICTIONARY_MODULE = 'wollok.lang.Dictionary' export const OBJECT_MODULE = 'wollok.lang.Object' export const EXCEPTION_MODULE = 'wollok.lang.Exception' export const CLOSURE_MODULE = 'wollok.lang.Closure' +export const VOID_WKO = 'wollok.lang.void' export const GAME_MODULE = 'wollok.game.game' diff --git a/src/extensions.ts b/src/extensions.ts index 203f3af0..9c7f1e0f 100644 --- a/src/extensions.ts +++ b/src/extensions.ts @@ -75,6 +75,8 @@ export const uniqueBy = (collection: T[], property: keyof T): T[] => return uniques }, []) +export const excludeNullish = (list: (T | undefined)[]): T[] => list.filter((item): item is T => item !== undefined) + export const count = (list: List, condition: (element: T) => boolean): number => list.filter(condition).length export function raise(error: Error): never { throw error } diff --git a/src/helpers.ts b/src/helpers.ts index 360d2475..a185e4b6 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,6 +1,7 @@ -import { BOOLEAN_MODULE, CLOSURE_EVALUATE_METHOD, CLOSURE_TO_STRING_METHOD, INITIALIZE_METHOD, KEYWORDS, NUMBER_MODULE, OBJECT_MODULE, STRING_MODULE, WOLLOK_BASE_PACKAGE } from './constants' +import { BOOLEAN_MODULE, CLOSURE_EVALUATE_METHOD, CLOSURE_MODULE, CLOSURE_TO_STRING_METHOD, INITIALIZE_METHOD, KEYWORDS, NUMBER_MODULE, OBJECT_MODULE, STRING_MODULE, VOID_WKO, WOLLOK_BASE_PACKAGE } from './constants' import { getPotentiallyUninitializedLazy } from './decorators' -import { count, is, isEmpty, last, List, match, notEmpty, otherwise, valueAsListOrEmpty, when } from './extensions' +import { count, is, isEmpty, last, List, match, notEmpty, otherwise, valueAsListOrEmpty, when, excludeNullish } from './extensions' +import { RuntimeObject, RuntimeValue } from './interpreter/runtimeModel' import { Assignment, Body, Class, CodeContainer, Describe, Entity, Environment, Expression, Field, If, Import, Literal, LiteralValue, Method, Module, Name, NamedArgument, New, Node, Package, Parameter, ParameterizedType, Problem, Program, Reference, Referenciable, Return, Self, Send, Sentence, Singleton, Super, Test, Throw, Try, Variable } from './model' export const LIBRARY_PACKAGES = ['wollok.lang', 'wollok.lib', 'wollok.game', 'wollok.vm', 'wollok.mirror'] @@ -8,6 +9,7 @@ export const LIBRARY_PACKAGES = ['wollok.lang', 'wollok.lib', 'wollok.game', 'wo // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // HELPER FUNCTIONS FOR VALIDATIONS // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + export const allParents = (module: Module): Module[] => module.supertypes.map(supertype => supertype.reference.target).flatMap(supertype => supertype?.hierarchy ?? []) @@ -215,7 +217,7 @@ export const usesField = (node: Node, field: Field): boolean => match(node)( when(Singleton)(node => { if (!node.isClosure()) return false - const applyMethod = node.methods.find(method => method.name === CLOSURE_EVALUATE_METHOD) + const applyMethod = node.methods.find(isApplyMethodForClosures) return !!applyMethod && usesField(applyMethod, field) }), when(Variable)(node => usesField(node.value, field)), @@ -329,7 +331,7 @@ export const firstNodeWithProblems = (node: Node): Node | undefined => { export const isError = (problem: Problem): boolean => problem.level === 'error' -export const parentModule = (node: Node): Module => (node.ancestors.find(ancestor => ancestor.is(Module))) as Module ?? node.environment.objectClass +export const parentModule = (node: Node): Module => getParentModule(node) ?? node.environment.objectClass export const parentImport = (node: Node): Import | undefined => node.ancestors.find(ancestor => ancestor.is(Import)) as Import @@ -380,23 +382,30 @@ export const methodByFQN = (environment: Environment, fqn: string): Method | und return entity.lookupMethod(methodName, Number.parseInt(methodArity, 10)) } -export const sendDefinitions = (environment: Environment) => (send: Send): Method[] => { - try { - return match(send.receiver)( - when(Reference)(node => { - const target = node.target - return target && is(Singleton)(target) ? - valueAsListOrEmpty(target.lookupMethod(send.message, send.args.length)) - : allMethodDefinitions(environment, send) - }), - when(New)(node => valueAsListOrEmpty(node.instantiated.target?.lookupMethod(send.message, send.args.length))), - when(Self)(_ => moduleFinderWithBackup(environment, send)( - (module) => valueAsListOrEmpty(module.lookupMethod(send.message, send.args.length)) - )), - ) - } catch (error) { - return allMethodDefinitions(environment, send) +export const sendDefinitions = (environment: Environment) => (send: Send): (Method | Field)[] => { + const originalDefinitions = (): Method[] => { + try { + return match(send.receiver)( + when(Reference)(node => { + const target = node.target + return target && is(Singleton)(target) ? + valueAsListOrEmpty(target.lookupMethod(send.message, send.args.length)) + : allMethodDefinitions(environment, send) + }), + when(New)(node => valueAsListOrEmpty(node.instantiated.target?.lookupMethod(send.message, send.args.length))), + when(Self)(_ => moduleFinderWithBackup(environment, send)( + (module) => valueAsListOrEmpty(module.lookupMethod(send.message, send.args.length)) + )), + ) + } catch (error) { + return allMethodDefinitions(environment, send) + } + } + const getDefinitionFromSyntheticMethod = (method: Method) => { + return method.parent.allFields.find((field) => field.name === method.name && field.isProperty) } + + return excludeNullish(originalDefinitions().map((method: Method) => method.isSynthetic ? getDefinitionFromSyntheticMethod(method) : method)) } export const allMethodDefinitions = (environment: Environment, send: Send): Method[] => { @@ -417,4 +426,55 @@ export const moduleFinderWithBackup = (environment: Environment, send: Send) => export const targetName = (target: Node | undefined, defaultName: Name): string => target?.is(Module) || target?.is(Variable) && getPotentiallyUninitializedLazy(target, 'parent')?.is(Package) ? target.fullyQualifiedName - : defaultName \ No newline at end of file + : defaultName + +export const getNodeDefinition = (environment: Environment) => (node: Node): Node[] => { + try { + return match(node)( + when(Reference)(node => valueAsListOrEmpty(node.target)), + when(Send)(sendDefinitions(environment)), + when(Super)(node => valueAsListOrEmpty(superMethodDefinition(node, getParentModule(node)))), + when(Self)(node => valueAsListOrEmpty(getParentModule(node))) + ) + } catch { + return [node] + } +} + +export const isApplyMethodForClosures = (method: Method): boolean => + method.name === CLOSURE_EVALUATE_METHOD && method.parent.fullyQualifiedName.startsWith(`${CLOSURE_MODULE}#`) // TODO: Maybe re-define isClosure() ? + +export const superMethodDefinition = (superNode: Super, methodModule: Module): Method | undefined => { + function isValidMethod(node: Node): node is Method { + return node.is(Method) && !isApplyMethodForClosures(node) + } + const currentMethod = superNode.ancestors.find(isValidMethod)! + return methodModule.lookupMethod(currentMethod.name, superNode.args.length, { lookupStartFQN: currentMethod.parent.fullyQualifiedName }) +} + +const getParentModule = (node: Node): Module => node.ancestors.find(is(Module)) as Module + +export const isVoid = (obj: RuntimeValue | RuntimeObject): boolean => obj?.module?.fullyQualifiedName === VOID_WKO + +export const assertNotVoid = (value: RuntimeObject, errorMessage: string): void => { + if (isVoid(value)) { + throw new RangeError(errorMessage) + } +} + +export const getExpressionFor = (node: Expression): string => + match(node)( + when(Send)(nodeSend => + `message ${nodeSend.message}/${nodeSend.args.length}`), + when(If)(_ => 'if expression'), + when(Reference)(nodeRef => `reference '${nodeRef.name}'`), + when(Literal)(nodeLiteral => `literal ${nodeLiteral.value}`), + when(Self)(_ => 'self'), + when(Expression)(_ => 'expression'), + ) + +export const showParameter = (obj: RuntimeObject): string => + `"${obj.getShortRepresentation().trim() || obj.module.fullyQualifiedName}"` + +export const getMethodContainer = (node: Node): Method | Program | Test | undefined => + last(node.ancestors.filter(parent => parent.is(Method) || parent.is(Program) || parent.is(Test))) as unknown as Method | Program | Test \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 3dfc15b7..23ba3e85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ export * from './interpreter/runtimeModel' export * from './typeSystem/constraintBasedTypeSystem' export * from './printer/exceptions' export * from './printer/utils' +export * from './validator/messageReporter' export { buildEnvironment, diff --git a/src/interpreter/interpreter.ts b/src/interpreter/interpreter.ts index da6bf616..a9bfc9b9 100644 --- a/src/interpreter/interpreter.ts +++ b/src/interpreter/interpreter.ts @@ -4,6 +4,8 @@ import WRENatives from '../wre/wre.natives' import { Evaluation, Execution, ExecutionDefinition, Natives, RuntimeObject, RuntimeValue, WollokException } from './runtimeModel' import * as parse from '../parser' import { notEmpty } from '../extensions' +import { WOLLOK_EXTRA_STACK_TRACE_HEADER } from '../constants' +import { isVoid } from '../helpers' export const interpret = (environment: Environment, natives: Natives): Interpreter => new Interpreter(Evaluation.build(environment, natives)) @@ -31,7 +33,7 @@ abstract class AbstractInterpreter { exec(node: Sentence): InterpreterResult exec(node: Node): InterpreterResult exec(node: Node): InterpreterResult { - return this.do(function*() { return yield* this.exec(node) }) + return this.do(function* () { return yield* this.exec(node) }) } run(programOrTestFQN: Name): InterpreterResult { @@ -39,39 +41,39 @@ abstract class AbstractInterpreter { } send(message: Name, receiver: RuntimeObject, ...args: RuntimeObject[]): InterpreterResult { - return this.do(function*() { return yield* this.send(message, receiver, ...args) }) + return this.do(function* () { return yield* this.send(message, receiver, ...args) }) } invoke(method: Method, receiver: RuntimeObject, ...args: RuntimeObject[]): InterpreterResult { - return this.do(function*() { return yield* this.invoke(method, receiver, ...args) }) + return this.do(function* () { return yield* this.invoke(method, receiver, ...args) }) } reify(value: boolean | number | string | null): InterpreterResult { - return this.do(function*() { return yield* this.reify(value) }) + return this.do(function* () { return yield* this.reify(value) }) } list(...value: RuntimeObject[]): InterpreterResult { - return this.do(function*() { return yield* this.list(...value) }) + return this.do(function* () { return yield* this.list(...value) }) } set(...value: RuntimeObject[]): InterpreterResult { - return this.do(function*() { return yield* this.set(...value) }) + return this.do(function* () { return yield* this.set(...value) }) } error(moduleOrFQN: Module | Name, locals?: Record, error?: Error): InterpreterResult { - return this.do(function*() { return yield* this.error(moduleOrFQN, locals, error) }) + return this.do(function* () { return yield* this.error(moduleOrFQN, locals, error) }) } instantiate(moduleOrFQN: Module | Name, locals: Record = {}): InterpreterResult { - return this.do(function*() { return yield* this.instantiate(moduleOrFQN, locals) }) + return this.do(function* () { return yield* this.instantiate(moduleOrFQN, locals) }) } } export type ExecutionResult = { - result: string; - error?: Error; - errored: boolean; + result: string + error?: Error + errored: boolean } const failureResult = (message: string, error?: Error): ExecutionResult => ({ @@ -85,6 +87,19 @@ const successResult = (result: string): ExecutionResult => ({ errored: false, }) +export const getStackTraceSanitized = (e?: Error): string[] => { + const indexOfTsStack = e?.stack?.indexOf(WOLLOK_EXTRA_STACK_TRACE_HEADER) + const fullStack = e?.stack?.slice(0, indexOfTsStack ?? -1) ?? '' + + return fullStack + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(' ', ' ') + .replaceAll(' ', ' ') + .split('\n') + .filter(stackTraceElement => stackTraceElement.trim()) +} + export class Interpreter extends AbstractInterpreter { constructor(evaluation: Evaluation) { super(evaluation) } @@ -95,7 +110,7 @@ export class Interpreter extends AbstractInterpreter { override do(executionDefinition: ExecutionDefinition): T { const execution = executionDefinition.call(this.evaluation) let next = execution.next() - while(!next.done) next = execution.next() + while (!next.done) next = execution.next() return next.value as InterpreterResult } @@ -120,9 +135,9 @@ export function interprete(interpreter: Interpreter, line: string): ExecutionRes } const result = interpreter.exec(sentenceOrImport) - const stringResult = result - ? result.showShortValue(interpreter) - : '' + const stringResult = !result || isVoid(result) + ? '' + : result.showShortValue(interpreter) return successResult(stringResult) } @@ -190,17 +205,17 @@ export class ExecutionDirector { this.breakpoints.push(...nextBreakpoints) } - finish(): ExecutionState & {done: true} { + finish(): ExecutionState & { done: true } { let result = this.resume() - while(!result.done) result = this.resume() + while (!result.done) result = this.resume() return result } resume(shouldHalt: (next: Node, evaluation: Evaluation) => boolean = () => false): ExecutionState { try { let next = this.execution.next() - while(!next.done) { - if(this.breakpoints.includes(next.value) || shouldHalt(next.value, this.evaluation)) + while (!next.done) { + if (this.breakpoints.includes(next.value) || shouldHalt(next.value, this.evaluation)) return { done: false, next: next.value } next = this.execution.next() diff --git a/src/interpreter/runtimeModel.ts b/src/interpreter/runtimeModel.ts index ff3ba768..48f1f3df 100644 --- a/src/interpreter/runtimeModel.ts +++ b/src/interpreter/runtimeModel.ts @@ -1,8 +1,8 @@ import { v4 as uuid } from 'uuid' -import { BOOLEAN_MODULE, CLOSURE_EVALUATE_METHOD, CLOSURE_MODULE, DATE_MODULE, DICTIONARY_MODULE, EXCEPTION_MODULE, INITIALIZE_METHOD, KEYWORDS, LIST_MODULE, NUMBER_MODULE, OBJECT_MODULE, PAIR_MODULE, RANGE_MODULE, SET_MODULE, STRING_MODULE, TO_STRING_METHOD, WOLLOK_BASE_PACKAGE, WOLLOK_EXTRA_STACK_TRACE_HEADER } from '../constants' +import { BOOLEAN_MODULE, CLOSURE_EVALUATE_METHOD, CLOSURE_MODULE, DATE_MODULE, DICTIONARY_MODULE, EXCEPTION_MODULE, INITIALIZE_METHOD, KEYWORDS, LIST_MODULE, NUMBER_MODULE, OBJECT_MODULE, PAIR_MODULE, RANGE_MODULE, SET_MODULE, STRING_MODULE, TO_STRING_METHOD, VOID_WKO, WOLLOK_BASE_PACKAGE, WOLLOK_EXTRA_STACK_TRACE_HEADER } from '../constants' import { get, is, last, List, match, otherwise, raise, when } from '../extensions' -import { getUninitializedAttributesForInstantiation, isNamedSingleton, loopInAssignment, targetName } from '../helpers' -import { Assignment, Body, Catch, Class, Describe, Entity, Environment, Expression, Field, Id, If, Literal, LiteralValue, Method, Module, Name, New, Node, Package, Program, Reference, Return, Self, Send, Singleton, Super, Test, Throw, Try, Variable } from '../model' +import { assertNotVoid, getExpressionFor, getMethodContainer, getUninitializedAttributesForInstantiation, isNamedSingleton, isVoid, loopInAssignment, showParameter, superMethodDefinition, targetName } from '../helpers' +import { Assignment, Body, Catch, Class, Describe, Entity, Environment, Expression, Field, Id, If, Literal, LiteralValue, Method, Module, Name, New, Node, Program, Reference, Return, Self, Send, Singleton, Super, Test, Throw, Try, Variable } from '../model' import { Interpreter } from './interpreter' const { isArray } = Array @@ -50,18 +50,19 @@ export class WollokException extends Error { get message(): string { const error: RuntimeObject = this.instance - error.assertIsException() - return `${error.innerValue ? error.innerValue.message : error.get('message')?.innerString ?? ''}\n${this.wollokStack}\n ${WOLLOK_EXTRA_STACK_TRACE_HEADER}` + assertIsException(error) + const errorMessage = error.innerValue ? error.innerValue.message : error.get('message')?.innerString ?? '' + return `${errorMessage}\n${this.wollokStack}\n ${WOLLOK_EXTRA_STACK_TRACE_HEADER}` } // TODO: Do we need to take this into consideration for Evaluation.copy()? This might be inside Exception objects constructor(readonly evaluation: Evaluation, readonly instance: RuntimeObject) { super() - instance.assertIsException() + assertIsException(instance) this.name = instance.innerValue - ? `${instance.module.fullyQualifiedName} wrapping TypeScript ${instance.innerValue.name}` + ? `${instance.module.fullyQualifiedName}: ${instance.innerValue.name}` : instance.module.fullyQualifiedName } } @@ -106,9 +107,11 @@ export abstract class Context { for (const [name, value] of this.locals.entries()) copy.set(name, - value instanceof RuntimeObject ? value.copy(contextCache) : - value ? this.get(name) : - value + value instanceof RuntimeObject ? + value.copy(contextCache) : + value ? + this.get(name) : + value ) return copy @@ -135,18 +138,17 @@ export class Frame extends Context { [Entity, (node: Entity) => `${node.fullyQualifiedName}`], // TODO: Add fqn to method when(Method)(node => `${node.parent.fullyQualifiedName}.${node.name}(${node.parameters.map(parameter => parameter.name).join(', ')})`), + when(Send)(node => `${node.message}/${node.args.length}`), when(Catch)(node => `catch(${node.parameter.name}: ${node.parameterType.name})`), when(Environment)(() => 'root'), otherwise((node: Node) => `${node.kind}`), ) } - // TODO: On error report, this tells the node line, but not the actual error line. - // For example, an error on a test would say the test start line, not the line where the error occurred. get sourceInfo(): string { const target = this.node.is(Method) && this.node.name === CLOSURE_EVALUATE_METHOD - ? this.node.parent - : this.node + ? this.currentNode.parent + : this.currentNode return target.sourceInfo } @@ -211,35 +213,8 @@ export class RuntimeObject extends Context { ) } - assertIsNumber(): asserts this is BasicRuntimeObject { - this.assertIs(NUMBER_MODULE, this.innerNumber) - } - - assertIsBoolean(): asserts this is BasicRuntimeObject { - this.assertIs(BOOLEAN_MODULE, this.innerBoolean) - } - - assertIsString(): asserts this is BasicRuntimeObject { - this.assertIs(STRING_MODULE, this.innerString) - } - - assertIsCollection(): asserts this is BasicRuntimeObject { - if (!this.innerCollection) throw new TypeError(`Malformed Runtime Object: Collection inner value should be a List but was ${this.innerValue}`) - } - - assertIsException(): asserts this is BasicRuntimeObject { - if (!this.module.inherits(this.module.environment.getNodeByFQN(EXCEPTION_MODULE))) throw new TypeError(`Expected an instance of Exception but got a ${this.module.fullyQualifiedName} instead`) - if (this.innerValue && !(this.innerValue instanceof Error)) { - throw this.innerValue//new TypeError('Malformed Runtime Object: Exception inner value, if defined, should be an Error') - } - } - - assertIsNotNull(): asserts this is BasicRuntimeObject> { - if (this.innerValue === null) throw new TypeError('Malformed Runtime Object: Object was expected to not be null') - } - protected assertIs(moduleFQN: Name, innerValue?: InnerValue): void { - if (this.module.fullyQualifiedName !== moduleFQN) throw new TypeError(`Expected an instance of ${moduleFQN} but got a ${this.module.fullyQualifiedName} instead`) + if (this.module.fullyQualifiedName !== moduleFQN) throw new TypeError(`Expected a ${moduleFQN} but got a ${this.module.fullyQualifiedName} instead`) if (innerValue === undefined) throw new TypeError(`Malformed Runtime Object: invalid inner value ${this.innerValue} for ${moduleFQN} instance`) } @@ -256,6 +231,11 @@ export class RuntimeObject extends Context { return this.module.name ?? 'Object' } + getShortLabel(): string { + if (!this.innerValue) return `a ${this.module.fullyQualifiedName}` + return this.innerString !== undefined ? `"${this.getShortRepresentation()}"`: this.getShortRepresentation() + } + getShortRepresentation(): string { return this.innerValue?.toString().trim() ?? '' } @@ -279,6 +259,42 @@ export class RuntimeObject extends Context { } +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// ASSERTION +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +export function assertIsNumber(obj: RuntimeObject, message: string, variableName: string, validateValue = true): asserts obj is BasicRuntimeObject { + if (validateValue) assertIsNotNull(obj, message, variableName) + if (obj.innerNumber === undefined) throw new TypeError(`Message ${message}: parameter ${showParameter(obj)} should be a number`) +} + +export function assertIsBoolean(obj: RuntimeObject, message: string, variableName: string): asserts obj is BasicRuntimeObject { + if (!obj) throw new TypeError(`Message ${message}: ${variableName} should be a boolean`) + if (obj.innerBoolean === undefined) throw new TypeError(`Message ${message}: parameter ${showParameter(obj)} should be a boolean`) +} + +export function assertIsString(obj: RuntimeObject | undefined, message: string, variableName: string, validateValue = true): asserts obj is BasicRuntimeObject { + if (!obj) throw new TypeError(`Message ${message}: ${variableName} should be a string`) + if (validateValue) assertIsNotNull(obj, message, variableName) + if (obj.innerString === undefined) throw new TypeError(`Message ${message}: parameter ${showParameter(obj)} should be a string`) +} + +export function assertIsCollection(obj: RuntimeObject): asserts obj is BasicRuntimeObject { + if (!obj.innerCollection) throw new TypeError(`Expected a List of values but was ${obj.innerValue}`) +} + +export function assertIsException(obj: RuntimeObject): asserts obj is BasicRuntimeObject { + if (!obj.module.inherits(obj.module.environment.getNodeByFQN(EXCEPTION_MODULE))) throw new TypeError(`Expected an exception but got a ${obj.module.fullyQualifiedName} instead`) + if (obj.innerValue && !(obj.innerValue instanceof Error)) { + throw obj.innerValue //new TypeError('Malformed Runtime Object: Exception inner value, if defined, should be an Error') + } +} + +export function assertIsNotNull(obj: RuntimeObject, message: string, variableName: string): asserts obj is BasicRuntimeObject> { + if (!obj || obj.innerValue === null) throw new RangeError(`Message ${message} does not support parameter '${variableName}' to be null`) +} + + // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // EVALUATION // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ @@ -299,35 +315,41 @@ export class Evaluation { static build(environment: Environment, natives: Natives): Evaluation { const evaluation = new Evaluation(new Map(), [new Frame(environment)], new Map(), new Map()) + // Set natives environment.forEach(node => { if (node.is(Method) && node.isNative()) evaluation.natives.set(node, get(natives, `${node.parent.fullyQualifiedName}.${node.name}`)!) }) + + // Instanciate globals const globalSingletons = environment.descendants.filter((node: Node): node is Singleton => isNamedSingleton(node)) for (const module of globalSingletons) evaluation.rootFrame.set(module.fullyQualifiedName, evaluation.instantiate(module)) - - const globalConstants = environment.descendants.filter((node: Node): node is Variable => node.is(Variable) && node.parent.is(Package)) + const globalConstants = environment.descendants.filter((node: Node): node is Variable => node.is(Variable) && node.isAtPackageLevel) for (const constant of globalConstants) evaluation.rootFrame.set(constant.fullyQualifiedName, evaluation.exec(constant.value)) + // Initialize lazy globals (for cyclic references) for (const module of globalSingletons) { const instance = evaluation.object(module.fullyQualifiedName) for (const field of module.allFields) { - const value = instance!.get(field.name) + const value = instance.get(field.name) if (value?.innerValue === null && field.value?.isSynthetic) { raise(new Error(`Error in ${module.name}: '${field.name}' attribute uninitialized`)) } } } - for (const constant of globalConstants) - evaluation.object(constant.fullyQualifiedName) - + for (const constant of globalConstants) { + const instance = evaluation.object(constant.fullyQualifiedName) + for (const local of instance.locals) + instance.get(local[0]) + } + // Done return evaluation } @@ -384,7 +406,7 @@ export class Evaluation { exec(node: Expression, frame?: Frame): Execution exec(node: Node, frame?: Frame): Execution *exec(node: Node, frame?: Frame): Execution { - if(frame) this.frameStack.push(frame) + if (frame) this.frameStack.push(frame) this.currentFrame.currentNode = node try { // TODO avoid casting @@ -410,7 +432,6 @@ export class Evaluation { } } catch (error) { if (error instanceof WollokException || error instanceof WollokReturn) throw error - const moduleFQN = error instanceof RangeError && error.message === 'Maximum call stack size exceeded' ? 'wollok.lang.StackOverflowException' : 'wollok.lang.EvaluationError' @@ -444,7 +465,7 @@ export class Evaluation { const args = node.parameters.map(parameter => this.currentFrame.get(parameter.name)!) - return (yield* native.call(this, this.currentFrame.get(KEYWORDS.SELF)!, ...args)) ?? undefined + return (yield* native.call(this, this.currentFrame.get(KEYWORDS.SELF)!, ...args)) ?? (yield* this.reifyVoid()) } else if (node.isConcrete()) { try { yield* this.exec(node.body!) @@ -463,24 +484,32 @@ export class Evaluation { for (const sentence of node.sentences) result = yield* this.exec(sentence) - return result + return isVoid(result) ? yield* this.reifyVoid() : result } protected *execVariable(node: Variable): Execution { + const variableFullName = targetName(node, node.name) + + if(this.currentFrame.locals.get(variableFullName)){ + throw new Error('Can\'t redefine a variable') + } + const value = yield* this.exec(node.value) + assertNotVoid(value, `Cannot assign to variable '${node.name}': ${getExpressionFor(node.value)} produces no value, cannot assign it to a variable`) + yield node - this.currentFrame.set(targetName(node, node.name), value) + this.currentFrame.set(variableFullName, value) } protected *execAssignment(node: Assignment): Execution { - const value = yield* this.exec(node.value) + const variableName = node.variable.target?.name + const value = yield* this.exec(node.value) + assertNotVoid(value, `${value.getShortLabel()} produces no value, cannot assign it to reference ${variableName}`) yield node - - if (node.variable.target?.isConstant) throw new Error(`Can't assign the constant ${node.variable.target?.name}`) - + if (node.variable.target?.isConstant) throw new Error(`Can't assign the constant ${variableName}`) const target = node.variable.target this.currentFrame.set(targetName(target, node.variable.name), value, true) @@ -495,14 +524,14 @@ export class Evaluation { protected *execReference(node: Reference): Execution { yield node - if (!node.scope) return this.currentFrame.get(node.name) ?? raise(new Error(`Could not resolve unlinked reference to ${node.name} or its a reference to void`)) + if (!node.scope) return this.currentFrame.get(node.name) ?? raise(new Error(`Could not resolve unlinked reference to ${node.name}`)) const target = node.target if (target?.is(Field) && loopInAssignment(target.value, target.name)) { raise(new Error(`Error initializing field ${target.name}: stack overflow`)) } - return this.currentFrame.get(targetName(target, node.name)) ?? raise(new Error(`Could not resolve reference to ${node.name} or its a reference to void`)) + return this.currentFrame.get(targetName(target, node.name)) ?? raise(new Error(`Could not resolve reference to ${node.name}`)) } protected *execSelf(node: Self): Execution { @@ -529,8 +558,16 @@ export class Evaluation { } protected *execNew(node: New): Execution { - const args: Record = {} - for (const arg of node.args) args[arg.name] = yield* this.exec(arg.value) + const args: Record> = {} + const isGlobal = Boolean(node.ancestors.find((node: Node): node is Variable => node.is(Variable) && node.isAtPackageLevel)) + for (const arg of node.args) { + const valueExecution = this.exec(arg.value, new Frame(arg.value, this.currentFrame)) + const value = isGlobal ? valueExecution : yield* valueExecution + if (value instanceof RuntimeObject && isVoid(value)) { + assertNotVoid(value, `new ${node.instantiated.target?.fullyQualifiedName}: value of parameter '${arg.name}' produces no value, cannot use it`) + } + args[arg.name] = value + } yield node @@ -556,24 +593,35 @@ export class Evaluation { if ((node.message === '&&' || node.message === 'and') && receiver.innerBoolean === false) return receiver if ((node.message === '||' || node.message === 'or') && receiver.innerBoolean === true) return receiver + assertNotVoid(receiver, `Cannot send message ${node.message}, receiver is an expression that produces no value.`) + const values: RuntimeObject[] = [] - for (const arg of node.args) values.push(yield* this.exec(arg)) + for (const [i, arg] of node.args.entries()) { + const value = yield* this.exec(arg) + const methodContainer = getMethodContainer(node) + assertNotVoid(value, `${methodContainer ? methodContainer.name + ' - while sending message' : 'Message'} ${receiver.module.name ? receiver.module.name + '.' : ''}${node.message}/${node.args.length}: parameter #${i + 1} produces no value, cannot use it`) + values.push(value) + } yield node - return yield* this.send(node.message, receiver, ...values) + const result = yield* this.send(node.message, receiver, ...values) + return result === undefined ? yield* this.reifyVoid() : result } protected *execSuper(node: Super): Execution { + const currentMethod = node.ancestors.find(is(Method))! const args: RuntimeObject[] = [] - for (const arg of node.args) args.push(yield* this.exec(arg)) + for (const [i, arg] of node.args.entries()) { + const value = yield* this.exec(arg) + assertNotVoid(value, `super call for message ${currentMethod.name}/${currentMethod.parameters.length}: parameter #${i + 1} produces no value, cannot use it`) + args.push(value) + } yield node const receiver = this.currentFrame.get(KEYWORDS.SELF)! - const currentMethod = node.ancestors.find(is(Method))! - //TODO: pass just the parent (not the FQN) to lookup? - const method = receiver.module.lookupMethod(currentMethod.name, node.args.length, { lookupStartFQN: currentMethod.parent.fullyQualifiedName }) + const method = superMethodDefinition(node, receiver.module) if (!method) return yield* this.send('messageNotUnderstood', receiver, yield* this.reify(currentMethod.name), yield* this.list(...args)) @@ -582,7 +630,10 @@ export class Evaluation { protected *execIf(node: If): Execution { const condition: RuntimeObject = yield* this.exec(node.condition) - condition.assertIsBoolean() + + const methodContainer = getMethodContainer(node) + assertNotVoid(condition, `${methodContainer ? 'Message ' + methodContainer.name + ' - ': ''}if condition produces no value, cannot use it`) + assertIsBoolean(condition, 'if', 'condition') yield node @@ -630,6 +681,7 @@ export class Evaluation { } *send(message: Name, receiver: RuntimeObject, ...args: RuntimeObject[]): Execution { + if (!receiver) throw new RangeError(`Message: ${message}: receiver produces no value. Cannot send message ${message}`) const method = receiver.module.lookupMethod(message, args.length) if (!method) return yield* this.send('messageNotUnderstood', receiver, yield* this.reify(message as string), yield* this.list(...args)) @@ -692,6 +744,14 @@ export class Evaluation { return instance } + *reifyVoid(): Execution { + const existing = this.rootFrame.get(VOID_WKO) + if (existing) return existing + const instance = new RuntimeObject(this.environment.getNodeByFQN(VOID_WKO), this.rootFrame, undefined) + this.rootFrame.set(VOID_WKO, instance) + return instance + } + *list(...value: RuntimeObject[]): Execution { return new RuntimeObject(this.environment.getNodeByFQN(LIST_MODULE), this.rootFrame, value) } @@ -710,14 +770,14 @@ export class Evaluation { return instance } - *instantiate(moduleOrFQN: Module | Name, locals?: Record): Execution { + *instantiate(moduleOrFQN: Module | Name, locals?: Record>): Execution { const module = typeof moduleOrFQN === 'string' ? this.environment.getNodeByFQN(moduleOrFQN) : moduleOrFQN const instance = new RuntimeObject(module, module.is(Singleton) && !module.name ? this.currentFrame : this.rootFrame) yield* this.init(instance, locals) return instance } - protected *init(instance: RuntimeObject, locals: Record = {}): Execution { + protected *init(instance: RuntimeObject, locals: Record> = {}): Execution { const allFieldNames = instance.module.allFields.map(({ name }) => name) for (const local of keys(locals)) if (!allFieldNames.includes(local)) diff --git a/src/linker.ts b/src/linker.ts index 75dfe767..c23ac82d 100644 --- a/src/linker.ts +++ b/src/linker.ts @@ -92,7 +92,6 @@ export const assignScopes = (root: Node): void => { ? parent?.parent.scope : parent?.scope assign(node, { scope: new LocalScope(containerScope) }) - parent?.scope?.register(...scopeContribution(node)) }) @@ -116,7 +115,9 @@ export const assignScopes = (root: Node): void => { node.scope.include(new LocalScope(undefined, ...contributions)) } } + }) + root.forEach((node, _parent) => { if (node.is(Module)) { node.scope.include(...node.hierarchy.slice(1).map(supertype => supertype.scope)) } diff --git a/src/model.ts b/src/model.ts index c1e6d345..3b591403 100644 --- a/src/model.ts +++ b/src/model.ts @@ -375,6 +375,7 @@ export class Program extends Entity(Node) { @cached sentences(): List { return this.body.sentences } + } diff --git a/src/validator/en.json b/src/validator/en.json new file mode 100644 index 00000000..d8f3e714 --- /dev/null +++ b/src/validator/en.json @@ -0,0 +1,60 @@ +{ + "catchShouldBeReachable": "Unreachable catch block", + "codeShouldBeReachable": "Unreachable code", + "getterMethodShouldReturnAValue": "Getter should return a value", + "linearizationShouldNotRepeatNamedArguments": "Reference {0} is initialized more than once during linearization", + "methodShouldExist": "Method does not exist or invalid number of arguments", + "methodShouldHaveDifferentSignature": "Duplicated method", + "nameShouldBeginWithLowercase": "The name {0} must start with lowercase", + "nameShouldBeginWithUppercase": "The name {0} must start with uppercase", + "nameShouldNotBeKeyword": "The name {0} is a keyword, you should pick another one", + "namedArgumentShouldExist": "Reference {0} not found in {1}", + "namedArgumentShouldNotAppearMoreThanOnce": "Reference {0} is initialized more than once", + "overridingMethodShouldHaveABody": "Overriding method must have a body", + "parameterShouldNotDuplicateExistingVariable": "Duplicated Name", + "possiblyReturningBlock": "This method is returning a block, consider removing the '=' before curly braces.", + "shouldCatchUsingExceptionHierarchy": "Can only catch wollok.lang.Exception or a subclass of it", + "shouldDefineConstInsteadOfVar": "Variable should be const", + "shouldHaveAssertInTest": "Tests must send at least one message to assert object", + "shouldHaveBody": "Method without body. You must implement it", + "shouldHaveNonEmptyName": "Tests must have a non-empty description", + "shouldImplementAllMethodsInHierarchy": "Methods in hierachy without super implementation: {0}", + "shouldImplementInheritedAbstractMethods": "You must implement all inherited abstract methods", + "shouldInitializeAllAttributes": "You must provide initial value to the following references: {0}", + "shouldInitializeGlobalReference": "Reference is never initialized", + "shouldMatchFileExtension": "The file extension doesn't allow this definition", + "shouldMatchSuperclassReturnValue": "Override method {0} does not match the returned type from its superclass (should either return a value o act as a side-effect method)", + "shouldNotAssignValueInLoop": "Infinite loop in value assignment", + "shouldNotBeEmpty": "Should not make an empty definition.", + "shouldNotCompareEqualityOfSingleton": "Comparing against named object is discouraged (missing polymorphism?)", + "shouldNotDefineEmptyDescribe": "Describe should not be empty", + "shouldNotDefineGlobalMutableVariables": "Global 'var' references are not allowed. You should use 'const' instead.", + "shouldNotDefineMoreThanOneSuperclass": "Bad Linearization: you cannot define multiple parent classes", + "shouldNotDefineUnnecesaryIf": "Unnecessary if always evaluates to true!", + "shouldNotDefineUnnecessaryCondition": "Unnecessary condition", + "shouldNotDefineUnusedVariables": "Unused variable", + "shouldNotDuplicateEntities": "This name is already defined (imported from {0})", + "shouldNotDuplicateFields": "There is already a field with this name in the hierarchy", + "shouldNotDuplicateGlobalDefinitions": "There is already a definition with this name in the hierarchy", + "shouldNotDuplicateLocalVariables": "There is already a local variable with this name in the hierarchy", + "shouldNotDuplicatePackageName": "Duplicated package", + "shouldNotDuplicateVariables": "There is already a variable with this name in the hierarchy", + "shouldNotDuplicateVariablesInLinearization": "There are attributes with the same name in the hierarchy: [{0}]", + "shouldNotHaveLoopInHierarchy": "Infinite loop in hierarchy", + "shouldNotImportMoreThanOnce": "This file is already imported", + "shouldNotImportSameFile": "Cannot import same file", + "shouldNotMarkMoreThanOneOnlyTest": "You should mark a single test with the flag 'only' (the others will not be executed)", + "shouldNotReassignConst": "Cannot modify constants", + "shouldNotUseOverride": "Method does not override anything", + "shouldNotUseReservedWords": "{0} is a reserved name for a core element", + "shouldNotUseVoidMethodAsValue": "Message send \"{0}\" produces no value (missing return in method?)", + "shouldNotUseVoidSingleton": "Named object 'void' produces no value, use 'null' instead", + "shouldOnlyInheritFromMixin": "Mixin can only inherit from another mixin", + "shouldPassValuesToAllAttributes": "{0} cannot be instantiated, you must pass values to the following attributes: {1}", + "shouldUseBooleanValueInIfCondition": "Expecting a boolean", + "shouldUseBooleanValueInLogicOperation": "Expecting a boolean", + "shouldUseConditionalExpression": "Bad usage of if! You must return the condition itself without using if.", + "shouldUseOverrideKeyword": "Method should be marked as override, since it overrides a superclass method", + "shouldUseSelfAndNotSingletonReference": "Don't use the name within the object. Use 'self' instead.", + "superclassShouldBeLastInLinearization": "Bad Linearization: superclass should be last in linearization" +} \ No newline at end of file diff --git a/src/validator/es.json b/src/validator/es.json new file mode 100644 index 00000000..cc52fcd0 --- /dev/null +++ b/src/validator/es.json @@ -0,0 +1,60 @@ +{ + "catchShouldBeReachable": "Este catch nunca se va a ejecutar debido a otro catch anterior", + "codeShouldBeReachable": "Este cΓ³digo nunca se va a ejecutar", + "getterMethodShouldReturnAValue": "El mΓ©todo getter debe retornar un valor", + "linearizationShouldNotRepeatNamedArguments": "La referencia {0} estΓ‘ inicializada mΓ‘s de una vez", + "methodShouldExist": "El mΓ©todo no existe o nΓΊmero incorrecto de argumentos", + "methodShouldHaveDifferentSignature": "MΓ©todo duplicado", + "nameShouldBeginWithLowercase": "El nombre {0} debe comenzar con minΓΊsculas", + "nameShouldBeginWithUppercase": "El nombre {0} debe comenzar con mayΓΊsculas", + "nameShouldNotBeKeyword": "El nombre {0} es una palabra reservada, debe cambiarla", + "namedArgumentShouldExist": "No se encuentra la referencia {0} en {1}", + "namedArgumentShouldNotAppearMoreThanOnce": "La referencia {0} estΓ‘ inicializada mΓ‘s de una vez", + "overridingMethodShouldHaveABody": "Si sobrescribe debe especificar el cuerpo del mΓ©todo", + "parameterShouldNotDuplicateExistingVariable": "Nombre duplicado", + "possiblyReturningBlock": "Este mΓ©todo devuelve un bloque, si no es la intenciΓ³n elimine el '=' antes de las llaves.", + "shouldCatchUsingExceptionHierarchy": "Solo se puede aplicar 'catch' a un objeto de tipo wollok.lang.Exception o una subclase", + "shouldDefineConstInsteadOfVar": "Esta variable deberΓ­a ser una constante", + "shouldHaveAssertInTest": "Los tests deben enviar al menos un mensaje al WKO \"assert\"", + "shouldHaveBody": "El mΓ©todo debe tener una implementaciΓ³n", + "shouldHaveNonEmptyName": "Los tests deben tener una descripciΓ³n no vacΓ­a", + "shouldImplementAllMethodsInHierarchy": "Existen mΓ©todos en la jerarquΓ­a que requieren implementaciΓ³n en super: {0}", + "shouldImplementInheritedAbstractMethods": "Debe implementar todos los mΓ©todos abstractos heredados", + "shouldInitializeAllAttributes": "Debe proveer un valor inicial a las siguientes referencias: {0}", + "shouldInitializeGlobalReference": "La referencia nunca se inicializa", + "shouldMatchFileExtension": "La extensiΓ³n del archivo no permite esta definiciΓ³n", + "shouldMatchSuperclassReturnValue": "El mΓ©todo ${0} que sobreescribe la subclase debe respetar el mismo tipo que el mΓ©todo de su superclase (sea un mΓ©todo que solo produce efecto o que devuelve un valor).", + "shouldNotAssignValueInLoop": "Se genera un loop infinito en la asignaciΓ³n del valor", + "shouldNotBeEmpty": "El elemento no puede estar vacΓ­o: falta escribir cΓ³digo.", + "shouldNotCompareEqualityOfSingleton": "No se aconseja comparar objetos nombrados, considere utilizar polimorfismo.", + "shouldNotDefineEmptyDescribe": "El describe no deberΓ­a estar vacΓ­o", + "shouldNotDefineGlobalMutableVariables": "Solo se permiten las variables globales de tipo const", + "shouldNotDefineMoreThanOneSuperclass": "LinearizaciΓ³n: no se puede definir mΓ‘s de una superclase", + "shouldNotDefineUnnecesaryIf": "If innecesario. Siempre se evalΓΊa como verdadero", + "shouldNotDefineUnnecessaryCondition": "CondiciΓ³n innecesaria", + "shouldNotDefineUnusedVariables": "Esta variable nunca se utiliza", + "shouldNotDuplicateEntities": "Este nombre ya estΓ‘ definido (importado de {0})", + "shouldNotDuplicateFields": "Ya existe un atributo con este nombre en la jerarquΓ­a", + "shouldNotDuplicateGlobalDefinitions": "Ya existe una definiciΓ³n con este nombre en la jerarquΓ­a", + "shouldNotDuplicateLocalVariables": "Ya existe una variable local con este nombre en la jerarquΓ­a", + "shouldNotDuplicatePackageName": "Package duplicado", + "shouldNotDuplicateVariables": "Ya existe una variable con este nombre en la jerarquΓ­a", + "shouldNotDuplicateVariablesInLinearization": "En la jerarquΓ­a hay atributos con el mismo nombre: [{0}]", + "shouldNotHaveLoopInHierarchy": "La jerarquΓ­a de clases produce un ciclo infinito", + "shouldNotImportMoreThanOnce": "Este archivo ya estΓ‘ importado", + "shouldNotImportSameFile": "No se puede importar el mismo archivo", + "shouldNotMarkMoreThanOneOnlyTest": "Solo un test puede marcarse como 'only' (los otros no se ejecutarΓ‘n)", + "shouldNotReassignConst": "No se pueden modificar las referencias constantes", + "shouldNotUseOverride": "Este mΓ©todo no sobrescribe ningΓΊn otro mΓ©todo", + "shouldNotUseReservedWords": "{0} es una palabra reservada por la biblioteca de Wollok", + "shouldNotUseVoidMethodAsValue": "El mensaje \"{0}\" no retorna ningΓΊn valor (quizΓ‘s te falte un return en el mΓ©todo)", + "shouldNotUseVoidSingleton": "El objeto nombrado 'void' no retorna ningΓΊn valor (puede usar 'null' en su lugar)", + "shouldOnlyInheritFromMixin": "Los mixines solo pueden heredar de otros mixines", + "shouldPassValuesToAllAttributes": "No se puede instanciar {0}. Falta pasar valores a los siguientes atributos: {1}", + "shouldUseBooleanValueInIfCondition": "Se espera un booleano", + "shouldUseBooleanValueInLogicOperation": "Se espera un booleano", + "shouldUseConditionalExpression": "EstΓ‘s usando incorrectamente el if. DevolvΓ© simplemente la expresiΓ³n booleana.", + "shouldUseOverrideKeyword": "DeberΓ­a marcarse el mΓ©todo con 'override', ya que sobrescribe el de sus superclases", + "shouldUseSelfAndNotSingletonReference": "No debe usar el nombre del objeto dentro del mismo. Use 'self'.", + "superclassShouldBeLastInLinearization": "LinearizaciΓ³n: la superclase deberΓ­a estar ΓΊltima en linearizaciΓ³n" +} \ No newline at end of file diff --git a/src/validator/index.ts b/src/validator/index.ts index 95e63fe6..f8e4c44b 100644 --- a/src/validator/index.ts +++ b/src/validator/index.ts @@ -25,8 +25,8 @@ import { Assignment, Body, Catch, Class, Code, Describe, Entity, Expression, Fie Level, Literal, Method, Mixin, Module, NamedArgument, New, Node, Package, Parameter, Problem, Program, Reference, Return, Self, Send, Sentence, Singleton, SourceMap, Super, Test, Throw, Try, Variable } from '../model' -import { allParents, assignsVariable, duplicatesLocalVariable, entityIsAlreadyUsedInImport, findMethod, finishesFlow, getContainer, getInheritedUninitializedAttributes, getReferencedModule, getUninitializedAttributesForInstantiation, getVariableContainer, hasDuplicatedVariable, inheritsCustomDefinition, isAlreadyUsedInImport, hasBooleanValue, isBooleanMessage, isBooleanOrUnknownType, isEqualMessage, isGetter, isImplemented, isUninitialized, loopInAssignment, methodExists, methodIsImplementedInSuperclass, methodsCallingToSuper, referencesSingleton, returnsAValue, sendsMessageToAssert, superclassMethod, supposedToReturnValue, targetSupertypes, unusedVariable, usesReservedWords, valueFor } from '../helpers' -import { sourceMapForBody, sourceMapForConditionInIf, sourceMapForNodeName, sourceMapForNodeNameOrFullNode, sourceMapForOnlyTest, sourceMapForOverrideMethod, sourceMapForUnreachableCode } from './sourceMaps' +import { allParents, assignsVariable, duplicatesLocalVariable, entityIsAlreadyUsedInImport, findMethod, finishesFlow, getContainer, getInheritedUninitializedAttributes, getReferencedModule, getUninitializedAttributesForInstantiation, getVariableContainer, hasDuplicatedVariable, inheritsCustomDefinition, isAlreadyUsedInImport, hasBooleanValue, isBooleanMessage, isBooleanOrUnknownType, isEqualMessage, isGetter, isImplemented, isUninitialized, loopInAssignment, methodExists, methodIsImplementedInSuperclass, methodsCallingToSuper, referencesSingleton, returnsAValue, sendsMessageToAssert, superclassMethod, supposedToReturnValue, targetSupertypes, unusedVariable, usesReservedWords, valueFor, parentModule } from '../helpers' +import { sourceMapForBody, sourceMapForConditionInIf, sourceMapForNodeName, sourceMapForNodeNameOrFullNode, sourceMapForOnlyTest, sourceMapForOverrideMethod, sourceMapForReturnValue, sourceMapForUnreachableCode, sourceMapForValue } from './sourceMaps' import { valuesForFileName, valuesForNodeName } from './values' const { entries } = Object @@ -111,7 +111,7 @@ export const methodShouldHaveDifferentSignature = error(node => export const shouldNotOnlyCallToSuper = warning(node => { const callsSuperWithSameArgs = (sentence?: Sentence) => sentence?.is(Super) && sentence.args.every((arg, index) => arg.is(Reference) && arg.target === node.parameters[index]) return isEmpty(node.sentences) || !node.sentences.every(sentence => - callsSuperWithSameArgs(sentence) || sentence.is(Return) && callsSuperWithSameArgs(sentence.value) + callsSuperWithSameArgs(sentence) && node.sentences.length == 1 || sentence.is(Return) && callsSuperWithSameArgs(sentence.value) ) }, undefined, sourceMapForBody) @@ -156,7 +156,7 @@ export const shouldOnlyInheritFromMixin = error(node => node.supertypes.e })) export const shouldUseOverrideKeyword = warning(node => - node.isOverride || !superclassMethod(node) || node.name == INITIALIZE_METHOD + node.isOverride || node.isSynthetic || !superclassMethod(node) || node.name == INITIALIZE_METHOD ) export const possiblyReturningBlock = warning(node => { @@ -225,7 +225,7 @@ export const shouldMatchSuperclassReturnValue = error(node => { const lastSentence = last(node.sentences) const superclassSentence = last(overridenMethod.sentences) return !lastSentence || !superclassSentence || lastSentence.is(Return) === superclassSentence.is(Return) || lastSentence.is(Throw) || superclassSentence.is(Throw) -}, undefined, sourceMapForBody) +}, valuesForNodeName, sourceMapForBody) export const shouldReturnAValueOnAllFlows = error(node => { const lastThenSentence = last(node.thenBody.sentences) @@ -295,8 +295,9 @@ valuesForNodeName, sourceMapForNodeName) export const shouldNotCompareEqualityOfSingleton = warning(node => { + const referencesUnwantedSingleton = (element: any) => referencesSingleton(element) const arg: Expression = node.args[0] - return !isEqualMessage(node) || !arg || !(referencesSingleton(arg) || referencesSingleton(node.receiver)) + return !isEqualMessage(node) || !arg || !(referencesUnwantedSingleton(arg) || referencesUnwantedSingleton(node.receiver)) }) export const shouldUseBooleanValueInIfCondition = error(node => @@ -357,8 +358,7 @@ export const methodShouldExist = error(node => methodExists(node)) export const shouldUseSuperOnlyOnOverridingMethod = error(node => { const method = node.ancestors.find(is(Method)) - const parentModule = node.ancestors.find(is(Module)) - if (parentModule?.is(Mixin)) return true + if (parentModule(node)?.is(Mixin)) return true if (!method) return false return !!superclassMethod(method) && method.matchesSignature(method.name, node.args.length) }) @@ -482,12 +482,21 @@ export const catchShouldBeReachable = error(node => { export const shouldNotDuplicateEntities = error(node => !node.name || !node.parent.is(Package) || node.parent.imports.every(importFile => !entityIsAlreadyUsedInImport(importFile.entity.target, node.name!)) , valuesForNodeName, -sourceMapForNodeName) +sourceMapForNodeName +) export const shouldNotImportSameFile = error(node => [TEST_FILE_EXTENSION, PROGRAM_FILE_EXTENSION].some(allowedExtension => node.parent.fileName?.endsWith(allowedExtension)) || node.entity.target !== node.parent ) +export const shouldNotUseVoidSingleton = error>(node => { + const isVoid = (value: Expression) => !!value && value.is(Reference) && value.name === 'void' + return !isVoid(node) +}, +valuesForNodeName, +(node) => node.is(Method) ? sourceMapForReturnValue(node) : sourceMapForValue(node), +) + export const shouldNotImportMoreThanOnce = warning(node => !node.parent.is(Package) || node.parent.imports.filter(importFile => importFile !== node).every(importFile => !isAlreadyUsedInImport(importFile.entity.target, node.entity.target)) ) @@ -516,6 +525,10 @@ export const shouldHaveDifferentName = error(node => { }, valuesForNodeName, sourceMapForNodeName) +export const shouldNotRedefineIdentity = error(node => { + return !(node.name === '===' && node.parameters.length === 1 && node.isOverride && !node.isNative()) +}, undefined, sourceMapForNodeName) + // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // PROBLEMS BY KIND // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ @@ -533,10 +546,10 @@ const validationsByKind = (node: Node): Record> => match when(Singleton)(() => ({ nameShouldBeginWithLowercase, inlineSingletonShouldBeAnonymous, topLevelSingletonShouldHaveAName, nameShouldNotBeKeyword, shouldInitializeInheritedAttributes, linearizationShouldNotRepeatNamedArguments, shouldNotDefineMoreThanOneSuperclass, superclassShouldBeLastInLinearization, shouldNotDuplicateGlobalDefinitions, shouldNotDuplicateVariablesInLinearization, shouldImplementInheritedAbstractMethods, shouldImplementAllMethodsInHierarchy, shouldNotUseReservedWords, shouldNotDuplicateEntities })), when(Mixin)(() => ({ nameShouldBeginWithUppercase, shouldNotHaveLoopInHierarchy, shouldOnlyInheritFromMixin, shouldNotDuplicateGlobalDefinitions, shouldNotDuplicateVariablesInLinearization, shouldNotDuplicateEntities })), when(Field)(() => ({ nameShouldBeginWithLowercase, shouldNotAssignToItselfInDeclaration, nameShouldNotBeKeyword, shouldNotDuplicateFields, shouldNotUseReservedWords, shouldNotDefineUnusedVariables, shouldDefineConstInsteadOfVar, shouldInitializeSingletonAttribute, shouldNotAssignValueInLoop })), - when(Method)(() => ({ onlyLastParameterCanBeVarArg, nameShouldNotBeKeyword, methodShouldHaveDifferentSignature, shouldNotOnlyCallToSuper, shouldUseOverrideKeyword, possiblyReturningBlock, shouldNotUseOverride, shouldMatchSuperclassReturnValue, shouldNotDefineNativeMethodsOnUnnamedSingleton, overridingMethodShouldHaveABody, getterMethodShouldReturnAValue, shouldHaveBody })), + when(Method)(() => ({ onlyLastParameterCanBeVarArg, nameShouldNotBeKeyword, methodShouldHaveDifferentSignature, shouldNotOnlyCallToSuper, shouldUseOverrideKeyword, possiblyReturningBlock, shouldNotUseOverride, shouldMatchSuperclassReturnValue, shouldNotDefineNativeMethodsOnUnnamedSingleton, overridingMethodShouldHaveABody, getterMethodShouldReturnAValue, shouldHaveBody, shouldNotRedefineIdentity })), when(Variable)(() => ({ nameShouldBeginWithLowercase, nameShouldNotBeKeyword, shouldNotAssignToItselfInDeclaration, shouldNotDuplicateLocalVariables, shouldNotDuplicateGlobalDefinitions, shouldNotDefineGlobalMutableVariables, shouldNotUseReservedWords, shouldInitializeGlobalReference, shouldDefineConstInsteadOfVar, shouldNotDuplicateEntities, shouldInitializeConst })), when(Assignment)(() => ({ shouldNotAssignToItself, shouldNotReassignConst })), - when(Reference)(() => ({ missingReference, shouldUseSelfAndNotSingletonReference, shouldReferenceToObjects })), + when(Reference)(() => ({ missingReference, shouldUseSelfAndNotSingletonReference, shouldReferenceToObjects, shouldNotUseVoidSingleton })), when(Self)(() => ({ shouldNotUseSelf })), when(New)(() => ({ shouldNotInstantiateAbstractClass, shouldPassValuesToAllAttributes })), when(Send)(() => ({ shouldNotCompareAgainstBooleanLiterals, shouldNotCompareEqualityOfSingleton, shouldUseBooleanValueInLogicOperation, methodShouldExist, codeShouldBeReachable, shouldNotDefineUnnecessaryCondition, shouldNotUseVoidMethodAsValue })), diff --git a/src/validator/messageReporter.ts b/src/validator/messageReporter.ts new file mode 100644 index 00000000..d64cac07 --- /dev/null +++ b/src/validator/messageReporter.ts @@ -0,0 +1,70 @@ + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// INTERNAL FUNCTIONS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +const convertToHumanReadable = (code: string, customMessages: Messages, language: LANGUAGES) => { + if (!code) { + return '' + } + const result = code.replace( + /[A-Z0-9]+/g, + (match) => ' ' + match.toLowerCase() + ) + return ( + validationI18nized(customMessages, language)[FAILURE] + + result.charAt(0).toUpperCase() + + result.slice(1, result.length) + ) +} + +const interpolateValidationMessage = (message: string, ...values: string[]) => + message.replace(/{\d*}/g, (match: string) => { + const index = match.replace('{', '').replace('}', '') + return values[+index] ?? '' + }) + +const validationI18nized = (customMessages: Messages, language: LANGUAGES) => customMessages[language] as Message + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// PUBLIC INTERFACE +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +export enum LANGUAGES { + SPANISH = 'es', + ENGLISH = 'en', +} + +export type Message = { [key: string]: string } + +export type Messages = { [key in LANGUAGES]: Message } + +export type ReportMessage = { + message: string, + values?: string[], + language?: LANGUAGES, + customMessages?: Messages, +} + +export const getMessage = ({ message, values, language = LANGUAGES.ENGLISH, customMessages = messages }: ReportMessage): string => + interpolateValidationMessage(validationI18nized(customMessages, language)[message] || convertToHumanReadable(message, customMessages, language), ...values ?? []) + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// VALIDATION MESSAGES DEFINITION +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +import validationMessagesEn from './en.json' +import validationMessagesEs from './es.json' + +const FAILURE = 'failure' + +const messages: Messages = { + [LANGUAGES.ENGLISH]: { + ...validationMessagesEn, + [FAILURE]: 'Rule failure: ', + }, + [LANGUAGES.SPANISH]: { + ...validationMessagesEs, + [FAILURE]: 'La siguiente regla fallΓ³: ', + }, +} \ No newline at end of file diff --git a/src/validator/sourceMaps.ts b/src/validator/sourceMaps.ts index 92800126..4719856f 100644 --- a/src/validator/sourceMaps.ts +++ b/src/validator/sourceMaps.ts @@ -4,7 +4,7 @@ import { KEYWORDS } from '../constants' import { List, isEmpty, last, match, when } from '../extensions' -import { CodeContainer, Entity, Field, If, Method, NamedArgument, Node, Parameter, Reference, Return, Send, Sentence, Singleton, SourceIndex, SourceMap, Test, Variable } from '../model' +import { CodeContainer, Entity, Expression, Field, If, Method, NamedArgument, Node, Parameter, Reference, Return, Send, Sentence, Singleton, SourceIndex, SourceMap, Test, Variable } from '../model' import { hasBooleanValue } from '../helpers' export const buildSourceMap = (node: Node, initialOffset: number, finalOffset: number): SourceMap | undefined => @@ -43,12 +43,12 @@ export const sourceMapForSentences = (sentences: List): SourceMap => n end: sourceMapForSentence(last(sentences)!)!.end, }) -// const sourceMapForReturnValue = (node: Method) => { -// if (!node.body || node.body === KEYWORDS.NATIVE || isEmpty(node.body.sentences)) return node.sourceMap -// const lastSentence = last(node.body.sentences)! -// if (!lastSentence.is(Return)) return lastSentence.sourceMap -// return lastSentence.value!.sourceMap -// } +export const sourceMapForReturnValue = (node: Method): SourceMap | undefined => { + if (!node.body || node.body === KEYWORDS.NATIVE || isEmpty(node.body.sentences)) return node.sourceMap + const lastSentence = last(node.body.sentences)! + if (!lastSentence.is(Return)) return lastSentence.sourceMap + return lastSentence.value!.sourceMap +} export const sourceMapForBody = (node: CodeContainer): SourceMap | undefined => { if (!node.body || node.body === KEYWORDS.NATIVE || isEmpty(node.body.sentences)) return node.sourceMap @@ -67,6 +67,11 @@ export const sourceMapForUnreachableCode = (node: If | Send): SourceMap => })), ) +export const sourceMapForValue = (node: Node & { value?: Expression }): SourceMap | undefined => { + if (!node.sourceMap) return undefined + return node.value ? node.value.sourceMap : node.sourceMap +} + // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // HELPERS // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ diff --git a/src/wre/game.ts b/src/wre/game.ts index e56f64f2..8a4c3c9d 100644 --- a/src/wre/game.ts +++ b/src/wre/game.ts @@ -1,18 +1,16 @@ import { GAME_MODULE } from '../constants' -import { Execution, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' +import { assertIsNumber, assertIsNotNull, Execution, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' const { round } = Math const game: Natives = { game: { - *addVisual(self: RuntimeObject, visual: RuntimeObject): Execution { - visual.assertIsNotNull() - if (!visual.module.lookupMethod('position', 0)) throw new TypeError('position') + *addVisual(self: RuntimeObject, positionable: RuntimeObject): Execution { + assertIsNotNull(positionable, 'addVisual', 'positionable') + if (!positionable.module.lookupMethod('position', 0)) throw new TypeError('Message addVisual: positionable lacks a position message') const visuals = self.get('visuals')!.innerCollection! - - if(visuals.includes(visual)) throw new TypeError(visual.module.fullyQualifiedName) - - visuals.push(visual) + if (visuals.includes(positionable)) throw new RangeError('Visual is already in the game! You cannot add duplicate elements') + visuals.push(positionable) }, *removeVisual(self: RuntimeObject, visual: RuntimeObject): Execution { @@ -71,7 +69,7 @@ const game: Natives = { }, *colliders(self: RuntimeObject, visual: RuntimeObject): Execution { - visual.assertIsNotNull() + assertIsNotNull(visual, 'colliders', 'visual') const position = (yield* this.send('position', visual))! const visualsAtPosition: RuntimeObject = (yield* this.send('getObjectsIn', self, position))! @@ -96,8 +94,8 @@ const game: Natives = { self.set('height', height) }, - *ground(self: RuntimeObject, ground: RuntimeObject): Execution { - self.set('ground', ground) + *ground(self: RuntimeObject, image: RuntimeObject): Execution { + self.set('ground', image) }, *boardGround(self: RuntimeObject, boardGround: RuntimeObject): Execution { @@ -125,7 +123,7 @@ const game: Natives = { const sounds = game.get('sounds')?.innerCollection if (!sounds) game.set('sounds', yield* this.list(self)) else { - if (sounds.includes(self)) throw new TypeError(self.module.fullyQualifiedName) + if (sounds.includes(self)) throw new RangeError('Sound is already in the game! You cannot add duplicate elements') else sounds.push(self) } @@ -166,9 +164,9 @@ const game: Natives = { if(!newVolume) return self.get('volume') const volume: RuntimeObject = newVolume - volume.assertIsNumber() + assertIsNumber(volume, 'volume', 'newVolume', false) - if (volume.innerNumber < 0 || volume.innerNumber > 1) throw new RangeError('newVolume') + if (volume.innerNumber < 0 || volume.innerNumber > 1) throw new RangeError('volumen: newVolume should be between 0 and 1') self.set('volume', volume) }, diff --git a/src/wre/lang.ts b/src/wre/lang.ts index d18ed347..4747d0ae 100644 --- a/src/wre/lang.ts +++ b/src/wre/lang.ts @@ -1,12 +1,92 @@ import { APPLY_METHOD, CLOSURE_EVALUATE_METHOD, CLOSURE_TO_STRING_METHOD, COLLECTION_MODULE, DATE_MODULE, KEYWORDS, TO_STRING_METHOD } from '../constants' import { hash, isEmpty, List } from '../extensions' -import { Evaluation, Execution, Frame, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' +import { assertNotVoid, showParameter } from '../helpers' +import { assertIsCollection, assertIsNumber, assertIsString, assertIsNotNull, Evaluation, Execution, Frame, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' import { Class, Node, Singleton } from '../model' const { abs, ceil, random, floor, round } = Math const { isInteger } = Number const { UTC } = Date + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// COMMON FUNCTIONS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +function *internalFilter(evaluation: Evaluation, self: RuntimeObject, closure: RuntimeObject, newCollection: (evaluation: Evaluation, result: RuntimeObject[]) => Execution): Execution { + assertIsNotNull(closure, 'filter', 'closure') + + const result: RuntimeObject[] = [] + for(const elem of [...self.innerCollection!]) { + const satisfies = (yield* evaluation.send(APPLY_METHOD, closure, elem)) as RuntimeObject + assertNotVoid(satisfies, 'Message filter: closure produces no value. Check the return type of the closure (missing return?)') + if (satisfies!.innerBoolean) { + result.push(elem) + } + } + + return yield* newCollection(evaluation, result) +} + +function *internalFindOrElse(evaluation: Evaluation, self: RuntimeObject, predicate: RuntimeObject, continuation: RuntimeObject): Execution { + assertIsNotNull(predicate, 'findOrElse', 'predicate') + assertIsNotNull(continuation, 'findOrElse', 'continuation') + + for(const elem of [...self.innerCollection!]) { + const value = (yield* evaluation.send(APPLY_METHOD, predicate, elem)) as RuntimeObject + assertNotVoid(value, 'Message findOrElse: predicate produces no value. Check the return type of the closure (missing return?)') + if (value!.innerBoolean!) return elem + } + + return yield* evaluation.send(APPLY_METHOD, continuation) +} + +function *internalFold(evaluation: Evaluation, self: RuntimeObject, initialValue: RuntimeObject, closure: RuntimeObject): Execution { + assertIsNotNull(closure, 'fold', 'closure') + + let acum = initialValue + for(const elem of [...self.innerCollection!]) { + acum = (yield* evaluation.send(APPLY_METHOD, closure, acum, elem))! + assertNotVoid(acum, 'Message fold: closure produces no value. Check the return type of the closure (missing return?)') + } + + return acum +} + +function *internalMax(evaluation: Evaluation, self: RuntimeObject): Execution { + const method = evaluation.environment.getNodeByFQN(COLLECTION_MODULE).lookupMethod('max', 0)! + return yield* evaluation.invoke(method, self) +} + +function *internalRemove(self: RuntimeObject, element: RuntimeObject): Execution { + const values = self.innerCollection! + const index = values.indexOf(element) + if (index >= 0) values.splice(index, 1) +} + +function *internalSize(evaluation: Evaluation, self: RuntimeObject): Execution { + return yield* evaluation.reify(self.innerCollection!.length) +} + +function *internalClear(self: RuntimeObject): Execution { + const values = self.innerCollection! + values.splice(0, values.length) +} + +function *internalJoin(evaluation: Evaluation, self: RuntimeObject, separator?: RuntimeObject): Execution { + const method = evaluation.environment.getNodeByFQN(COLLECTION_MODULE).lookupMethod('join', separator ? 1 : 0)! + return yield* evaluation.invoke(method, self, ...separator ? [separator]: []) +} + +function *internalContains(evaluation: Evaluation, self: RuntimeObject, value: RuntimeObject): Execution { + const method = evaluation.environment.getNodeByFQN(COLLECTION_MODULE).lookupMethod('contains', 1)! + return yield* evaluation.invoke(method, self, value) +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// NATIVE DEFINITIONS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + const lang: Natives = { Exception: { @@ -54,9 +134,9 @@ const lang: Natives = { }, *generateDoesNotUnderstandMessage(_self: RuntimeObject, target: RuntimeObject, messageName: RuntimeObject, parametersSize: RuntimeObject): Execution { - target.assertIsString() - messageName.assertIsString() - parametersSize.assertIsNumber() + assertIsString(target, 'generateDoesNotUnderstandMessage', 'target', false) + assertIsString(messageName, 'generateDoesNotUnderstandMessage', 'messageName', false) + assertIsNumber(parametersSize, 'generateDoesNotUnderstandMessage', 'parametersSize', false) const argsText = new Array(parametersSize.innerNumber).fill(null).map((_, i) => `arg ${i}`) const text = `${target.innerString} does not understand ${messageName.innerString}(${argsText})` @@ -65,59 +145,44 @@ const lang: Natives = { }, *checkNotNull(_self: RuntimeObject, value: RuntimeObject, message: RuntimeObject): Execution { - message.assertIsString() + assertIsString(message, 'checkNotNull', 'message', false) - if (value.innerValue === null) yield* this.send('error', value, message) + if (value.innerValue === null) throw new RangeError(`Message ${message.innerValue} does not allow to receive null values`) }, }, Collection: { - *findOrElse(self: RuntimeObject, predicate: RuntimeObject, continuation: RuntimeObject): Execution { - for(const elem of [...self.innerCollection!]) - if((yield* this.send(APPLY_METHOD, predicate, elem))!.innerBoolean) return elem - return yield* this.send(APPLY_METHOD, continuation) + *findOrElse(self: RuntimeObject, predicate: RuntimeObject, continuation: RuntimeObject): Execution { + return yield* internalFindOrElse(this, self, predicate, continuation) }, + }, Set: { - *anyOne(self: RuntimeObject): Execution { const values = self.innerCollection! - if(isEmpty(values)) throw new RangeError('anyOne') + if(isEmpty(values)) throw new RangeError('anyOne: list should not be empty') return values[floor(random() * values.length)] }, *fold(self: RuntimeObject, initialValue: RuntimeObject, closure: RuntimeObject): Execution { - let acum = initialValue - for(const elem of [...self.innerCollection!]) - acum = (yield* this.send(APPLY_METHOD, closure, acum, elem))! - - return acum + return yield* internalFold(this, self, initialValue, closure) }, *filter(self: RuntimeObject, closure: RuntimeObject): Execution { - const result: RuntimeObject[] = [] - for(const elem of [...self.innerCollection!]) - if((yield* this.send(APPLY_METHOD, closure, elem))!.innerBoolean) - result.push(elem) - - return yield* this.set(...result) + return yield* internalFilter(this, self, closure, (evaluation: Evaluation, result: RuntimeObject[]) => evaluation.set(...result)) }, *max(self: RuntimeObject): Execution { - const method = this.environment.getNodeByFQN(COLLECTION_MODULE).lookupMethod('max', 0)! - return yield* this.invoke(method, self) + return yield* internalMax(this, self) }, *findOrElse(self: RuntimeObject, predicate: RuntimeObject, continuation: RuntimeObject): Execution { - for(const elem of [...self.innerCollection!]) - if((yield* this.send(APPLY_METHOD, predicate, elem))!.innerBoolean!) return elem - - return yield* this.send(APPLY_METHOD, continuation) + return yield* internalFindOrElse(this, self, predicate, continuation) }, *add(self: RuntimeObject, element: RuntimeObject): Execution { @@ -130,28 +195,23 @@ const lang: Natives = { }, *remove(self: RuntimeObject, element: RuntimeObject): Execution { - const values = self.innerCollection! - const index = values.indexOf(element) - if (index >= 0) values.splice(index, 1) + return yield* internalRemove(self, element) }, *size(self: RuntimeObject): Execution { - return yield* this.reify(self.innerCollection!.length) + return yield* internalSize(this, self) }, *clear(self: RuntimeObject): Execution { - const values = self.innerCollection! - values.splice(0, values.length) + return yield* internalClear(self) }, *join(self: RuntimeObject, separator?: RuntimeObject): Execution { - const method = this.environment.getNodeByFQN(COLLECTION_MODULE).lookupMethod('join', separator ? 1 : 0)! - return yield* this.invoke(method, self, ...separator ? [separator]: []) + return yield* internalJoin(this, self, separator) }, *contains(self: RuntimeObject, value: RuntimeObject): Execution { - const method = this.environment.getNodeByFQN(COLLECTION_MODULE).lookupMethod('contains', 1)! - return yield* this.invoke(method, self, value) + return yield* internalContains(this, self, value) }, *['=='](self: RuntimeObject, other: RuntimeObject): Execution { @@ -168,19 +228,20 @@ const lang: Natives = { List: { - *get(self: RuntimeObject, index: RuntimeObject): Execution { - index.assertIsNumber() + assertIsNumber(index, 'get', 'index') const values = self.innerCollection! const indexValue = index.innerNumber - if(indexValue < 0 || indexValue >= values.length) throw new RangeError('index') + if(indexValue < 0 || indexValue >= values.length) throw new RangeError(`get: index should be between 0 and ${values.length - 1}`) return values[round(indexValue)] }, *sortBy(self: RuntimeObject, closure: RuntimeObject): Execution { + assertIsNotNull(closure, 'sortBy', 'closure') + function*quickSort(this: Evaluation, list: List): Generator> { if(list.length < 2) return [...list] @@ -188,11 +249,14 @@ const lang: Natives = { const before: RuntimeObject[] = [] const after: RuntimeObject[] = [] - for(const elem of tail) - if((yield* this.send(APPLY_METHOD, closure, elem, head))!.innerBoolean) + for(const elem of tail) { + const comparison = (yield* this.send(APPLY_METHOD, closure, elem, head)) as RuntimeObject + assertNotVoid(comparison, 'Message sortBy: closure produces no value. Check the return type of the closure (missing return?)') + if (comparison!.innerBoolean) before.push(elem) else after.push(elem) + } const sortedBefore = yield* quickSort.call(this, before) const sortedAfter = yield* quickSort.call(this, after) @@ -207,37 +271,23 @@ const lang: Natives = { }, *filter(self: RuntimeObject, closure: RuntimeObject): Execution { - const result: RuntimeObject[] = [] - for(const elem of [...self.innerCollection!]) - if((yield* this.send(APPLY_METHOD, closure, elem))!.innerBoolean) - result.push(elem) - - return yield* this.list(...result) + return yield* internalFilter(this, self, closure, (evaluation: Evaluation, result: RuntimeObject[]) => evaluation.list(...result)) }, *contains(self: RuntimeObject, value: RuntimeObject): Execution { - const method = this.environment.getNodeByFQN(COLLECTION_MODULE).lookupMethod('contains', 1)! - return yield* this.invoke(method, self, value) + return yield* internalContains(this, self, value) }, *max(self: RuntimeObject): Execution { - const method = this.environment.getNodeByFQN(COLLECTION_MODULE).lookupMethod('max', 0)! - return yield* this.invoke(method, self) + return yield* internalMax(this, self) }, *fold(self: RuntimeObject, initialValue: RuntimeObject, closure: RuntimeObject): Execution { - let acum = initialValue - for(const elem of [...self.innerCollection!]) - acum = (yield* this.send(APPLY_METHOD, closure, acum, elem))! - - return acum + return yield* internalFold(this, self, initialValue, closure) }, *findOrElse(self: RuntimeObject, predicate: RuntimeObject, continuation: RuntimeObject): Execution { - for(const elem of [...self.innerCollection!]) - if((yield* this.send(APPLY_METHOD, predicate, elem))!.innerBoolean) return elem - - return yield* this.send(APPLY_METHOD, continuation) + return yield* internalFindOrElse(this, self, predicate, continuation) }, *add(self: RuntimeObject, element: RuntimeObject): Execution { @@ -245,23 +295,19 @@ const lang: Natives = { }, *remove(self: RuntimeObject, element: RuntimeObject): Execution { - const values = self.innerCollection! - const index = values.indexOf(element) - if (index >= 0) values.splice(index, 1) + return yield* internalRemove(self, element) }, *size(self: RuntimeObject): Execution { - return yield* this.reify(self.innerCollection!.length) + return yield* internalSize(this, self) }, *clear(self: RuntimeObject): Execution { - const values = self.innerCollection! - values.splice(0, values.length) + return yield* internalClear(self) }, *join(self: RuntimeObject, separator?: RuntimeObject): Execution { - const method = this.environment.getNodeByFQN(COLLECTION_MODULE).lookupMethod('join', separator ? 1 : 0)! - return yield* this.invoke(method, self, ...separator ? [separator]: []) + return yield* internalJoin(this, self, separator) }, *['=='](self: RuntimeObject, other: RuntimeObject): Execution { @@ -302,8 +348,8 @@ const lang: Natives = { }, *put(self: RuntimeObject, key: RuntimeObject, value: RuntimeObject): Execution { - key.assertIsNotNull() - value.assertIsNotNull() + assertIsNotNull(key, 'put', '_key') + assertIsNotNull(value, 'put', '_value') const buckets = self.get('')!.innerCollection! const index = hash(`${key.innerNumber ?? key.innerString ?? key.module.fullyQualifiedName}`) % buckets.length @@ -321,6 +367,8 @@ const lang: Natives = { }, *basicGet(self: RuntimeObject, key: RuntimeObject): Execution { + assertIsNotNull(key, 'basicGet', '_key') + const buckets = self.get('')!.innerCollection! const index = hash(`${key.innerNumber ?? key.innerString ?? key.module.fullyQualifiedName}`) % buckets.length const bucket = buckets[index].innerCollection! @@ -376,6 +424,8 @@ const lang: Natives = { }, *forEach(self: RuntimeObject, closure: RuntimeObject): Execution { + assertIsNotNull(closure, 'forEach', 'closure') + const buckets = self.get('')!.innerCollection! for (const bucket of buckets) { @@ -407,7 +457,7 @@ const lang: Natives = { }, *coerceToPositiveInteger(self: RuntimeObject): Execution { - if (self.innerNumber! < 0) throw new RangeError('self') + if (self.innerNumber! < 0) throw new RangeError('coerceToPositiveInteger: self should be zero or positive number') const num = self.innerNumber!.toString() const decimalPosition = num.indexOf('.') @@ -421,39 +471,39 @@ const lang: Natives = { }, *['+'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber() + assertIsNumber(other, '(+)', 'other') return yield* this.reify(self.innerNumber! + other.innerNumber) }, *['-'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber() + assertIsNumber(other, '(-)', 'other') return yield* this.reify(self.innerNumber! - other.innerNumber) }, *['*'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber() + assertIsNumber(other, '(*)', 'other') return yield* this.reify(self.innerNumber! * other.innerNumber) }, *['/'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber() + assertIsNumber(other, '(/)', 'other') - if (other.innerNumber === 0) throw new RangeError('other') + if (other.innerNumber === 0) throw new RangeError('Message (/): quotient should not be zero') return yield* this.reify(self.innerNumber! / other.innerNumber) }, *['**'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber() + assertIsNumber(other, '(**)', 'other') return yield* this.reify(self.innerNumber! ** other.innerNumber) }, *['%'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber() + assertIsNumber(other, '(%)', 'other') return yield* this.reify(self.innerNumber! % other.innerNumber) }, @@ -463,13 +513,13 @@ const lang: Natives = { }, *['>'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber() + assertIsNumber(other, '(>)', 'other') return yield* this.reify(self.innerNumber! > other.innerNumber) }, *['<'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber() + assertIsNumber(other, '(<)', 'other') return yield* this.reify(self.innerNumber! < other.innerNumber) }, @@ -483,17 +533,17 @@ const lang: Natives = { }, *roundUp(self: RuntimeObject, decimals: RuntimeObject): Execution { - decimals.assertIsNumber() + assertIsNumber(decimals, 'roundUp', '_decimals') - if (decimals.innerNumber! < 0) throw new RangeError('decimals') + if (decimals.innerNumber! < 0) throw new RangeError('roundUp: decimals should be zero or positive number') return yield* this.reify(ceil(self.innerNumber! * 10 ** decimals.innerNumber!) / 10 ** decimals.innerNumber!) }, *truncate(self: RuntimeObject, decimals: RuntimeObject): Execution { - decimals.assertIsNumber() + assertIsNumber(decimals, 'truncate', '_decimals') - if (decimals.innerNumber < 0) throw new RangeError('decimals') + if (decimals.innerNumber < 0) throw new RangeError('truncate: decimals should be zero or positive number') const num = self.innerNumber!.toString() const decimalPosition = num.indexOf('.') @@ -502,9 +552,9 @@ const lang: Natives = { : self }, - *randomUpTo(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber() - return yield* this.reify(random() * (other.innerNumber! - self.innerNumber!) + self.innerNumber!) + *randomUpTo(self: RuntimeObject, max: RuntimeObject): Execution { + assertIsNumber(max, 'randomUpTo', 'max') + return yield* this.reify(random() * (max.innerNumber! - self.innerNumber!) + self.innerNumber!) }, *round(self: RuntimeObject): Execution { @@ -512,7 +562,7 @@ const lang: Natives = { }, *gcd(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber() + assertIsNumber(other, 'gcd', 'other') const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b) @@ -533,36 +583,37 @@ const lang: Natives = { }, *concat(self: RuntimeObject, other: RuntimeObject): Execution { + assertIsNotNull(other, 'concat', 'other') return yield* this.reify(self.innerString! + (yield * this.send(TO_STRING_METHOD, other))!.innerString!) }, - *startsWith(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsString() + *startsWith(self: RuntimeObject, prefix: RuntimeObject): Execution { + assertIsString(prefix, 'startsWith', 'prefix') - return yield* this.reify(self.innerString!.startsWith(other.innerString)) + return yield* this.reify(self.innerString!.startsWith(prefix.innerString)) }, - *endsWith(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsString() + *endsWith(self: RuntimeObject, suffix: RuntimeObject): Execution { + assertIsString(suffix, 'startsWith', 'suffix') - return yield* this.reify(self.innerString!.endsWith(other.innerString)) + return yield* this.reify(self.innerString!.endsWith(suffix.innerString)) }, *indexOf(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsString() + assertIsString(other, 'indexOf', 'other') const index = self.innerString!.indexOf(other.innerString) - if (index < 0) throw new RangeError('other') + if (index < 0) throw new RangeError('indexOf: other should be zero or positive number') return yield* this.reify(index) }, *lastIndexOf(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsString() + assertIsString(other, 'lastIndexOf', 'other') const index = self.innerString!.lastIndexOf(other.innerString) - if (index < 0) throw new RangeError('other') + if (index < 0) throw new RangeError('lastIndexOf: other should be zero or positive nummber') return yield* this.reify(index) }, @@ -582,39 +633,39 @@ const lang: Natives = { return yield* this.reify(self.innerString!.split('').reverse().join('')) }, - *['<'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsString() + *['<'](self: RuntimeObject, aString: RuntimeObject): Execution { + assertIsString(aString, '(<)', 'aString') - return yield* this.reify(self.innerString! < other.innerString) + return yield* this.reify(self.innerString! < aString.innerString) }, - *['>'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsString() + *['>'](self: RuntimeObject, aString: RuntimeObject): Execution { + assertIsString(aString, '(>)', 'aString') - return yield* this.reify(self.innerString! > other.innerString) + return yield* this.reify(self.innerString! > aString.innerString) }, - *contains(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsString() + *contains(self: RuntimeObject, element: RuntimeObject): Execution { + assertIsString(element, 'contains', 'element') - return yield* this.reify(self.innerString!.indexOf(other.innerString) >= 0) + return yield* this.reify(self.innerString!.indexOf(element.innerString) >= 0) }, *substring(self: RuntimeObject, startIndex: RuntimeObject, endIndex?: RuntimeObject): Execution { - startIndex.assertIsNumber() + assertIsNumber(startIndex, 'substring', 'startIndex') const start = startIndex.innerNumber const end = endIndex?.innerNumber - if (start < 0) throw new RangeError('startIndex') - if (endIndex && end === undefined || end !== undefined && end < 0) throw new RangeError('endIndex') + if (start < 0) throw new RangeError('substring: startIndex should be zero or positive number') + if (endIndex && end === undefined || end !== undefined && end < 0) throw new RangeError('substring: endIndex should be zero or positive number') return yield* this.reify(self.innerString!.substring(start, end)) }, *replace(self: RuntimeObject, expression: RuntimeObject, replacement: RuntimeObject): Execution { - expression.assertIsString() - replacement.assertIsString() + assertIsString(expression, 'replace', 'expression') + assertIsString(replacement, 'replace', 'replacement') return yield* this.reify(self.innerString!.replace(new RegExp(expression.innerString, 'g'), replacement.innerString)) }, @@ -660,6 +711,8 @@ const lang: Natives = { Range: { *forEach(self: RuntimeObject, closure: RuntimeObject): Execution { + assertIsNotNull(closure, 'forEach', 'closure') + const start = self.get('start')!.innerNumber! const end = self.get('end')!.innerNumber! const step = self.get('step')!.innerNumber! @@ -695,7 +748,7 @@ const lang: Natives = { Closure: { *apply(this: Evaluation, self: RuntimeObject, args: RuntimeObject): Execution { - args.assertIsCollection() + assertIsCollection(args) const method = self.module.lookupMethod(CLOSURE_EVALUATE_METHOD, args.innerCollection.length) if (!method) return yield* this.send('messageNotUnderstood', self, yield* this.reify(APPLY_METHOD), args) @@ -705,7 +758,8 @@ const lang: Natives = { frame.set(KEYWORDS.SELF, self.parentContext?.get(KEYWORDS.SELF)) - return yield* this.exec(method, frame) + const result = yield* this.exec(method, frame) + return result === undefined ? yield* this.reifyVoid() : result }, *toString(this: Evaluation, self: RuntimeObject): Execution { @@ -746,7 +800,7 @@ const lang: Natives = { }, *plusDays(self: RuntimeObject, days: RuntimeObject): Execution { - days.assertIsNumber() + assertIsNumber(days, 'plusDays', '_days') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -762,7 +816,7 @@ const lang: Natives = { }, *minusDays(self: RuntimeObject, days: RuntimeObject): Execution { - days.assertIsNumber() + assertIsNumber(days, 'minusDays', '_days') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -778,7 +832,7 @@ const lang: Natives = { }, *plusMonths(self: RuntimeObject, months: RuntimeObject): Execution { - months.assertIsNumber() + assertIsNumber(months, 'plusMonths', '_months') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -796,7 +850,7 @@ const lang: Natives = { }, *minusMonths(self: RuntimeObject, months: RuntimeObject): Execution { - months.assertIsNumber() + assertIsNumber(months, 'minusMonths', '_months') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -812,7 +866,7 @@ const lang: Natives = { }, *plusYears(self: RuntimeObject, years: RuntimeObject): Execution { - years.assertIsNumber() + assertIsNumber(years, 'plusYears', '_years') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -831,7 +885,7 @@ const lang: Natives = { }, *minusYears(self: RuntimeObject, years: RuntimeObject): Execution { - years.assertIsNumber() + assertIsNumber(years, 'minusYears', '_years') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -858,16 +912,17 @@ const lang: Natives = { ) }, - *['-'](self: RuntimeObject, other: RuntimeObject): Execution { - if (other.module !== self.module) throw new TypeError('other') + *['-'](self: RuntimeObject, aDate: RuntimeObject): Execution { + assertIsNotNull(aDate, '(-)', '_aDate') + if (aDate.module !== self.module) throw new TypeError(`Message (-): parameter ${showParameter(aDate)} should be a Date`) const ownDay = self.get('day')!.innerNumber! const ownMonth = self.get('month')!.innerNumber! - 1 const ownYear = self.get('year')!.innerNumber! - const otherDay = other.get('day')!.innerNumber! - const otherMonth = other.get('month')!.innerNumber! - 1 - const otherYear = other.get('year')!.innerNumber! + const otherDay = aDate.get('day')!.innerNumber! + const otherMonth = aDate.get('month')!.innerNumber! - 1 + const otherYear = aDate.get('year')!.innerNumber! const msPerDay = 1000 * 60 * 60 * 24 const ownUTC = UTC(ownYear, ownMonth, ownDay) @@ -876,16 +931,17 @@ const lang: Natives = { return yield* this.reify(floor((ownUTC - otherUTC) / msPerDay)) }, - *['<'](self: RuntimeObject, other: RuntimeObject): Execution { - if (other.module !== self.module) throw new TypeError('other') + *['<'](self: RuntimeObject, aDate: RuntimeObject): Execution { + assertIsNotNull(aDate, '(<)', '_aDate') + if (aDate.module !== self.module) throw new TypeError(`Message (<): parameter ${showParameter(aDate)} should be a Date`) const ownDay = self.get('day')!.innerNumber! const ownMonth = self.get('month')!.innerNumber! - 1 const ownYear = self.get('year')!.innerNumber! - const otherDay = other.get('day')!.innerNumber! - const otherMonth = other.get('month')!.innerNumber! - 1 - const otherYear = other.get('year')!.innerNumber! + const otherDay = aDate.get('day')!.innerNumber! + const otherMonth = aDate.get('month')!.innerNumber! - 1 + const otherYear = aDate.get('year')!.innerNumber! const value = new Date(ownYear, ownMonth, ownDay) const otherValue = new Date(otherYear, otherMonth, otherDay) @@ -893,16 +949,17 @@ const lang: Natives = { return yield* this.reify(value < otherValue) }, - *['>'](self: RuntimeObject, other: RuntimeObject): Execution { - if (other.module !== self.module) throw new TypeError('other') + *['>'](self: RuntimeObject, aDate: RuntimeObject): Execution { + assertIsNotNull(aDate, '(>)', '_aDate') + if (aDate.module !== self.module) throw new TypeError(`Message (>): parameter ${showParameter(aDate)} should be a Date`) const ownDay = self.get('day')!.innerNumber! const ownMonth = self.get('month')!.innerNumber! - 1 const ownYear = self.get('year')!.innerNumber! - const otherDay = other.get('day')!.innerNumber! - const otherMonth = other.get('month')!.innerNumber! - 1 - const otherYear = other.get('year')!.innerNumber! + const otherDay = aDate.get('day')!.innerNumber! + const otherMonth = aDate.get('month')!.innerNumber! - 1 + const otherYear = aDate.get('year')!.innerNumber! const value = new Date(ownYear, ownMonth, ownDay) const otherValue = new Date(otherYear, otherMonth, otherDay) diff --git a/src/wre/mirror.ts b/src/wre/mirror.ts index a07c1621..9a9f4e83 100644 --- a/src/wre/mirror.ts +++ b/src/wre/mirror.ts @@ -1,16 +1,16 @@ -import { Execution, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' +import { assertIsString, Execution, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' const mirror: Natives = { ObjectMirror: { *resolve(self: RuntimeObject, attributeName: RuntimeObject): Execution { - attributeName.assertIsString() + assertIsString(attributeName, 'resolve', 'attributeName') return self.get('target')?.get(attributeName.innerString) }, *instanceVariableFor(self: RuntimeObject, name: RuntimeObject): Execution { - name.assertIsString() + assertIsString(name, 'instanceVariableFor', 'name') return yield* this.instantiate('wollok.mirror.InstanceVariableMirror', { target: self, diff --git a/test/game.test.ts b/test/game.test.ts index d9f21c57..6c71287f 100644 --- a/test/game.test.ts +++ b/test/game.test.ts @@ -51,7 +51,7 @@ describe('Wollok Game', () => { interpreter.run('actions.genericError') logs.should.be.deep.eq([ 'wollok.lang.Exception: ERROR', - '\tat actions.genericError [actions.wpgm:33]']) + '\tat actions.genericError [actions.wpgm:37]']) }) it('with file name game (devil test)', () => { diff --git a/test/helpers.test.ts b/test/helpers.test.ts index c78c0d13..8092a8e1 100644 --- a/test/helpers.test.ts +++ b/test/helpers.test.ts @@ -1,7 +1,8 @@ -import { should, use } from 'chai' +import { expect, should, use } from 'chai' import sinonChai from 'sinon-chai' -import { BOOLEAN_MODULE, Body, Class, Describe, Environment, Evaluation, Field, Import, Interpreter, isError, LIST_MODULE, Literal, Method, methodByFQN, NUMBER_MODULE, New, OBJECT_MODULE, Package, Parameter, Reference, STRING_MODULE, Self, Send, Singleton, Test, Variable, WRENatives, allAvailableMethods, allScopedVariables, allVariables, implicitImport, isNamedSingleton, isNotImportedIn, link, linkSentenceInNode, literalValueToClass, mayExecute, parentModule, parse, projectPackages, sendDefinitions, hasNullValue, hasBooleanValue, projectToJSON } from '../src' +import { BOOLEAN_MODULE, Body, Class, Describe, Environment, Evaluation, Field, Import, Interpreter, isError, LIST_MODULE, Literal, Method, methodByFQN, NUMBER_MODULE, New, OBJECT_MODULE, Package, Parameter, Reference, STRING_MODULE, Self, Send, Singleton, Test, Variable, WRENatives, allAvailableMethods, allScopedVariables, allVariables, implicitImport, isNamedSingleton, isNotImportedIn, link, linkSentenceInNode, literalValueToClass, mayExecute, parentModule, parse, projectPackages, hasNullValue, hasBooleanValue, projectToJSON, getNodeDefinition, ParameterizedType, sendDefinitions, Super, SourceMap, isVoid, VOID_WKO, REPL, buildEnvironment, assertNotVoid, showParameter, getMethodContainer, Program, getExpressionFor, Expression, If, Return } from '../src' import { WREEnvironment, environmentWithEntities } from './utils' +import { RuntimeObject } from '../src/interpreter/runtimeModel' use(sinonChai) should() @@ -243,9 +244,11 @@ describe('Wollok helpers', () => { }) - describe('sendDefinitions', () => { + describe('getNodeDefinition', () => { + + // Necessary for the methods not to be synthetic + const sourceMap = new SourceMap({ start: { offset: 1, line: 1, column: 1 }, end: { offset: 9, line: 2, column: 3 } }) - const MINIMAL_LANG = environmentWithEntities(OBJECT_MODULE) const environment = getLinkedEnvironment(link([ new Package({ name: 'A', @@ -253,12 +256,20 @@ describe('Wollok helpers', () => { new Class({ name: 'Bird', members: [ + new Field({ + name: 'energy', + isConstant: false, + isProperty: true, + value: new Literal({ value: 100 }), + }), new Method({ name: 'fly', + sourceMap, body: new Body({ sentences: [] }), }), new Method({ name: 'sing', + sourceMap, body: new Body({ sentences: [ new Send({ @@ -271,11 +282,53 @@ describe('Wollok helpers', () => { }), ], }), + new Class({ + name: 'Cage', + members: [ + new Method({ + name: 'size', + sourceMap, + body: new Body({ + sentences: [ + new Literal({ value: 10 }), + ], + }), + isOverride: false, + }), + ], + }), + new Class({ + name: 'SpecialCage', + members: [ + new Method({ + name: 'size', + sourceMap, + body: new Body({ + sentences: [ + new Send({ + receiver: new Super(), + message: '+', + args: [new Literal({ value: 5 })], + }), + ], + }), + isOverride: true, + }), + ], + supertypes: [new ParameterizedType({ reference: new Reference({ name: 'A.Cage' }) })], + }), new Singleton({ name: 'trainer', members: [ + new Field({ + name: 'displayName', + isConstant: false, + isProperty: true, + value: new Literal({ value: 'John ' }), + }), new Method({ name: 'play', + sourceMap, body: new Body({ sentences: [ new Send({ @@ -288,6 +341,7 @@ describe('Wollok helpers', () => { }), new Method({ name: 'pick', + sourceMap, body: new Body({ sentences: [ new Send({ @@ -310,6 +364,7 @@ describe('Wollok helpers', () => { }), new Method({ name: 'play', + sourceMap, body: new Body({ sentences: [ new Send({ @@ -327,24 +382,31 @@ describe('Wollok helpers', () => { }), new Method({ name: 'fly', + sourceMap, body: new Body({ sentences: [] }), }), ], }), ], }), - ], MINIMAL_LANG)) + ], WREEnvironment)) const trainerWKO = environment.getNodeByFQN('A.trainer') as Singleton const anotherTrainerWKO = environment.getNodeByFQN('A.anotherTrainer') as Singleton const birdClass = environment.getNodeByFQN('A.Bird') as Class - const pickTrainerMethod = trainerWKO.allMethods[1] as Method + const trainerPlayMethod = trainerWKO.allMethods[0] as Method + const trainerPickMethod = trainerWKO.allMethods[1] as Method + const anotherTrainerPlayMethod = anotherTrainerWKO.allMethods[0] as Method const anotherTrainerFlyMethod = anotherTrainerWKO.allMethods[1] as Method const birdFlyMethod = birdClass.allMethods[0] as Method + const cageClass = environment.getNodeByFQN('A.Cage') as Class + const cageSizeMethod = cageClass.allMethods[0] as Method + const specialCageClass = environment.getNodeByFQN('A.SpecialCage') as Class + const specialCageSizeMethod = specialCageClass.allMethods[0] as Method it('should return the methods of a class when using new', () => { const sendToNewBird = trainerWKO.allMethods[0].sentences[0] as Send - const definitions = sendDefinitions(environment)(sendToNewBird) + const definitions = getNodeDefinition(environment)(sendToNewBird) definitions.should.deep.equal([birdFlyMethod]) }) @@ -360,7 +422,7 @@ describe('Wollok helpers', () => { it('should return the methods of a singleton when calling to the WKO', () => { const sendToTrainer = anotherTrainerWKO.allMethods[0].sentences[0] as Send const definitions = sendDefinitions(environment)(sendToTrainer) - definitions.should.deep.equal([pickTrainerMethod]) + definitions.should.deep.equal([trainerPickMethod]) }) it('should return all methods definitions matching message & arity when calling to a class', () => { @@ -375,6 +437,24 @@ describe('Wollok helpers', () => { definitions.should.deep.equal([birdFlyMethod]) }) + it('should return the properties of an entity when calling to wko', () => { + const sendToName = new Send({ + receiver: trainerWKO, + message: 'displayName', + }) + const definitions = sendDefinitions(environment)(sendToName) + definitions.should.deep.equal([trainerWKO.allFields[0]]) + }) + + it('should return the properties of an entity when calling to class methods', () => { + const sendToName = new Send({ + receiver: new Reference({ name: 'A.bird' }), + message: 'energy', + }) + const definitions = sendDefinitions(environment)(sendToName) + definitions.should.deep.equal([birdClass.allFields[0]]) + }) + it('should return all methods with the same interface when calling to self is not linked to a module', () => { const sendToSelf = new Send({ receiver: new Self(), @@ -393,6 +473,31 @@ describe('Wollok helpers', () => { definitions.should.deep.equal([birdFlyMethod, anotherTrainerFlyMethod]) }) + it('should match a reference to the corresponding field', () => { + const pepitaReference = (anotherTrainerPlayMethod.sentences[1] as Send).receiver + const pepitaField = anotherTrainerWKO.allFields[0] + const definitions = getNodeDefinition(environment)(pepitaReference) + definitions.should.deep.equal([pepitaField]) + }) + + it('should match a reference to the corresponding class for a new instance', () => { + const newBirdReference = ((trainerPlayMethod.sentences[0] as Send).receiver as New).instantiated + const definitions = getNodeDefinition(environment)(newBirdReference) + definitions.should.deep.equal([birdClass]) + }) + + it('should return the parent method when asking for a super definition', () => { + const callToSuperCageSize = (specialCageSizeMethod.sentences[0] as Send).receiver as Super + const definitions = getNodeDefinition(environment)(callToSuperCageSize) + definitions.should.deep.equal([cageSizeMethod]) + }) + + it('should return self when asking for a self definition', () => { + const callToSelfTrainer = (trainerPickMethod.sentences[0] as Send).receiver as Self + const definitions = getNodeDefinition(environment)(callToSelfTrainer) + definitions.should.deep.equal([trainerWKO]) + }) + }) describe('allVariables', () => { @@ -664,4 +769,199 @@ describe('Wollok helpers', () => { }) + describe('isVoid', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pajarito { + method volar() { + } + } + `, + }]) + const evaluation = Evaluation.build(replEnvironment, WRENatives) + + it('should return true for void singleton', () => { + isVoid(new RuntimeObject(replEnvironment.getNodeByFQN(VOID_WKO), evaluation.currentFrame, undefined)).should.be.true + }) + + it('should return false for Wollok elements', () => { + isVoid(new RuntimeObject(replEnvironment.getNodeByFQN(NUMBER_MODULE), evaluation.currentFrame, 42)).should.be.false + }) + + it('should return false for custom definitions', () => { + isVoid(new RuntimeObject(replEnvironment.getNodeByFQN(REPL + '.pajarito'), evaluation.currentFrame, undefined)).should.be.false + }) + + }) + + describe('assertNotVoid', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pajarito { + method volar() { + } + } + `, + }]) + const evaluation = Evaluation.build(replEnvironment, WRENatives) + + it('should throw error if value is void', () => { + expect(() => assertNotVoid(new RuntimeObject(replEnvironment.getNodeByFQN(VOID_WKO), evaluation.currentFrame, undefined), 'Something failed')).to.throw('Something failed') + }) + + it('should not throw error if value is not void', () => { + assertNotVoid(new RuntimeObject(replEnvironment.getNodeByFQN(NUMBER_MODULE), evaluation.currentFrame, 2), 'Something failed') + }) + + }) + + describe('showParameter', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pajarito { + method volar() { + } + } + `, + }]) + const evaluation = Evaluation.build(replEnvironment, WRENatives) + + it('should show a number', () => { + showParameter(new RuntimeObject(replEnvironment.getNodeByFQN(NUMBER_MODULE), evaluation.currentFrame, 2)).should.equal('"2"') + }) + + it('should show a string', () => { + showParameter(new RuntimeObject(replEnvironment.getNodeByFQN(STRING_MODULE), evaluation.currentFrame, 'pepita')).should.equal('"pepita"') + }) + + it('should show fqn for custom modules', () => { + showParameter(new RuntimeObject(replEnvironment.getNodeByFQN(REPL + '.pajarito'), evaluation.currentFrame, undefined)).should.equal(`"${REPL}.pajarito"`) + }) + + }) + + describe('getMethodContainer', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pajarito { + energia = 100 + method volar() { + energia = energia + 10 + } + }`, + }, { + name: 'test', + content: ` + describe "some describe" { + test "some test" { + assert.equals(1, 1) + } + } + `, + }, { + name: 'program', + content: ` + program Prueba { + const a = 1 + const b = a + 1 + console.println(a) + console.println(b) + } + `, + }, + ]) + + it('should find method container for a method', () => { + const birdSingleton = replEnvironment.getNodeByFQN(REPL + '.pajarito') as Singleton + const volarMethod = birdSingleton.allMethods[0] as Method + const volarSentence = volarMethod.sentences[0] + getMethodContainer(volarSentence)!.should.equal(volarMethod) + }) + + it('should find method container for a test', () => { + const firstDescribe = replEnvironment.getNodeByFQN('test."some describe"') as Describe + const firstTest = firstDescribe.allMembers[0] as Test + const assertSentence = firstTest.sentences[0] + getMethodContainer(assertSentence)!.should.equal(firstTest) + }) + + it('should find method container for a program', () => { + const program = replEnvironment.getNodeByFQN('program.Prueba') as Program + const anySentence = program.sentences()[3] + getMethodContainer(anySentence)!.should.equal(program) + }) + + }) + + describe('getExpression', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pajarito { + energia = 100 + contenta = false + + method jugar() { + contenta = true + } + + method volar() { + if (energia > 100) { + self.jugar() + } + return energia + } + + method valorBase() = 2 + + method bad() { + throw new Exception(message = "Do not call me!") + } + }`, + }, + ]) + + it('should show if expression', () => { + const birdSingleton = replEnvironment.getNodeByFQN(REPL + '.pajarito') as Singleton + const volarMethod = birdSingleton.allMethods[1] as Method + const ifExpression = volarMethod.sentences[0] as Expression + getExpressionFor(ifExpression)!.should.equal('if expression') + }) + + it('should show send expression', () => { + const birdSingleton = replEnvironment.getNodeByFQN(REPL + '.pajarito') as Singleton + const volarMethod = birdSingleton.allMethods[1] as Method + const sendExpression = (volarMethod.sentences[0] as If).thenBody.sentences[0] as Expression + getExpressionFor(sendExpression)!.should.equal('message jugar/0') + }) + + it('should show reference expression', () => { + const birdSingleton = replEnvironment.getNodeByFQN(REPL + '.pajarito') as Singleton + const volarMethod = birdSingleton.allMethods[1] as Method + const referenceExpression = (volarMethod.sentences[1] as Return).value as Expression + getExpressionFor(referenceExpression)!.should.equal('reference \'energia\'') + }) + + it('should show literal expression', () => { + const birdSingleton = replEnvironment.getNodeByFQN(REPL + '.pajarito') as Singleton + const valorBaseMethod = birdSingleton.allMethods[2] as Method + const literalExpression = (valorBaseMethod.sentences[0] as Return).value as Expression + getExpressionFor(literalExpression)!.should.equal('literal 2') + }) + + it('should show self expression', () => { + const birdSingleton = replEnvironment.getNodeByFQN(REPL + '.pajarito') as Singleton + const volarMethod = birdSingleton.allMethods[1] as Method + const selfExpression = ((volarMethod.sentences[0] as If).thenBody.sentences[0] as Send).receiver as Expression + getExpressionFor(selfExpression)!.should.equal('self') + }) + + it('should show default expression', () => { + const birdSingleton = replEnvironment.getNodeByFQN(REPL + '.pajarito') as Singleton + const badMethod = birdSingleton.allMethods[3] as Method + const throwException = badMethod.sentences[0] as Expression + getExpressionFor(throwException)!.should.equal('expression') + }) + + }) + }) \ No newline at end of file diff --git a/test/interpreter.test.ts b/test/interpreter.test.ts index 2b132e8c..1ed3d17c 100644 --- a/test/interpreter.test.ts +++ b/test/interpreter.test.ts @@ -1,8 +1,8 @@ import { expect, should, use } from 'chai' import { restore } from 'sinon' import sinonChai from 'sinon-chai' -import { EXCEPTION_MODULE, Evaluation, REPL, WRENatives } from '../src' -import { DirectedInterpreter, interprete, Interpreter } from '../src/interpreter/interpreter' +import { EXCEPTION_MODULE, Evaluation, REPL, WRENatives, buildEnvironment } from '../src' +import { DirectedInterpreter, getStackTraceSanitized, interprete, Interpreter } from '../src/interpreter/interpreter' import link from '../src/linker' import { Body, Class, Field, Literal, Method, Package, ParameterizedType, Reference, Return, Send, Singleton, SourceIndex, SourceMap } from '../src/model' import { WREEnvironment } from './utils' @@ -10,6 +10,12 @@ import { WREEnvironment } from './utils' use(sinonChai) should() +const assertBasicError = (error?: Error) => { + expect(error).not.to.be.undefined + expect(error!.message).to.contain('Derived from TypeScript stack') + expect(error!.stack).to.contain('at Evaluation.exec') +} + const WRE = link([ new Package({ name: 'wollok', @@ -41,7 +47,7 @@ describe('Wollok Interpreter', () => { it('should be able to execute unlinked sentences', () => { const environment = link([ new Package({ - name:'p', + name: 'p', members: [ new Singleton({ name: 'o', @@ -75,7 +81,7 @@ describe('Wollok Interpreter', () => { it('should fail if there is an uninitialized field in a singleton', () => { const environment = link([ new Package({ - name:'p', + name: 'p', members: [ new Singleton({ name: 'o', @@ -96,7 +102,7 @@ describe('Wollok Interpreter', () => { it('should not fail if there is an explicit null initialization for a field in a singleton', () => { const environment = link([ new Package({ - name:'p', + name: 'p', members: [ new Singleton({ name: 'o', @@ -134,6 +140,12 @@ describe('Wollok Interpreter', () => { describe('interpret API function', () => { let interpreter: Interpreter + const expectError = (command: string, ...errorMessage: string[]) => { + const { error } = interprete(interpreter, command) + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal(errorMessage) + } + const checkSuccessfulResult = (expression: string, expectedResult: string) => { const { result, errored, error } = interprete(interpreter, expression) error?.message.should.be.equal('') @@ -256,6 +268,479 @@ describe('Wollok Interpreter', () => { it('for closure', () => { checkSuccessfulResult('{1 + 2}', '{1 + 2}') }) + + it('should be able to execute sentences related to a hierarchy defined in different packages', () => { + const replEnvironment = buildEnvironment([{ + name: 'jefeDeDepartamento.wlk', content: ` + import medico.* + + class Jefe inherits Medico { + const subordinados = #{} + + override method atenderA(unaPersona) { + subordinados.anyOne().atenderA(unaPersona) + } + } + `, + }, { + name: 'medico.wlk', content: ` + import persona.* + + class Medico inherits Persona { + const dosis + + override method contraerEnfermedad(unaEnfermedad) { + super(unaEnfermedad) + self.atenderA(self) + } + method atenderA(unaPersona) { + unaPersona.recibirMedicamento(dosis) + } + + } + `, + }, { + name: 'persona.wlk', content: ` + class Persona { + const enfermedades = [] + + method contraerEnfermedad(unaEnfermedad) { + + enfermedades.add(unaEnfermedad) + } + + method saludar() = "hola" + } + `, + }, { + name: REPL, content: ` + import medico.* + + object testit { + method test() = new Medico(dosis = 200).saludar() + } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error, result } = interprete(interpreter, 'testit.test()') + expect(error).to.be.undefined + expect(result).to.equal('"hola"') + }) + + it('should be able to execute sentences related to a hierarchy defined in different packages - 2', () => { + const replEnvironment = buildEnvironment([{ + name: 'medico.wlk', content: ` + import persona.* + + class Medico inherits Persona { + const dosis + + override method contraerEnfermedad(unaEnfermedad) { + super(unaEnfermedad) + self.atenderA(self) + } + + method atenderA(unaPersona) { + unaPersona.recibirMedicamento(dosis) + } + + } + `, + }, { + name: 'pediatra.wlk', content: ` + import jefeDeDepartamento.* + + class Pediatra inherits Jefe { + const property fechaIngreso = new Date() + + method esNuevo() = fechaIngreso.year() < 2022 + } + `, + }, { + name: 'jefeDeDepartamento.wlk', content: ` + import medico.* + + class Jefe inherits Medico { + const subordinados = #{} + + override method atenderA(unaPersona) { + subordinados.anyOne().atenderA(unaPersona) + } + } + `, + }, { + name: 'persona.wlk', content: ` + class Persona { + const enfermedades = [] + + method contraerEnfermedad(unaEnfermedad) { + + enfermedades.add(unaEnfermedad) + } + + method saludar() = "hola" + } + `, + }, { + name: REPL, content: ` + import pediatra.* + + object testit { + method test() = new Pediatra(dosis = 200).saludar() + } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error, result } = interprete(interpreter, 'testit.test()') + expect(error).to.be.undefined + expect(result).to.equal('"hola"') + }) + + }) + + describe('sanitize stack trace', () => { + + it('should filter Typescript stack', () => { + const { error } = interprete(interpreter, '2.coso()') + expect(error).not.to.be.undefined + expect(error!.message).to.contain('Derived from TypeScript stack') + expect(error!.stack).to.contain('at Evaluation.execThrow') + expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.MessageNotUnderstoodException: 2 does not understand coso()']) + }) + + it('should wrap RangeError errors', () => { + const { error } = interprete(interpreter, '[1, 2, 3].get(3)') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: RangeError: get: index should be between 0 and 2']) + }) + + it('should wrap TypeError errors', () => { + const { error } = interprete(interpreter, '1 < "hola"') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: TypeError: Message (<): parameter "hola" should be a number']) + }) + + it('should wrap custom TypeError errors', () => { + expectError('new Date() - 2', 'wollok.lang.EvaluationError: TypeError: Message (-): parameter "2" should be a Date') + expectError('new Date() < "hola"', 'wollok.lang.EvaluationError: TypeError: Message (<): parameter "hola" should be a Date') + expectError('new Date() > []', 'wollok.lang.EvaluationError: TypeError: Message (>): parameter "wollok.lang.List" should be a Date') + }) + + it('should wrap Typescript Error errors', () => { + const { error } = interprete(interpreter, 'new Date(day = 1, month = 2, year = 2001, nonsense = 2)') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: Error: Can\'t initialize wollok.lang.Date with value for unexistent field nonsense']) + }) + + it('should wrap RuntimeModel errors', () => { + const { error } = interprete(interpreter, 'new Sound()') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: Error: Sound cannot be instantiated, you must pass values to the following attributes: file']) + }) + + it('should wrap null validation errors', () => { + const { error } = interprete(interpreter, '5 + null') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: RangeError: Message (+) does not support parameter \'other\' to be null']) + }) + + it('should wrap void validation errors for void parameter', () => { + const { error } = interprete(interpreter, '5 + [1,2,3].add(4)') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: RangeError: Message Number.+/1: parameter #1 produces no value, cannot use it']) + }) + + it('should wrap void validation errors when sending a message to a void object', () => { + expectError('([1].add(2)).add(3)', 'wollok.lang.EvaluationError: RangeError: Cannot send message add, receiver is an expression that produces no value.') + }) + + it('should wrap void validation errors for void parameter in super call', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + class Bird { + var energy = 100 + method fly(minutes) { + energy = 4 * minutes + energy + } + } + + class MockingBird inherits Bird { + override method fly(minutes) { + super([1, 2].add(4)) + } + } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, 'new MockingBird().fly(2)') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal( + [ + 'wollok.lang.EvaluationError: RangeError: super call for message fly/1: parameter #1 produces no value, cannot use it', + ' at REPL.MockingBird.fly(minutes) [REPL:11]', + ] + ) + }) + + it('should wrap void validation errors for void condition in if', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + class Bird { + var energy = 100 + method fly(minutes) { + if ([1, 2].add(3)) { + energy = 50 + } + } + } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, 'new Bird().fly(2)') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal( + [ + 'wollok.lang.EvaluationError: RangeError: Message fly - if condition produces no value, cannot use it', + ' at REPL.Bird.fly(minutes) [REPL:5]', + ] + ) + }) + + it('Can\'t redefine a const with a var', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + const variableName = 1 + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, 'var variableName = 2') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal( + [ + 'wollok.lang.EvaluationError: Error: Can\'t redefine a variable', + + ] + ) + const { result } = interprete(interpreter, 'variableName') + expect(+result).to.equal(1) + }) + + it('Can\'t redefine a const with a const', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + const variableName = 1 + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, 'const variableName = 2') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal( + [ + 'wollok.lang.EvaluationError: Error: Can\'t redefine a variable', + + ] + ) + const { result } = interprete(interpreter, 'variableName') + expect(+result).to.equal(1) + }) + + it('Can\'t redefine a var with a const', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + var variableName = 1 + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, 'const variableName = 2') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal( + [ + 'wollok.lang.EvaluationError: Error: Can\'t redefine a variable', + + ] + ) + const { result } = interprete(interpreter, 'variableName') + expect(+result).to.equal(1) + }) + + it('Can\'t redefine a var with a var', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + var variableName = 1 + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, 'var variableName = 2') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal( + [ + 'wollok.lang.EvaluationError: Error: Can\'t redefine a variable', + + ] + ) + const { result } = interprete(interpreter, 'variableName') + expect(+result).to.equal(1) + }) + + it('should wrap void validation errors for assignment to void value', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pepita { + method volar() { + } + }`, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + expectError('const a = pepita.volar()', 'wollok.lang.EvaluationError: RangeError: Cannot assign to variable \'a\': message volar/0 produces no value, cannot assign it to a variable') + expectError('const a = if (4 > 5) true else pepita.volar()', 'wollok.lang.EvaluationError: RangeError: Cannot assign to variable \'a\': if expression produces no value, cannot assign it to a variable') + expectError('const a = [1].add(2)', 'wollok.lang.EvaluationError: RangeError: Cannot assign to variable \'a\': message add/1 produces no value, cannot assign it to a variable') + }) + + it('should wrap void validation errors for void method used in expression', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pepita { + method volar() { + } + }`, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, '5 + pepita.volar()') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal([ + 'wollok.lang.EvaluationError: RangeError: Message Number.+/1: parameter #1 produces no value, cannot use it', + ]) + }) + + it('should handle errors when using void values in new named parameters', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + class Bird { + var energy = 100 + var name = "Pepita" + } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + expectError('new Bird(energy = void)', 'wollok.lang.EvaluationError: RangeError: new REPL.Bird: value of parameter \'energy\' produces no value, cannot use it') + expectError('new Bird(energy = 150, name = [1].add(2))', 'wollok.lang.EvaluationError: RangeError: new REPL.Bird: value of parameter \'name\' produces no value, cannot use it') + }) + + it('should show Wollok stack', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object comun { + method volar() { + self.despegar() + } + + method despegar() { + return new Date().plusDays(new Date()) + } + } + + class Ave { + var energy = 100 + const formaVolar = comun + + method volar() { + formaVolar.volar() + } + }`, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, 'new Ave().volar()') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal([ + 'wollok.lang.EvaluationError: TypeError: Message plusDays: parameter "wollok.lang.Date" should be a number', + ' at REPL.comun.despegar() [REPL:8]', + ' at REPL.comun.volar() [REPL:4]', + ' at REPL.Ave.volar() [REPL:17]', + ]) + }) + + it('should handle errors when using void return values for wko', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pepita { + method unMetodo() { + return [1,2,3].add(4) + 5 + } + } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, 'pepita.unMetodo()') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal([ + 'wollok.lang.EvaluationError: RangeError: Cannot send message +, receiver is an expression that produces no value.', + ' at REPL.pepita.unMetodo() [REPL:4]', + ]) + }) + + it('should handle errors when using void closures inside native list methods', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + const pepita = object { method energia(total) { } } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + expectError('[1, 2].filter { n => pepita.energia(n) }', 'wollok.lang.EvaluationError: RangeError: Message filter: closure produces no value. Check the return type of the closure (missing return?)') + expectError('[1, 2].findOrElse({ n => pepita.energia(n) }, {})', 'wollok.lang.EvaluationError: RangeError: Message findOrElse: predicate produces no value. Check the return type of the closure (missing return?)') + expectError('[1, 2].fold(0, { acum, total => pepita.energia(1) })', 'wollok.lang.EvaluationError: RangeError: Message fold: closure produces no value. Check the return type of the closure (missing return?)') + expectError('[1, 2].sortBy({ a, b => pepita.energia(1) })', 'wollok.lang.EvaluationError: RangeError: Message sortBy: closure produces no value. Check the return type of the closure (missing return?)') + }) + + it('should handle errors when using void closures inside native set methods', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + const pepita = object { method energia(total) { } } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + expectError('#{1, 2}.filter { n => pepita.energia(n) }', 'wollok.lang.EvaluationError: RangeError: Message filter: closure produces no value. Check the return type of the closure (missing return?)') + expectError('#{1, 2}.findOrElse({ n => pepita.energia(n) }, {})', 'wollok.lang.EvaluationError: RangeError: Message findOrElse: predicate produces no value. Check the return type of the closure (missing return?)') + expectError('#{1, 2}.fold(0, { acum, total => pepita.energia(1) })', 'wollok.lang.EvaluationError: RangeError: Message fold: closure produces no value. Check the return type of the closure (missing return?)') + }) + + it('should handle errors when using void closures inside Wollok list methods', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + const pepita = object { method energia(total) { } } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + expectError('[1, 2].map { n => pepita.energia(n) }', 'wollok.lang.EvaluationError: RangeError: map - while sending message List.add/1: parameter #1 produces no value, cannot use it') + }) + + it('should handle errors when using void parameters', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + const pepita = object { method energia() { } } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + expect('[].add(pepita.energia())', 'wollok.lang.EvaluationError: RangeError: Message List.add/1: parameter #1 produces no value, cannot use it') + }) + + }) + + it('should handle void values for assert', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pajarito { + method volar() { + } + } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + expectError('assert.that(pajarito.volar())', 'wollok.lang.EvaluationError: RangeError: Message assert.that/1: parameter #1 produces no value, cannot use it') + }) + + it('should allow a forEach to receive a void closure', () => { + const { errored } = interprete(interpreter, '[1, 2, 3].forEach({ element => [].add(4) })') + expect(errored).to.be.false }) }) diff --git a/test/linker.test.ts b/test/linker.test.ts index 922da80e..c960b66e 100644 --- a/test/linker.test.ts +++ b/test/linker.test.ts @@ -2,7 +2,7 @@ import { expect, should, use } from 'chai' import { GAME_MODULE, OBJECT_MODULE } from '../src' import { getPotentiallyUninitializedLazy } from '../src/decorators' import link, { canBeReferenced, linkSentenceInNode } from '../src/linker' -import { Body, Class, Closure, Describe, Environment, Field, Import, Method, Mixin, NamedArgument, Node, Package, Parameter, ParameterizedType, Reference, Return, Sentence, Singleton, Test, Variable } from '../src/model' +import { Body, Class, Closure, Describe, Environment, Field, Import, Method, Mixin, NamedArgument, Node, Package, Parameter, ParameterizedType, Reference, Return, Sentence, Singleton, Test, Variable, Literal } from '../src/model' import * as parse from '../src/parser' import { linkerAssertions } from './assertions' import { environmentWithEntities, WREEnvironment } from './utils' @@ -464,6 +464,65 @@ describe('Wollok linker', () => { C.methods[0].sentences[0].should.target(C.fields[0]) }) + it('should target references to members inherited from superclass in different packages', () => { + const environment = link([ + new Package({ + name: 'aaa', + imports: [ + new Import({ isGeneric: true, entity: new Reference({ name: 'bbb' }) }), + ], + members: [ + new Class({ + name: 'C', + supertypes: [new ParameterizedType({ reference: new Reference({ name: 'B' }) })], + members: [ + new Method({ + name: 'm2', + body: new Body({ sentences: [new Literal({ value: '2' })] }), + }), + ], + }), + ], + }), + new Package({ + name: 'bbb', + imports: [ + new Import({ isGeneric: true, entity: new Reference({ name: 'zzz' }) }), + ], + members: [ + new Class({ + name: 'B', + supertypes: [new ParameterizedType({ reference: new Reference({ name: 'A' }) })], + members: [ + new Method({ + name: 'm', + body: new Body({ sentences: [new Reference({ name: 'x' })] }), + }), + ], + }), + ], + }), + new Package({ + name: 'zzz', + members: [ + new Class({ + name: 'A', members: [ + new Field({ name: 'x', isConstant: false }), + ], + }), + ], + }), + ], WREEnvironment) + + const C = environment.getNodeByFQN('aaa.C') + const B = environment.getNodeByFQN('bbb.B') + const A = environment.getNodeByFQN('zzz.A') + + C.supertypes[0].reference.should.target(B) + B.supertypes[0].reference.should.target(A) + B.methods[0].sentences[0].should.target(A.fields[0]) + }) + it('should target references overriden on mixins to members inherited from superclass', () => { const environment = link([ new Package({ diff --git a/test/messageReporter.test.ts b/test/messageReporter.test.ts new file mode 100644 index 00000000..048425b5 --- /dev/null +++ b/test/messageReporter.test.ts @@ -0,0 +1,84 @@ +import { expect, should } from 'chai' +import { getMessage, LANGUAGES } from '../src' + +should() + +const MISSING_WOLLOK_TS_CLI = 'missing_wollok_ts_cli' +const EXAMPLE_WITH_VALUES = 'example' +const BAD_INTERPOLATION_MESSAGE = 'bad_interpolation_message' + +const getCustomMessages = () => { + const lspMessagesEn = { + [MISSING_WOLLOK_TS_CLI]: 'Missing configuration WollokLSP/cli-path in order to run Wollok tasks', + [EXAMPLE_WITH_VALUES]: '{0} needs a previous {1} installation', + [BAD_INTERPOLATION_MESSAGE]: '{a} is not well defined, like { }', + } + const lspMessagesEs = { + [MISSING_WOLLOK_TS_CLI]: 'Falta la configuraciΓ³n WollokLSP/cli-path para poder ejecutar tareas de Wollok', + [EXAMPLE_WITH_VALUES]: '{0} debe tener instalado {1} previamente', + [BAD_INTERPOLATION_MESSAGE]: '{a} estΓ‘ mal definido, como { }', + } + + return { + en: lspMessagesEn, + es: lspMessagesEs, + } +} + +describe('message reporter', () => { + describe('get message', () => { + + it('should convert a camel case english message into a human readable message', () => { + expect(getMessage({ message: 'shouldConvertHumanReadableMessage' })).to.equal('Rule failure: Should convert human readable message') + }) + + it('should convert a camel case spanish message into a human readable message', () => { + expect(getMessage({ message: 'shouldConvertHumanReadableMessage', language: LANGUAGES.SPANISH })).to.equal('La siguiente regla fallΓ³: Should convert human readable message') + }) + + it('should convert an existing english message into a human readable message', () => { + expect(getMessage({ message: 'possiblyReturningBlock' })).to.equal('This method is returning a block, consider removing the \'=\' before curly braces.') + }) + + it('should convert an existing spanish message into a human readable message', () => { + expect(getMessage({ message: 'possiblyReturningBlock', language: LANGUAGES.SPANISH })).to.equal('Este mΓ©todo devuelve un bloque, si no es la intenciΓ³n elimine el \'=\' antes de las llaves.') + }) + + it('should convert an existing english message with values into a human readable message', () => { + expect(getMessage({ message: 'shouldPassValuesToAllAttributes', values: ['Ave', 'energia, calor'] })).to.equal('Ave cannot be instantiated, you must pass values to the following attributes: energia, calor') + }) + + it('should convert an existing spanish message with values into a human readable message', () => { + expect(getMessage({ message: 'shouldPassValuesToAllAttributes', language: LANGUAGES.SPANISH, values: ['Ave', 'energia, calor'] })).to.equal('No se puede instanciar Ave. Falta pasar valores a los siguientes atributos: energia, calor') + }) + + it('should convert a custom english message into a human readable message', () => { + expect(getMessage({ message: MISSING_WOLLOK_TS_CLI, customMessages: getCustomMessages() })).to.equal('Missing configuration WollokLSP/cli-path in order to run Wollok tasks') + }) + + it('should convert a custom spanish message into a human readable message', () => { + expect(getMessage({ message: MISSING_WOLLOK_TS_CLI, customMessages: getCustomMessages(), language: LANGUAGES.SPANISH })).to.equal('Falta la configuraciΓ³n WollokLSP/cli-path para poder ejecutar tareas de Wollok') + }) + + it('should convert a custom english message with values into a human readable message', () => { + expect(getMessage({ message: EXAMPLE_WITH_VALUES, customMessages: getCustomMessages(), values: ['wollok-lsp-ide', 'wollok-ts-cli'] })).to.equal('wollok-lsp-ide needs a previous wollok-ts-cli installation') + }) + + it('should convert a custom spanish message with values into a human readable message', () => { + expect(getMessage({ message: EXAMPLE_WITH_VALUES, customMessages: getCustomMessages(), values: ['wollok-lsp-ide', 'wollok-ts-cli'], language: LANGUAGES.SPANISH })).to.equal('wollok-lsp-ide debe tener instalado wollok-ts-cli previamente') + }) + + it('edge case: should return empty string if message is empty', () => { + expect(getMessage({ message: '' })).to.equal('') + }) + + it('edge case: should leave a blank if values are not passed', () => { + expect(getMessage({ message: EXAMPLE_WITH_VALUES, customMessages: getCustomMessages(), values: ['wollok-lsp-ide'] })).to.equal('wollok-lsp-ide needs a previous installation') + }) + + it('edge case: should leave a blank if message has a bad definition', () => { + expect(getMessage({ message: BAD_INTERPOLATION_MESSAGE, customMessages: getCustomMessages(), values: ['wollok-lsp-ide', 'wollok-ts-cli'] })).to.equal('{a} is not well defined, like { }') + }) + + }) +}) \ No newline at end of file