From cfdf366784438e411ad93e9a9c5ca3c30a7b5de3 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sun, 6 Jun 2021 13:00:59 -0600 Subject: [PATCH] parseQuery fails when bind variable con ... #153 #153 - A new parser option has been added named `ignoreParseErrors`, which will remove invalid parts of a query if there are parsing errors. The general structure of the query must be valid and the `SELECT` and `WHERE` clauses must both be valid, but any other clause may be removed from the parsed output if there are errors parsing the query and `ignoreParseErrors` is set to `true`. This option has been added to the documentation application. resolves #153 --- CHANGELOG.md | 10 + README.md | 10 +- .../modules/my/queryParser/queryParser.html | 9 + .../src/modules/my/queryParser/queryParser.ts | 38 ++- package-lock.json | 233 +++++++----------- src/parser/parser.ts | 229 ++++++++++------- src/parser/visitor.ts | 51 ++-- test/test-cases.ts | 16 ++ 8 files changed, 331 insertions(+), 265 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 347e380..e3ea948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 4.1.0 + +June 6, 2021 + +#153 - A new parser option has been added named `ignoreParseErrors`, which will remove invalid parts of a query if there are parsing errors. + +The general structure of the query must be valid and the `SELECT` and `WHERE` clauses must both be valid, but any other clause may be removed from the parsed output if there are errors parsing the query and `ignoreParseErrors` is set to `true`. + +This option has been added to the documentation application. + ## 4.0.0 April 13, 20201 diff --git a/README.md b/README.md index bacb075..c093b77 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This library uses [Chevrotain](https://github.com/SAP/chevrotain) to parse queries. Prior to version 2.0.0, [antlr4](https://github.com/antlr/antlr4) was used. Migrating from version 1 to version 2? [Check out the changelog](CHANGELOG.md#200) for a full list of changes. + Migrating from version 2 to version 3? [Check out the changelog](CHANGELOG.md#300) for a full list of changes. Want to try it out? [Check out the demo](https://paustint.github.io/soql-parser-js/). @@ -80,10 +81,11 @@ Many of hte utility functions are provided to easily determine the shape of spec **ParseQueryConfig** -| Property | Type | Description | required | default | -| ---------------------- | ------- | ---------------------------------------------------------------------------------------------------- | -------- | ------- | -| allowApexBindVariables | boolean | Determines if apex variables are allowed in parsed query. Example: `WHERE Id IN :accountIds`. | FALSE | FALSE | -| logErrors | boolean | If true, then additional detail will be logged to the console if there is a lexing or parsing error. | FALSE | FALSE | +| Property | Type | Description | required | default | +| ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- | +| allowApexBindVariables | boolean | Determines if apex variables are allowed in parsed query. Example: `WHERE Id IN :accountIds`. Only simple Apex is supported. Function calls are not supported. (e.x. `accountMap.keyset()` is not supported) | FALSE | FALSE | +| ignoreParseErrors | boolean | If set to true, then queries with partially invalid syntax will still be parsed, but any clauses with invalid parts will be omitted. The SELECT clause and FROM clause must always be valid, but all other clauses can contain invalid parts. | FALSE | FALSE | +| logErrors | boolean | If true, parsing and lexing errors will be logged to the console. | FALSE | FALSE | **SoqlComposeConfig** diff --git a/docs/src/modules/my/queryParser/queryParser.html b/docs/src/modules/my/queryParser/queryParser.html index b2726ac..577a5f8 100644 --- a/docs/src/modules/my/queryParser/queryParser.html +++ b/docs/src/modules/my/queryParser/queryParser.html @@ -4,6 +4,15 @@

