diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d715922..8391f06 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,7 +1,7 @@ name: CI on: [push, pull_request] jobs: - Build: + build: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] diff --git a/README.md b/README.md index 3bb1df8..339817f 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,20 @@ import { lintExpression } from "@bpmn-io/feel-lint" lintExpression('foo = bar'); ``` -### Codemirror plugin +You may pass custom language configuration to the editor: -The `cmFeelLinter` function returns a codemirror linting source that you can use as a extension -in you codemirror instance. +```javascript +lintExpression('> 10, "yes", mike\'s name', { + dialect: 'unaryTests', + context: { + "mike's name": "Mike the might" + } +}); +``` + +### CodeMirror plugin + +The `cmFeelLinter` function returns a [`LintSource`](https://codemirror.net/docs/ref/#lint.LintSource) that you can use to extend your [CodeMirror](https://codemirror.net/) instance. ```javascript import { cmFeelLinter } from "@bpmn-io/feel-lint" diff --git a/lib/text/index.js b/lib/text/index.js index 1093d08..748e665 100644 --- a/lib/text/index.js +++ b/lib/text/index.js @@ -1,15 +1,29 @@ -import { parser } from 'lezer-feel'; +import { parser, trackVariables } from 'lezer-feel'; import lintAll from '../shared/index.js'; /** * Create an array of syntax errors for the given expression. * * @param {String} expression + * @param { { + * dialect?: 'expression' | 'unaryTests', + * context?: Record, + * parserDialect?: string + * } } [lintOptions] + * * @returns {LintMessage[]} array of syntax errors */ -export function lintExpression(expression) { +export function lintExpression(expression, { + dialect = 'expression', + parserDialect, + context = {} +} = {}) { - const syntaxTree = parser.parse(expression); + const syntaxTree = parser.configure({ + top: dialect === 'unaryTests' ? 'UnaryTests' : 'Expression', + dialect: parserDialect, + contextTracker: trackVariables(context) + }).parse(expression); const lintMessages = lintAll({ syntaxTree, diff --git a/package-lock.json b/package-lock.json index 331ff53..9903b4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@codemirror/language": "^6.10.8", - "lezer-feel": "^1.2.3" + "lezer-feel": "^1.7.0" }, "devDependencies": { "@codemirror/lang-json": "^6.0.1", @@ -865,9 +865,10 @@ "integrity": "sha512-Wmvlm4q6tRpwiy20TnB3yyLTZim38Tkc50dPY8biQRwqE+ati/wD84rm3N15hikvdT4uSg9phs9ubjvcLmkpKg==" }, "node_modules/@lezer/highlight": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.6.tgz", - "integrity": "sha512-cmSJYa2us+r3SePpRCjN5ymCqCPv+zyXmDl0ciWtVaNiORT/MxM7ZgOMQZADD0o51qOaOg24qc/zBViOIwAjJg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" } @@ -885,9 +886,10 @@ } }, "node_modules/@lezer/lr": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.14.tgz", - "integrity": "sha512-z5mY4LStlA3yL7aHT/rqgG614cfcvklS+8oFRFBYrs4YaWLJyKKM4+nN6KopToX0o9Hj6zmH6M5kinOYuy06ug==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" } @@ -5056,12 +5058,14 @@ } }, "node_modules/lezer-feel": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lezer-feel/-/lezer-feel-1.2.4.tgz", - "integrity": "sha512-ASi0yQd6A8a2xeNF+b5Sr7fPcko236i81q9yzMbzi81lKc93CZ3SRR7rgCZgHMVifVthofZRoNczR5lenCRmlw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lezer-feel/-/lezer-feel-1.7.0.tgz", + "integrity": "sha512-UC8h3Nu4llRPISEUhv+Ne7bNkdxjf4+/DcU4KfO8zKxycWxev8d2BoVnGlG17zbQDtQJBD39ZQvWtjCeTFm69g==", + "license": "MIT", "dependencies": { - "@lezer/highlight": "^1.1.6", - "@lezer/lr": "^1.3.12" + "@lezer/highlight": "^1.2.1", + "@lezer/lr": "^1.4.2", + "min-dash": "^4.2.1" }, "engines": { "node": "*" @@ -5262,6 +5266,12 @@ "node": ">= 0.6" } }, + "node_modules/min-dash": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-4.2.2.tgz", + "integrity": "sha512-qbhSYUxk6mBaF096B3JOQSumXbKWHenmT97cSpdNzgkWwGjhjhE/KZODCoDNhI2I4C9Cb6R/Q13S4BYkUSXoXQ==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -8236,9 +8246,9 @@ "integrity": "sha512-Wmvlm4q6tRpwiy20TnB3yyLTZim38Tkc50dPY8biQRwqE+ati/wD84rm3N15hikvdT4uSg9phs9ubjvcLmkpKg==" }, "@lezer/highlight": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.6.tgz", - "integrity": "sha512-cmSJYa2us+r3SePpRCjN5ymCqCPv+zyXmDl0ciWtVaNiORT/MxM7ZgOMQZADD0o51qOaOg24qc/zBViOIwAjJg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", "requires": { "@lezer/common": "^1.0.0" } @@ -8255,9 +8265,9 @@ } }, "@lezer/lr": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.14.tgz", - "integrity": "sha512-z5mY4LStlA3yL7aHT/rqgG614cfcvklS+8oFRFBYrs4YaWLJyKKM4+nN6KopToX0o9Hj6zmH6M5kinOYuy06ug==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "requires": { "@lezer/common": "^1.0.0" } @@ -11200,12 +11210,13 @@ } }, "lezer-feel": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lezer-feel/-/lezer-feel-1.2.4.tgz", - "integrity": "sha512-ASi0yQd6A8a2xeNF+b5Sr7fPcko236i81q9yzMbzi81lKc93CZ3SRR7rgCZgHMVifVthofZRoNczR5lenCRmlw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lezer-feel/-/lezer-feel-1.7.0.tgz", + "integrity": "sha512-UC8h3Nu4llRPISEUhv+Ne7bNkdxjf4+/DcU4KfO8zKxycWxev8d2BoVnGlG17zbQDtQJBD39ZQvWtjCeTFm69g==", "requires": { - "@lezer/highlight": "^1.1.6", - "@lezer/lr": "^1.3.12" + "@lezer/highlight": "^1.2.1", + "@lezer/lr": "^1.4.2", + "min-dash": "^4.2.1" } }, "lines-and-columns": { @@ -11351,6 +11362,11 @@ "mime-db": "1.52.0" } }, + "min-dash": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-4.2.2.tgz", + "integrity": "sha512-qbhSYUxk6mBaF096B3JOQSumXbKWHenmT97cSpdNzgkWwGjhjhE/KZODCoDNhI2I4C9Cb6R/Q13S4BYkUSXoXQ==" + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", diff --git a/package.json b/package.json index a61136d..6a0fe83 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,6 @@ }, "dependencies": { "@codemirror/language": "^6.10.8", - "lezer-feel": "^1.2.3" + "lezer-feel": "^1.7.0" } } diff --git a/test/spec/editorLinter.spec.js b/test/spec/editorLinter.spec.js index f73fec4..6537b9d 100644 --- a/test/spec/editorLinter.spec.js +++ b/test/spec/editorLinter.spec.js @@ -4,6 +4,7 @@ import { LanguageSupport } from '@codemirror/language'; import { feelLanguage } from 'lang-feel'; import { cmFeelLinter } from '../../lib'; + describe('lint - Editor', function() { it('should accept valid expression', function() { @@ -21,6 +22,20 @@ describe('lint - Editor', function() { }); + it('should accept valid with parserDialect=camunda', function() { + + // when + const view = createFeelViewer('a.`b - c`', { dialect: 'camunda' }); + const lint = cmFeelLinter(); + + // when + const results = lint(view); + + // then + expect(results).to.have.length(0); + }); + + it('should not return syntax error on empty document', function() { // given @@ -39,7 +54,7 @@ describe('lint - Editor', function() { it('should return syntax error', function() { // given - const view = createFeelViewer('= 15'); + const view = createFeelViewer('^15'); const lint = cmFeelLinter(); // when @@ -57,7 +72,7 @@ describe('lint - Editor', function() { it('should return 0-width syntax error', function() { // given - const view = createFeelViewer('15 == 15'); + const view = createFeelViewer('15 =^ 15'); const lint = cmFeelLinter(); // when @@ -67,7 +82,7 @@ describe('lint - Editor', function() { expect(results).to.have.length(1); expect(results[0].severity).to.eql('error'); expect(results[0].source).to.eql('Syntax Error'); - expect(results[0].message).to.eql('Unrecognized token in '); + expect(results[0].message).to.eql('Unrecognized token in '); }); @@ -75,12 +90,12 @@ describe('lint - Editor', function() { // helpers ////////// -function createFeelViewer(doc) { +function createFeelViewer(doc, config = {}) { return new EditorView({ state: EditorState.create({ doc, extensions: [ - new LanguageSupport(feelLanguage, [ ]) + new LanguageSupport(feelLanguage.configure(config), [ ]) ] }) }); diff --git a/test/spec/textLinter.spec.js b/test/spec/textLinter.spec.js index 9a832d0..c09c514 100644 --- a/test/spec/textLinter.spec.js +++ b/test/spec/textLinter.spec.js @@ -1,7 +1,6 @@ - - import { lintExpression } from '../../lib'; + describe('lint - Text', function() { it('should accept valid expression', function() { @@ -18,6 +17,53 @@ describe('lint - Text', function() { }); + it('should accept valid unaryTests', function() { + + // given + const expression = '> 10, 100, "FOOBAR"'; + + // when + const results = lintExpression(expression, { + dialect: 'unaryTests' + }); + + // then + expect(results).to.have.length(0); + }); + + + it('should accept valid with parserDialect=camunda', function() { + + // given + const expression = 'a.`b - c`'; + + // when + const results = lintExpression(expression, { + parserDialect: 'camunda' + }); + + // then + expect(results).to.have.length(0); + }); + + + it('should accept valid expression with contextual value', function() { + + // given + const expression = 'get or else(10, 100)'; + + // when + const results = lintExpression(expression, { + context: { + 'get or else': (value, _default) => typeof value === 'undefined' ? _default : value + } + }); + + // then + expect(results).to.have.length(0); + }); + + it('should return syntax error on empty document', function() { // given @@ -38,7 +84,7 @@ describe('lint - Text', function() { it('should return syntax error', function() { // given - const expression = '= 15'; + const expression = '^15'; // when const results = lintExpression(expression); @@ -55,7 +101,7 @@ describe('lint - Text', function() { it('should return 0-width syntax error', function() { // given - const expression = '15 == 15'; + const expression = '15 =^15'; // when const results = lintExpression(expression); @@ -64,7 +110,7 @@ describe('lint - Text', function() { expect(results).to.have.length(1); expect(results[0].severity).to.eql('error'); expect(results[0].type).to.eql('Syntax Error'); - expect(results[0].message).to.eql('Unrecognized token in '); + expect(results[0].message).to.eql('Unrecognized token in '); });