diff --git a/package-lock.json b/package-lock.json index ab8afa25..6815e3a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -98,7 +97,6 @@ "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.27.5.tgz", "integrity": "sha512-HLkYQfRICudzcOtjGwkPvGc5nF1b4ljLZh1IRDj50lRZ718NAKVgQpIAUX8bfg6u/yuSKY3L7E0YzIV+OxrB8Q==", "license": "MIT", - "peer": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", @@ -656,7 +654,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -668,7 +665,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -679,7 +675,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1697,7 +1692,6 @@ "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1754,7 +1748,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -1910,7 +1903,6 @@ "resolved": "https://registry.npmjs.org/@salesforce/eslint-plugin-lightning/-/eslint-plugin-lightning-1.0.1.tgz", "integrity": "sha512-oyUVSNUA0WkkQr3BRtcAYhYotzIpqZtfMpUVMhROPN8YjDGu6CzCoC3/1i4ySIevgmH3J83KypwoqvRfoQf8Ww==", "license": "MIT", - "peer": true, "peerDependencies": { "eslint": "^7 || ^8" } @@ -1952,7 +1944,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2139,7 +2130,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", @@ -2168,7 +2158,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz", "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -2371,7 +2360,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2385,7 +2373,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2399,7 +2386,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2413,7 +2399,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2427,7 +2412,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2441,7 +2425,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2455,7 +2438,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2469,7 +2451,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2483,7 +2464,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2497,7 +2477,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2511,7 +2490,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2525,7 +2503,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2539,7 +2516,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2553,7 +2529,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2567,7 +2542,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2581,7 +2555,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2598,7 +2571,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2612,7 +2584,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2626,7 +2597,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2638,7 +2608,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3149,7 +3118,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -3974,7 +3942,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4076,7 +4043,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz", "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==", "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -4161,7 +4127,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.12.1.tgz", "integrity": "sha512-Rxo7r4jSANMBkXLICJKS0gjacgyopfNAsoS0e3R9AHnjoKuQOaaPfmsDJPi8UWwygI099OV/K/JhpYRVkxD4AA==", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/utils": "^8.0.0" }, @@ -4896,7 +4861,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5965,7 +5929,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -8594,7 +8557,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8852,7 +8814,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9404,7 +9365,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -9423,10 +9383,10 @@ }, "packages/code-analyzer-core": { "name": "@salesforce/code-analyzer-core", - "version": "0.42.0", + "version": "0.43.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "csv-stringify": "^6.6.0", "js-yaml": "^4.1.1", @@ -9488,6 +9448,18 @@ "url": "https://eslint.org/donate" } }, + "packages/code-analyzer-core/node_modules/@salesforce/code-analyzer-engine-api": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.34.0.tgz", + "integrity": "sha512-eZo+jfpzoDxWQIrnhKH8o2NJ+poYiEdusxezYTTnfGPmuQLRipq+a2Yz8j3+2zGjvNsJ0Up8p0POIdOZqd7mZw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node": "^20.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "packages/code-analyzer-core/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -9682,10 +9654,11 @@ }, "packages/code-analyzer-engine-api": { "name": "@salesforce/code-analyzer-engine-api", - "version": "0.34.0", + "version": "0.35.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@types/node": "^20.0.0" + "@types/node": "^20.0.0", + "minimatch": "^9.0.0" }, "devDependencies": { "@eslint/js": "^9.39.2", @@ -9742,7 +9715,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9921,7 +9893,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -9932,7 +9903,7 @@ }, "packages/code-analyzer-eslint-engine": { "name": "@salesforce/code-analyzer-eslint-engine", - "version": "0.39.0", + "version": "0.40.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { "@babel/preset-react": "^7.28.5", @@ -9940,8 +9911,8 @@ "@lwc/eslint-plugin-lwc": "^3.3.0", "@lwc/eslint-plugin-lwc-platform": "^6.3.0", "@salesforce-ux/eslint-plugin-slds": "^1.1.0", - "@salesforce/code-analyzer-engine-api": "0.34.0", - "@salesforce/code-analyzer-eslint8-engine": "0.11.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", + "@salesforce/code-analyzer-eslint8-engine": "0.12.0-SNAPSHOT", "@salesforce/eslint-config-lwc": "^4.1.2", "@salesforce/eslint-plugin-lightning": "^2.0.0", "@types/node": "^20.0.0", @@ -10016,7 +9987,6 @@ "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.9.tgz", "integrity": "sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==", "license": "MIT", - "peer": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", @@ -10166,6 +10136,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/code-analyzer-eslint-engine/node_modules/@salesforce/code-analyzer-engine-api": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.34.0.tgz", + "integrity": "sha512-eZo+jfpzoDxWQIrnhKH8o2NJ+poYiEdusxezYTTnfGPmuQLRipq+a2Yz8j3+2zGjvNsJ0Up8p0POIdOZqd7mZw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node": "^20.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "packages/code-analyzer-eslint-engine/node_modules/@salesforce/eslint-config-lwc": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@salesforce/eslint-config-lwc/-/eslint-config-lwc-4.1.2.tgz", @@ -10207,7 +10189,6 @@ "resolved": "https://registry.npmjs.org/@salesforce/eslint-plugin-lightning/-/eslint-plugin-lightning-2.0.0.tgz", "integrity": "sha512-lC3GL2j6B2wAGeTFWT0h47BFg+0R7naqqlQW+ANvNSaIC/qEB+tNSRcdAZ8DRTojsI3GRdpgq3FTB1llbrFBng==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -10232,7 +10213,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10292,7 +10272,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10485,7 +10464,7 @@ }, "packages/code-analyzer-eslint8-engine": { "name": "@salesforce/code-analyzer-eslint8-engine", - "version": "0.11.0", + "version": "0.12.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { "@babel/core": "7.27.4", @@ -10493,7 +10472,7 @@ "@eslint/js": "8.57.1", "@lwc/eslint-plugin-lwc": "2.2.0", "@lwc/eslint-plugin-lwc-platform": "5.2.0", - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@salesforce/eslint-config-lwc": "3.7.2", "@salesforce/eslint-plugin-lightning": "1.0.1", "@types/node": "^20.0.0", @@ -10519,6 +10498,18 @@ "node": ">=20.0.0" } }, + "packages/code-analyzer-eslint8-engine/node_modules/@salesforce/code-analyzer-engine-api": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.34.0.tgz", + "integrity": "sha512-eZo+jfpzoDxWQIrnhKH8o2NJ+poYiEdusxezYTTnfGPmuQLRipq+a2Yz8j3+2zGjvNsJ0Up8p0POIdOZqd7mZw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node": "^20.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "packages/code-analyzer-eslint8-engine/node_modules/@salesforce/eslint-config-lwc": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/@salesforce/eslint-config-lwc/-/eslint-config-lwc-3.7.2.tgz", @@ -10612,7 +10603,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.30.1", @@ -10688,7 +10678,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.30.1", "@typescript-eslint/types": "8.30.1", @@ -10798,7 +10787,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.10.0.tgz", "integrity": "sha512-hyMWUxkBH99HpXT3p8hc7REbEZK3D+nk8vHXGgpB+XXsi0gO4PxMSP+pjfUzb67GnV9yawV9a53eUmcde1CCZA==", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" }, @@ -10833,7 +10821,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10889,10 +10876,10 @@ }, "packages/code-analyzer-flow-engine": { "name": "@salesforce/code-analyzer-flow-engine", - "version": "0.33.0", + "version": "0.34.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "@types/semver": "^7.7.1", "semver": "^7.7.3" @@ -10948,6 +10935,18 @@ "url": "https://eslint.org/donate" } }, + "packages/code-analyzer-flow-engine/node_modules/@salesforce/code-analyzer-engine-api": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.34.0.tgz", + "integrity": "sha512-eZo+jfpzoDxWQIrnhKH8o2NJ+poYiEdusxezYTTnfGPmuQLRipq+a2Yz8j3+2zGjvNsJ0Up8p0POIdOZqd7mZw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node": "^20.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "packages/code-analyzer-flow-engine/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -11142,10 +11141,10 @@ }, "packages/code-analyzer-pmd-engine": { "name": "@salesforce/code-analyzer-pmd-engine", - "version": "0.35.0", + "version": "0.36.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "@types/semver": "^7.7.1", "semver": "^7.7.3" @@ -11201,6 +11200,18 @@ "url": "https://eslint.org/donate" } }, + "packages/code-analyzer-pmd-engine/node_modules/@salesforce/code-analyzer-engine-api": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.34.0.tgz", + "integrity": "sha512-eZo+jfpzoDxWQIrnhKH8o2NJ+poYiEdusxezYTTnfGPmuQLRipq+a2Yz8j3+2zGjvNsJ0Up8p0POIdOZqd7mZw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node": "^20.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "packages/code-analyzer-pmd-engine/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -11395,10 +11406,10 @@ }, "packages/code-analyzer-regex-engine": { "name": "@salesforce/code-analyzer-regex-engine", - "version": "0.32.0", + "version": "0.33.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "isbinaryfile": "^5.0.0", "p-limit": "^3.1.0" @@ -11454,6 +11465,18 @@ "url": "https://eslint.org/donate" } }, + "packages/code-analyzer-regex-engine/node_modules/@salesforce/code-analyzer-engine-api": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.34.0.tgz", + "integrity": "sha512-eZo+jfpzoDxWQIrnhKH8o2NJ+poYiEdusxezYTTnfGPmuQLRipq+a2Yz8j3+2zGjvNsJ0Up8p0POIdOZqd7mZw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node": "^20.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "packages/code-analyzer-regex-engine/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -11660,10 +11683,10 @@ }, "packages/code-analyzer-retirejs-engine": { "name": "@salesforce/code-analyzer-retirejs-engine", - "version": "0.31.0", + "version": "0.32.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "isbinaryfile": "^5.0.0", "node-stream-zip": "^1.15.0", @@ -11720,6 +11743,18 @@ "url": "https://eslint.org/donate" } }, + "packages/code-analyzer-retirejs-engine/node_modules/@salesforce/code-analyzer-engine-api": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.34.0.tgz", + "integrity": "sha512-eZo+jfpzoDxWQIrnhKH8o2NJ+poYiEdusxezYTTnfGPmuQLRipq+a2Yz8j3+2zGjvNsJ0Up8p0POIdOZqd7mZw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node": "^20.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "packages/code-analyzer-retirejs-engine/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -11926,10 +11961,10 @@ }, "packages/code-analyzer-sfge-engine": { "name": "@salesforce/code-analyzer-sfge-engine", - "version": "0.17.0", + "version": "0.18.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "semver": "^7.7.3" }, @@ -11985,6 +12020,18 @@ "url": "https://eslint.org/donate" } }, + "packages/code-analyzer-sfge-engine/node_modules/@salesforce/code-analyzer-engine-api": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.34.0.tgz", + "integrity": "sha512-eZo+jfpzoDxWQIrnhKH8o2NJ+poYiEdusxezYTTnfGPmuQLRipq+a2Yz8j3+2zGjvNsJ0Up8p0POIdOZqd7mZw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node": "^20.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "packages/code-analyzer-sfge-engine/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -12182,7 +12229,7 @@ "version": "0.1.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0" }, "devDependencies": { @@ -12236,6 +12283,18 @@ "url": "https://eslint.org/donate" } }, + "packages/ENGINE-TEMPLATE/node_modules/@salesforce/code-analyzer-engine-api": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.34.0.tgz", + "integrity": "sha512-eZo+jfpzoDxWQIrnhKH8o2NJ+poYiEdusxezYTTnfGPmuQLRipq+a2Yz8j3+2zGjvNsJ0Up8p0POIdOZqd7mZw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node": "^20.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "packages/ENGINE-TEMPLATE/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", diff --git a/packages/ENGINE-TEMPLATE/package.json b/packages/ENGINE-TEMPLATE/package.json index 4dfc5bf2..4fa23b83 100644 --- a/packages/ENGINE-TEMPLATE/package.json +++ b/packages/ENGINE-TEMPLATE/package.json @@ -14,7 +14,7 @@ "types": "dist/index.d.ts", "dependencies": { "@types/node": "^20.0.0", - "@salesforce/code-analyzer-engine-api": "0.34.0" + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT" }, "devDependencies": { "@eslint/js": "^9.39.2", diff --git a/packages/code-analyzer-core/package.json b/packages/code-analyzer-core/package.json index 90e11207..3299e29e 100644 --- a/packages/code-analyzer-core/package.json +++ b/packages/code-analyzer-core/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-core", "description": "Core Package for the Salesforce Code Analyzer", - "version": "0.42.0", + "version": "0.43.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -16,7 +16,7 @@ }, "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "csv-stringify": "^6.6.0", "js-yaml": "^4.1.1", diff --git a/packages/code-analyzer-core/src/code-analyzer.ts b/packages/code-analyzer-core/src/code-analyzer.ts index ca189122..04e53467 100644 --- a/packages/code-analyzer-core/src/code-analyzer.ts +++ b/packages/code-analyzer-core/src/code-analyzer.ts @@ -24,7 +24,7 @@ import * as engApi from "@salesforce/code-analyzer-engine-api" import {Clock, RealClock} from '@salesforce/code-analyzer-engine-api/utils'; import {Selector, toSelector} from "./selectors"; import {EventEmitter} from "node:events"; -import {CodeAnalyzerConfig, ConfigDescription, EngineOverrides, FIELDS, RuleOverride} from "./config"; +import {CodeAnalyzerConfig, ConfigDescription, EngineOverrides, FIELDS, Ignores, RuleOverride} from "./config"; import { EngineProgressAggregator, FileSystem, @@ -157,6 +157,8 @@ export class CodeAnalyzer { * analyze the few files that you are targeting. If a targets array is not specified, then the entire list of * workspaces files and folders will be targeted. * + * Files matching patterns specified in the ignores.files configuration will be excluded from the workspace. + * * @param workspaceFilesAndFolders string array of files and/or folders to include in the workspace * @param targets optional string array of files and/or folders */ @@ -174,7 +176,11 @@ export class CodeAnalyzer { validatedTargets = (await Promise.all(targetPromises)).flat(); } - const workspace: Workspace = new WorkspaceImpl(workspaceId, validatedWorkspaceFilesAndFolders, validatedTargets); + // Get ignore patterns from config + const ignores: Ignores = this.config.getIgnores(); + const ignorePatterns: string[] = ignores.files; + + const workspace: Workspace = new WorkspaceImpl(workspaceId, validatedWorkspaceFilesAndFolders, validatedTargets, ignorePatterns); // It appears that each of the engines is calling these methods all at the same time and so if we had N engines // each creating N promises, the cache hasn't been populated, and so we are doing the work N times. If we @@ -646,8 +652,10 @@ export class CodeAnalyzer { */ class WorkspaceImpl implements Workspace { private readonly delegate: engApi.Workspace; - constructor(workspaceId: string, absWorkspaceFilesAndFolders: string[], absTargets?: string[]) { - this.delegate = new engApi.Workspace(workspaceId, absWorkspaceFilesAndFolders, absTargets); + + constructor(workspaceId: string, absWorkspaceFilesAndFolders: string[], absTargets?: string[], ignorePatterns: string[] = []) { + // Pass ignore patterns directly to engApi.Workspace which handles filtering internally + this.delegate = new engApi.Workspace(workspaceId, absWorkspaceFilesAndFolders, absTargets, ignorePatterns); } getWorkspaceId(): string { @@ -662,11 +670,11 @@ class WorkspaceImpl implements Workspace { return this.delegate.getRawTargets(); } - getWorkspaceFiles(): Promise { + async getWorkspaceFiles(): Promise { return this.delegate.getWorkspaceFiles(); } - getTargetedFiles(): Promise { + async getTargetedFiles(): Promise { return this.delegate.getTargetedFiles(); } diff --git a/packages/code-analyzer-core/src/config.ts b/packages/code-analyzer-core/src/config.ts index a73834f2..627670f1 100644 --- a/packages/code-analyzer-core/src/config.ts +++ b/packages/code-analyzer-core/src/config.ts @@ -20,7 +20,9 @@ export const FIELDS = { ENGINES: 'engines', SEVERITY: 'severity', TAGS: 'tags', - DISABLE_ENGINE: 'disable_engine' + DISABLE_ENGINE: 'disable_engine', + IGNORES: 'ignores', + FILES: 'files' } as const; /** @@ -37,12 +39,20 @@ export type RuleOverride = { tags?: string[] } +/** + * Object containing the user specified ignores configuration for files to skip during scanning + */ +export type Ignores = { + files: string[] +} + type TopLevelConfig = { config_root: string log_folder: string log_level: LogLevel rules: Record engines: Record + ignores: Ignores root_working_folder: string, // INTERNAL USE ONLY preserve_all_working_folders: boolean // INTERNAL USE ONLY custom_engine_plugin_modules: string[] // INTERNAL USE ONLY @@ -55,6 +65,7 @@ export const DEFAULT_CONFIG: TopLevelConfig = { log_level: LogLevel.Debug, rules: {}, engines: {}, + ignores: { files: [] }, root_working_folder: os.tmpdir(), // INTERNAL USE ONLY preserve_all_working_folders: false, // INTERNAL USE ONLY custom_engine_plugin_modules: [], // INTERNAL USE ONLY @@ -143,7 +154,7 @@ export class CodeAnalyzerConfig { validateAbsoluteFolder(rawConfig.config_root, FIELDS.CONFIG_ROOT); const configExtractor: engApi.ConfigValueExtractor = new engApi.ConfigValueExtractor(rawConfig, '', configRoot); configExtractor.addKeysThatBypassValidation([FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES, FIELDS.PRESERVE_ALL_WORKING_FOLDERS, FIELDS.ROOT_WORKING_FOLDER]); // Hidden fields bypass validation - configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL ,FIELDS.RULES, FIELDS.ENGINES]); + configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL, FIELDS.RULES, FIELDS.ENGINES, FIELDS.IGNORES]); const config: TopLevelConfig = { config_root: configRoot, log_folder: configExtractor.extractFolder(FIELDS.LOG_FOLDER, DEFAULT_CONFIG.log_folder)!, @@ -154,7 +165,8 @@ export class CodeAnalyzerConfig { root_working_folder: configExtractor.extractFolder(FIELDS.ROOT_WORKING_FOLDER, DEFAULT_CONFIG.root_working_folder)!, preserve_all_working_folders: configExtractor.extractBoolean(FIELDS.PRESERVE_ALL_WORKING_FOLDERS, DEFAULT_CONFIG.preserve_all_working_folders)!, rules: extractRulesValue(configExtractor), - engines: extractEnginesValue(configExtractor) + engines: extractEnginesValue(configExtractor), + ignores: extractIgnoresValue(configExtractor) } return new CodeAnalyzerConfig(config); } @@ -195,6 +207,12 @@ export class CodeAnalyzerConfig { valueType: 'object', defaultValue: {}, wasSuppliedByUser: !deepEquals(this.config.engines, DEFAULT_CONFIG.engines) + }, + ignores: { + descriptionText: getMessage('ConfigFieldDescription_ignores'), + valueType: 'object', + defaultValue: { files: [] }, + wasSuppliedByUser: !deepEquals(this.config.ignores, DEFAULT_CONFIG.ignores) } } }; @@ -276,6 +294,14 @@ export class CodeAnalyzerConfig { public getEngineOverridesFor(engineName: string): EngineOverrides { return engApi.getValueUsingCaseInsensitiveKey(this.config.engines, engineName) as EngineOverrides || {}; } + + /** + * Returns a {@link Ignores} instance containing the user specified file patterns to ignore during scanning. + * The patterns can be file paths, folder paths, or glob patterns. + */ + public getIgnores(): Ignores { + return this.config.ignores; + } } function extractLogLevel(configExtractor: engApi.ConfigValueExtractor): LogLevel { @@ -322,6 +348,77 @@ function extractEnginesValue(configExtractor: engApi.ConfigValueExtractor): Reco return enginesExtractor.getObject() as Record; } +function extractIgnoresValue(configExtractor: engApi.ConfigValueExtractor): Ignores { + const ignoresExtractor: engApi.ConfigValueExtractor = configExtractor.extractObjectAsExtractor(FIELDS.IGNORES, DEFAULT_CONFIG.ignores); + ignoresExtractor.validateContainsOnlySpecifiedKeys([FIELDS.FILES]); + const files: string[] = ignoresExtractor.extractArray(FIELDS.FILES, validateGlobPattern, DEFAULT_CONFIG.ignores.files) || []; + return { files }; +} + +/** + * Validates that a value is a string and is a valid glob pattern. + * Throws an error if the pattern is empty or has unbalanced brackets/braces/parentheses. + */ +function validateGlobPattern(value: unknown, fieldPath: string): string { + // First validate it's a string + const pattern = engApi.ValueValidator.validateString(value, fieldPath); + + // Check for empty pattern + if (pattern.length === 0) { + throw new Error(getMessage('InvalidGlobPatternEmpty', fieldPath)); + } + + // Check for unbalanced special characters + const validationResult = validateGlobPatternSyntax(pattern); + if (!validationResult.valid) { + throw new Error(getMessage('InvalidGlobPattern', fieldPath, pattern, validationResult.issue!)); + } + + return pattern; +} + +/** + * Validates glob pattern syntax for common issues like unbalanced brackets. + */ +function validateGlobPatternSyntax(pattern: string): { valid: boolean; issue?: string } { + let bracketDepth = 0; + let braceDepth = 0; + let parenDepth = 0; + let escaped = false; + + for (const char of pattern) { + if (escaped) { + escaped = false; + continue; + } + if (char === '\\') { + escaped = true; + continue; + } + + switch (char) { + case '[': bracketDepth++; break; + case ']': bracketDepth--; break; + case '{': braceDepth++; break; + case '}': braceDepth--; break; + case '(': parenDepth++; break; + case ')': parenDepth--; break; + } + + // Check for negative depth (closing without opening) + if (bracketDepth < 0) return { valid: false, issue: 'unmatched closing bracket ]' }; + if (braceDepth < 0) return { valid: false, issue: 'unmatched closing brace }' }; + if (parenDepth < 0) return { valid: false, issue: 'unmatched closing parenthesis )' }; + } + + // Check for unclosed brackets + if (bracketDepth !== 0) return { valid: false, issue: 'unclosed bracket [' }; + if (braceDepth !== 0) return { valid: false, issue: 'unclosed brace {' }; + if (parenDepth !== 0) return { valid: false, issue: 'unclosed parenthesis (' }; + + return { valid: true }; +} + function parseAndValidate(parseFcn: () => unknown): object { let data; try { diff --git a/packages/code-analyzer-core/src/index.ts b/packages/code-analyzer-core/src/index.ts index f1d53597..d45dec3b 100644 --- a/packages/code-analyzer-core/src/index.ts +++ b/packages/code-analyzer-core/src/index.ts @@ -6,6 +6,7 @@ export type { ConfigDescription, ConfigFieldDescription, EngineOverrides, + Ignores, RuleOverrides, RuleOverride } from "./config" diff --git a/packages/code-analyzer-core/src/messages.ts b/packages/code-analyzer-core/src/messages.ts index e16c4091..e63e2603 100644 --- a/packages/code-analyzer-core/src/messages.ts +++ b/packages/code-analyzer-core/src/messages.ts @@ -47,6 +47,16 @@ const MESSAGE_CATALOG : MessageCatalog = { ` {property_name} is the name of a property that you would like to override.\n` + `Each engine may have its own set of properties available to help customize that particular engine's behavior.`, + ConfigFieldDescription_ignores: + `Configuration for ignoring files during analysis.\n` + + ` files: An array of glob patterns specifying files to exclude from scanning.\n` + + `---- [Example usage]: ---------------------\n` + + `ignores:\n` + + ` files:\n` + + ` - "**/node_modules/**"\n` + + ` - "**/*.test.js"\n` + + `-------------------------------------------`, + GenericEngineConfigOverview: `%s ENGINE CONFIGURATION`, @@ -124,6 +134,12 @@ const MESSAGE_CATALOG : MessageCatalog = { ConfigContentNotAnObject: `The configuration content is invalid since it is of type %s instead of type object.`, + InvalidGlobPatternEmpty: + `The configuration field '%s' contains an empty glob pattern. Glob patterns must not be empty strings.`, + + InvalidGlobPattern: + `The configuration field '%s' contains an invalid glob pattern '%s': %s`, + RulePropertyOverridden: `The %s value of rule '%s' of engine '%s' was overridden according to the specified configuration. The old value '%s' was replaced with the new value '%s'.`, diff --git a/packages/code-analyzer-core/test/code-analyzer.test.ts b/packages/code-analyzer-core/test/code-analyzer.test.ts index 37feeb29..a8f67488 100644 --- a/packages/code-analyzer-core/test/code-analyzer.test.ts +++ b/packages/code-analyzer-core/test/code-analyzer.test.ts @@ -179,6 +179,130 @@ describe("Tests for the createWorkspace method", () => { }); }); +describe("Tests for ignores configuration in createWorkspace", () => { + it("When ignores.files contains glob patterns, then workspace files are NOT filtered (for complete graph building)", async () => { + const config = CodeAnalyzerConfig.fromObject({ + ignores: { + files: ["**/*.cls"] + } + }); + const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem()); + + const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]); + + const workspaceFiles = await workspace.getWorkspaceFiles(); + // Workspace files should NOT be filtered by ignore patterns (allows engines like SFGE to build complete graphs) + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')); + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls')); + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt')); + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'folderWithExt.cls', 'placeholder.txt')); + }); + + it("When ignores.files contains glob patterns, then matching files are excluded from targeted files", async () => { + const config = CodeAnalyzerConfig.fromObject({ + ignores: { + files: ["**/*.txt"] + } + }); + const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem()); + + const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]); + + const targetedFiles = await workspace.getTargetedFiles(); + // All .txt files should be excluded from targeted files + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt')); + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile1InSub2.txt')); + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'folderWithExt.cls', 'placeholder.txt')); + // .cls files should still be included + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')); + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls')); + }); + + it("When ignores.files contains specific file patterns, then only those targeted files are excluded", async () => { + const config = CodeAnalyzerConfig.fromObject({ + ignores: { + files: ["someFile.cls"] + } + }); + const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem()); + + const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]); + + const targetedFiles = await workspace.getTargetedFiles(); + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')); + // Other .cls files should still be included since pattern doesn't have ** + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls')); + }); + + it("When ignores.files contains folder patterns, then targeted files in matching folders are excluded", async () => { + const config = CodeAnalyzerConfig.fromObject({ + ignores: { + files: ["sub1/sub2/**"] + } + }); + const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem()); + + const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]); + + const targetedFiles = await workspace.getTargetedFiles(); + // Files in sub2 folder should be excluded from targeted files + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile1InSub2.txt')); + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile2InSub2.txt')); + // Files in other folders should still be included + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt')); + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls')); + }); + + it("When ignores.files contains multiple patterns, then all matching targeted files are excluded", async () => { + const config = CodeAnalyzerConfig.fromObject({ + ignores: { + files: ["**/*.cls", "**/*InSub2*"] + } + }); + const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem()); + + const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]); + + const targetedFiles = await workspace.getTargetedFiles(); + // All .cls files should be excluded from targeted files + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')); + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls')); + // Files matching *InSub2* should be excluded + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile1InSub2.txt')); + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile2InSub2.txt')); + // Other files should still be included + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt')); + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'folderWithExt.cls', 'placeholder.txt')); + }); + + it("When ignores.files is empty, then no files are excluded", async () => { + const config = CodeAnalyzerConfig.fromObject({ + ignores: { + files: [] + } + }); + const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem()); + + const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]); + + const workspaceFiles = await workspace.getWorkspaceFiles(); + // All files should be present (except those normally excluded by the engine API like node_modules and .dotfiles) + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')); + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt')); + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls')); + }); + + it("When no ignores config is provided, then default behavior is used (no extra exclusions)", async () => { + const codeAnalyzerDefault = new CodeAnalyzer(CodeAnalyzerConfig.withDefaults(), new FakeFileSystem()); + + const workspace: Workspace = await codeAnalyzerDefault.createWorkspace([SAMPLE_WORKSPACE_FOLDER]); + + const workspaceFiles = await workspace.getWorkspaceFiles(); + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')); + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt')); + }); +}); + describe("Tests for the run method of CodeAnalyzer", () => { let sampleRunOptions: RunOptions; let fileSystem: FakeFileSystem; diff --git a/packages/code-analyzer-core/test/config.test.ts b/packages/code-analyzer-core/test/config.test.ts index e39496eb..7f43eac2 100644 --- a/packages/code-analyzer-core/test/config.test.ts +++ b/packages/code-analyzer-core/test/config.test.ts @@ -1,4 +1,4 @@ -import {CodeAnalyzerConfig, SeverityLevel} from "../src"; +import {CodeAnalyzerConfig, Ignores, SeverityLevel} from "../src"; import * as os from "node:os"; import * as path from "node:path"; import {getMessageFromCatalog, LogLevel, SHARED_MESSAGE_CATALOG} from "@salesforce/code-analyzer-engine-api"; @@ -25,6 +25,7 @@ describe("Tests for creating and accessing configuration values", () => { expect(conf.getEngineOverridesFor("stubEngine1")).toEqual({}); expect(conf.getRuleOverridesFor("stubEngine2")).toEqual({}); expect(conf.getEngineOverridesFor("stubEngine2")).toEqual({}); + expect(conf.getIgnores()).toEqual({ files: [] }); }); it("When configuration file does not exist, then throw an error", () => { @@ -177,7 +178,7 @@ describe("Tests for creating and accessing configuration values", () => { it("When top level config has an unknown key, then we error", () => { expect(() => CodeAnalyzerConfig.fromObject({doesNotExist: 3})).toThrow( getMessageFromCatalog(SHARED_MESSAGE_CATALOG,'ConfigObjectContainsInvalidKey','', 'doesNotExist', - '["config_root","engines","log_folder","log_level","rules"]')); + '["config_root","engines","ignores","log_folder","log_level","rules"]')); }); it("When engines value is not an object then we throw an error", () => { @@ -393,6 +394,12 @@ describe("Tests for creating and accessing configuration values", () => { valueType: 'object', defaultValue: {}, wasSuppliedByUser: false + }, + ignores: { + descriptionText: getMessage('ConfigFieldDescription_ignores'), + valueType: 'object', + defaultValue: { files: [] }, + wasSuppliedByUser: false } }); }); @@ -416,3 +423,159 @@ describe("Tests for creating and accessing configuration values", () => { expect(configDescription.fieldDescriptions.engines.wasSuppliedByUser).toEqual(false); }); }); + +describe("Tests for ignores configuration", () => { + it("When constructing config withDefaults then ignores has empty files array", () => { + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.withDefaults(); + const ignores: Ignores = conf.getIgnores(); + expect(ignores).toEqual({ files: [] }); + }); + + it("When constructing config with ignores.files array, then values are parsed correctly", () => { + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromYamlString(` +ignores: + files: + - "src/*.cls" + - "**/*.test.js" + - "**/node_modules/**" +`); + const ignores: Ignores = conf.getIgnores(); + expect(ignores.files).toEqual([ + "src/*.cls", + "**/*.test.js", + "**/node_modules/**" + ]); + }); + + it("When constructing config with empty ignores object, then files defaults to empty array", () => { + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromYamlString(` +ignores: {} +`); + const ignores: Ignores = conf.getIgnores(); + expect(ignores).toEqual({ files: [] }); + }); + + it("When constructing config with ignores.files as null, then files defaults to empty array", () => { + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromYamlString(` +ignores: + files: null +`); + const ignores: Ignores = conf.getIgnores(); + expect(ignores).toEqual({ files: [] }); + }); + + it("When ignores is not an object then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: "invalid"})).toThrow( + getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigValueMustBeOfType', 'ignores', 'object', 'string')); + }); + + it("When ignores.files is not an array then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: "invalid"}})).toThrow( + getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigValueMustBeOfType', 'ignores.files', 'array', 'string')); + }); + + it("When ignores.files contains non-string values then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: ["valid", 123]}})).toThrow( + getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigValueMustBeOfType', 'ignores.files[1]', 'string', 'number')); + }); + + it("When ignores.files contains null value, then we throw an error", () => { + // In YAML, a bare dash (- ) becomes null + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: [null, "valid.js"]}})).toThrow( + getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigValueMustBeOfType', 'ignores.files[0]', 'string', 'null')); + }); + + it("When ignores contains unknown keys then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: [], unknownKey: "value"}})).toThrow( + getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigObjectContainsInvalidKey', 'ignores', 'unknownKey', '["files"]')); + }); + + it("When getConfigDescription is called, then ignores field is included", () => { + const configDescription: ConfigDescription = CodeAnalyzerConfig.withDefaults().getConfigDescription(); + expect(configDescription.fieldDescriptions.ignores).toBeDefined(); + expect(configDescription.fieldDescriptions.ignores.valueType).toEqual('object'); + expect(configDescription.fieldDescriptions.ignores.defaultValue).toEqual({ files: [] }); + expect(configDescription.fieldDescriptions.ignores.wasSuppliedByUser).toEqual(false); + }); + + it("When getConfigDescription is called with ignores supplied, then wasSuppliedByUser is true", () => { + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromObject({ + ignores: { files: ["**/*.test.js"] } + }); + const configDescription: ConfigDescription = conf.getConfigDescription(); + expect(configDescription.fieldDescriptions.ignores.wasSuppliedByUser).toEqual(true); + }); +}); + +describe("Tests for glob pattern validation in ignores", () => { + it("When ignores.files contains an empty string, then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: [""]}})).toThrow( + getMessage('InvalidGlobPatternEmpty', 'ignores.files[0]')); + }); + + it("When ignores.files contains a pattern with unclosed bracket, then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: ["**/[abc"]}})).toThrow( + getMessage('InvalidGlobPattern', 'ignores.files[0]', '**/[abc', 'unclosed bracket [')); + }); + + it("When ignores.files contains a pattern with unmatched closing bracket, then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: ["**/*.js]"]}})).toThrow( + getMessage('InvalidGlobPattern', 'ignores.files[0]', '**/*.js]', 'unmatched closing bracket ]')); + }); + + it("When ignores.files contains a pattern with unclosed brace, then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: ["**/*.{js,ts"]}})).toThrow( + getMessage('InvalidGlobPattern', 'ignores.files[0]', '**/*.{js,ts', 'unclosed brace {')); + }); + + it("When ignores.files contains a pattern with unmatched closing brace, then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: ["**/*.js}"]}})).toThrow( + getMessage('InvalidGlobPattern', 'ignores.files[0]', '**/*.js}', 'unmatched closing brace }')); + }); + + it("When ignores.files contains a pattern with unclosed parenthesis, then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: ["**/!(test"]}})).toThrow( + getMessage('InvalidGlobPattern', 'ignores.files[0]', '**/!(test', 'unclosed parenthesis (')); + }); + + it("When ignores.files contains a pattern with unmatched closing parenthesis, then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({ignores: {files: ["**/*.js)"]}})).toThrow( + getMessage('InvalidGlobPattern', 'ignores.files[0]', '**/*.js)', 'unmatched closing parenthesis )')); + }); + + it("When ignores.files contains escaped brackets, they are not counted as unbalanced", () => { + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromObject({ + ignores: { files: ["**/\\[special\\].js"] } + }); + expect(conf.getIgnores().files).toEqual(["**/\\[special\\].js"]); + }); + + it("When ignores.files contains valid nested braces, they are accepted", () => { + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromObject({ + ignores: { files: ["**/*.{js,{ts,tsx}}"] } + }); + expect(conf.getIgnores().files).toEqual(["**/*.{js,{ts,tsx}}"]); + }); + + it("When ignores.files contains valid glob patterns, no error is thrown", () => { + const validPatterns = [ + "src/*.cls", + "**/*.test.js", + "**/node_modules/**", + "packages/*/dist/**", + "**/*.{js,ts}", + "**/[abc]*.js", + "!(test)/**" + ]; + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromObject({ + ignores: { files: validPatterns } + }); + expect(conf.getIgnores().files).toEqual(validPatterns); + }); + + it("When the invalid pattern is in the middle of the array, the error reports correct index", () => { + expect(() => CodeAnalyzerConfig.fromObject({ + ignores: { files: ["valid/*.js", "also-valid/**", "**/invalid{pattern"] } + })).toThrow(getMessage('InvalidGlobPattern', 'ignores.files[2]', '**/invalid{pattern', 'unclosed brace {')); + }); +}); diff --git a/packages/code-analyzer-engine-api/package.json b/packages/code-analyzer-engine-api/package.json index 24a16f76..9f91ad9b 100644 --- a/packages/code-analyzer-engine-api/package.json +++ b/packages/code-analyzer-engine-api/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-engine-api", "description": "Engine API Package for the Salesforce Code Analyzer", - "version": "0.34.0", + "version": "0.35.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -16,7 +16,8 @@ }, "types": "dist/index.d.ts", "dependencies": { - "@types/node": "^20.0.0" + "@types/node": "^20.0.0", + "minimatch": "^9.0.0" }, "devDependencies": { "@eslint/js": "^9.39.2", diff --git a/packages/code-analyzer-engine-api/src/workspace.ts b/packages/code-analyzer-engine-api/src/workspace.ts index 3eedbf0a..14c6da68 100644 --- a/packages/code-analyzer-engine-api/src/workspace.ts +++ b/packages/code-analyzer-engine-api/src/workspace.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import {Minimatch} from "minimatch"; import {calculateLongestCommonParentFolderOf} from "./utils"; const NON_DOT_FOLDERS_TO_EXCLUDE: string[] = ['node_modules']; @@ -15,6 +16,7 @@ export class Workspace { private readonly workspaceId: string; private readonly rawAbsFilesAndFolders: string[]; private readonly rawAbsTargets?: string[]; + private readonly ignorePatterns: string[]; private cachedRawFilesAndFolders?: string[]; private cachedRawTargets?: string[]; @@ -23,6 +25,7 @@ export class Workspace { private cachedTargetedFiles?: string[]; private workspaceRoot?: string | null; + private cachedIgnoreMatchers?: Minimatch[]; /** * Creates a {@link Workspace} instance associated with a specified list of files and folders. @@ -40,11 +43,13 @@ export class Workspace { * @param workspaceId Optional workspace identifier * @param absFilesAndFolders Absolute file and folder paths that make up the workspace * @param absTargets optional string array of files and/or folders + * @param ignorePatterns optional array of glob patterns for files to ignore during scanning */ - constructor(workspaceId: string, absFilesAndFolders: string[], absTargets?: string[]) { + constructor(workspaceId: string, absFilesAndFolders: string[], absTargets?: string[], ignorePatterns: string[] = []) { this.workspaceId = workspaceId; this.rawAbsFilesAndFolders = absFilesAndFolders; this.rawAbsTargets = absTargets; + this.ignorePatterns = ignorePatterns; } /** @@ -110,14 +115,25 @@ export class Workspace { * Any files underneath the workspace root that Code Analyzer chooses to ignore (like .gitignore files, files in * node_modules folders, etc.) are automatically excluded unless they were explicitly provided when constructing * the workspace. + * + * Note: User-specified ignore patterns are NOT applied to workspace files. This allows engines like SFGE to build + * a complete graph of the codebase while still respecting ignore patterns for targeted files (violations). */ async getWorkspaceFiles(): Promise { if (!this.cachedWorkspaceFiles) { - this.cachedWorkspaceFiles = (await expandToListAllFiles(this.getRawFilesAndFolders())).filter(f => !this.shouldExclude(f)); + this.cachedWorkspaceFiles = (await expandToListAllFiles(this.getRawFilesAndFolders())).filter(f => !this.shouldExcludeFromWorkspace(f)); } return this.cachedWorkspaceFiles; } + /** + * Returns whether a path should be excluded from workspace files. + * This only checks built-in exclusions (node_modules, dot files, etc.) and NOT user-specified ignore patterns. + */ + private shouldExcludeFromWorkspace(fileOrFolder: string): boolean { + return this.isExcludeCandidate(fileOrFolder) && !this.excludeCandidateWasExplicitlyProvided(fileOrFolder); + } + /** * The list of files that an engine should target in its analysis. * @@ -135,11 +151,14 @@ export class Workspace { * the workspace. */ async getTargetedFiles(): Promise { - if (!this.getRawTargets()) { - return await this.getWorkspaceFiles(); - } if (!this.cachedTargetedFiles) { - this.cachedTargetedFiles = (await expandToListAllFiles(this.getRawTargets()!)).filter(f => !this.shouldExclude(f)); + if (!this.getRawTargets()) { + // When no explicit targets, use workspace files but apply ignore patterns + const workspaceFiles = await this.getWorkspaceFiles(); + this.cachedTargetedFiles = workspaceFiles.filter(f => !this.matchesIgnorePattern(f)); + } else { + this.cachedTargetedFiles = (await expandToListAllFiles(this.getRawTargets()!)).filter(f => !this.shouldExclude(f)); + } } return this.cachedTargetedFiles; } @@ -156,8 +175,38 @@ export class Workspace { * they choose to do so. */ private shouldExclude(fileOrFolder: string): boolean { + // Check user-specified ignore patterns first + if (this.matchesIgnorePattern(fileOrFolder)) { + return true; + } + // Then check built-in exclusions (node_modules, dot files, etc.) return this.isExcludeCandidate(fileOrFolder) && !this.excludeCandidateWasExplicitlyProvided(fileOrFolder); } + + /** + * Returns whether a file matches any of the user-specified ignore patterns. + * Patterns are matched against the file path relative to the workspace root. + */ + private matchesIgnorePattern(fileOrFolder: string): boolean { + if (this.ignorePatterns.length === 0) { + return false; + } + // Lazily compile matchers for performance + if (!this.cachedIgnoreMatchers) { + this.cachedIgnoreMatchers = this.ignorePatterns.map( + pattern => new Minimatch(pattern, { dot: true, matchBase: true }) + ); + } + // Get relative path for matching (without leading separator) + const relativePath = this.makeRelativeToWorkspaceRoot(fileOrFolder); + let pathToMatch = relativePath.startsWith(path.sep) ? relativePath.slice(1) : relativePath; + // Normalize to POSIX separators for cross-platform compatibility + if (path.sep !== '/') { + pathToMatch = pathToMatch.split(path.sep).join('/'); + } + + return this.cachedIgnoreMatchers.some(matcher => matcher.match(pathToMatch)); + } private isExcludeCandidate(fileOrFolder: string): boolean { const relativeFileOrFolder: string = this.makeRelativeToWorkspaceRoot(fileOrFolder); if (relativeFileOrFolder.length === 0) { // folder is equal to the workspace root diff --git a/packages/code-analyzer-engine-api/test/workspace.test.ts b/packages/code-analyzer-engine-api/test/workspace.test.ts index f1c06df2..49e8982f 100644 --- a/packages/code-analyzer-engine-api/test/workspace.test.ts +++ b/packages/code-analyzer-engine-api/test/workspace.test.ts @@ -444,6 +444,67 @@ describe('Tests for the Workspace class', () => { ].sort()); }); }); + + describe('Tests for ignorePatterns behavior', () => { + it('When ignorePatterns are provided, workspace files are NOT filtered by them', async () => { + // This allows engines like SFGE to build complete graphs + const workspace: Workspace = new Workspace('id', [SAMPLE_WORKSPACE_FOLDER], undefined, ['**/someFile.cls']); + const workspaceFiles = await workspace.getWorkspaceFiles(); + // someFile.cls should still be included in workspace files (ignore patterns don't apply to workspace files) + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')); + // Other .cls files should also be included + expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls')); + }); + + it('When ignorePatterns are provided with explicit targets, targeted files ARE filtered by them', async () => { + const workspace: Workspace = new Workspace('id', [SAMPLE_WORKSPACE_FOLDER], [SAMPLE_WORKSPACE_FOLDER], ['**/someFile.cls']); + const targetedFiles = await workspace.getTargetedFiles(); + // someFile.cls should be excluded from targeted files + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')); + // Other .cls files should still be included + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls')); + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.cls')); + }); + + it('When ignorePatterns are provided without explicit targets, targeted files ARE filtered by them', async () => { + // When no targets provided, getTargetedFiles returns workspace files filtered by ignore patterns + const workspace: Workspace = new Workspace('id', [SAMPLE_WORKSPACE_FOLDER], undefined, ['**/someFile.cls']); + const targetedFiles = await workspace.getTargetedFiles(); + // someFile.cls should be excluded from targeted files + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')); + // Other .cls files should still be included + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls')); + expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.cls')); + }); + + it('When ignorePatterns match all files, targeted files is empty but workspace files is not', async () => { + const workspace: Workspace = new Workspace('id', [SAMPLE_WORKSPACE_FOLDER], undefined, ['**/*']); + const workspaceFiles = await workspace.getWorkspaceFiles(); + const targetedFiles = await workspace.getTargetedFiles(); + // Workspace files should not be empty + expect(workspaceFiles.length).toBeGreaterThan(0); + // Targeted files should be empty (all filtered by ignore pattern) + expect(targetedFiles).toEqual([]); + }); + + it('When running on Windows (simulated), ignore patterns still work with backslash paths', async () => { + // Mock path.sep to simulate Windows - jest.replaceProperty auto-restores after test + jest.replaceProperty(path, 'sep', '\\'); + + // Create a workspace - note: actual file paths on disk still use native separators + // but this tests that our normalization logic handles backslashes correctly + const workspace: Workspace = new Workspace('id', [SAMPLE_WORKSPACE_FOLDER], undefined, ['**/someFile.cls']); + + // The pattern uses forward slashes (standard glob syntax) + // On Windows, file paths would have backslashes + // Our fix should normalize them before matching + const targetedFiles = await workspace.getTargetedFiles(); + + // someFile.cls should be excluded even with Windows path separators + // Note: On non-Windows, this test verifies the normalization code path doesn't break anything + expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')); + }); + }); }); function getAbsoluteRootFolder(): string { diff --git a/packages/code-analyzer-eslint-engine/package.json b/packages/code-analyzer-eslint-engine/package.json index d8679128..f093949f 100644 --- a/packages/code-analyzer-eslint-engine/package.json +++ b/packages/code-analyzer-eslint-engine/package.json @@ -18,8 +18,8 @@ "@lwc/eslint-plugin-lwc": "^3.3.0", "@lwc/eslint-plugin-lwc-platform": "^6.3.0", "@salesforce-ux/eslint-plugin-slds": "^1.1.0", - "@salesforce/code-analyzer-engine-api": "0.34.0", - "@salesforce/code-analyzer-eslint8-engine": "0.11.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", + "@salesforce/code-analyzer-eslint8-engine": "0.12.0-SNAPSHOT", "@salesforce/eslint-config-lwc": "^4.1.2", "@salesforce/eslint-plugin-lightning": "^2.0.0", "@types/node": "^20.0.0", diff --git a/packages/code-analyzer-eslint8-engine/package.json b/packages/code-analyzer-eslint8-engine/package.json index 8d71d9ae..57a5c347 100644 --- a/packages/code-analyzer-eslint8-engine/package.json +++ b/packages/code-analyzer-eslint8-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-eslint8-engine", "description": "Plugin package that adds 'eslint' (version 8) as an engine into Salesforce Code Analyzer", - "version": "0.11.0", + "version": "0.12.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -18,7 +18,7 @@ "@eslint/js": "8.57.1", "@lwc/eslint-plugin-lwc": "2.2.0", "@lwc/eslint-plugin-lwc-platform": "5.2.0", - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@salesforce/eslint-config-lwc": "3.7.2", "@salesforce/eslint-plugin-lightning": "1.0.1", "@types/node": "^20.0.0", diff --git a/packages/code-analyzer-flow-engine/package.json b/packages/code-analyzer-flow-engine/package.json index b54b966f..5c7e28e6 100644 --- a/packages/code-analyzer-flow-engine/package.json +++ b/packages/code-analyzer-flow-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-flow-engine", "description": "Plugin package that adds 'Flow Scanner' as an engine into Salesforce Code Analyzer", - "version": "0.33.0", + "version": "0.34.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "@types/semver": "^7.7.1", "semver": "^7.7.3" diff --git a/packages/code-analyzer-pmd-engine/package.json b/packages/code-analyzer-pmd-engine/package.json index 331a98cb..2b8a47ec 100644 --- a/packages/code-analyzer-pmd-engine/package.json +++ b/packages/code-analyzer-pmd-engine/package.json @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "@types/semver": "^7.7.1", "semver": "^7.7.3" diff --git a/packages/code-analyzer-regex-engine/package.json b/packages/code-analyzer-regex-engine/package.json index f7b53cef..c9fd2982 100644 --- a/packages/code-analyzer-regex-engine/package.json +++ b/packages/code-analyzer-regex-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-regex-engine", "description": "Plugin package that adds 'regex' as an engine into Salesforce Code Analyzer", - "version": "0.32.0", + "version": "0.33.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "isbinaryfile": "^5.0.0", "p-limit": "^3.1.0" diff --git a/packages/code-analyzer-retirejs-engine/package.json b/packages/code-analyzer-retirejs-engine/package.json index 017df96d..c8bd095c 100644 --- a/packages/code-analyzer-retirejs-engine/package.json +++ b/packages/code-analyzer-retirejs-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-retirejs-engine", "description": "Plugin package that adds 'retire-js' as an engine into Salesforce Code Analyzer", - "version": "0.31.0", + "version": "0.32.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "isbinaryfile": "^5.0.0", "node-stream-zip": "^1.15.0", diff --git a/packages/code-analyzer-sfge-engine/package.json b/packages/code-analyzer-sfge-engine/package.json index bf38d219..30a2b3ef 100644 --- a/packages/code-analyzer-sfge-engine/package.json +++ b/packages/code-analyzer-sfge-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-sfge-engine", "description": "Plugin package that adds 'Salesforce Graph Engine' as an engine into Salesforce Code Analyzer", - "version": "0.17.0", + "version": "0.18.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.34.0", + "@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT", "@types/node": "^20.0.0", "semver": "^7.7.3" }, diff --git a/packages/code-analyzer-sfge-engine/src/engine.ts b/packages/code-analyzer-sfge-engine/src/engine.ts index ac3d3a04..9c46bc7b 100644 --- a/packages/code-analyzer-sfge-engine/src/engine.ts +++ b/packages/code-analyzer-sfge-engine/src/engine.ts @@ -68,6 +68,13 @@ export class SfgeEngine extends Engine { return { violations: [] }; } + // Get targeted files and return early if empty - prevents SFGE from analyzing all workspace files + const targetedFiles: string[] = await runOptions.workspace.getTargetedFiles(); + if (targetedFiles.length === 0) { + this.emitRunRulesProgressEvent(100); + return { violations: [] }; + } + await this.validateWorkspaceCompleteness(runOptions.workspace); const allRulesInfoList: SfgeRuleInfo[] = await this.getSfgeRuleInfoList( @@ -95,7 +102,7 @@ export class SfgeEngine extends Engine { const sfgeResults: SfgeRunResult[] = await this.sfgeWrapper.invokeRunCommand( selectedRuleInfoList, - await runOptions.workspace.getTargetedFiles(), + targetedFiles, relevantWorkspaceFiles, sfgeRunOptions, runOptions.workingFolder, diff --git a/packages/code-analyzer-sfge-engine/test/engine.test.ts b/packages/code-analyzer-sfge-engine/test/engine.test.ts index d92c9396..685b58d6 100644 --- a/packages/code-analyzer-sfge-engine/test/engine.test.ts +++ b/packages/code-analyzer-sfge-engine/test/engine.test.ts @@ -133,6 +133,37 @@ describe('SfgeEngine', () => { expect(progressEvents.map(e => e.percentComplete)).toEqual([2, 100]); }); + it('When targets are empty, no violations are returned and SFGE is not invoked', async () => { + // This test verifies that when the workspace contains .cls files but targets are empty + // (e.g., after being filtered out by ignores), SFGE returns early without invoking + // the Java process. This prevents SFGE from scanning all workspace files when + // no targets are provided. + const engine: SfgeEngine = new SfgeEngine(DEFAULT_SFGE_ENGINE_CONFIG, fixedClock); + // Workspace folder has .cls files, but we pass empty targets + const workspace: Workspace = new Workspace( + 'id', + [path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace')], + [] // Empty targets - simulates all targets being filtered out by ignores + ); + const logEvents: LogEvent[] = []; + engine.onEvent(EventType.LogEvent, (e: LogEvent) => logEvents.push(e)); + const progressEvents: RunRulesProgressEvent[] = []; + engine.onEvent(EventType.RunRulesProgressEvent, (e: RunRulesProgressEvent) => progressEvents.push(e)); + const ruleNames: string[] = ['ApexFlsViolation']; + + // ====== TESTED BEHAVIOR ====== + const results: EngineRunResults = await engine.runRules(ruleNames, createRunOptions(workspace)); + + // ====== ASSERTIONS ====== + // No violations should be returned + expect(results.violations).toHaveLength(0); + // SFGE should not be invoked, so no log events about calling Java commands + expect(logEvents).toHaveLength(0); + // The progress events should skip from the very first one (2%) to the very last one (100%) + // This confirms we returned early without running SFGE + expect(progressEvents.map(e => e.percentComplete)).toEqual([2, 100]); + }); + it.each([ {case: 'a folder with relevant files that do not violate the selected rules', workspacePaths: [path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace')]}, {