diff --git a/messages/SfgeEngine.md b/messages/SfgeEngine.md index a60b487a9..6bb4ce0c8 100644 --- a/messages/SfgeEngine.md +++ b/messages/SfgeEngine.md @@ -5,7 +5,3 @@ Please wait # messages.spinnerStart Analyzing with Salesforce Graph Engine. See %s for details. - -# errors.failedWithoutProjectDir - -The --projectdir|-p flag is missing. Rerun your command with --projectdir|-p to allow Graph Engine to run, or with --engine|-e to exclude Graph Engine from execution. \ No newline at end of file diff --git a/messages/run-common.md b/messages/run-common.md index 807d9221b..74ba6bc44 100644 --- a/messages/run-common.md +++ b/messages/run-common.md @@ -32,11 +32,11 @@ Writes output to a file. # flags.projectdirSummary -provide root directory of project +root directory of project # flags.projectdirDescription -Provides the relative or absolute root project directory used to set the context for Graph Engine's analysis. Project directory must be a path, not a glob. Specify multiple values as a comma-separated list. +Provides the relative or absolute root project directories used to set the context for Graph Engine's analysis. Specify multiple values as a comma-separated list. Each project directory must be a path, not a glob. If --projectdir isn’t specified, a default value is calculated. The default value is a directory that contains all the target files. # flags.sevthresholdSummary @@ -81,3 +81,15 @@ The selected output format doesn't match the output file type. Output format: %s # validations.projectdirMustExist --projectdir must specify existing paths + +# validations.noFilesFoundInTarget + +No files were found in the target. --target must contain at least one file. + +# info.resolvedTarget + +The --target flag wasn't specified so the default target '.' will be used. + +# info.resolvedProjectDir + +The --projectdir flag wasn’t specified so the calculated project directory '%s' will be used. diff --git a/messages/run-dfa.md b/messages/run-dfa.md index 0efcee1b1..1912e57c6 100644 --- a/messages/run-dfa.md +++ b/messages/run-dfa.md @@ -33,7 +33,7 @@ Specifies number of rule evaluation threads, or how many entrypoints can be eval # flags.rulethreadtimeoutSummary -specify timeout for individual rule threads in milliseconds. Alternatively, set the timeout value using environment variable `SFGE_RULE_THREAD_TIMEOUT`. Default: 90000 ms +specify timeout for individual rule threads in milliseconds. Alternatively, set the timeout value using environment variable `SFGE_RULE_THREAD_TIMEOUT`. Default: 900000 ms # flags.rulethreadtimeoutDescription @@ -49,11 +49,11 @@ Specifies Java Virtual Machine arguments to override system defaults while execu # flags.targetSummary -return location of source code +source code location # flags.targetDescription -Returns the source code location. Use glob patterns or specify individual methods with #-syntax. Multiple values are specified as a comma-separated list. +Specifies the source code location. Use glob patterns or specify individual methods with #-syntax. Multiple values are specified as a comma-separated list. Default is ".". # flags.withpilotSummary @@ -71,10 +71,6 @@ Method-level targets supplied to --target cannot be globs Method-level target %s must be a real file -# validations.projectdirIsRequired - ---projectdir is required for this command. - # examples The paths specified for --projectdir must contain all files specified through --target cumulatively. diff --git a/messages/run-pathless.md b/messages/run-pathless.md index db341aa81..8f313161d 100644 --- a/messages/run-pathless.md +++ b/messages/run-pathless.md @@ -20,7 +20,7 @@ source code location # flags.targetDescription -Source code location. May use glob patterns. Specify multiple values as a comma-separated list. +Specifies the source code location. May use glob patterns. Specify multiple values as a comma-separated list. Default is ".". # flags.envSummary diff --git a/package.json b/package.json index c25f5893c..66ce561e6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/sfdx-scanner", "description": "Static code scanner that applies quality and security rules to Apex code, and provides feedback.", - "version": "3.20.0", + "version": "3.21.0", "author": "ISV SWAT", "bugs": "https://github.com/forcedotcom/sfdx-scanner/issues", "dependencies": { diff --git a/retire-js/RetireJsVulns.json b/retire-js/RetireJsVulns.json index efa03be62..d5aa2f530 100644 --- a/retire-js/RetireJsVulns.json +++ b/retire-js/RetireJsVulns.json @@ -570,7 +570,8 @@ "/(§§version§§)/jquery.mobile(\\.min)?\\.js" ], "filecontent": [ - "/\\*!?(?:\n \\*)? jQuery Mobile(?: -)? v(§§version§§)" + "/\\*!?[\\s*]*jQuery Mobile(?: -)? v?(§§version§§)", + "// Version of the jQuery Mobile Framework[\\s]+version: *[\"'](§§version§§)[\"']," ], "hashes": {} } @@ -2319,7 +2320,8 @@ "meta\\.revision=\"Ember@(§§version§§)\"", "e\\(\"ember/version\",\\[\"exports\"\\],function\\(e\\)\\{\"use strict\";?[\\s]*e(?:\\.|\\[\")default(?:\"\\])?=\"(§§version§§)\"", "\\(\"ember/version\",\\[\"exports\"\\],function\\(e\\)\\{\"use strict\";.{1,70}\\.default=\"(§§version§§)\"", - "/\\*![\\s]+\\* @overview Ember - JavaScript Application Framework[\\s\\S]{0,400}\\* @version (§§version§§)" + "/\\*![\\s]+\\* @overview Ember - JavaScript Application Framework[\\s\\S]{0,400}\\* @version (§§version§§)", + "// Version: (§§version§§)[\\s]+\\(function\\(\\) *\\{[\\s]*/\\*\\*[\\s]+@module ember[\\s]" ], "hashes": {} } @@ -3090,7 +3092,7 @@ "angular(?:js)?-(§§version§§)(.min)?\\.js" ], "filecontent": [ - "/\\*[ \n]+AngularJS v(§§version§§)", + "/\\*[\\*\\s]+(?:@license )?AngularJS v(§§version§§)", "http://errors\\.angularjs\\.org/(§§version§§)/" ], "hashes": {} @@ -3216,7 +3218,8 @@ ], "filecontent": [ "//[ ]+Backbone.js (§§version§§)", - "a=t.Backbone=\\{\\}\\}a.VERSION=\"(§§version§§)\"" + "a=t.Backbone=\\{\\}\\}a.VERSION=\"(§§version§§)\"", + "Backbone\\.VERSION *= *[\"'](§§version§§)[\"']" ], "hashes": {} } @@ -3321,7 +3324,7 @@ "below": "3.0.8", "severity": "high", "cwe": [ - "CWE-79" + "CWE-94" ], "identifiers": { "summary": "Versions of `handlebars` prior to 3.0.8 or 4.5.2 are vulnerable to Arbitrary Code Execution. The package's lookup helper fails to properly validate templates, allowing attackers to submit templates that execute arbitrary JavaScript in the system. It can be used to run arbitrary code in a server processing Handlebars templates or on a victim's browser (effectively serving as Cross-Site Scripting).\n\nThe following template can be used to demonstrate the vulnerability: \n```{{#with \"constructor\"}}\n\t{{#with split as |a|}}\n\t\t{{pop (push \"alert('Vulnerable Handlebars JS');\")}}\n\t\t{{#with (concat (lookup join (slice 0 1)))}}\n\t\t\t{{#each (slice 2 3)}}\n\t\t\t\t{{#with (apply 0 a)}}\n\t\t\t\t\t{{.}}\n\t\t\t\t{{/with}}\n\t\t\t{{/each}}\n\t\t{{/with}}\n\t{{/with}}\n{{/with}}```\n\n\n## Recommendation\n\nUpgrade to version 3.0.8, 4.5.2 or later.", @@ -3531,7 +3534,7 @@ "below": "4.5.2", "severity": "high", "cwe": [ - "CWE-79" + "CWE-94" ], "identifiers": { "summary": "Versions of `handlebars` prior to 3.0.8 or 4.5.2 are vulnerable to Arbitrary Code Execution. The package's lookup helper fails to properly validate templates, allowing attackers to submit templates that execute arbitrary JavaScript in the system. It can be used to run arbitrary code in a server processing Handlebars templates or on a victim's browser (effectively serving as Cross-Site Scripting).\n\nThe following template can be used to demonstrate the vulnerability: \n```{{#with \"constructor\"}}\n\t{{#with split as |a|}}\n\t\t{{pop (push \"alert('Vulnerable Handlebars JS');\")}}\n\t\t{{#with (concat (lookup join (slice 0 1)))}}\n\t\t\t{{#each (slice 2 3)}}\n\t\t\t\t{{#with (apply 0 a)}}\n\t\t\t\t\t{{.}}\n\t\t\t\t{{/with}}\n\t\t\t{{/each}}\n\t\t{{/with}}\n\t{{/with}}\n{{/with}}```\n\n\n## Recommendation\n\nUpgrade to version 3.0.8, 4.5.2 or later.", @@ -3656,7 +3659,8 @@ "Handlebars=\\{VERSION:(?:'|\")(§§version§§)(?:'|\")", "this.Handlebars=\\{\\};[\n\r \t]+\\(function\\([a-z]\\)\\{[a-z].VERSION=(?:'|\")(§§version§§)(?:'|\")", "exports.HandlebarsEnvironment=[\\s\\S]{70,120}exports.VERSION=(?:'|\")(§§version§§)(?:'|\")", - "/\\*+![\\s]+(?:@license)?[\\s]+handlebars v(§§version§§)" + "/\\*+![\\s]+(?:@license)?[\\s]+handlebars v+(§§version§§)", + "window\\.Handlebars=.,.\\.VERSION=\"(§§version§§)\"" ], "hashes": {} } @@ -4599,7 +4603,8 @@ "\\.version=\"(§§version§§)\".{20,60}\"isBefore\".{20,60}\"isAfter\".{200,500}\\.isMoment=", "\\.version=\"(§§version§§)\".{20,300}duration.{2,100}\\.isMoment=", "\\.isMoment\\(.{50,400}_isUTC.{50,400}=\"(§§version§§)\"", - "=\"(§§version§§)\".{300,1000}Years:31536e6.{60,80}\\.isMoment" + "=\"(§§version§§)\".{300,1000}Years:31536e6.{60,80}\\.isMoment", + "// Moment.js is freely distributable under the terms of the MIT license.[\\s]+//[\\s]+// Version (§§version§§)" ] } }, @@ -4634,7 +4639,8 @@ "/underscore\\.js/(§§version§§)/underscore(-min)?\\.js" ], "filecontent": [ - "//[\\s]*Underscore.js (§§version§§)" + "//[\\s]*Underscore.js (§§version§§)", + "// *Underscore\\.js[\\s\\S]{1,2500}_\\.VERSION *= *['\"](§§version§§)['\"]" ] } }, @@ -4679,50 +4685,51 @@ ], "severity": "medium", "identifiers": { - "summary": "XSS in data-target property of scrollspy", + "summary": "XSS in data-container property of tooltip", "issue": "20184", "CVE": [ - "CVE-2018-14041" + "CVE-2018-14042" ], - "githubID": "GHSA-pj7m-g53m-7638" + "githubID": "GHSA-7mvr-5x2g-wfc8" }, "info": [ - "https://github.com/advisories/GHSA-pj7m-g53m-7638", "https://github.com/twbs/bootstrap/issues/20184" ] }, { "below": "3.4.0", + "severity": "medium", "cwe": [ "CWE-79" ], - "severity": "medium", "identifiers": { - "summary": "XSS in data-container property of tooltip", - "issue": "20184", + "summary": "In Bootstrap before 3.4.0, XSS is possible in the affix configuration target property.", "CVE": [ - "CVE-2018-14042" - ] + "CVE-2018-20677" + ], + "githubID": "GHSA-ph58-4vrj-w6hr" }, "info": [ - "https://github.com/twbs/bootstrap/issues/20184" + "https://github.com/advisories/GHSA-ph58-4vrj-w6hr" ] }, { "below": "3.4.0", - "severity": "medium", "cwe": [ "CWE-79" ], + "severity": "medium", "identifiers": { - "summary": "In Bootstrap before 3.4.0, XSS is possible in the affix configuration target property.", + "summary": "XSS in data-target property of scrollspy", + "issue": "20184", "CVE": [ - "CVE-2018-20677" + "CVE-2018-14041" ], - "githubID": "GHSA-ph58-4vrj-w6hr" + "githubID": "GHSA-pj7m-g53m-7638" }, "info": [ - "https://github.com/advisories/GHSA-ph58-4vrj-w6hr" + "https://github.com/advisories/GHSA-pj7m-g53m-7638", + "https://github.com/twbs/bootstrap/issues/20184" ] }, { @@ -4822,15 +4829,14 @@ ], "severity": "medium", "identifiers": { - "summary": "XSS in data-target property of scrollspy", + "summary": "XSS in data-container property of tooltip", "issue": "20184", "CVE": [ - "CVE-2018-14041" + "CVE-2018-14042" ], - "githubID": "GHSA-pj7m-g53m-7638" + "githubID": "GHSA-7mvr-5x2g-wfc8" }, "info": [ - "https://github.com/advisories/GHSA-pj7m-g53m-7638", "https://github.com/twbs/bootstrap/issues/20184" ] }, @@ -4842,13 +4848,15 @@ ], "severity": "medium", "identifiers": { - "summary": "XSS in data-container property of tooltip", + "summary": "XSS in data-target property of scrollspy", "issue": "20184", "CVE": [ - "CVE-2018-14042" - ] + "CVE-2018-14041" + ], + "githubID": "GHSA-pj7m-g53m-7638" }, "info": [ + "https://github.com/advisories/GHSA-pj7m-g53m-7638", "https://github.com/twbs/bootstrap/issues/20184" ] }, @@ -5403,7 +5411,9 @@ "/ext-base-(§§version§§)(\\.min)?\\.js" ], "filecontent": [ - "/*!\n * Ext JS Library (§§version§§)" + "/*!\n * Ext JS Library (§§version§§)", + "Ext = \\{[\\s]*/\\*[^/]+/[\\s]*version *: *['\"](§§version§§)['\"]", + "var version *= *['\"](§§version§§)['\"], *Version;[\\s]*Ext.Version *= *Version *= *Ext.extend" ] } }, @@ -6421,13 +6431,521 @@ "extractors": { "filecontent": [ "/\\*!(?:[\\s]+\\*)? Select2 (§§version§§)", - "/\\*[\\s]+Copyright 20[0-9]{2} [I]gor V[a]ynberg[\\s]+Version: (§§version§§)[\\s\\S]{1,4000}(\\.attr\\(\"class\",\"select2-sizer\"|\\.data\\(document,\"select2-lastpos\"|document\\)\\.data\\(\"select2-lastpos\")" + "/\\*[\\s]+Copyright 20[0-9]{2} [I]gor V[a]ynberg[\\s]+Version: (§§version§§)[\\s\\S]{1,5000}(\\.attr\\(\"class\",\"select2-sizer\"|\\.data\\(document, *\"select2-lastpos\"|document\\)\\.data\\(\"select2-lastpos\"|SingleSelect2, *MultiSelect2|window.Select2 *!== *undefined)" ], "uri": [ "(§§version§§)/(js/)?select2(.min)?\\.js" ] } }, + "blueimp-file-upload": { + "vulnerabilities": [ + { + "below": "9.22.1", + "cwe": [ + "CWE-434" + ], + "severity": "high", + "identifiers": { + "summary": "Unrestricted Upload of File with Dangerous Type in blueimp-file-upload", + "CVE": [ + "CVE-2018-9206" + ], + "githubID": "GHSA-4cj8-g9cp-v5wr" + }, + "info": [ + "https://github.com/advisories/GHSA-4cj8-g9cp-v5wr", + "https://nvd.nist.gov/vuln/detail/CVE-2018-9206", + "https://github.com/advisories/GHSA-4cj8-g9cp-v5wr", + "https://wpvulndb.com/vulnerabilities/9136", + "https://www.exploit-db.com/exploits/45790/", + "https://www.exploit-db.com/exploits/46182/", + "https://www.oracle.com/technetwork/security-advisory/cpujan2019-5072801.html", + "http://www.securityfocus.com/bid/105679", + "http://www.securityfocus.com/bid/106629", + "http://www.vapidlabs.com/advisory.php?v=204" + ] + } + ], + "extractors": { + "filecontent": [ + "/\\*[\\s*]+jQuery File Upload User Interface Plugin (§§version§§)[\\s*]+https://github.com/blueimp" + ], + "uri": [ + "/blueimp-file-upload/(§§version§§)/jquery.fileupload(-ui)?(\\.min)?\\.js" + ] + } + }, + "c3": { + "vulnerabilities": [ + { + "below": "0.4.11", + "cwe": [ + "CWE-79" + ], + "severity": "medium", + "identifiers": { + "summary": "Cross-Site Scripting in c3", + "CVE": [ + "CVE-2016-1000240" + ], + "githubID": "GHSA-gvg7-pp82-cff3" + }, + "info": [ + "https://github.com/advisories/GHSA-gvg7-pp82-cff3", + "https://nvd.nist.gov/vuln/detail/CVE-2016-1000240", + "https://github.com/c3js/c3/issues/1536", + "https://github.com/c3js/c3/pull/1675", + "https://github.com/c3js/c3/commit/de3864650300488a63d0541620e9828b00e94b42", + "https://github.com/c3js/c3", + "https://www.npmjs.com/advisories/138" + ] + } + ], + "extractors": { + "uri": [ + "/(§§version§§)/c3(\\.min)?\\.js" + ], + "filecontent": [ + "[\\s]+var c3 ?= ?\\{ ?version: ?['\"](§§version§§)['\"] ?\\};[\\s]+var c3_chart_fn," + ] + } + }, + "lodash": { + "vulnerabilities": [ + { + "below": "4.17.5", + "cwe": [ + "CWE-471" + ], + "severity": "low", + "identifiers": { + "summary": "Prototype Pollution in lodash", + "CVE": [ + "CVE-2018-3721" + ], + "githubID": "GHSA-fvqr-27wr-82fm" + }, + "info": [ + "https://github.com/advisories/GHSA-fvqr-27wr-82fm", + "https://nvd.nist.gov/vuln/detail/CVE-2018-3721", + "https://github.com/lodash/lodash/commit/d8e069cc3410082e44eb18fcf8e7f3d08ebe1d4a", + "https://hackerone.com/reports/310443", + "https://github.com/advisories/GHSA-fvqr-27wr-82fm", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://www.npmjs.com/advisories/577" + ] + }, + { + "below": "4.17.11", + "cwe": [ + "CWE-400" + ], + "severity": "high", + "identifiers": { + "summary": "Prototype Pollution in lodash", + "CVE": [ + "CVE-2018-16487" + ], + "githubID": "GHSA-4xc9-xhrj-v574" + }, + "info": [ + "https://github.com/advisories/GHSA-4xc9-xhrj-v574", + "https://nvd.nist.gov/vuln/detail/CVE-2018-16487", + "https://github.com/lodash/lodash/commit/90e6199a161b6445b01454517b40ef65ebecd2ad", + "https://hackerone.com/reports/380873", + "https://github.com/advisories/GHSA-4xc9-xhrj-v574", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://www.npmjs.com/advisories/782" + ] + }, + { + "below": "4.17.11", + "cwe": [ + "CWE-400" + ], + "severity": "medium", + "identifiers": { + "summary": "Regular Expression Denial of Service (ReDoS) in lodash", + "CVE": [ + "CVE-2019-1010266" + ], + "githubID": "GHSA-x5rq-j2xg-h7qm" + }, + "info": [ + "https://github.com/advisories/GHSA-x5rq-j2xg-h7qm", + "https://nvd.nist.gov/vuln/detail/CVE-2019-1010266", + "https://github.com/lodash/lodash/issues/3359", + "https://github.com/lodash/lodash/commit/5c08f18d365b64063bfbfa686cbb97cdd6267347", + "https://github.com/lodash/lodash/wiki/Changelog", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://snyk.io/vuln/SNYK-JS-LODASH-73639" + ] + }, + { + "below": "4.17.12", + "cwe": [ + "CWE-20" + ], + "severity": "high", + "identifiers": { + "summary": "Prototype Pollution in lodash", + "CVE": [ + "CVE-2019-10744" + ], + "githubID": "GHSA-jf85-cpcp-j695" + }, + "info": [ + "https://github.com/advisories/GHSA-jf85-cpcp-j695", + "https://nvd.nist.gov/vuln/detail/CVE-2019-10744", + "https://github.com/lodash/lodash/pull/4336", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://security.netapp.com/advisory/ntap-20191004-0005/", + "https://snyk.io/vuln/SNYK-JS-LODASH-450202", + "https://support.f5.com/csp/article/K47105354?utm_source=f5support&utm_medium=RSS", + "https://www.npmjs.com/advisories/1065", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html" + ] + }, + { + "atOrAbove": "3.7.0", + "below": "4.17.19", + "cwe": [ + "CWE-1321", + "CWE-770" + ], + "severity": "high", + "identifiers": { + "summary": "Prototype Pollution in lodash", + "CVE": [ + "CVE-2020-8203" + ], + "githubID": "GHSA-p6mc-m468-83gw" + }, + "info": [ + "https://github.com/advisories/GHSA-p6mc-m468-83gw", + "https://nvd.nist.gov/vuln/detail/CVE-2020-8203", + "https://github.com/lodash/lodash/issues/4744", + "https://github.com/lodash/lodash/issues/4874", + "https://github.com/github/advisory-database/pull/2884", + "https://github.com/lodash/lodash/commit/c84fe82760fb2d3e03a63379b297a1cc1a2fce12", + "https://hackerone.com/reports/712065", + "https://hackerone.com/reports/864701", + "https://github.com/lodash/lodash", + "https://github.com/lodash/lodash/wiki/Changelog#v41719", + "https://web.archive.org/web/20210914001339/https://github.com/lodash/lodash/issues/4744" + ] + }, + { + "below": "4.17.21", + "cwe": [ + "CWE-1333", + "CWE-400" + ], + "severity": "medium", + "identifiers": { + "summary": "Regular Expression Denial of Service (ReDoS) in lodash", + "CVE": [ + "CVE-2020-28500" + ], + "githubID": "GHSA-29mw-wpgm-hmr9" + }, + "info": [ + "https://github.com/advisories/GHSA-29mw-wpgm-hmr9", + "https://nvd.nist.gov/vuln/detail/CVE-2020-28500", + "https://github.com/lodash/lodash/pull/5065", + "https://github.com/lodash/lodash/pull/5065/commits/02906b8191d3c100c193fe6f7b27d1c40f200bb7", + "https://github.com/lodash/lodash/commit/c4847ebe7d14540bb28a8b932a9ce1b9ecbfee1a", + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://github.com/lodash/lodash", + "https://github.com/lodash/lodash/blob/npm/trimEnd.js%23L8", + "https://security.netapp.com/advisory/ntap-20210312-0006/", + "https://snyk.io/vuln/SNYK-JAVA-ORGFUJIONWEBJARS-1074896", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARS-1074894", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWER-1074892", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBLODASH-1074895", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1074893", + "https://snyk.io/vuln/SNYK-JS-LODASH-1018905", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://www.oracle.com/security-alerts/cpujul2022.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html" + ] + }, + { + "below": "4.17.21", + "cwe": [ + "CWE-77", + "CWE-94" + ], + "severity": "high", + "identifiers": { + "summary": "Command Injection in lodash", + "CVE": [ + "CVE-2021-23337" + ], + "githubID": "GHSA-35jh-r3h4-6jhm" + }, + "info": [ + "https://github.com/advisories/GHSA-35jh-r3h4-6jhm", + "https://nvd.nist.gov/vuln/detail/CVE-2021-23337", + "https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c", + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://github.com/lodash/lodash", + "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js#L14851", + "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js%23L14851", + "https://security.netapp.com/advisory/ntap-20210312-0006/", + "https://snyk.io/vuln/SNYK-JAVA-ORGFUJIONWEBJARS-1074932", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARS-1074930", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWER-1074928", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBLODASH-1074931", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1074929", + "https://snyk.io/vuln/SNYK-JS-LODASH-1040724", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://www.oracle.com/security-alerts/cpujul2022.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html" + ] + } + ], + "extractors": { + "filecontent": [ + "/\\*[\\s*!]+(?:@license)?[\\s*]+(?:Lo-Dash|lodash|Lodash) v?(§§version§§)[\\s\\S]{1,200}Build: `lodash modern -o", + "/\\*[\\s*!]+(?:@license)?[\\s*]+(?:Lo-Dash|lodash|Lodash) v?(§§version§§) <", + "/\\*[\\s*!]+(?:@license)?[\\s*]+(?:Lo-Dash|lodash|Lodash) v?(§§version§§) lodash.com/license", + "=\"(§§version§§)\"[\\s\\S]{1,300}__lodash_hash_undefined__", + "/\\*[\\s*]+@license[\\s*]+(?:Lo-Dash|lodhash|Lodash)[\\s\\S]{1,500}var VERSION *= *['\"](§§version§§)['\"]", + "var VERSION=\"(§§version§§)\";var BIND_FLAG=1,BIND_KEY_FLAG=2,CURRY_BOUND_FLAG=4,CURRY_FLAG=8" + ], + "uri": [ + "/(§§version§§)/lodash(\\.min)?\\.js" + ] + } + }, + "ua-parser-js": { + "vulnerabilities": [ + { + "atOrAbove": "0", + "below": "0.7.22", + "cwe": [ + "CWE-400" + ], + "severity": "high", + "identifiers": { + "summary": "Regular Expression Denial of Service in ua-parser-js", + "CVE": [ + "CVE-2020-7733" + ], + "githubID": "GHSA-662x-fhqg-9p8v" + }, + "info": [ + "https://github.com/advisories/GHSA-662x-fhqg-9p8v", + "https://nvd.nist.gov/vuln/detail/CVE-2020-7733", + "https://github.com/faisalman/ua-parser-js/commit/233d3bae22a795153a7e6638887ce159c63e557d", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBFAISALMAN-674666", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-674665", + "https://snyk.io/vuln/SNYK-JS-UAPARSERJS-610226", + "https://www.oracle.com//security-alerts/cpujul2021.html" + ] + }, + { + "atOrAbove": "0", + "below": "0.7.22", + "cwe": [ + "CWE-400" + ], + "severity": "high", + "identifiers": { + "summary": "Regular Expression Denial of Service in ua-parser-js", + "CVE": [ + "CVE-2020-7733" + ], + "githubID": "GHSA-662x-fhqg-9p8v" + }, + "info": [ + "https://github.com/advisories/GHSA-662x-fhqg-9p8v", + "https://nvd.nist.gov/vuln/detail/CVE-2020-7733", + "https://github.com/faisalman/ua-parser-js/commit/233d3bae22a795153a7e6638887ce159c63e557d", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBFAISALMAN-674666", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-674665", + "https://snyk.io/vuln/SNYK-JS-UAPARSERJS-610226", + "https://www.oracle.com//security-alerts/cpujul2021.html" + ] + }, + { + "atOrAbove": "0", + "below": "0.7.23", + "cwe": [ + "CWE-400" + ], + "severity": "high", + "identifiers": { + "summary": "ua-parser-js Regular Expression Denial of Service vulnerability", + "CVE": [ + "CVE-2020-7793" + ], + "githubID": "GHSA-394c-5j6w-4xmx" + }, + "info": [ + "https://github.com/advisories/GHSA-394c-5j6w-4xmx", + "https://nvd.nist.gov/vuln/detail/CVE-2020-7793", + "https://github.com/faisalman/ua-parser-js/commit/6d1f26df051ba681463ef109d36c9cf0f7e32b18", + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBFAISALMAN-1050388", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1050387", + "https://snyk.io/vuln/SNYK-JS-UAPARSERJS-1023599" + ] + }, + { + "atOrAbove": "0.7.14", + "below": "0.7.24", + "cwe": [ + "CWE-400" + ], + "severity": "high", + "identifiers": { + "summary": "Regular Expression Denial of Service (ReDoS) in ua-parser-js", + "CVE": [ + "CVE-2021-27292" + ], + "githubID": "GHSA-78cj-fxph-m83p" + }, + "info": [ + "https://github.com/advisories/GHSA-78cj-fxph-m83p", + "https://nvd.nist.gov/vuln/detail/CVE-2021-27292", + "https://github.com/faisalman/ua-parser-js/commit/809439e20e273ce0d25c1d04e111dcf6011eb566", + "https://github.com/pygments/pygments/commit/2e7e8c4a7b318f4032493773732754e418279a14", + "https://gist.github.com/b-c-ds/6941d80d6b4e694df4bc269493b7be76" + ] + }, + { + "atOrAbove": "0.7.29", + "below": "0.7.30", + "cwe": [ + "CWE-829", + "CWE-912" + ], + "severity": "high", + "identifiers": { + "summary": "Embedded malware in ua-parser-js", + "CVE": [], + "githubID": "GHSA-pjwm-rvh2-c87w" + }, + "info": [ + "https://github.com/advisories/GHSA-pjwm-rvh2-c87w", + "https://github.com/faisalman/ua-parser-js/issues/536", + "https://github.com/faisalman/ua-parser-js/issues/536#issuecomment-949772496", + "https://github.com/faisalman/ua-parser-js", + "https://www.npmjs.com/package/ua-parser-js" + ] + }, + { + "atOrAbove": "0", + "below": "0.7.33", + "cwe": [ + "CWE-1333", + "CWE-400" + ], + "severity": "high", + "identifiers": { + "summary": "ReDoS Vulnerability in ua-parser-js version", + "CVE": [ + "CVE-2022-25927" + ], + "githubID": "GHSA-fhg7-m89q-25r3" + }, + "info": [ + "https://github.com/advisories/GHSA-fhg7-m89q-25r3", + "https://github.com/faisalman/ua-parser-js/security/advisories/GHSA-fhg7-m89q-25r3", + "https://nvd.nist.gov/vuln/detail/CVE-2022-25927", + "https://github.com/faisalman/ua-parser-js/commit/a6140a17dd0300a35cfc9cff999545f267889411", + "https://github.com/faisalman/ua-parser-js", + "https://security.snyk.io/vuln/SNYK-JS-UAPARSERJS-3244450" + ] + }, + { + "atOrAbove": "0.8.0", + "below": "0.8.1", + "cwe": [ + "CWE-829", + "CWE-912" + ], + "severity": "high", + "identifiers": { + "summary": "Embedded malware in ua-parser-js", + "CVE": [], + "githubID": "GHSA-pjwm-rvh2-c87w" + }, + "info": [ + "https://github.com/advisories/GHSA-pjwm-rvh2-c87w", + "https://github.com/faisalman/ua-parser-js/issues/536", + "https://github.com/faisalman/ua-parser-js/issues/536#issuecomment-949772496", + "https://github.com/faisalman/ua-parser-js", + "https://www.npmjs.com/package/ua-parser-js" + ] + }, + { + "atOrAbove": "1.0.0", + "below": "1.0.1", + "cwe": [ + "CWE-829", + "CWE-912" + ], + "severity": "high", + "identifiers": { + "summary": "Embedded malware in ua-parser-js", + "CVE": [], + "githubID": "GHSA-pjwm-rvh2-c87w" + }, + "info": [ + "https://github.com/advisories/GHSA-pjwm-rvh2-c87w", + "https://github.com/faisalman/ua-parser-js/issues/536", + "https://github.com/faisalman/ua-parser-js/issues/536#issuecomment-949772496", + "https://github.com/faisalman/ua-parser-js", + "https://www.npmjs.com/package/ua-parser-js" + ] + }, + { + "atOrAbove": "0.8.0", + "below": "1.0.33", + "cwe": [ + "CWE-1333", + "CWE-400" + ], + "severity": "high", + "identifiers": { + "summary": "ReDoS Vulnerability in ua-parser-js version", + "CVE": [ + "CVE-2022-25927" + ], + "githubID": "GHSA-fhg7-m89q-25r3" + }, + "info": [ + "https://github.com/advisories/GHSA-fhg7-m89q-25r3", + "https://github.com/faisalman/ua-parser-js/security/advisories/GHSA-fhg7-m89q-25r3", + "https://nvd.nist.gov/vuln/detail/CVE-2022-25927", + "https://github.com/faisalman/ua-parser-js/commit/a6140a17dd0300a35cfc9cff999545f267889411", + "https://github.com/faisalman/ua-parser-js", + "https://security.snyk.io/vuln/SNYK-JS-UAPARSERJS-3244450" + ] + } + ], + "extractors": { + "uri": [ + "/(§§version§§)/ua-parser(.min)?.js" + ], + "filecontent": [ + "/\\* UAParser.js v(§§version§§)", + "/\\*[*!](?:@license)?[\\s]+\\* UAParser.js v(§§version§§)", + "// UAParser.js v(§§version§§)", + ".\\.VERSION=\"(§§version§§)\",.\\.BROWSER=\\{NAME:.,MAJOR:\"major\",VERSION:.\\},.\\.CPU=\\{ARCHITECTURE:", + ".\\.VERSION=\"(§§version§§)\",.\\.BROWSER=.\\(\\[[^\\]]{1,20}\\]\\),.\\.CPU=", + "LIBVERSION=\"(§§version§§)\",EMPTY=\"\",UNKNOWN=\"\\?\",FUNC_TYPE=\"function\",UNDEF_TYPE=\"undefined\"", + ".=\"(§§version§§)\",.=\"\",.=\"\\?\",.=\"function\",.=\"undefined\",.=\"object\",(.=\"string\",)?.=\"major\",.=\"model\",.=\"name\",.=\"type\",.=\"vendor\"" + ] + } + }, "dont check": { "vulnerabilities": [], "extractors": { diff --git a/sfge/src/main/java/com/salesforce/apex/jorje/ASTConstants.java b/sfge/src/main/java/com/salesforce/apex/jorje/ASTConstants.java index d707f905b..89923f739 100644 --- a/sfge/src/main/java/com/salesforce/apex/jorje/ASTConstants.java +++ b/sfge/src/main/java/com/salesforce/apex/jorje/ASTConstants.java @@ -28,6 +28,7 @@ import apex.jorje.semantic.ast.expression.SoqlExpression; import apex.jorje.semantic.ast.expression.SoslExpression; import apex.jorje.semantic.ast.expression.SuperMethodCallExpression; +import apex.jorje.semantic.ast.expression.SuperVariableExpression; import apex.jorje.semantic.ast.expression.ThisMethodCallExpression; import apex.jorje.semantic.ast.expression.ThisVariableExpression; import apex.jorje.semantic.ast.expression.TriggerVariableExpression; @@ -148,6 +149,8 @@ public static String getVertexLabel(Class clazz) { public static final String STANDARD_CONDITION = getVertexLabel(StandardCondition.class); public static final String SUPER_METHOD_CALL_EXPRESSION = getVertexLabel(SuperMethodCallExpression.class); + public static final String SUPER_VARIABLE_EXPRESSION = + getVertexLabel(SuperVariableExpression.class); public static final String THIS_METHOD_CALL_EXPRESSION = getVertexLabel(ThisMethodCallExpression.class); public static final String THIS_VARIABLE_EXPRESSION = diff --git a/sfge/src/main/java/com/salesforce/graph/ops/MethodUtil.java b/sfge/src/main/java/com/salesforce/graph/ops/MethodUtil.java index b305b098a..8d72bef9f 100644 --- a/sfge/src/main/java/com/salesforce/graph/ops/MethodUtil.java +++ b/sfge/src/main/java/com/salesforce/graph/ops/MethodUtil.java @@ -645,38 +645,66 @@ public static List getPaths( // current class or a static // method on a another class String definingType; - final String methodName = methodCallExpression.getMethodName(); - String fullMethodName = methodCallExpression.getFullMethodName(); - if (methodName.equals(fullMethodName)) { - // The method is being called on a class onto itself - definingType = vertex.getDefiningType(); + List potentialDefiningTypes = new ArrayList<>(); + if (methodCallExpression.isThisReference()) { + // A call to `this.someMethod()` means the method must be on the defining type. + definingType = methodCallExpression.getDefiningType(); + potentialDefiningTypes.add( + ApexStandardLibraryUtil.getCanonicalName(definingType)); + } else if (methodCallExpression.isEmptyReference()) { + // For an empty reference (e.g., `someMethod()` as opposed to + // `whatever.someMethod()`), the first place we should look is + // the call's own defining type. + definingType = methodCallExpression.getDefiningType(); + potentialDefiningTypes.add( + ApexStandardLibraryUtil.getCanonicalName(definingType)); + // Inner classes can also invoke their outer class's static methods with an + // empty reference, so if this is an inner class, we should check the outer + // class too. + if (definingType.contains(".")) { + potentialDefiningTypes.add(definingType.split("\\.")[0]); + } + } else if (methodCallExpression.isSuperVariableReference()) { + // A call to `super.someMethod()` definitely means that the method is on the + // super-type. + definingType = + ((ReferenceExpressionVertex) + (methodCallExpression.getReferenceExpression())) + .getSuperVariableExpression() + .orElseThrow( + () -> + new UnexpectedException( + "Missing SuperVariableExpression")) + .getCanonicalType(); + potentialDefiningTypes.add( + ApexStandardLibraryUtil.getCanonicalName(definingType)); } else { // TODO: Pass information to #getInvoked that this needs to be a static method - definingType = String.join(".", vertex.getChainedNames()); - } - // The defining type could be an aliased reference to an inner class, so check that - // first. - String potentialInnerClassDefType = - ClassUtil.getMoreSpecificClassName(vertex, definingType).orElse(null); - if (potentialInnerClassDefType != null) { - invoked = - getInvoked( - g, - potentialInnerClassDefType, - (MethodCallExpressionVertex) vertex, - symbols) - .orElse(null); + // A method call on a non-empty, non-this reference (e.g., + // `whatever.someMethod()`) lives on the thing being referenced. + definingType = String.join(".", methodCallExpression.getChainedNames()); + potentialDefiningTypes.add( + ApexStandardLibraryUtil.getCanonicalName(definingType)); + // A defining type without a period might be an aliased reference to a local + // inner class. + if (!definingType.contains(".")) { + // Check if an inner class exists, and if so, then check that before + // checking the outer class. + String potentialInnerClassDefType = + ClassUtil.getMoreSpecificClassName(vertex, definingType) + .orElse(null); + if (potentialInnerClassDefType != null) { + potentialDefiningTypes.add(0, potentialInnerClassDefType); + } + } } - // If we found no inner classes, check outer classes. - if (invoked == null) { - definingType = ApexStandardLibraryUtil.getCanonicalName(definingType); + for (String potentialDefiningType : potentialDefiningTypes) { invoked = - getInvoked( - g, - definingType, - (MethodCallExpressionVertex) vertex, - symbols) + getInvoked(g, potentialDefiningType, methodCallExpression, symbols) .orElse(null); + if (invoked != null) { + break; + } } } } else if (vertex instanceof NewObjectExpressionVertex) { diff --git a/sfge/src/main/java/com/salesforce/graph/vertex/MethodCallExpressionVertex.java b/sfge/src/main/java/com/salesforce/graph/vertex/MethodCallExpressionVertex.java index 30beb5c2e..5ed8fab13 100644 --- a/sfge/src/main/java/com/salesforce/graph/vertex/MethodCallExpressionVertex.java +++ b/sfge/src/main/java/com/salesforce/graph/vertex/MethodCallExpressionVertex.java @@ -102,7 +102,7 @@ public Optional getClassRefExpression() { if (abstractReferenceExpression instanceof ReferenceExpressionVertex) { ReferenceExpressionVertex referenceExpression = (ReferenceExpressionVertex) abstractReferenceExpression; - return referenceExpression.gtClassRefExpression(); + return referenceExpression.getClassRefExpression(); } return Optional.empty(); } @@ -311,6 +311,20 @@ public boolean isEmptyReference() { return referenceExpression.get() instanceof EmptyReferenceExpressionVertex; } + /** + * @return - True if the method is qualified by a super expression. i.e., returns true for + * {@code super.someMethod()} but not {@code this.someMethod()}. + */ + public boolean isSuperVariableReference() { + if (referenceExpression.get() instanceof ReferenceExpressionVertex) { + return ((ReferenceExpressionVertex) referenceExpression.get()) + .getSuperVariableExpression() + .isPresent(); + } else { + return false; + } + } + private LazyVertex _getReferenceVertex() { return new LazyVertex<>( () -> diff --git a/sfge/src/main/java/com/salesforce/graph/vertex/ReferenceExpressionVertex.java b/sfge/src/main/java/com/salesforce/graph/vertex/ReferenceExpressionVertex.java index 42e571eee..49391ccea 100644 --- a/sfge/src/main/java/com/salesforce/graph/vertex/ReferenceExpressionVertex.java +++ b/sfge/src/main/java/com/salesforce/graph/vertex/ReferenceExpressionVertex.java @@ -16,13 +16,19 @@ public class ReferenceExpressionVertex extends AbstractReferenceExpressionVertex * Presence of this vertex indicates that the reference was qualified with a 'this' reference */ private final LazyOptionalVertex thisVariableExpression; + /** + * Presence of this vertex indicates that the reference was qualified with a {@code super} + * reference. + */ + private final LazyOptionalVertex superVariableExpression; ReferenceExpressionVertex(Map properties) { super(properties); - // TODO: Efficiency. This does 2 queries, is it safe to get the child and then look at the + // TODO: Efficiency. This does 3 queries, is it safe to get the child and then look at the // type? this.classRefExpressionVertex = _getClassRefExpression(); this.thisVariableExpression = _getThisVariableExpression(); + this.superVariableExpression = _getSuperVariableExpression(); } @Override @@ -49,10 +55,14 @@ public Optional getThisVariableExpression() { return thisVariableExpression.get(); } - public Optional gtClassRefExpression() { + public Optional getClassRefExpression() { return classRefExpressionVertex.get(); } + public Optional getSuperVariableExpression() { + return superVariableExpression.get(); + } + public String getReferenceType() { return getString(Schema.REFERENCE_TYPE); } @@ -77,4 +87,12 @@ private LazyOptionalVertex _getClassRefExpression() { .out(Schema.CHILD) .hasLabel(ASTConstants.NodeType.CLASS_REF_EXPRESSION)); } + + private LazyOptionalVertex _getSuperVariableExpression() { + return new LazyOptionalVertex<>( + () -> + g().V(getId()) + .out(Schema.CHILD) + .hasLabel(ASTConstants.NodeType.SUPER_VARIABLE_EXPRESSION)); + } } diff --git a/sfge/src/main/java/com/salesforce/graph/vertex/SuperVariableExpressionVertex.java b/sfge/src/main/java/com/salesforce/graph/vertex/SuperVariableExpressionVertex.java new file mode 100644 index 000000000..935b8aece --- /dev/null +++ b/sfge/src/main/java/com/salesforce/graph/vertex/SuperVariableExpressionVertex.java @@ -0,0 +1,47 @@ +package com.salesforce.graph.vertex; + +import com.salesforce.exception.UnexpectedException; +import com.salesforce.graph.symbols.SymbolProvider; +import com.salesforce.graph.symbols.SymbolProviderVertexVisitor; +import com.salesforce.graph.visitor.PathVertexVisitor; +import java.util.Map; + +public class SuperVariableExpressionVertex extends TODO_FIX_HIERARCHY_ChainedVertex + implements Typeable { + SuperVariableExpressionVertex(Map properties) { + this(properties, null); + } + + SuperVariableExpressionVertex(Map properties, Object supplementalParam) { + super(properties, supplementalParam); + } + + @Override + public boolean visit(PathVertexVisitor visitor, SymbolProvider symbols) { + return visitor.visit(this, symbols); + } + + @Override + public boolean visit(SymbolProviderVertexVisitor visitor) { + return visitor.visit(this); + } + + @Override + public void afterVisit(PathVertexVisitor visitor, SymbolProvider symbols) { + visitor.afterVisit(this, symbols); + } + + @Override + public void afterVisit(SymbolProviderVertexVisitor visitor) { + visitor.afterVisit(this); + } + + @Override + public String getCanonicalType() { + UserClassVertex parentClass = + getParentClass().orElseThrow(() -> new UnexpectedException("Missing parent class")); + return parentClass + .getSuperClassName() + .orElseThrow(() -> new UnexpectedException("Missing super class")); + } +} diff --git a/sfge/src/test/java/com/salesforce/graph/symbols/apex/IndeterminantTest.java b/sfge/src/test/java/com/salesforce/graph/symbols/apex/IndeterminantTest.java index e92ccc42f..4539f5a7e 100644 --- a/sfge/src/test/java/com/salesforce/graph/symbols/apex/IndeterminantTest.java +++ b/sfge/src/test/java/com/salesforce/graph/symbols/apex/IndeterminantTest.java @@ -37,7 +37,7 @@ public void setup() { */ @ValueSource( strings = {"Boolean", "Id", "Integer", "List", "Map", "String"}) - @ParameterizedTest(name = "{0}") + @ParameterizedTest(name = "{displayName}: variableType = {0}") public void testUnresolvedUserMethodReturnIsIndeterminant(String variableType) { String sourceCode = "public class MyClass {\n" @@ -61,7 +61,7 @@ public void testUnresolvedUserMethodReturnIsIndeterminant(String variableType) { /** Method parameters have their type synthesized based on the variable declaration. */ @ValueSource( strings = {"Boolean", "Id", "Integer", "List", "Map", "String"}) - @ParameterizedTest(name = "{0}") + @ParameterizedTest(name = "{displayName}: variableType = {0}") public void testUnresolvedUserMethodParameterIsIndeterminant(String variableType) { String sourceCode = "public class MyClass {\n" @@ -94,7 +94,7 @@ public static Stream systemMethodSource() { * return type of the method */ @MethodSource("systemMethodSource") - @ParameterizedTest(name = "variableType=({0}):method=({1})") + @ParameterizedTest(name = "{displayName}: variableType=({0}):method=({1})") public void testUnresolvedSystemMethodReturnAssignmentIsIndeterminant( String variableType, String method) { String sourceCode = @@ -124,7 +124,7 @@ public void testUnresolvedSystemMethodReturnAssignmentIsIndeterminant( * return type of the method */ @MethodSource("systemMethodSource") - @ParameterizedTest(name = "variableType=({0}):method=({1})") + @ParameterizedTest(name = "{displayName}: variableType=({0}):method=({1})") public void testUnresolvedSystemMethodReturnPassAsParameterIsIndeterminant( String variableType, String method) { String sourceCode = diff --git a/sfge/src/test/java/com/salesforce/rules/unusedmethod/InstanceMethodsTest.java b/sfge/src/test/java/com/salesforce/rules/unusedmethod/InstanceMethodsTest.java index 2a23c26e2..ab8ea150f 100644 --- a/sfge/src/test/java/com/salesforce/rules/unusedmethod/InstanceMethodsTest.java +++ b/sfge/src/test/java/com/salesforce/rules/unusedmethod/InstanceMethodsTest.java @@ -142,7 +142,6 @@ public void instanceInvokedBySubclass_expectNoViolation( * used. */ @Test - @Disabled // TODO: FIX AND ENABLE THIS TEST public void instanceInvokedByOverridingSubclass_expectNoViolation() { // Fill in the source code template. String[] sourceCodes = diff --git a/sfge/src/test/java/com/salesforce/rules/unusedmethod/StaticMethodsTest.java b/sfge/src/test/java/com/salesforce/rules/unusedmethod/StaticMethodsTest.java index b0e3ab645..a65be7bdb 100644 --- a/sfge/src/test/java/com/salesforce/rules/unusedmethod/StaticMethodsTest.java +++ b/sfge/src/test/java/com/salesforce/rules/unusedmethod/StaticMethodsTest.java @@ -75,9 +75,9 @@ public void staticInvokedByOwnClass_expectNoViolation( */ @CsvSource({ // Every combination of private/public tested method and implicit/explicit type reference. - // "public, testedMethod()", // TODO: FIX AND ENABLE THIS TEST + "public, testedMethod()", "public, MyClass.testedMethod()", - // "private, testedMethod()", // TODO: FIX AND ENABLE TEST + "private, testedMethod()", "private, MyClass.testedMethod()", }) @ParameterizedTest(name = "{displayName}: {0} static invoked as {1}") @@ -176,11 +176,7 @@ public void staticInvokedBySubclass_expectNoViolation( * references to outer type. */ @ValueSource( - strings = { - // "testedMethod()", // TODO: FIX AND ENABLE THIS TEST - "ParentClass.testedMethod()", - "ChildClass.testedMethod()" - }) + strings = {"testedMethod()", "ParentClass.testedMethod()", "ChildClass.testedMethod()"}) @ParameterizedTest(name = "{displayName}: Invoked as {0}") public void staticInvokedByInnerOfSubclass_expectNoViolation(String invocation) { // spotless:off diff --git a/src/commands/scanner/rule/add.ts b/src/commands/scanner/rule/add.ts index 4210ffdee..3cf4f9545 100644 --- a/src/commands/scanner/rule/add.ts +++ b/src/commands/scanner/rule/add.ts @@ -37,6 +37,6 @@ export default class Add extends ScannerCommand { }; protected createAction(logger: Logger, display: Display): Action { - return new RuleAddAction(logger, display, new InputProcessorImpl(this.config.version)); + return new RuleAddAction(logger, display, new InputProcessorImpl(this.config.version, display)); } } diff --git a/src/commands/scanner/rule/remove.ts b/src/commands/scanner/rule/remove.ts index 3be819435..2e2f3b59b 100644 --- a/src/commands/scanner/rule/remove.ts +++ b/src/commands/scanner/rule/remove.ts @@ -39,6 +39,6 @@ export default class Remove extends ScannerCommand { }; protected createAction(logger: Logger, display: Display): Action { - return new RuleRemoveAction(logger, display, new InputProcessorImpl(this.config.version)); + return new RuleRemoveAction(logger, display, new InputProcessorImpl(this.config.version, display)); } } diff --git a/src/commands/scanner/run.ts b/src/commands/scanner/run.ts index 967335f1c..ce67dad98 100644 --- a/src/commands/scanner/run.ts +++ b/src/commands/scanner/run.ts @@ -53,8 +53,7 @@ export default class Run extends ScannerRunCommand { summary: getMessage(BundleName.Run, 'flags.targetSummary'), description: getMessage(BundleName.Run, 'flags.targetDescription'), delimiter: ',', - multiple: true, - required: true + multiple: true })(), // END: Targeting-related flags. // BEGIN: Engine config flags. @@ -89,7 +88,7 @@ export default class Run extends ScannerRunCommand { }; protected createAction(logger: Logger, display: Display): Action { - const inputProcessor: InputProcessor = new InputProcessorImpl(this.config.version); + const inputProcessor: InputProcessor = new InputProcessorImpl(this.config.version, display); const ruleFilterFactory: RuleFilterFactory = new RuleFilterFactoryImpl(); const engineOptionsFactory: EngineOptionsFactory = new RunEngineOptionsFactory(inputProcessor); const resultsProcessorFactory: ResultsProcessorFactory = new ResultsProcessorFactoryImpl(); diff --git a/src/commands/scanner/run/dfa.ts b/src/commands/scanner/run/dfa.ts index 63a4572cc..2c24f2c3d 100644 --- a/src/commands/scanner/run/dfa.ts +++ b/src/commands/scanner/run/dfa.ts @@ -41,7 +41,6 @@ export default class Dfa extends ScannerRunCommand { char: 't', summary: getMessage(BundleName.RunDfa, 'flags.targetSummary'), description: getMessage(BundleName.RunDfa, 'flags.targetDescription'), - required: true, delimiter: ',', multiple: true })(), @@ -77,7 +76,7 @@ export default class Dfa extends ScannerRunCommand { }; protected createAction(logger: Logger, display: Display): Action { - const inputProcessor: InputProcessor = new InputProcessorImpl(this.config.version); + const inputProcessor: InputProcessor = new InputProcessorImpl(this.config.version, display); const ruleFilterFactory: RuleFilterFactory = new RuleFilterFactoryImpl(); const engineOptionsFactory: EngineOptionsFactory = new RunDfaEngineOptionsFactory(inputProcessor); const resultsProcessorFactory: ResultsProcessorFactory = new ResultsProcessorFactoryImpl(); diff --git a/src/lib/DefaultRuleManager.ts b/src/lib/DefaultRuleManager.ts index 515dc45ba..62e4ec78f 100644 --- a/src/lib/DefaultRuleManager.ts +++ b/src/lib/DefaultRuleManager.ts @@ -289,12 +289,14 @@ export class DefaultRuleManager implements RuleManager { const ruleTargetsInitialLength: number = ruleTargets.length; // Positive patterns might use method-level targeting. We only want to do path evaluation against the part // that's actually a path. - const targetPortions = target.split('#'); + const targetPortions = splitOnMethodSpecifier(target) // The array will always have at least one entry, since if there's no '#' then it will return a singleton array. const targetPath = targetPortions[0]; if (globby.hasMagic(target)) { // The target is a magic glob. Retrieve paths in the working directory that match it, and then filter against // our pattern matcher. + // NOTE: We should consider in the future to resolve target paths based off of the projectdir instead of + // the present working directory. const matchingTargets = await globby(targetPath); // Map relative files to absolute paths. This solves ambiguity of current working directory const absoluteMatchingTargets = matchingTargets.map(t => path.resolve(t)); @@ -346,3 +348,9 @@ export class DefaultRuleManager implements RuleManager { return ruleTargets; } } + +function splitOnMethodSpecifier(targetPath: string): string[] { + // Unlike targetPath.split('#'), this solution only splits on the last '#' instead of all of them + const lastHashPos: number = targetPath.lastIndexOf('#'); + return lastHashPos < 0 ? [targetPath] : [targetPath.substring(0, lastHashPos), targetPath.substring(lastHashPos+1)] +} diff --git a/src/lib/EngineOptionsFactory.ts b/src/lib/EngineOptionsFactory.ts index 9cdca360b..d159586a1 100644 --- a/src/lib/EngineOptionsFactory.ts +++ b/src/lib/EngineOptionsFactory.ts @@ -1,5 +1,5 @@ import {Inputs, LooseObject, SfgeConfig} from "../types"; -import {CUSTOM_CONFIG, INTERNAL_ERROR_CODE} from "../Constants"; +import {CUSTOM_CONFIG, ENGINE, INTERNAL_ERROR_CODE} from "../Constants"; import {InputProcessor} from "./InputProcessor"; import {TYPESCRIPT_ENGINE_OPTIONS} from "./eslint/TypescriptEslintStrategy"; import {SfError} from "@salesforce/core"; @@ -22,18 +22,16 @@ abstract class CommonEngineOptionsFactory implements EngineOptionsFactory { this.inputProcessor = inputProcessor; } + protected abstract shouldSfgeRun(inputs: Inputs): boolean; + createEngineOptions(inputs: Inputs): EngineOptions { const options: Map = new Map(); - - // We should only add a GraphEngine config if we were given a --projectdir flag. - const projectDirPaths: string[] = this.inputProcessor.resolveProjectDirPaths(inputs); - if (projectDirPaths.length > 0) { + if (this.shouldSfgeRun(inputs)) { const sfgeConfig: SfgeConfig = { - projectDirs: projectDirPaths + projectDirs: this.inputProcessor.resolveProjectDirPaths(inputs) }; options.set(CUSTOM_CONFIG.SfgeConfig, JSON.stringify(sfgeConfig)); } - return options; } @@ -44,6 +42,10 @@ export class RunEngineOptionsFactory extends CommonEngineOptionsFactory { super(inputProcessor); } + protected shouldSfgeRun(inputs: Inputs): boolean { + return inputs.engine && (inputs.engine as string[]).includes(ENGINE.SFGE); + } + public override createEngineOptions(inputs: Inputs): EngineOptions { const engineOptions: EngineOptions = super.createEngineOptions(inputs); @@ -89,6 +91,11 @@ export class RunDfaEngineOptionsFactory extends CommonEngineOptionsFactory { super(inputProcessor); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected shouldSfgeRun(_inputs: Inputs): boolean { + return true; + } + public override createEngineOptions(inputs: Inputs): EngineOptions { const engineOptions: EngineOptions = super.createEngineOptions(inputs); diff --git a/src/lib/InputProcessor.ts b/src/lib/InputProcessor.ts index d7ed2f177..f8fd8b6b8 100644 --- a/src/lib/InputProcessor.ts +++ b/src/lib/InputProcessor.ts @@ -1,10 +1,17 @@ import {Inputs} from "../types"; -import normalize = require('normalize-path'); -import path = require('path'); -import untildify = require("untildify"); import {RunOptions} from "./RuleManager"; import {RunOutputOptions} from "./output/RunResultsProcessor"; import {inferFormatFromOutfile, OutputFormat} from "./output/OutputFormat"; +import {SfError} from "@salesforce/core"; +import {BundleName, getMessage} from "../MessageCatalog"; +import {INTERNAL_ERROR_CODE} from "../Constants"; +import {Display} from "./Display"; +import normalize = require('normalize-path'); +import path = require('path'); +import fs = require('fs'); +import untildify = require("untildify"); +import globby = require('globby'); +import {Tokens} from "@salesforce/core/lib/messages"; /** * Service for processing inputs @@ -23,9 +30,13 @@ export interface InputProcessor { export class InputProcessorImpl implements InputProcessor { private readonly sfVersion: string; + private readonly display: Display; + private readonly displayedKeys: Set; - public constructor(sfVersion: string) { + public constructor(sfVersion: string, display: Display) { this.sfVersion = sfVersion; + this.display = display + this.displayedKeys = new Set(); } public resolvePaths(inputs: Inputs): string[] { @@ -35,18 +46,35 @@ export class InputProcessorImpl implements InputProcessor { } public resolveProjectDirPaths(inputs: Inputs): string[] { - // TODO: Stop allowing an array of paths - move towards only 1 path (to resolve into 1 output path) + // If projectdir is provided, then return it since at this point it has already been validated to exist if (inputs.projectdir && (inputs.projectdir as string[]).length > 0) { - return (inputs.projectdir as string[]).map(p => path.resolve(p)); + return (inputs.projectdir as string[]).map(p => path.resolve(normalize(untildify(p)))) } - return []; + + // If projectdir is not provided then: + // * We calculate the first common parent directory that includes all the target files. + // --> If none of its parent folders contain a sfdx-project.json file, then we return this first common parent. + // --> Otherwise we return the folder that contains the sfdx-project.json file. + const commonParentFolder = getFirstCommonParentFolder(this.getAllTargetFiles(inputs)); + let projectFolder: string = findFolderThatContainsSfdxProjectFile(commonParentFolder); + projectFolder = projectFolder.length > 0 ? projectFolder : commonParentFolder + this.displayInfoOnlyOnce('info.resolvedProjectDir', [projectFolder]) + return [projectFolder]; } public resolveTargetPaths(inputs: Inputs): string[] { // Turn the paths into normalized Unix-formatted paths and strip out any single- or double-quotes, because // sometimes shells are stupid and will leave them in there. - const target: string[] = (inputs.target || []) as string[]; - return target.map(path => normalize(untildify(path)).replace(/['"]/g, '')); + // Note that we do not do a path.resolve since the target input can be globby (which we handle elsewhere). + + // If possible, in the future we should resolve all globs here instead of in the DefaultRuleManager. + // Also, I would recommend that we eventually resolve globs based on the projectdir (since it acts as a + // root directory) instead of the present working directory. + if (!inputs.target) { + this.displayInfoOnlyOnce('info.resolvedTarget') + } + const targetPaths: string[] = (inputs.target || ['.']) as string[]; + return targetPaths.map(path => normalize(untildify(path)).replace(/['"]/g, '')); } @@ -62,11 +90,27 @@ export class InputProcessorImpl implements InputProcessor { public createRunOutputOptions(inputs: Inputs): RunOutputOptions { return { format: outputFormatFromInputs(inputs), - verboseViolations: inputs["verbose-violations"] as boolean, + verboseViolations: inputs['verbose-violations'] as boolean, severityForError: inputs['severity-threshold'] as number, outfile: inputs.outfile as string }; } + + private getAllTargetFiles(inputs: Inputs): string[] { + const targetPaths: string[] = this.resolveTargetPaths(inputs).map(p => trimMethodSpecifier(p)) + const allAbsoluteTargetFiles: string[] = globby.sync(targetPaths).map(p => path.resolve(p)); + if (allAbsoluteTargetFiles.length == 0) { + throw new SfError(getMessage(BundleName.CommonRun, 'validations.noFilesFoundInTarget'), null, null, INTERNAL_ERROR_CODE); + } + return allAbsoluteTargetFiles; + } + + private displayInfoOnlyOnce(messageKey: string, tokens?: Tokens) { + if (!this.displayedKeys.has(messageKey)) { + this.display.displayInfo(getMessage(BundleName.CommonRun, messageKey, tokens)); + this.displayedKeys.add(messageKey); + } + } } function outputFormatFromInputs(inputs: Inputs): OutputFormat { @@ -80,3 +124,43 @@ function outputFormatFromInputs(inputs: Inputs): OutputFormat { return OutputFormat.TABLE; } } + +function trimMethodSpecifier(targetPath: string): string { + const lastHashPos: number = targetPath.lastIndexOf('#'); + return lastHashPos < 0 ? targetPath : targetPath.substring(0, lastHashPos) +} + +function getFirstCommonParentFolder(targetFiles: string[]) { + const longestCommonStr: string = getLongestCommonPrefix(targetFiles); + const commonParentFolder = getParentFolderOf(longestCommonStr); + return commonParentFolder.length == 0 ? path.sep : commonParentFolder; +} + +function getLongestCommonPrefix(strs: string[]): string { + // To find the longest common prefix, we first get the select the shortest string from our list of strings + const shortestStr = strs.reduce((s1, s2) => s1.length <= s2.length ? s1 : s2); + + // Then we check that each string's ith character is the same as the shortest strings ith character + for (let i = 0; i < shortestStr.length; i++) { + if(!strs.every(str => str[i] === shortestStr[i])) { + // If we find a string that doesn't match the ith character, we return the common prefix from [0,i) + return shortestStr.substring(0, i) + } + } + return shortestStr; +} + +function findFolderThatContainsSfdxProjectFile(folder: string): string { + let folderToCheck: string = folder; + while (folderToCheck.length > 0) { + if (fs.existsSync(path.resolve(folderToCheck, 'sfdx-project.json'))) { + return folderToCheck; + } + folderToCheck = getParentFolderOf(folderToCheck); + } + return ''; +} + +function getParentFolderOf(fileOrFolder: string): string { + return fileOrFolder.substring(0, fileOrFolder.lastIndexOf(path.sep)) +} diff --git a/src/lib/ScannerRunCommand.ts b/src/lib/ScannerRunCommand.ts index 9f342893a..ae304a181 100644 --- a/src/lib/ScannerRunCommand.ts +++ b/src/lib/ScannerRunCommand.ts @@ -1,7 +1,5 @@ import {Flags} from '@salesforce/sf-plugins-core'; import {ScannerCommand} from './ScannerCommand'; -import untildify = require('untildify'); -import normalize = require('normalize-path'); import {BundleName, getMessage} from "../MessageCatalog"; import {OutputFormat} from "./output/OutputFormat"; @@ -48,11 +46,12 @@ export abstract class ScannerRunCommand extends ScannerCommand { }), // END: Flags related to results processing. // BEGIN: Flags related to targeting. - projectdir: Flags.custom({ // TODO: FIGURE OUT WHY WE NEED THIS ON BOTH "run" AND "run dfa" + projectdir: Flags.custom({ char: 'p', summary: getMessage(BundleName.CommonRun, 'flags.projectdirSummary'), description: getMessage(BundleName.CommonRun, 'flags.projectdirDescription'), - parse: val => Promise.resolve(val.split(',').map(d => normalize(untildify(d)))) + delimiter: ',', + multiple: true })(), // END: Flags related to targeting. }; diff --git a/src/lib/actions/AbstractRunAction.ts b/src/lib/actions/AbstractRunAction.ts index eff771a29..a840368ce 100644 --- a/src/lib/actions/AbstractRunAction.ts +++ b/src/lib/actions/AbstractRunAction.ts @@ -19,6 +19,8 @@ import {inferFormatFromOutfile, OutputFormat} from "../output/OutputFormat"; import {ResultsProcessor} from "../output/ResultsProcessor"; import {ResultsProcessorFactory} from "../output/ResultsProcessorFactory"; import {JsonReturnValueHolder} from "../output/JsonReturnValueHolder"; +import untildify = require('untildify'); +import normalize = require('normalize-path'); /** * Abstract Action to share a common implementation behind the "run" and "run dfa" commands @@ -48,13 +50,13 @@ export abstract class AbstractRunAction implements Action { const fh = new FileHandler(); // If there's a --projectdir flag, its entries must be non-glob paths pointing to existing directories. if (inputs.projectdir) { - // TODO: MOVE AWAY FROM ALLOWING AN ARRAY OF DIRECTORIES HERE AND ERROR IF THERE IS MORE THAN ONE DIRECTORY - for (const dir of (inputs.projectdir as string[])) { - if (globby.hasMagic(dir)) { + for (const p of (inputs.projectdir as string [])) { + const projectDir: string = normalize(untildify(p)) + if (globby.hasMagic(projectDir)) { throw new SfError(getMessage(BundleName.CommonRun, 'validations.projectdirCannotBeGlob')); - } else if (!(await fh.exists(dir))) { + } else if (!(await fh.exists(projectDir))) { throw new SfError(getMessage(BundleName.CommonRun, 'validations.projectdirMustExist')); - } else if (!(await fh.stats(dir)).isDirectory()) { + } else if (!(await fh.stats(projectDir)).isDirectory()) { throw new SfError(getMessage(BundleName.CommonRun, 'validations.projectdirMustBeDir')); } } diff --git a/src/lib/actions/RunAction.ts b/src/lib/actions/RunAction.ts index e1c2cd566..26a3ee9ed 100644 --- a/src/lib/actions/RunAction.ts +++ b/src/lib/actions/RunAction.ts @@ -29,9 +29,11 @@ export class RunAction extends AbstractRunAction { this.display.displayInfo(getMessage(BundleName.Run, 'output.filtersIgnoredCustom', [])); } // None of the pathless engines support method-level targeting, so attempting to use it should result in an error. - for (const target of (inputs.target as string[])) { - if (target.indexOf('#') > -1) { - throw new SfError(getMessage(BundleName.Run, 'validations.methodLevelTargetingDisallowed', [target])); + if (inputs.target) { + for (const target of (inputs.target as string[])) { + if (target.indexOf('#') > -1) { + throw new SfError(getMessage(BundleName.Run, 'validations.methodLevelTargetingDisallowed', [target])); + } } } } diff --git a/src/lib/actions/RunDfaAction.ts b/src/lib/actions/RunDfaAction.ts index af216fa63..1b6338131 100644 --- a/src/lib/actions/RunDfaAction.ts +++ b/src/lib/actions/RunDfaAction.ts @@ -24,22 +24,18 @@ export class RunDfaAction extends AbstractRunAction { await super.validateInputs(inputs); const fh = new FileHandler(); - // The superclass will validate that --projectdir is well-formed, - // but doesn't require that the flag actually be present. - // So we should make sure it exists here. - if (!inputs.projectdir || (inputs.projectdir as string[]).length === 0) { - throw new SfError(getMessage(BundleName.RunDfa, 'validations.projectdirIsRequired')); - } // Entries in the target array may specify methods, but only if the entry is neither a directory nor a glob. - for (const target of (inputs.target as string[])) { - // The target specifies a method if it includes the `#` syntax. - if (target.indexOf('#') > -1) { - if(globby.hasMagic(target)) { - throw new SfError(getMessage(BundleName.RunDfa, 'validations.methodLevelTargetCannotBeGlob')); - } - const potentialFilePath = target.split('#')[0]; - if (!(await fh.isFile(potentialFilePath))) { - throw new SfError(getMessage(BundleName.RunDfa, 'validations.methodLevelTargetMustBeRealFile', [potentialFilePath])); + if (inputs.target) { + for (const target of (inputs.target as string[])) { + // The target specifies a method if it includes the `#` syntax. + if (target.indexOf('#') > -1) { + if (globby.hasMagic(target)) { + throw new SfError(getMessage(BundleName.RunDfa, 'validations.methodLevelTargetCannotBeGlob')); + } + const potentialFilePath = target.split('#')[0]; + if (!(await fh.isFile(potentialFilePath))) { + throw new SfError(getMessage(BundleName.RunDfa, 'validations.methodLevelTargetMustBeRealFile', [potentialFilePath])); + } } } } diff --git a/src/lib/sfge/SfgePathlessEngine.ts b/src/lib/sfge/SfgePathlessEngine.ts index 5fc242bf6..3f2165f65 100644 --- a/src/lib/sfge/SfgePathlessEngine.ts +++ b/src/lib/sfge/SfgePathlessEngine.ts @@ -1,9 +1,7 @@ -import {SfError} from '@salesforce/core'; import {AbstractSfgeEngine, SfgeViolation} from "./AbstractSfgeEngine"; -import {Rule, RuleGroup, RuleTarget, RuleViolation, SfgeConfig} from '../../types'; +import {Rule, RuleGroup, RuleTarget, RuleViolation} from '../../types'; import {CUSTOM_CONFIG, RuleType} from '../../Constants'; import * as EngineUtils from "../util/CommonEngineUtils"; -import {BundleName, getMessage} from "../../MessageCatalog"; export class SfgePathlessEngine extends AbstractSfgeEngine { /** @@ -32,16 +30,7 @@ export class SfgePathlessEngine extends AbstractSfgeEngine { // For the non-DFA Graph Engine variant, we need to make sure that we have the // necessary info to run the engine, since the relevant flags aren't required // for `scanner run`. - if (engineOptions.has(CUSTOM_CONFIG.SfgeConfig)) { - const sfgeConfig: SfgeConfig = JSON.parse(engineOptions.get(CUSTOM_CONFIG.SfgeConfig)) as SfgeConfig; - if (sfgeConfig.projectDirs && sfgeConfig.projectDirs.length > 0) { - // If we've got a config with projectDirs, we're set. - return true; - } - } - // If we're here, it's because we're missing the necessary info to run this engine. - // We should throw an error indicating this. - throw new SfError(getMessage(BundleName.SfgeEngine, 'errors.failedWithoutProjectDir')); + return engineOptions.has(CUSTOM_CONFIG.SfgeConfig); } protected getSubVariantName(): string { diff --git a/test/lib/InputProcessor.test.ts b/test/lib/InputProcessor.test.ts new file mode 100644 index 000000000..2ee587ea5 --- /dev/null +++ b/test/lib/InputProcessor.test.ts @@ -0,0 +1,156 @@ +import {InputProcessor, InputProcessorImpl} from "../../src/lib/InputProcessor"; +import {assert, expect} from "chai"; +import * as path from "path"; +import {Inputs} from "../../src/types"; +import {BundleName, getMessage} from "../../src/MessageCatalog"; +import untildify = require("untildify"); +import normalize = require("normalize-path"); +import {FakeDisplay} from "./FakeDisplay"; + +describe("InputProcessorImpl Tests", async () => { + let display: FakeDisplay; + let inputProcessor: InputProcessor; + beforeEach(async () => { + display = new FakeDisplay(); + inputProcessor = new InputProcessorImpl("2.11.8", display); + }); + + describe("resolveTargetPaths Tests", async () => { + it("Specified glob target stays as glob", async () => { + // Note that we may want to change this behavior in the future instead of waiting to resolve the globs + // in the DefaultRuleManager. But for now, adding this test. + const inputs: Inputs = { + target: ['test\\**\\*.page', '!test/code-fixtures/cpd', '~/*.class'] + }; + const resolvedTargetPaths: string[] = inputProcessor.resolveTargetPaths(inputs); + expect(resolvedTargetPaths).to.have.length(3); + expect(resolvedTargetPaths).to.contain('test/**/*.page'); + expect(resolvedTargetPaths).to.contain('!test/code-fixtures/cpd'); + expect(resolvedTargetPaths).to.contain(normalize(untildify('~/*.class'))) + }) + + it("Specified target with method specifier", async () => { + const inputs: Inputs = { + target: ['test/code-fixtures/apex/SomeTestClass.cls#testMethodWithoutAsserts'] + }; + const resolvedTargetPaths: string[] = inputProcessor.resolveTargetPaths(inputs); + expect(resolvedTargetPaths).to.have.length(1); + expect(resolvedTargetPaths).to.contain('test/code-fixtures/apex/SomeTestClass.cls#testMethodWithoutAsserts'); + + expect(display.getOutputText()).to.equal('') + }) + + it("Unspecified target resolves to current directory", async () => { + const inputs: Inputs = {} + const resolvedTargetPaths: string[] = inputProcessor.resolveTargetPaths(inputs); + expect(resolvedTargetPaths).to.have.length(1); + expect(resolvedTargetPaths).to.contain('.'); + + expect(display.getOutputText()).to.equal('[Info]: ' + + getMessage(BundleName.CommonRun, 'info.resolvedTarget')) + }) + }) + + describe("resolveProjectDirPath Tests", async () => { + it("Specified relative projectdir", async () => { + const inputs: Inputs = { + projectdir: ['test/code-fixtures'] + }; + const resolvedProjectDirs: string[] = inputProcessor.resolveProjectDirPaths(inputs); + expect(resolvedProjectDirs).to.contain(toAbsPath('test/code-fixtures')) + }) + + it("Specified absolute projectdir", async () => { + const inputs: Inputs = { + projectdir: [toAbsPath('test/code-fixtures')] + }; + const resolvedProjectDirs: string[] = inputProcessor.resolveProjectDirPaths(inputs); + expect(resolvedProjectDirs).to.contain(toAbsPath('test/code-fixtures')) + }) + + it("Specified tildified projectdir", async () => { + const inputs: Inputs = { + projectdir: ['~/someFolder'] + }; + const resolvedProjectDirs: string[] = inputProcessor.resolveProjectDirPaths(inputs); + expect(resolvedProjectDirs).to.contain(toAbsPath(normalize(untildify('~/someFolder')))) + }) + + it("Unspecified projectdir and unspecified target", async() => { + const inputs: Inputs = {} + const resolvedProjectDirs: string[] = inputProcessor.resolveProjectDirPaths(inputs); + expect(resolvedProjectDirs).to.contain(toAbsPath('.')); + + expect(display.getOutputArray()).to.have.length(2) + expect(display.getOutputArray()).to.contain('[Info]: ' + + getMessage(BundleName.CommonRun, 'info.resolvedTarget')) + expect(display.getOutputArray()).to.contain('[Info]: ' + + getMessage(BundleName.CommonRun, 'info.resolvedProjectDir', [toAbsPath('')])) + }) + + it("Unspecified projectdir with non-glob relative targets supplied", async () => { + const inputs: Inputs = { + target: ['test/code-fixtures/apex', 'test/catalog-fixtures/DefaultCatalogFixture.json'] + }; + const resolvedProjectDirs: string[] = inputProcessor.resolveProjectDirPaths(inputs); + expect(resolvedProjectDirs).to.contain(toAbsPath('test')); + + expect(display.getOutputText()).to.equal('[Info]: ' + + getMessage(BundleName.CommonRun, 'info.resolvedProjectDir', [toAbsPath('test')])) + }) + + it("Unspecified projectdir with glob targets supplied (with sfdx-project.json in parents)", async () => { + const resolvedProjectDirs: string[] = inputProcessor.resolveProjectDirPaths({ + target: ['test/**/*.page', '!test/code-fixtures/cpd'] + }); + // Note that test/code-fixtures/projects/app/force-app/main/default/pages is the first most common parent + // but test/code-fixtures/projects/app contains a sfdx-project.json and so we return this instead + expect(resolvedProjectDirs).to.contain(toAbsPath('test/code-fixtures/projects/app')); + }) + + it("Unspecified projectdir with glob targets supplied (with no sfdx-project.json in parents)", async () => { + const resolvedProjectDirs: string[] = inputProcessor.resolveProjectDirPaths({ + target: ['test/code-fixtures/**/*.cls'] + }); + expect(resolvedProjectDirs).to.contain(toAbsPath('test/code-fixtures')); + }) + + it("Unspecified projectdir with target containing method specifiers", async () => { + const resolvedProjectDirs: string[] = inputProcessor.resolveProjectDirPaths({ + target: [ + 'test/code-fixtures/apex/SomeTestClass.cls#testMethodWithoutAsserts', + 'test/code-fixtures/apex/SomeOtherTestClass.cls#someTestMethodWithoutAsserts', + ] + }); + expect(resolvedProjectDirs).to.contain(toAbsPath('test/code-fixtures/apex')); + }) + + it("Unspecified projectdir with non-glob target that resolves to no files", async () => { + const inputs: Inputs = { + target: ['thisFileDoesNotExist.xml', 'thisFileAlsoDoesNotExist.json'] + }; + try { + inputProcessor.resolveProjectDirPaths(inputs); + assert.fail("Expected error to be thrown") + } catch (e) { + expect(e.message).to.equal(getMessage(BundleName.CommonRun, 'validations.noFilesFoundInTarget')); + } + }) + + it("Unspecified projectdir with glob target that resolves to no files", async () => { + const inputs: Inputs = { + target: ['**.filesOfThisTypeShouldNotExist'] + }; + try { + inputProcessor.resolveProjectDirPaths(inputs); + assert.fail("Expected error to be thrown") + } catch (e) { + expect(e.message).to.equal(getMessage(BundleName.CommonRun, 'validations.noFilesFoundInTarget')); + } + }) + }) +}) + +function toAbsPath(fileOrFolder: string): string { + return path.resolve(fileOrFolder) +} diff --git a/test/lib/sfge/SfgePathlessEngine.test.ts b/test/lib/sfge/SfgePathlessEngine.test.ts index 8196d9cfe..07f68d8e2 100644 --- a/test/lib/sfge/SfgePathlessEngine.test.ts +++ b/test/lib/sfge/SfgePathlessEngine.test.ts @@ -4,7 +4,6 @@ import {SfgeConfig} from '../../../src/types'; import {CUSTOM_CONFIG} from '../../../src/Constants'; import {SfgePathlessEngine} from '../../../src/lib/sfge/SfgePathlessEngine'; import * as TestOverrides from '../../test-related-lib/TestOverrides'; -import {BundleName, getMessage} from "../../../src/MessageCatalog"; TestOverrides.initializeTestSetup(); @@ -52,7 +51,7 @@ describe('SfgePathlessEngine', () => { }); describe('#shouldEngineRun()', () => { - it('Returns true when SfgeConfig has non-empty projectdirs array', async () => { + it('Returns true when SfgeConfig has non-empty projectdir array', async () => { // ==== SETUP ==== const engine = new SfgePathlessEngine(); await engine.init(); @@ -67,51 +66,5 @@ describe('SfgePathlessEngine', () => { // ==== ASSERTIONS ==== expect(shouldEngineRun).to.be.true; }); - - it('Throws error when SfgeConfig has empty projectdirs array', async () => { - // ==== SETUP ==== - const engine = new SfgePathlessEngine(); - await engine.init(); - const sfgeConfig: SfgeConfig = { - projectDirs: [] - }; - const engineOptions: Map = new Map(); - engineOptions.set(CUSTOM_CONFIG.SfgeConfig, JSON.stringify(sfgeConfig)); - // ==== TESTED METHOD ==== - const invocationOfShouldEngineRun = () => { - // The only parameter that matters should be the engine options. - return engine.shouldEngineRun([], [], [], engineOptions); - }; - // ==== ASSERTIONS ==== - expect(invocationOfShouldEngineRun).to.throw(getMessage(BundleName.SfgeEngine, 'errors.failedWithoutProjectDir', [])); - }); - - it('Throws error when SfgeConfig lacks projectdirs array', async () => { - // ==== SETUP ==== - const engine = new SfgePathlessEngine(); - await engine.init(); - const engineOptions: Map = new Map(); - engineOptions.set(CUSTOM_CONFIG.SfgeConfig, "{}"); - // ==== TESTED METHOD ==== - const invocationOfShouldEngineRun = () => { - // The only parameter that matters should be the engine options. - return engine.shouldEngineRun([], [], [], engineOptions); - }; - // ==== ASSERTIONS ==== - expect(invocationOfShouldEngineRun).to.throw(getMessage(BundleName.SfgeEngine, 'errors.failedWithoutProjectDir', [])); - }); - - it('Throws error when SfgeConfig is outright absent', async () => { - // ==== SETUP ==== - const engine = new SfgePathlessEngine(); - await engine.init(); - // ==== TESTED METHOD ==== - const invocationOfShouldEngineRun = () => { - // The only parameter that matters should be the engine options. - return engine.shouldEngineRun([], [], [], new Map()); - }; - // ==== ASSERTIONS ==== - expect(invocationOfShouldEngineRun).to.throw(getMessage(BundleName.SfgeEngine, 'errors.failedWithoutProjectDir', [])); - }); }); });