diff --git a/cli/package-lock.json b/cli/package-lock.json index e731ebc..aca7275 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ibm/sourceorbit", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ibm/sourceorbit", - "version": "1.1.0", + "version": "1.1.1", "license": "Apache 2", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", @@ -1430,9 +1430,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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, "dependencies": { "balanced-match": "^1.0.0", @@ -3731,6 +3731,48 @@ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", @@ -3903,14 +3945,17 @@ } }, "node_modules/vite": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz", - "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -3995,6 +4040,32 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.8.tgz", diff --git a/cli/src/languages/rpgle.ts b/cli/src/languages/rpgle.ts index c2a3b62..67f50ff 100644 --- a/cli/src/languages/rpgle.ts +++ b/cli/src/languages/rpgle.ts @@ -26,7 +26,7 @@ export function setupParser(targets: Targets): Parser { // Keep making the path less specific until we find a possible include let parts = includeFile.split(`/`); while (!file && parts.length > 0) { - file = targets.resolveLocalFile(includeFile); + file = await targets.resolveLocalFile(includeFile); if (!file) { parts.shift(); @@ -39,9 +39,9 @@ export function setupParser(targets: Targets): Parser { includeFile = `${parent}/${includeFile}`; - file = targets.resolveLocalFile(includeFile); + file = await targets.resolveLocalFile(includeFile); } else { - file = targets.resolveLocalFile(includeFile); + file = await targets.resolveLocalFile(includeFile); } if (file) { diff --git a/cli/src/targets.ts b/cli/src/targets.ts index 831d3d1..57e3be8 100644 --- a/cli/src/targets.ts +++ b/cli/src/targets.ts @@ -1,4 +1,3 @@ -import glob from 'glob'; import path from 'path'; import Cache from "vscode-rpgle/language/models/cache"; import { IncludeStatement } from "vscode-rpgle/language/parserTypes"; @@ -11,7 +10,7 @@ import { rpgExtensions, clExtensions, ddsExtension, sqlExtensions, srvPgmExtensi import Parser from "vscode-rpgle/language/parser"; import { setupParser } from './languages/rpgle'; import { Logger } from './logger'; -import { asPosix, getReferenceObjectsFrom, getSystemNameFromPath, toLocalPath } from './utils'; +import { asPosix, getReferenceObjectsFrom, getSystemNameFromPath, globalEntryIsValid, toLocalPath } from './utils'; import { extCanBeProgram, getObjectType } from './builders/environment'; import { isSqlFunction } from './languages/sql'; import { ReadFileSystem } from './readFileSystem'; @@ -294,40 +293,43 @@ export class Targets { return this.getResolvedObjects().find(o => (o.systemName === lookFor.name || o.longName?.toUpperCase() === lookFor.name) && (lookFor.types === undefined || lookFor.types.includes(o.type))); } - public resolveLocalFile(name: string, baseFile?: string): string | undefined { + public async resolveLocalFile(name: string, baseFile?: string): Promise { name = name.toUpperCase(); if (this.resolvedSearches[name]) return this.resolvedSearches[name]; if (!this.pathCache) { - // We don't really want to spam the FS - // So we can a list of files which can then - // use in glob again later. this.pathCache = {}; - glob.sync(`**/*`, { + (await this.fs.getFiles(this.getCwd(), `**/*`, { cwd: this.cwd, absolute: true, nocase: true, - }).forEach(localPath => { + })).forEach(localPath => { this.pathCache[localPath] = true; }); } - let globString = `**/${name}*`; + const searchCache = (): string|undefined => { + for (let entry in this.pathCache) { + if (Array.isArray(this.pathCache[entry])) { + const subEntry = this.pathCache[entry].find(e => globalEntryIsValid(e, name)); + if (subEntry) { + return subEntry; + } + } else { + if (globalEntryIsValid(entry, name)) { + return entry; + } + } + } + } - // TODO: replace with rfs.getFiles - const results = glob.sync(globString, { - cwd: this.cwd, - absolute: true, - nocase: true, - ignore: baseFile ? `**/${baseFile}` : undefined, - cache: this.pathCache - }); + const result = searchCache(); - if (results[0]) { + if (result) { // To local path is required because glob returns posix paths - const localPath = toLocalPath(results[0]) + const localPath = toLocalPath(result) this.resolvedSearches[name] = localPath; return localPath; } diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 6bdd1ef..cb4e192 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -172,7 +172,7 @@ export function getReferenceObjectsFrom(content: string) { return pseudoObjects; } -export function fromCl(cl: string): {command: string, parameters: CommandParameters} { +export function fromCl(cl: string): { command: string, parameters: CommandParameters } { let gotCommandnName = false; let parmDepth = 0; @@ -253,7 +253,59 @@ export function toCl(command: string, parameters?: CommandParameters) { } export function checkFileExists(file) { - return fs.promises.access(file, fs.constants.F_OK) + return fs.promises.access(file, fs.constants.F_OK) .then(() => true) .catch(() => false) +} + +export function globalEntryIsValid(fullPath: string, search: string, ignoreBase?: string) { + search = search.toUpperCase(); + + if (ignoreBase) { + if (fullPath.toUpperCase().endsWith(ignoreBase.toUpperCase())) { + return false; + } + } + + const baseParts = fullPath.toUpperCase().split(path.sep); + const nameParts = search.split(path.sep); + + // Check the preceding parts of the path match + if (nameParts.length > 1) { + for (let i = 1; i < nameParts.length - 1; i++) { + const bPart = baseParts[baseParts.length - i]; + const nPart = nameParts[nameParts.length - i - 1]; + + if (bPart && nPart) { + if (bPart !== nPart) return false; + } + } + } + + const baseName = nameParts[nameParts.length - 1]; + + // Check different variations of the name. + + if (baseName.endsWith('*')) { + // If the name ends with *, we do a startsWith check + const tempName = search.substring(0, search.length - 1); + const lastChar = tempName.charAt(tempName.length - 1); + const messedUpFullName = fullPath.lastIndexOf(lastChar) >= 0 ? fullPath.substring(0, fullPath.lastIndexOf(lastChar) + 1) : fullPath; + if (messedUpFullName.toUpperCase().endsWith(tempName)) return true; + } else if (!baseName.includes('.')) { + // If the name does not include a dot, we check if the current part (without extension) matches + let currentPart = baseParts[baseParts.length - 1]; + if (currentPart.includes('.')) { + currentPart = currentPart.substring(0, currentPart.indexOf('.')); + } + + if (currentPart === baseName) { + return true; + } + + } else if (baseParts[baseParts.length - 1] === baseName) { + return true; + } + + return false; } \ No newline at end of file diff --git a/cli/test/autofix.test.ts b/cli/test/autofix.test.ts index 06e2c8e..daf6d9a 100644 --- a/cli/test/autofix.test.ts +++ b/cli/test/autofix.test.ts @@ -168,6 +168,8 @@ test(`Fix includes in same directory`, async () => { const programs = targets.getTargetsOfType(`PGM`); expect(programs.length).toBe(1); + expect(programs[0].headers.length).toBe(1); + let allLogs = targets.logger.getAllLogs(); expect(Object.keys(allLogs).length).toBe(1)