+ + +
diff --git a/docs/src/modules/my/queryParser/queryParser.ts b/docs/src/modules/my/queryParser/queryParser.ts index a71ba24..83cbf8a 100644 --- a/docs/src/modules/my/queryParser/queryParser.ts +++ b/docs/src/modules/my/queryParser/queryParser.ts @@ -21,22 +21,25 @@ export default class QueryParser extends LightningElement { @track parsedQuery: Query; @track parsedQueryJson: string; @track composedQuery: string; + @track allowApexBindVariables = false; + @track ignoreParseErrors = false; hasError = false; hasRendered = false; renderedCallback() { if (!this.hasRendered) { - // @ts-ignore type-mismatch - const element = this.template.querySelector('code.javascript'); - element.innerText = `// parseQuery(soqlQuery);`; - hljs.highlightBlock(element); + this.setExampleJs(); this.hasRendered = true; } } parseQuery() { try { - this.parsedQuery = parseQuery(this._query || ''); + this.parsedQuery = parseQuery(this._query || '', { + allowApexBindVariables: this.allowApexBindVariables, + ignoreParseErrors: this.ignoreParseErrors, + logErrors: true + }); this.parsedQueryJson = JSON.stringify(this.parsedQuery, null, 2); this.hasError = false; } catch (ex) { @@ -47,6 +50,13 @@ export default class QueryParser extends LightningElement { this.dispatchEvent(new CustomEvent('queryerror', { detail: this.hasError })); } + setExampleJs() { + // @ts-ignore type-mismatch + const element = this.template.querySelector('code.javascript'); + element.innerText = `parseQuery(soqlQuery, { allowApexBindVariables: ${this.allowApexBindVariables}, ignoreParseErrors: ${this.ignoreParseErrors} });`; + hljs.highlightBlock(element); + } + highlight() { // @ts-ignore type-mismatch const element = this.template.querySelector('code.json'); @@ -55,4 +65,22 @@ export default class QueryParser extends LightningElement { hljs.highlightBlock(element); } } + + handleChange(event) { + const { name, value } = event.detail; + switch (name) { + case 'allowApexBindVariables': { + this.allowApexBindVariables = value; + break; + } + case 'ignoreParseErrors': { + this.ignoreParseErrors = value; + break; + } + default: + break; + } + this.setExampleJs(); + this.parseQuery(); + } } diff --git a/package-lock.json b/package-lock.json index 22b83aa..355af8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,27 +34,27 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", "dev": true, "dependencies": { - "@babel/highlight": "^7.10.4" + "@babel/highlight": "^7.12.13" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", "dev": true }, "node_modules/@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.14.0", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } @@ -2853,15 +2853,6 @@ "node": ">= 6" } }, - "node_modules/fast-glob/node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, - "engines": { - "node": ">=8.6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -3268,9 +3259,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", "dev": true }, "node_modules/growl": { @@ -3775,6 +3766,18 @@ "is-ci": "bin.js" } }, + "node_modules/is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -4337,16 +4340,16 @@ } }, "node_modules/micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, "dependencies": { "braces": "^3.0.1", - "picomatch": "^2.0.5" + "picomatch": "^2.2.3" }, "engines": { - "node": ">=8" + "node": ">=8.6" } }, "node_modules/miller-rabin": { @@ -5701,12 +5704,15 @@ } }, "node_modules/picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true, "engines": { - "node": ">=8" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pidtree": { @@ -6001,16 +6007,6 @@ "node": ">=0.10" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=8.6" - } - }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -6437,12 +6433,6 @@ "node": ">=8" } }, - "node_modules/release-it/node_modules/signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, "node_modules/release-it/node_modules/string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -6543,12 +6533,16 @@ "dev": true }, "node_modules/resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "dev": true, "dependencies": { + "is-core-module": "^2.2.0", "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-alpn": { @@ -6900,9 +6894,9 @@ "dev": true }, "node_modules/signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, "node_modules/slash": { @@ -7092,9 +7086,9 @@ } }, "node_modules/source-map-support": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", - "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, "dependencies": { "buffer-from": "^1.0.0", @@ -7427,16 +7421,6 @@ "node": ">= 6.9.0" } }, - "node_modules/terser/node_modules/source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -8423,12 +8407,6 @@ "node": ">=8" } }, - "node_modules/webpack-cli/node_modules/signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, "node_modules/webpack-cli/node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8878,27 +8856,27 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", "dev": true, "requires": { - "@babel/highlight": "^7.10.4" + "@babel/highlight": "^7.12.13" } }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", "dev": true }, "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.14.0", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } @@ -11326,12 +11304,6 @@ "requires": { "is-glob": "^4.0.1" } - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true } } }, @@ -11679,9 +11651,9 @@ } }, "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", "dev": true }, "growl": { @@ -12087,6 +12059,15 @@ "ci-info": "^3.1.1" } }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -12538,13 +12519,13 @@ "dev": true }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, "requires": { "braces": "^3.0.1", - "picomatch": "^2.0.5" + "picomatch": "^2.2.3" } }, "miller-rabin": { @@ -13657,9 +13638,9 @@ } }, "picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true }, "pidtree": { @@ -13902,15 +13883,6 @@ "optional": true, "requires": { "picomatch": "^2.2.1" - }, - "dependencies": { - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, - "optional": true - } } }, "rechoir": { @@ -14243,12 +14215,6 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -14327,11 +14293,12 @@ "dev": true }, "resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "dev": true, "requires": { + "is-core-module": "^2.2.0", "path-parse": "^1.0.6" } }, @@ -14628,9 +14595,9 @@ } }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, "slash": { @@ -14787,9 +14754,9 @@ } }, "source-map-support": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", - "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -15050,18 +15017,6 @@ "commander": "^2.20.0", "source-map": "~0.6.1", "source-map-support": "~0.5.12" - }, - "dependencies": { - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - } } }, "terser-webpack-plugin": { @@ -16006,12 +15961,6 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 2c4d874..1d9f7dc 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -3,6 +3,7 @@ import * as lexer from './lexer'; export interface ParseQueryConfig { allowApexBindVariables?: boolean; + ignoreParseErrors?: boolean; logErrors?: boolean; } @@ -40,14 +41,16 @@ export class SoqlParser extends CstParser { // Set to true to allow apex bind variables, such as "WHERE Id IN :accountIds" public allowApexBindVariables = false; + public ignoreParseErrors = false; - constructor() { + constructor({ ignoreParseErrors }: { ignoreParseErrors: boolean } = { ignoreParseErrors: false }) { super(lexer.allTokens, { // true in production (webpack replaces this string) skipValidations: false, + recoveryEnabled: ignoreParseErrors, // nodeLocationTracking: 'full', // not sure if needed, could look at }); - + this.ignoreParseErrors = ignoreParseErrors; this.performSelfAnalysis(); } @@ -108,94 +111,126 @@ export class SoqlParser extends CstParser { } } - private selectClause = this.RULE('selectClause', () => { - this.CONSUME(lexer.Select); - this.AT_LEAST_ONE_SEP({ - SEP: lexer.Comma, - DEF: () => { - this.OR( - this.$_selectClause || - (this.$_selectClause = [ - // selectClauseFunctionIdentifier must be first because the alias could also be an identifier - { ALT: () => this.SUBRULE(this.selectClauseFunctionIdentifier, { LABEL: 'field' }) }, - { ALT: () => this.SUBRULE(this.selectClauseSubqueryIdentifier, { LABEL: 'field' }) }, - { ALT: () => this.SUBRULE(this.selectClauseTypeOf, { LABEL: 'field' }) }, - { ALT: () => this.SUBRULE(this.selectClauseIdentifier, { LABEL: 'field' }) }, - ]), - ); - }, - }); - }); + private selectClause = this.RULE( + 'selectClause', + () => { + this.CONSUME(lexer.Select); + this.AT_LEAST_ONE_SEP({ + SEP: lexer.Comma, + DEF: () => { + this.OR( + this.$_selectClause || + (this.$_selectClause = [ + // selectClauseFunctionIdentifier must be first because the alias could also be an identifier + { ALT: () => this.SUBRULE(this.selectClauseFunctionIdentifier, { LABEL: 'field' }) }, + { ALT: () => this.SUBRULE(this.selectClauseSubqueryIdentifier, { LABEL: 'field' }) }, + { ALT: () => this.SUBRULE(this.selectClauseTypeOf, { LABEL: 'field' }) }, + { ALT: () => this.SUBRULE(this.selectClauseIdentifier, { LABEL: 'field' }) }, + ]), + ); + }, + }); + }, + { resyncEnabled: false }, + ); - private selectClauseFunctionIdentifier = this.RULE('selectClauseFunctionIdentifier', () => { - this.OR( - this.$_selectClauseFunctionIdentifier || - (this.$_selectClauseFunctionIdentifier = [ - { ALT: () => this.SUBRULE(this.dateFunction, { LABEL: 'fn' }) }, - { ALT: () => this.SUBRULE(this.aggregateFunction, { LABEL: 'fn', ARGS: [true] }) }, - { ALT: () => this.SUBRULE(this.locationFunction, { LABEL: 'fn' }) }, - { ALT: () => this.SUBRULE(this.fieldsFunction, { LABEL: 'fn' }) }, - { ALT: () => this.SUBRULE(this.otherFunction, { LABEL: 'fn' }) }, - ]), - ); - this.OPTION(() => this.CONSUME(lexer.Identifier, { LABEL: 'alias' })); - }); + private selectClauseFunctionIdentifier = this.RULE( + 'selectClauseFunctionIdentifier', + () => { + this.OR( + this.$_selectClauseFunctionIdentifier || + (this.$_selectClauseFunctionIdentifier = [ + { ALT: () => this.SUBRULE(this.dateFunction, { LABEL: 'fn' }) }, + { ALT: () => this.SUBRULE(this.aggregateFunction, { LABEL: 'fn', ARGS: [true] }) }, + { ALT: () => this.SUBRULE(this.locationFunction, { LABEL: 'fn' }) }, + { ALT: () => this.SUBRULE(this.fieldsFunction, { LABEL: 'fn' }) }, + { ALT: () => this.SUBRULE(this.otherFunction, { LABEL: 'fn' }) }, + ]), + ); + this.OPTION(() => this.CONSUME(lexer.Identifier, { LABEL: 'alias' })); + }, + { resyncEnabled: false }, + ); - private selectClauseSubqueryIdentifier = this.RULE('selectClauseSubqueryIdentifier', () => { - this.CONSUME(lexer.LParen); - this.SUBRULE(this.selectStatement); - this.CONSUME(lexer.RParen); - }); + private selectClauseSubqueryIdentifier = this.RULE( + 'selectClauseSubqueryIdentifier', + () => { + this.CONSUME(lexer.LParen); + this.SUBRULE(this.selectStatement); + this.CONSUME(lexer.RParen); + }, + { resyncEnabled: false }, + ); - private selectClauseTypeOf = this.RULE('selectClauseTypeOf', () => { - this.CONSUME(lexer.Typeof); - this.CONSUME(lexer.Identifier, { LABEL: 'typeOfField' }); - this.AT_LEAST_ONE({ - DEF: () => { - this.SUBRULE(this.selectClauseTypeOfThen); - }, - }); - this.OPTION(() => { - this.SUBRULE(this.selectClauseTypeOfElse); - }); - this.CONSUME(lexer.End); - }); + private selectClauseTypeOf = this.RULE( + 'selectClauseTypeOf', + () => { + this.CONSUME(lexer.Typeof); + this.CONSUME(lexer.Identifier, { LABEL: 'typeOfField' }); + this.AT_LEAST_ONE({ + DEF: () => { + this.SUBRULE(this.selectClauseTypeOfThen); + }, + }); + this.OPTION(() => { + this.SUBRULE(this.selectClauseTypeOfElse); + }); + this.CONSUME(lexer.End); + }, + { resyncEnabled: false }, + ); - private selectClauseIdentifier = this.RULE('selectClauseIdentifier', () => { - this.CONSUME(lexer.Identifier, { LABEL: 'field' }); - this.OPTION(() => this.CONSUME1(lexer.Identifier, { LABEL: 'alias' })); - }); + private selectClauseIdentifier = this.RULE( + 'selectClauseIdentifier', + () => { + this.CONSUME(lexer.Identifier, { LABEL: 'field' }); + this.OPTION(() => this.CONSUME1(lexer.Identifier, { LABEL: 'alias' })); + }, + { resyncEnabled: false }, + ); - private selectClauseTypeOfThen = this.RULE('selectClauseTypeOfThen', () => { - this.CONSUME(lexer.When); - this.CONSUME(lexer.Identifier, { LABEL: 'typeOfField' }); - this.CONSUME(lexer.Then); - this.AT_LEAST_ONE_SEP({ - SEP: lexer.Comma, - DEF: () => { - this.CONSUME1(lexer.Identifier, { LABEL: 'field' }); - }, - }); - }); + private selectClauseTypeOfThen = this.RULE( + 'selectClauseTypeOfThen', + () => { + this.CONSUME(lexer.When); + this.CONSUME(lexer.Identifier, { LABEL: 'typeOfField' }); + this.CONSUME(lexer.Then); + this.AT_LEAST_ONE_SEP({ + SEP: lexer.Comma, + DEF: () => { + this.CONSUME1(lexer.Identifier, { LABEL: 'field' }); + }, + }); + }, + { resyncEnabled: false }, + ); - private selectClauseTypeOfElse = this.RULE('selectClauseTypeOfElse', () => { - this.CONSUME(lexer.Else); - this.AT_LEAST_ONE_SEP({ - SEP: lexer.Comma, - DEF: () => { - this.CONSUME(lexer.Identifier, { LABEL: 'field' }); - }, - }); - }); + private selectClauseTypeOfElse = this.RULE( + 'selectClauseTypeOfElse', + () => { + this.CONSUME(lexer.Else); + this.AT_LEAST_ONE_SEP({ + SEP: lexer.Comma, + DEF: () => { + this.CONSUME(lexer.Identifier, { LABEL: 'field' }); + }, + }); + }, + { resyncEnabled: false }, + ); - private fromClause = this.RULE('fromClause', () => { - this.CONSUME(lexer.From); - this.CONSUME(lexer.Identifier); - this.OPTION({ - GATE: () => !(this.LA(1).tokenType === lexer.Offset && this.LA(2).tokenType === lexer.UnsignedInteger), - DEF: () => this.CONSUME1(lexer.Identifier, { LABEL: 'alias' }), - }); - }); + private fromClause = this.RULE( + 'fromClause', + () => { + this.CONSUME(lexer.From); + this.CONSUME(lexer.Identifier); + this.OPTION({ + GATE: () => !(this.LA(1).tokenType === lexer.Offset && this.LA(2).tokenType === lexer.UnsignedInteger), + DEF: () => this.CONSUME1(lexer.Identifier, { LABEL: 'alias' }), + }); + }, + { resyncEnabled: false }, + ); private usingScopeClause = this.RULE('usingScopeClause', () => { this.CONSUME(lexer.Using); @@ -672,36 +707,50 @@ export class SoqlParser extends CstParser { } } -const parser = new SoqlParser(); +let parser = new SoqlParser(); export function parse(soql: string, options?: ParseQueryConfig) { - options = options || { allowApexBindVariables: false, logErrors: false }; + const { allowApexBindVariables, logErrors, ignoreParseErrors } = options || { + allowApexBindVariables: false, + logErrors: false, + ignoreParseErrors: false, + }; const lexResult = lexer.lex(soql); if (lexResult.errors.length > 0) { - if (options.logErrors) { + if (logErrors) { console.log('Lexing Errors:'); console.log(lexResult.errors); } throw new LexingError(lexResult.errors[0]); } + // get new instance if ignoreParseErrors changes + if (parser.ignoreParseErrors !== ignoreParseErrors) { + parser = new SoqlParser({ ignoreParseErrors }); + } + // setting a new input will RESET the parser instance's state. parser.input = lexResult.tokens; // If true, allows WHERE foo = :bar - parser.allowApexBindVariables = options.allowApexBindVariables || false; + parser.allowApexBindVariables = allowApexBindVariables || false; const cst = parser.selectStatement(); if (parser.errors.length > 0) { - if (options.logErrors) { + if (logErrors) { console.log('Parsing Errors:'); console.log(parser.errors); } - throw new ParsingError(parser.errors[0]); + if (!ignoreParseErrors) { + throw new ParsingError(parser.errors[0]); + } } - return cst; + return { + cst, + parseErrors: parser.errors.map(err => new ParsingError(err)), + }; } diff --git a/src/parser/visitor.ts b/src/parser/visitor.ts index f8277e8..ec6199a 100644 --- a/src/parser/visitor.ts +++ b/src/parser/visitor.ts @@ -1,4 +1,4 @@ -import { IToken } from 'chevrotain'; +import { CstNode, IToken } from 'chevrotain'; import { Condition, ConditionWithValueQuery, @@ -230,51 +230,53 @@ class SOQLVisitor extends BaseSoqlVisitor { }); } - if (ctx.usingScopeClause) { + if (ctx.usingScopeClause && !ctx.usingScopeClause[0].recoveredNode) { output.usingScope = this.visit(ctx.usingScopeClause); } - if (ctx.whereClause) { + if (ctx.whereClause && !ctx.whereClause[0].recoveredNode) { output.where = this.visit(ctx.whereClause); } if (ctx.withClause) { - ctx.withClause.forEach((item: any) => { - const { withSecurityEnforced, withDataCategory } = this.visit(item); - if (withSecurityEnforced) { - output.withSecurityEnforced = withSecurityEnforced; - } - if (withDataCategory) { - output.withDataCategory = withDataCategory; - } - }); + ctx.withClause + .filter(item => !item.recoveredNode) + .forEach(item => { + const { withSecurityEnforced, withDataCategory } = this.visit(item); + if (withSecurityEnforced) { + output.withSecurityEnforced = withSecurityEnforced; + } + if (withDataCategory) { + output.withDataCategory = withDataCategory; + } + }); } - if (ctx.groupByClause) { + if (ctx.groupByClause && !ctx.groupByClause[0].recoveredNode) { output.groupBy = this.visit(ctx.groupByClause); } - if (ctx.havingClause) { + if (ctx.havingClause && !ctx.havingClause[0].recoveredNode) { output.having = this.visit(ctx.havingClause); } - if (ctx.orderByClause) { + if (ctx.orderByClause && !ctx.orderByClause[0].recoveredNode) { output.orderBy = this.visit(ctx.orderByClause); } - if (ctx.limitClause) { + if (ctx.limitClause && !ctx.limitClause[0].recoveredNode) { output.limit = Number(this.visit(ctx.limitClause)); } - if (ctx.offsetClause) { + if (ctx.offsetClause && !ctx.offsetClause[0].recoveredNode) { output.offset = Number(this.visit(ctx.offsetClause)); } - if (ctx.forViewOrReference) { + if (ctx.forViewOrReference && !ctx.forViewOrReference[0].recoveredNode) { output.for = this.visit(ctx.forViewOrReference); } - if (ctx.updateTrackingViewstat) { + if (ctx.updateTrackingViewstat && !ctx.updateTrackingViewstat[0].recoveredNode) { output.update = this.visit(ctx.updateTrackingViewstat); } @@ -844,19 +846,20 @@ const visitor = new SOQLVisitor(); * @param soql */ export function parseQuery(soql: string, options?: ParseQueryConfig): Query { - const query: Query = visitor.visit(parse(soql, options)); + const { cst } = parse(soql, options); + const query: Query = visitor.visit(cst); return query; } /** - * Lex and parse query (without walking parsed results) - * to determine if query is valid + * Lex and parse query (without walking parsed results) to determine if query is valid. + * options.ignoreParseErrors will not be honored * @param soql */ export function isQueryValid(soql: string, options?: ParseQueryConfig): boolean { try { - parse(soql, options); - return true; + const { parseErrors } = parse(soql, options); + return parseErrors.length === 0 ? true : false; } catch (ex) { return false; } diff --git a/test/test-cases.ts b/test/test-cases.ts index 098e88a..6f6d5d3 100644 --- a/test/test-cases.ts +++ b/test/test-cases.ts @@ -2263,6 +2263,22 @@ export const testCases: TestCase[] = [ ], }, }, + { + testCase: 112, + options: { allowApexBindVariables: true, ignoreParseErrors: true }, + soql: `SELECT Id, (SELECT Id FROM Contacts WHERE Id IN :contactMap.keySet()) FROM Account WHERE Id IN :accountMap.keySet()`, + soqlComposed: `SELECT Id, (SELECT Id FROM Contacts) FROM Account`, + output: { + fields: [ + { + type: 'Field', + field: 'Id', + }, + { type: 'FieldSubquery', subquery: { fields: [{ type: 'Field', field: 'Id' }], relationshipName: 'Contacts' } }, + ], + sObject: 'Account', + }, + }, ]; export default testCases;