diff --git a/cli/.vscode/launch.json b/cli/.vscode/launch.json index 44e9b98..fe34933 100644 --- a/cli/.vscode/launch.json +++ b/cli/.vscode/launch.json @@ -24,7 +24,7 @@ "cwd": "${workspaceFolder:cli}", "program": "${workspaceFolder:cli}/dist/index.js", "sourceMaps": true, - "args": ["-d", "/Users/barry/Repos/ibmi-company_system", "--verbose", "--mcp", "5500"], + "args": ["-d", "/Users/barry/Repos/ibmi-company_system", "--verbose", "-bf", "make"], "preLaunchTask": { "type": "npm", "script": "webpack:dev" diff --git a/cli/src/builders/environment.ts b/cli/src/builders/environment.ts index 27a92f5..bbec3e6 100644 --- a/cli/src/builders/environment.ts +++ b/cli/src/builders/environment.ts @@ -40,10 +40,6 @@ export function getBranchLibraryName(currentBranch: string) { return `VS${(str(currentBranch, 0) >>> 0).toString(16).toUpperCase()}`; } -export function extCanBeProgram(ext: string): boolean { - return ([`MODULE`, `PGM`].includes(getObjectType(ext))); -} - export function getTrueBasename(name: string) { // Logic to handle second extension, caused by bob. const sourceObjectTypes = [`.PGM`, `.SRVPGM`, `.TEST`]; @@ -60,55 +56,6 @@ export function getTrueBasename(name: string) { return name; } -export function getObjectType(ext: string): ObjectType { - switch (ext.toLowerCase()) { - case `dspf`: - case `prtf`: - case `pf`: - case `lf`: - case `sql`: - case `table`: - case `view`: - case `index`: - case `alias`: - case `sqludf`: - case `sqludt`: - case `sqlalias`: - case `sqlseq`: - case `sequence`: - case `msgf`: - return "FILE"; - - case `dtaara`: - return "DTAARA"; - - case `cmd`: - return "CMD"; - - case `rpgle`: - case `sqlrpgle`: - case `clle`: - case `cl`: - return "MODULE"; - - case `binder`: - case `bnd`: - case `function`: - return `SRVPGM`; - - case `procedure`: - case `trigger`: - case `sqlprc`: - case `sqltrg`: - return `PGM`; - - case `bnddir`: - return `BNDDIR`; - } - - return undefined; -} - export function getDefaultCompiles(): CompileAttribute { const binderSourceCompile: CompileData = { becomes: `SRVPGM`, diff --git a/cli/src/extensions.ts b/cli/src/extensions.ts index 2b78aa4..b4706db 100644 --- a/cli/src/extensions.ts +++ b/cli/src/extensions.ts @@ -1,13 +1,2 @@ -export const rpgExtensions = [`sqlrpgle`, `rpgle`]; -export const clExtensions = [`clle`, `cl`, `clp`]; -export const ddsExtension = [`pf`, `lf`, `dspf`, `prtf`]; -export const sqlExtensions = [`sql`, `table`, `view`, `index`, `alias`, `sqlprc`, `sqludf`, `sqludt`, `sqltrg`, `sqlalias`, `sqlseq`]; -export const srvPgmExtensions = [`binder`, `bnd`]; -export const cmdExtensions = [`cmd`]; -export const objectExtensions = [`dtaara`, `mnucmd`, `msgf`, `dtaq`, `bnddir`]; - -export const allExtensions = [...rpgExtensions, ...clExtensions, ...ddsExtension, ...sqlExtensions, ...srvPgmExtensions, ...cmdExtensions, ...objectExtensions]; -export const scanGlob = `**/*.{${allExtensions.join(`,`)},${allExtensions.map(e => e.toUpperCase()).join(`,`)}}`; - export const referencesFileName = `.objrefs`; \ No newline at end of file diff --git a/cli/src/index.ts b/cli/src/index.ts index 0aa24b6..bb63507 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -8,7 +8,7 @@ import path from 'path'; import { BuildFiles, cliSettings, error, infoOut, warningOut } from './cli'; import { BobProject } from "./builders/bob"; import { ImpactMarkdown } from "./builders/imd"; -import { allExtensions, referencesFileName } from "./extensions"; +import { referencesFileName } from "./extensions"; import { getBranchLibraryName } from "./builders/environment"; import { renameFiles, replaceIncludes } from './utils'; import { ReadFileSystem } from './readFileSystem'; @@ -23,7 +23,7 @@ if (isCli || process.env.VSCODE_INSPECTOR_OPTIONS) { async function main() { const parms = process.argv.slice(2); let cwd = process.cwd(); - let scanGlob = `**/*.{${allExtensions.join(`,`)},${allExtensions.map(e => e.toUpperCase()).join(`,`)}}`; + let scanGlob: string|undefined = undefined; for (let i = 0; i < parms.length; i++) { switch (parms[i]) { @@ -162,6 +162,10 @@ async function main() { let files: string[]; + if (!scanGlob) { + scanGlob = targets.getSearchGlob(); + } + try { files = await fs.getFiles(cwd, scanGlob); } catch (e) { @@ -295,6 +299,5 @@ async function listDeps(cwd: string, targets: Targets, query: string) { export { Targets } from './targets'; export { MakeProject } from './builders/make'; export { BobProject } from "./builders/bob"; -export { ImpactMarkdown } from "./builders/imd" -export { allExtensions } from "./extensions"; +export { ImpactMarkdown } from "./builders/imd" export * as Utils from './utils'; \ No newline at end of file diff --git a/cli/src/languages/rpgle.ts b/cli/src/languages/rpgle.ts deleted file mode 100644 index 362d7ae..0000000 --- a/cli/src/languages/rpgle.ts +++ /dev/null @@ -1,78 +0,0 @@ - -import { readFileSync } from 'fs'; -import * as path from 'path'; - -import Parser from "vscode-rpgle/language/parser"; -import { Targets } from '../targets'; - -let includeFileCache: { [path: string]: string } = {}; - -export function setupParser(targets: Targets): Parser { - const parser = new Parser(); - - parser.setIncludeFileFetch(async (baseFile: string, includeFile: string) => { - if (includeFile.startsWith(`'`) && includeFile.endsWith(`'`)) { - includeFile = includeFile.substring(1, includeFile.length - 1); - } - - let file: string; - - if (includeFile.includes(`,`)) { - // If the member include path is qualified with a source file - // then we should convert to be a unix style path so we can - // search the explicit directories. - includeFile = includeFile.replace(/,/g, `/`) + `.*`; - - // Keep making the path less specific until we find a possible include - let parts = includeFile.split(`/`); - while (!file && parts.length > 0) { - file = await targets.resolveLocalFile(includeFile); - - if (!file) { - parts.shift(); - includeFile = parts.join(`/`); - } - } - } else if (!includeFile.includes(`/`)) { - const parent = path.basename(path.dirname(baseFile)); - console.log(parent); - includeFile = `${parent}/${includeFile}`; - - - file = await targets.resolveLocalFile(includeFile); - } else { - file = await targets.resolveLocalFile(includeFile); - } - - if (file) { - if (includeFileCache[file]) { - return { - found: true, - uri: file, - content: includeFileCache[file] - } - - } else { - const content = await targets.rfs.readFile(file); - includeFileCache[file] = content; - - return { - found: true, - uri: file, - content: content - } - } - } - - return { - found: false - }; - }); - - parser.setTableFetch(async (table: string, aliases = false) => { - // Can't support tables in CLI mode I suppose? - return []; - }); - - return parser; -} \ No newline at end of file diff --git a/cli/src/readFileSystem.ts b/cli/src/readFileSystem.ts index 647bf8d..49cc1ac 100644 --- a/cli/src/readFileSystem.ts +++ b/cli/src/readFileSystem.ts @@ -4,12 +4,11 @@ import glob from "glob"; import path from 'path'; import os from 'os'; import { getFiles } from './utils'; -import { scanGlob } from './extensions'; export class ReadFileSystem { constructor() {} - async getFiles(cwd: string, globPath = scanGlob, additionalOpts: any = {}): Promise { + async getFiles(cwd: string, globPath, additionalOpts: any = {}): Promise { return getFiles(cwd, globPath, additionalOpts); } diff --git a/cli/src/targets.ts b/cli/src/targets.ts deleted file mode 100644 index 4509aec..0000000 --- a/cli/src/targets.ts +++ /dev/null @@ -1,1703 +0,0 @@ -import path from 'path'; -import Cache from "vscode-rpgle/language/models/cache"; -import { IncludeStatement } from "vscode-rpgle/language/parserTypes"; -import { infoOut, warningOut } from './cli'; -import { DefinitionType, File, Module, CLParser } from 'vscode-clle/language'; -import { DisplayFile as dds } from "vscode-displayfile/src/dspf"; -import Document from "vscode-db2i/src/language/sql/document"; -import { ObjectRef, StatementType } from 'vscode-db2i/src/language/sql/types'; -import { rpgExtensions, clExtensions, ddsExtension, sqlExtensions, srvPgmExtensions, cmdExtensions } from './extensions'; -import Parser from "vscode-rpgle/language/parser"; -import { setupParser } from './languages/rpgle'; -import { Logger } from './logger'; -import { asPosix, getReferenceObjectsFrom, getSystemNameFromPath, globalEntryIsValid, toLocalPath } from './utils'; -import { extCanBeProgram, getObjectType } from './builders/environment'; -import { isSqlFunction } from './languages/sql'; -import { ReadFileSystem } from './readFileSystem'; - -export type ObjectType = "PGM" | "SRVPGM" | "MODULE" | "FILE" | "BNDDIR" | "DTAARA" | "CMD" | "MENU" | "DTAQ"; - -const ignoredObjects = [`QSYSPRT`, `QCMDEXC`, `*LDA.DTAARA`, `QDCXLATE`, `QUSRJOBI`, `QTQCVRT`, `QWCRDTAA`, `QUSROBJD`, `QUSRMBRD`, `QUSROBJD`, `QUSLOBJ`, `QUSRTVUS`, `QUSCRTUS`]; - -const sqlTypeExtension = { - 'TABLE': `table`, - 'VIEW': `view`, - 'PROCEDURE': `sqlprc`, - 'FUNCTION': `sqludf`, - 'TRIGGER': `sqltrg`, - 'ALIAS': `sqlalias`, - 'SEQUENCE': `sqlseq` -}; - -const DEFAULT_BINDER_TARGET: ILEObject = { systemName: `$(APP_BNDDIR)`, type: `BNDDIR` }; - -const TextRegex = /\%TEXT.*(?=\n|\*)/gm - -export interface ILEObject { - systemName: string; - longName?: string; - type: ObjectType; - text?: string, - relativePath?: string; - extension?: string; - - reference?: boolean; - - /** exported functions */ - exports?: string[]; - /** each function import in the object */ - imports?: string[]; - - /** headers. only supports RPGLE and is not recursive */ - headers?: string[]; -} - -export interface ILEObjectTarget extends ILEObject { - deps: ILEObject[]; -} - -export interface TargetSuggestions { - renames?: boolean; - includes?: boolean; -} - -export interface ImpactedObject { - ileObject: ILEObject, - children: ImpactedObject[] -} - -interface RpgLookup { - lookup: string, - line?: number -} - -interface FileOptions { - isFree?: boolean; - text?: string; -} - -/** - * This class is responsible for storing all the targets - * and their dependencies. It also handles the parsing - * of files and the creation of targets. - * - * const files = getAllFilesInDir(`.`); - * const targets = new Targets(cwd); - * targets.handlePseudoFile(pseudoFilePath); - * targets.loadObjectsFromPaths(files); - * await Promise.all(files.map(f => targets.parseFile(f))); - * targets.resolveBinder(); - */ - -export class Targets { - private rpgParser: Parser; - - /* pathCache and resolvedSearches are used for file resolving. */ - private pathCache: { [path: string]: true | string[] } | undefined; - private resolvedSearches: { [query: string]: string } = {}; - - private assumePrograms = false; - - private resolvedObjects: { [localPath: string]: ILEObject } = {}; - private resolvedExports: { [name: string]: ILEObject } = {}; - private targets: { [name: string]: ILEObjectTarget } = {}; - - private needsBinder = false; - private projectBindingDirectory = DEFAULT_BINDER_TARGET; - - private suggestions: TargetSuggestions = {}; - - public logger: Logger; - - constructor(private cwd: string, private fs: ReadFileSystem) { - this.rpgParser = setupParser(this); - this.logger = new Logger(); - } - - public getCwd() { - return this.cwd; - } - - get rfs() { - return this.fs; - } - - public setAssumePrograms(assumePrograms: boolean) { - this.assumePrograms = assumePrograms; - } - - public setSuggestions(newSuggestions: TargetSuggestions) { - this.suggestions = newSuggestions; - } - - public getBinderTarget() { - return this.projectBindingDirectory; - } - - public getRelative(fullPath: string) { - return path.relative(this.cwd, fullPath); - } - - private storeResolved(localPath: string, ileObject: ILEObject) { - this.resolvedObjects[localPath] = ileObject; - } - - public async loadProject(withRef?: string) { - if (withRef) { - await this.handleRefsFile(path.join(this.cwd, withRef)); - } - - const initialFiles = await this.fs.getFiles(this.cwd); - await this.loadObjectsFromPaths(initialFiles); - await Promise.allSettled(initialFiles.map(f => this.parseFile(f))); - } - - public async resolvePathToObject(localPath: string, newText?: string) { - if (this.resolvedObjects[localPath]) { - if (newText) this.resolvedObjects[localPath].text = newText; - return this.resolvedObjects[localPath]; - } - - const detail = path.parse(localPath); - const relativePath = this.getRelative(localPath); - - const extension = detail.ext.length > 1 ? detail.ext.substring(1) : detail.ext; - const hasProgramAttribute = detail.name.toUpperCase().endsWith(`.PGM`); - const isProgram = this.assumePrograms ? extCanBeProgram(extension) : hasProgramAttribute; - const name = getSystemNameFromPath(hasProgramAttribute ? detail.name.substring(0, detail.name.length - 4) : detail.name); - const type: ObjectType = (isProgram ? "PGM" : this.getObjectType(relativePath, extension)); - - const theObject: ILEObject = { - systemName: name, - type: type, - text: newText, - relativePath, - extension - }; - - // If this file is an SQL file, we need to look to see if it has a long name as we need to resolve all names here - if (sqlExtensions.includes(extension.toLowerCase())) { - const ref = await this.sqlObjectDataFromPath(localPath); - if (ref) { - if (ref.object.system) theObject.systemName = ref.object.system.toUpperCase(); - if (ref.object.name) theObject.longName = ref.object.name; - // theObject.type = ref.type; - } - } - - if (type === `BNDDIR`) { - this.projectBindingDirectory = theObject; - } - - // This allows us to override the .objrefs if the source actually exists. - if (this.isReferenceObject(theObject, true)) { - this.logger.fileLog(relativePath, { - type: `info`, - message: `The object ${theObject.systemName}.${theObject.type} is defined in the references file even though the source exists for it.` - }); - } - - this.storeResolved(localPath, theObject); - - return theObject; - } - - /** - * This can be expensive. It should only be called: - * before loadObjectsFromPaths and parseFile are called. - * @param filePath Fully qualified path to the file. Assumed to exist. - */ - public async handleRefsFile(filePath: string) { - const content = await this.fs.readFile(filePath); - - const pseudoObjects = getReferenceObjectsFrom(content); - - for (const ileObject of pseudoObjects) { - if (!this.searchForObject(ileObject)) { - const key = `${ileObject.systemName}.${ileObject.type}`; - ileObject.reference = true; - this.resolvedObjects[key] = ileObject; - } - }; - } - - public isReferenceObject(ileObject: ILEObject, remove?: boolean) { - const key = `${ileObject.systemName}.${ileObject.type}`; - const existing = this.resolvedObjects[key]; - const isRef = Boolean(existing && existing.reference); - - if (isRef && remove) { - this.resolvedObjects[key] = undefined; - } - - return isRef; - } - - public removeObjectByPath(localPath: string) { - const resolvedObject = this.resolvedObjects[localPath]; - - if (resolvedObject) { - // First, delete the simple caches - this.resolvedObjects[localPath] = undefined; - - return this.removeObject(resolvedObject); - } - - return [] - } - - public removeObject(resolvedObject: ILEObject) { - let impactedTargets: ILEObject[] = []; - - for (const targetId in this.targets) { - const target = this.targets[targetId]; - - if (target) { - const depIndex = target.deps.findIndex(d => (d.systemName === resolvedObject.systemName && d.type === resolvedObject.type) || d.relativePath === resolvedObject.relativePath); - - if (depIndex >= 0) { - impactedTargets.push(target); - target.deps.splice(depIndex, 1); - - if (target.relativePath) { - this.logger.fileLog(target.relativePath, { - type: `info`, - message: `This object depended on ${resolvedObject.systemName}.${resolvedObject.type} before it was deleted.` - }) - } - } - } - } - - // Remove it as a global target - this.targets[`${resolvedObject.systemName}.${resolvedObject.type}`] = undefined; - this.resolvedSearches[`${resolvedObject.systemName}.${resolvedObject.type}`] = undefined; - - // Remove possible logs - if (resolvedObject.relativePath) { - this.logger.flush(resolvedObject.relativePath) - } - - return impactedTargets; - } - - /** - * Resolves a search to an object. Use `systemName` parameter for short and long name. - */ - public searchForObject(lookFor: ILEObject) { - return this.getResolvedObjects().find(o => (lookFor.systemName === o.systemName || (o.longName && lookFor.systemName === o.longName)) && o.type === lookFor.type); - } - - public searchForAnyObject(lookFor: { name: string, types?: ObjectType[] }) { - lookFor.name = lookFor.name.toUpperCase(); - return this.getResolvedObjects().find(o => (o.systemName === lookFor.name || o.longName?.toUpperCase() === lookFor.name) && (lookFor.types === undefined || lookFor.types.includes(o.type))); - } - - public async resolveLocalFile(name: string, baseFile?: string): Promise { - name = name.toUpperCase(); - - if (this.resolvedSearches[name]) return this.resolvedSearches[name]; - - if (!this.pathCache) { - this.pathCache = {}; - - (await this.fs.getFiles(this.getCwd(), `**/*`, { - cwd: this.cwd, - absolute: true, - nocase: true, - })).forEach(localPath => { - this.pathCache[localPath] = true; - }); - } - - 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; - } - } - } - } - - const result = searchCache(); - - if (result) { - // To local path is required because glob returns posix paths - const localPath = toLocalPath(result) - this.resolvedSearches[name] = localPath; - return localPath; - } - } - - private getObjectType(relativePath: string, ext: string): ObjectType { - const objType = getObjectType(ext); - - if (!objType) { - this.logger.fileLog(relativePath, { - type: `warning`, - message: `'${ext}' not found a matching object type. Defaulting to '${ext}'` - }); - - return (ext.toUpperCase() as ObjectType); - } - - return objType; - } - - public loadObjectsFromPaths(paths: string[]) { - // optimiseFileList(paths); //Ensure we load SQL files first - return Promise.all(paths.map(p => this.resolvePathToObject(p))); - } - - public async parseFile(filePath: string) { - const pathDetail = path.parse(filePath); - const relative = this.getRelative(filePath); - - let success = true; - - if (pathDetail.ext.length > 1) { - if (!this.suggestions.renames) { - // Don't clear the logs if we're suggestion renames. - this.logger.flush(relative); - } - - const ext = pathDetail.ext.substring(1).toLowerCase(); - - try { - const content = await this.fs.readFile(filePath); - const eol = content.indexOf(`\r\n`) >= 0 ? `\r\n` : `\n`; - - // Really only applied to rpg - const isFree = (content.length >= 6 ? content.substring(0, 6).toLowerCase() === `**free` : false); - - let textMatch; - try { - [textMatch] = content.match(TextRegex); - if (textMatch) { - if (textMatch.startsWith(`%TEXT`)) textMatch = textMatch.substring(5); - if (textMatch.endsWith(`*`)) textMatch = textMatch.substring(0, textMatch.length - 1); - textMatch = textMatch.trim(); - } - } catch (e) { } - - const options: FileOptions = { - isFree, - text: textMatch - }; - - if (rpgExtensions.includes(ext)) { - const rpgDocs = await this.rpgParser.getDocs( - filePath, - content, - { - ignoreCache: true, - withIncludes: true - } - ); - - if (rpgDocs) { - const ileObject = await this.resolvePathToObject(filePath, options.text); - this.createRpgTarget(ileObject, filePath, rpgDocs, options); - } - - } - else if (clExtensions.includes(ext)) { - const clDocs = new CLParser(); - const tokens = clDocs.parseDocument(content); - - const module = new Module(); - module.parseStatements(tokens); - - const ileObject = await this.resolvePathToObject(filePath); - this.createClTarget(ileObject, filePath, module, options); - } - else if (ddsExtension.includes(ext)) { - const ddsFile = new dds(); - ddsFile.parse(content.split(eol)); - - const ileObject = await this.resolvePathToObject(filePath, options.text); - this.createDdsFileTarget(ileObject, filePath, ddsFile, options); - } - else if (sqlExtensions.includes(ext)) { - const sqlDoc = new Document(content); - this.createSqlTargets(filePath, sqlDoc, options); - } - else if (srvPgmExtensions.includes(ext)) { - const clDocs = new CLParser(); - const tokens = clDocs.parseDocument(content); - - const module = new Module(); - module.parseStatements(tokens); - - const ileObject = await this.resolvePathToObject(filePath, options.text); - this.createSrvPgmTarget(ileObject, filePath, module, options); - } - else if (cmdExtensions.includes(ext)) { - this.createCmdTarget(filePath, options); - } - } catch (e) { - this.logger.fileLog(relative, { - message: `Failed to parse file.`, - type: `warning` - }); - - console.log(relative); - console.log(e); - - success = false; - } - - infoOut(``); - } else { - success = false; - } - - return success; - - } - - private createCmdTarget(localPath, options: FileOptions = {}) { - this.resolvePathToObject(localPath, options.text); - - // Since cmd source doesn't explicity contains deps, we resolve later on - } - - private createSrvPgmTarget(ileObject: ILEObject, localPath: string, module: Module, options: FileOptions = {}) { - const target: ILEObjectTarget = { - ...ileObject, - deps: [], - exports: [] - }; - - if (ileObject.extension === `binder`) { - const pathDetail = path.parse(localPath); - - if (this.suggestions.renames) { - this.logger.fileLog(ileObject.relativePath, { - message: `Rename suggestion`, - type: `rename`, - change: { - rename: { - path: localPath, - newName: pathDetail.name + `.bnd` - } - } - }); - } else { - this.logger.fileLog(ileObject.relativePath, { - message: `Extension is '${ileObject.extension}'. Consolidate by using 'bnd'?`, - type: `warning`, - }); - } - } - - const validStatements = module.statements.filter(s => { - const possibleObject = s.getObject(); - return (possibleObject && possibleObject.name && [`STRPGMEXP`, `ENDPGMEXP`, `EXPORT`].includes(possibleObject.name.toUpperCase())); - }); - - for (const statement of validStatements) { - const currentCommand = statement.getObject().name.toUpperCase(); - if (currentCommand === `EXPORT`) { - const parms = statement.getParms(); - const symbolTokens = parms[`SYMBOL`]; - - if (symbolTokens.block && symbolTokens.block.length === 1 && symbolTokens.block[0].type === `string` && symbolTokens.block[0].value) { - target.exports.push(trimQuotes(symbolTokens.block[0].value)); - } else - if (symbolTokens.block && symbolTokens.block.length === 1 && symbolTokens.block[0].type === `word` && symbolTokens.block[0].value) { - target.exports.push(trimQuotes(symbolTokens.block[0].value, `"`)); - } else { - this.logger.fileLog(ileObject.relativePath, { - message: `Invalid EXPORT found. Single quote string expected.`, - type: `warning`, - range: { - start: symbolTokens.range.start, - end: symbolTokens.range.end - } - }) - } - - } else - if (currentCommand === `ENDPGMEXP`) { - // Return, we only really care about the first export block - break; - } - } - - // Exports are always uppercase - target.exports = target.exports.map(e => e.toUpperCase()); - - this.addNewTarget(target); - } - - /** - * Handles all DDS types: pf, lf, dspf - */ - private createDdsFileTarget(ileObject: ILEObject, localPath: string, dds: dds, options: FileOptions = {}) { - const target: ILEObjectTarget = { - ...ileObject, - deps: [] - }; - - infoOut(`${ileObject.systemName}.${ileObject.type}: ${ileObject.relativePath}`); - - // We have a local cache of refs found so we don't keep doing global lookups - // on objects we already know to depend on in this object. - - let alreadyFoundRefs: string[] = []; - - const handleObjectPath = (currentKeyword: string, recordFormat: any, value: string) => { - const qualified = value.split(`/`); - - let objectName: string | undefined; - if (qualified.length === 2 && qualified[0].toLowerCase() === `*libl`) { - objectName = qualified[1]; - } else if (qualified.length === 1) { - objectName = qualified[0]; - } - - if (objectName) { - const upperName = objectName.toUpperCase(); - if (alreadyFoundRefs.includes(upperName)) return; - - const resolvedPath = this.searchForObject({ systemName: upperName, type: `FILE` }); - if (resolvedPath) { - target.deps.push(resolvedPath); - alreadyFoundRefs.push(upperName); - } - else { - this.logger.fileLog(ileObject.relativePath, { - message: `no object found for reference '${objectName}'`, - type: `warning`, - line: recordFormat.range.start - }); - } - } else { - this.logger.fileLog(ileObject.relativePath, { - message: `${currentKeyword} reference not included as possible reference to library found.`, - type: `info`, - line: recordFormat.range.start - }); - } - } - - // PFILE -> https://www.ibm.com/docs/en/i/7.5?topic=80-pfile-physical-file-keywordlogical-files-only - // REF -> https://www.ibm.com/docs/en/i/7.5?topic=80-ref-reference-keywordphysical-files-only - - const ddsRefKeywords = [`PFILE`, `REF`, `JFILE`]; - - for (const recordFormat of dds.formats) { - - // Look through this record format keywords for the keyword we're looking for - for (const keyword of ddsRefKeywords) { - const keywordObj = recordFormat.keywords.find(k => k.name === keyword); - if (keywordObj) { - const wholeValue: string = keywordObj.value; - const parts = wholeValue.split(` `).filter(x => x.length > 0); - - // JFILE can have multiple files referenced in it, whereas - // REF and PFILE can only have one at the first element - const pathsToCheck = (keyword === `JFILE` ? parts.length : 1); - - for (let i = 0; i < pathsToCheck; i++) { - handleObjectPath(keyword, recordFormat, parts[i]); - } - } - } - - // REFFLD -> https://www.ibm.com/docs/en/i/7.5?topic=80-reffld-referenced-field-keywordphysical-files-only - - // Then, let's loop through the fields in this format and see if we can find REFFLD - for (const field of recordFormat.fields) { - const refFld = field.keywords.find(k => k.name === `REFFLD`); - - if (refFld) { - const [fieldRef, fileRef] = refFld.value.trim().split(` `); - - if (fileRef) { - handleObjectPath(`REFFLD`, recordFormat, fileRef); - } - } - } - } - - if (target.deps.length > 0) - infoOut(`Depends on: ${target.deps.map(d => `${d.systemName}.${d.type}`).join(` `)}`); - - this.addNewTarget(target); - } - - private createClTarget(ileObject: ILEObject, localPath: string, module: Module, options: FileOptions = {}) { - const pathDetail = path.parse(localPath); - const target: ILEObjectTarget = { - ...ileObject, - deps: [] - }; - - infoOut(`${ileObject.systemName}.${ileObject.type}: ${ileObject.relativePath}`); - - if (ileObject.extension?.toLowerCase() === `clp`) { - if (this.suggestions.renames) { - this.logger.fileLog(ileObject.relativePath, { - message: `Rename suggestion`, - type: `rename`, - change: { - rename: { - path: localPath, - newName: pathDetail.name + `.pgm.clle` - } - } - }); - } else { - this.logger.fileLog(ileObject.relativePath, { - message: `Extension is '${ileObject.extension}', but Source Orbit doesn't support CLP. Is it possible the extension should use '.pgm.clle'?`, - type: `warning`, - }); - } - - } else { - if (ileObject.type === `MODULE`) { - if (this.suggestions.renames) { - this.logger.fileLog(ileObject.relativePath, { - message: `Rename suggestion`, - type: `rename`, - change: { - rename: { - path: localPath, - newName: pathDetail.name + `.pgm` + pathDetail.ext - } - } - }); - } else { - this.logger.fileLog(ileObject.relativePath, { - message: `Type detected as ${ileObject.type} but Source Orbit doesn't support CL modules. Is it possible the extension should include '.pgm'?`, - type: `warning`, - }); - } - } - } - - const files = module.getDefinitionsOfType(DefinitionType.File); - - // Loop through local file defs to find a possible dep - files.forEach(def => { - const possibleObject = def.file; - if (possibleObject) { - if (possibleObject.library?.toUpperCase() === `*LIBL`) { - possibleObject.library = undefined; // This means lookup as normal - } - - if (possibleObject.library) { - this.logger.fileLog(ileObject.relativePath, { - message: `Definition to ${possibleObject.library}/${possibleObject.name} ignored due to qualified path.`, - range: { - start: def.range.start, - end: def.range.end - }, - type: `info`, - }); - - } else { - if (ignoredObjects.includes(possibleObject.name.toUpperCase())) return; - - const resolvedPath = this.searchForObject({ systemName: possibleObject.name.toUpperCase(), type: `FILE` }); - if (resolvedPath) target.deps.push(resolvedPath); - else { - this.logger.fileLog(ileObject.relativePath, { - message: `no object found for reference '${possibleObject.name}'`, - range: { - start: def.range.start, - end: def.range.end - }, - type: `warning`, - }); - } - } - } - }); - - module.statements.filter(s => { - const possibleObject = s.getObject(); - return (possibleObject && possibleObject.name && possibleObject.name === `CALL`); - }).forEach(s => { - - const parms = s.getParms(); - const pgmParm = parms[`PGM`]; - - if (pgmParm && pgmParm.block) { - const block = pgmParm.block; - if (block.length === 1) { - const name = block[0].value!; - - if (ignoredObjects.includes(name.toUpperCase())) return; - - const resolvedPath = this.searchForObject({ systemName: name.toUpperCase(), type: `PGM` }); - if (resolvedPath) target.deps.push(resolvedPath); - else { - this.logger.fileLog(ileObject.relativePath, { - message: `no object found for reference '${name}'`, - range: { - start: pgmParm.range.start, - end: pgmParm.range.end - }, - type: `warning`, - }); - } - } else { - this.logger.fileLog(ileObject.relativePath, { - message: `PGM call not included as possible reference to library.`, - range: { - start: pgmParm.range.start, - end: pgmParm.range.end - }, - type: `info`, - }); - } - } - }); - - // We also look to see if there is a `.cmd` object with the same name - const possibleCommandObject = this.searchForObject({ systemName: ileObject.systemName, type: `CMD` }); - if (possibleCommandObject) this.createOrAppend(possibleCommandObject, target); - - if (target.deps.length > 0) - infoOut(`Depends on: ${target.deps.map(d => `${d.systemName}.${d.type}`).join(` `)}`); - - this.addNewTarget(target); - } - - private createSqlTargets(localPath: string, document: Document, options: FileOptions = {}) { - const pathDetail = path.parse(localPath); - const relativePath = this.getRelative(localPath); - - const groups = document.getStatementGroups(); - - // TODO: Note, this returns high level definitions. - // If the index/view/etc specifies a table dep, - // they will not appear as a dependency - - const createCount = groups.filter(g => g.statements[0].type === StatementType.Create).length; - - if (createCount > 1) { - this.logger.fileLog(relativePath, { - message: `Includes multiple create statements. They should be in individual sources. This file will not be parsed.`, - type: `warning`, - }); - - return; - } - - for (const group of groups) { - const statement = group.statements[0]; - const defs = statement.getObjectReferences(); - const mainDef = defs[0]; - - if (mainDef && mainDef.createType && mainDef.object.name) { - const tokens = mainDef.tokens; - if (mainDef.object.schema) { - this.logger.fileLog(relativePath, { - message: `${mainDef.object.schema}/${mainDef.object.name} (${mainDef.createType}) reference not included as possible reference to library found.`, - range: { - start: tokens[0].range.start, - end: tokens[tokens.length - 1].range.end - }, - type: `warning`, - }); - - } else { - switch (statement.type) { - // Alters are a little weird in that they can exist - // in any file, so we can't assume the current source - // is the name of the object. Sad times - case StatementType.Alter: - // We don't do anything for alter currently - // because it's too easy to create circular deps. - // This is bad!! - this.logger.fileLog(relativePath, { - message: `${mainDef.object.name} (${mainDef.createType}) alter not tracked due to possible circular dependency.`, - range: { - start: tokens[0].range.start, - end: tokens[tokens.length - 1].range.end - }, - type: `info`, - }); - - // let currentTarget: ILEObjectTarget|undefined; - // const resolvedPath = this.resolveLocalObjectQuery(mainDef.object.name + `.*`); - // const currentRelative = path.basename(resolvedPath); - // if (resolvedPath) { - // currentTarget = { - // ...this.resolveObject(resolvedPath), - // deps: [] - // }; - // } - - // if (currentTarget) { - // info(`${currentTarget.name}.${currentTarget.type}`); - // info(`\tSource: ${currentTarget.relativePath}`); - - // if (defs.length > 1) { - // for (const def of defs.slice(1)) { - // const subResolvedPath = this.resolveLocalObjectQuery(def.object.name + `.*`, currentRelative); - // if (subResolvedPath) currentTarget.deps.push(this.resolveObject(subResolvedPath)) - // else info(`\tNo object found for reference '${def.object.name}'`); - // } - // } - - // if (currentTarget.deps.length > 0) { - // info(`Depends on: ${currentTarget.deps.map(d => `${d.name}.${d.type}`).join(` `)}`); - - // this.pushDep(currentTarget); - // } - // } - break; - - // Creates should be in their own unique file - case StatementType.Create: - let hasLongName = mainDef.object.name && mainDef.object.name.length > 10 ? mainDef.object.name : undefined; - let objectName = mainDef.object.system || trimQuotes(mainDef.object.name, `"`); - - const extension = pathDetail.ext.substring(1); - - let ileObject: ILEObject = { - systemName: objectName.toUpperCase(), - longName: hasLongName, - type: this.getObjectType(relativePath, mainDef.createType), - text: options.text, - relativePath, - extension - } - - let suggestRename = false; - const sqlFileName = pathDetail.name; - - // First check the file name - if (ileObject.systemName.length <= 10) { - if (ileObject.systemName.toUpperCase() !== sqlFileName.toUpperCase() && ileObject.longName !== sqlFileName) { - suggestRename = true; - } - } - - // Then make an extension suggestion - if (extension.toUpperCase() === `SQL` && mainDef.createType) { - suggestRename = true; - } - - // Let them know to use a system name in the create statement if one is not present - if (ileObject.systemName.length > 10 && mainDef.object.system === undefined) { - this.logger.fileLog(ileObject.relativePath, { - message: `${ileObject.systemName} (${ileObject.type}) name is longer than 10 characters. Consider using 'FOR SYSTEM NAME' in the CREATE statement.`, - type: `warning`, - range: { - start: tokens[0].range.start, - end: tokens[tokens.length - 1].range.end - }, - }); - - suggestRename = false; - } - - let newTarget: ILEObjectTarget = { - ...ileObject, - deps: [] - }; - - infoOut(`${newTarget.systemName}.${newTarget.type}: ${newTarget.relativePath}`); - - // Now, let's go through all the other statements in this group (BEGIN/END) - // and grab any references to other objects :eyes: - let otherDefs = defs.slice(1); - - for (let i = 1; i < group.statements.length; i++) { - const currentStatement = group.statements[i]; - if ([StatementType.Alter, StatementType.Insert, StatementType.Delete, StatementType.With, StatementType.Select, StatementType.Call].includes(currentStatement.type)) { - otherDefs.push(...group.statements[i].getObjectReferences()); - } - } - - for (const def of otherDefs) { - const refTokens = def.tokens; - const simpleName = trimQuotes(def.object.name, `"`); - // TODO: do we need to look for SRVPGM (function) or PGM (procedure) here? - const resolvedObject = this.searchForAnyObject({ name: simpleName, types: [`FILE`, `SRVPGM`, `PGM`] }); - if (resolvedObject) { - if (!newTarget.deps.find(d => d.systemName === resolvedObject.systemName && d.type === resolvedObject.type)) { - newTarget.deps.push(resolvedObject); - } - } - else if (!isSqlFunction(def.object.name)) { - this.logger.fileLog(newTarget.relativePath, { - message: `No object found for reference '${def.object.name}'`, - type: `warning`, - range: { - start: refTokens[0].range.start, - end: refTokens[refTokens.length - 1].range.end - }, - }); - } - } - - if (newTarget.deps.length > 0) { - infoOut(`Depends on: ${newTarget.deps.map(d => `${d.systemName}.${d.type}`).join(` `)}`); - } - - // So we can later resolve the path to the created object - this.storeResolved(localPath, ileObject); - - this.addNewTarget(newTarget); - - // If the extension is SQL, let's make better suggestions - // based on the create type in the CREATE statement - if (suggestRename) { - const newExtension = sqlTypeExtension[mainDef.createType.toUpperCase()]; - - if (newExtension) { - const possibleName = (ileObject.longName ? ileObject.longName : ileObject.systemName.toLowerCase()) + `.` + newExtension; - - if (this.suggestions.renames) { - const renameLogPath = relativePath; - - // We need to make sure the .rpgleinc rename is most important - if (this.logger.exists(renameLogPath, `rename`)) { - this.logger.flush(renameLogPath); - } - - this.logger.fileLog(renameLogPath, { - message: `Rename suggestion`, - type: `rename`, - change: { - rename: { - path: localPath, - newName: possibleName - } - } - }); - } else { - this.logger.fileLog(relativePath, { - message: `Extension should be based on type. Suggested name is '${possibleName}'`, - type: `warning`, - }); - } - } - } - - break; - } - - } - } - } - } - - private createRpgTarget(ileObject: ILEObject, localPath: string, cache: Cache, options: FileOptions = {}) { - const pathDetail = path.parse(localPath); - // define internal imports - ileObject.imports = cache.procedures - .filter((proc: any) => proc.keyword[`EXTPROC`] && !proc.keyword[`EXPORT`]) - .map(ref => { - const keyword = ref.keyword; - let importName: string = ref.name; - const extproc: string | boolean = keyword[`EXTPROC`]; - if (extproc) { - if (extproc === true) importName = ref.name; - else importName = extproc; - } - - if (importName.includes(`:`)) { - const parmParms = importName.split(`:`); - importName = parmParms.filter(p => !p.startsWith(`*`)).join(``); - } - - if (importName.startsWith(`*`)) { - importName = ref.name; - } else { - importName = trimQuotes(importName); - } - - return importName; - }); - - // define exported functions - if (cache.keyword[`NOMAIN`]) { - ileObject.type = `MODULE`; - - // Note that we store exports as uppercase. - ileObject.exports = cache.procedures - .filter((proc: any) => proc.keyword[`EXPORT`]) - .map(ref => ref.name.toUpperCase()); - } - - infoOut(`${ileObject.systemName}.${ileObject.type}: ${ileObject.relativePath}`); - - if (cache.includes && cache.includes.length > 0) { - ileObject.headers = []; - - cache.includes.forEach((include: IncludeStatement) => { - // RPGLE includes are always returned as posix paths - // even on Windows. We need to do some magic to convert here for Windows systems - include.toPath = toLocalPath(include.toPath); - - const includeDetail = path.parse(include.toPath); - - if (includeDetail.ext.toLowerCase() !== `.rpgleinc`) { - const possibleName = includeDetail.name.toLowerCase().endsWith(`.pgm`) ? includeDetail.name.substring(0, includeDetail.name.length - 4) : includeDetail.name; - - if (this.suggestions.renames) { - const renameLogPath = this.getRelative(include.toPath); - - // We need to make sure the .rpgleinc rename is most important - if (this.logger.exists(renameLogPath, `rename`)) { - this.logger.flush(renameLogPath); - } - - this.logger.fileLog(renameLogPath, { - message: `Rename suggestion`, - type: `rename`, - change: { - rename: { - path: include.toPath, - newName: `${possibleName}.rpgleinc` - } - } - }); - } else { - this.logger.fileLog(this.getRelative(include.toPath), { - message: `referenced as include, but should use the '.rpgleinc' extension.`, - type: `warning`, - }); - } - } - - const theIncludePath = asPosix(this.getRelative(include.toPath)); - - ileObject.headers.push(theIncludePath); - - if (this.suggestions.includes) { - this.logger.fileLog(ileObject.relativePath, { - message: `Will update to use unix style path.`, - type: `includeFix`, - line: include.line, - change: { - lineContent: (options.isFree ? `` : ``.padEnd(6)) + `/copy '${theIncludePath}'` - } - }); - } else { - this.logger.fileLog(ileObject.relativePath, { - message: `Include at line ${include.line} found, to path '${theIncludePath}'`, - type: `info`, - line: include.line, - }); - } - }); - } - - const target: ILEObjectTarget = { - ...ileObject, - deps: [] - }; - - // This usually means .pgm is in the name - if (ileObject.type === `PGM` && cache.keyword[`NOMAIN`]) { - const possibleName = pathDetail.name.toLowerCase().endsWith(`.pgm`) ? pathDetail.name.substring(0, pathDetail.name.length - 4) : pathDetail.name; - - if (this.suggestions.renames) { - this.logger.fileLog(ileObject.relativePath, { - message: `Rename suggestion`, - type: `rename`, - change: { - rename: { - path: localPath, - newName: possibleName + pathDetail.ext - } - } - }) - } else { - this.logger.fileLog(ileObject.relativePath, { - message: `type detected as ${ileObject.type} but NOMAIN keyword found.`, - type: `warning`, - }); - } - } - - // This usually means it's source name is a module (no .pgm) but doesn't have NOMAIN. - // We need to do this for other language too down the line - if (ileObject.type === `MODULE` && !cache.keyword[`NOMAIN`]) { - if (this.suggestions.renames) { - this.logger.fileLog(ileObject.relativePath, { - message: `Rename suggestion`, - type: `rename`, - change: { - rename: { - path: localPath, - newName: pathDetail.name + `.pgm` + pathDetail.ext - } - } - }); - } else { - this.logger.fileLog(ileObject.relativePath, { - message: `type detected as ${ileObject.type} but NOMAIN keyword was not found. Is it possible the extension should include '.pgm'?`, - type: `warning`, - }); - } - } - - if (cache.keyword[`BNDDIR`]) { - this.logger.fileLog(ileObject.relativePath, { - message: `has the BNDDIR keyword. 'binders' property in iproj.json should be used instead.`, - type: `info`, - }); - } - - // Find external programs - cache.procedures - .filter((proc: any) => proc.keyword[`EXTPGM`]) - .map((ref): RpgLookup => { - const keyword = ref.keyword; - let fileName = ref.name; - const extpgm = keyword[`EXTPGM`]; - if (extpgm) { - if (extpgm === true) fileName = ref.name; - else fileName = trimQuotes(extpgm); - } - - return { - lookup: fileName.toUpperCase(), - line: ref.position ? ref.position.range.line : undefined - }; - }) - .forEach((ref: RpgLookup) => { - // Don't add ignored objects (usually system APIs) - if (ignoredObjects.includes(ref.lookup)) return; - // Don't add itself - if (ref.lookup === ileObject.systemName) return; - - const resolvedObject = this.searchForObject({ systemName: ref.lookup, type: `PGM` }); - if (resolvedObject) { - // because of legacy fixed CALL, there can be dupliicate EXTPGMs with the same name :( - if (!target.deps.some(d => d.systemName === resolvedObject.systemName && d.type && resolvedObject.type)) { - target.deps.push(resolvedObject) - } - } - - else { - this.logger.fileLog(ileObject.relativePath, { - message: `No object found for reference '${ref.lookup}'`, - type: `warning`, - line: ref.line - }); - } - }); - - // Scan the multiple scopes available in an RPGLE program - const scopes = [cache, ...cache.procedures.map(p => p.scope)].filter(s => s); - - for (const scope of scopes) { - - // Find external data structure sources - scope.structs - .filter((struct: any) => struct.keyword[`EXTNAME`]) - .map((struct): RpgLookup => { - const keyword = struct.keyword; - const value = trimQuotes(keyword[`EXTNAME`]); - - return { - lookup: value.split(`:`)[0].toUpperCase(), - line: struct.position ? struct.position.range.line : undefined - }; - }) - .forEach((ref: RpgLookup) => { - const resolvedObject = this.searchForObject({ systemName: ref.lookup, type: `FILE` }); - if (resolvedObject) target.deps.push(resolvedObject) - else { - this.logger.fileLog(ileObject.relativePath, { - message: `No object found for reference '${ref.lookup}'`, - type: `warning`, - line: ref.line - }); - } - }); - - // Find external files - scope.files - .map((file): RpgLookup => { - let possibleName: string = file.name; - const keyword = file.keyword; - - const extNameValue = keyword[`EXTFILE`]; - if (extNameValue) { - possibleName = trimQuotes(extNameValue).split(`:`)[0] - } - - if (possibleName.toLowerCase() === `*extdesc`) { - const extDescValue = keyword[`EXTDESC`]; - if (extDescValue) { - possibleName = trimQuotes(extDescValue); - } else { - this.logger.fileLog(ileObject.relativePath, { - message: `*EXTDESC is used for '${file.name}' but EXTDESC keyword not found`, - type: `warning`, - }); - } - } - - return { - lookup: possibleName.toUpperCase(), - line: file.position ? file.position.range.line : undefined - }; - }) - .forEach((ref: RpgLookup) => { - if (ignoredObjects.includes(ref.lookup)) return; - - const previouslyScanned = target.deps.some((r => (ref.lookup === r.systemName || ref.lookup === r.longName?.toUpperCase()) && r.type === `FILE`)); - if (previouslyScanned) return; - - const resolvedObject = this.searchForObject({ systemName: ref.lookup, type: `FILE` }); - if (resolvedObject) target.deps.push(resolvedObject) - else { - this.logger.fileLog(ileObject.relativePath, { - message: `No object found for reference '${ref.lookup}'`, - type: `warning`, - line: ref.line - }); - } - }); - - // We ignore anything with hardcoded schemas - scope.sqlReferences - .filter(ref => !ref.description) - .map((ref): RpgLookup => ({ - lookup: trimQuotes(ref.name, `"`).toUpperCase(), - line: ref.position ? ref.position.range.line : undefined - })) - .forEach((ref: RpgLookup) => { - const previouslyScanned = target.deps.some((r => (ref.lookup === r.systemName || ref.lookup === r.longName?.toUpperCase()) && r.type === `FILE`)); - if (previouslyScanned) return; - const resolvedObject = this.searchForObject({ systemName: ref.lookup, type: `FILE` }); - if (resolvedObject) target.deps.push(resolvedObject) - else if (!isSqlFunction(ref.lookup)) { - this.logger.fileLog(ileObject.relativePath, { - message: `No object found for reference '${ref.lookup}'`, - type: `warning`, - line: ref.line - }); - } - }); - - // Find external data areas - scope.structs - .filter((struct: any) => struct.keyword[`DTAARA`]) - .map((ref): RpgLookup => { - const keyword = ref.keyword; - let fileName: string = ref.name; - const dtaara = keyword[`DTAARA`]; - if (dtaara) { - if (dtaara === true) fileName = ref.name; - else fileName = trimQuotes(dtaara); - } - - return { - lookup: fileName.toUpperCase(), - line: ref.position ? ref.position.range.line : undefined - }; - }) - .forEach((ref: RpgLookup) => { - if (ignoredObjects.includes(ref.lookup.toUpperCase())) return; - - const resolvedObject = this.searchForObject({ systemName: ref.lookup, type: `DTAARA` }); - if (resolvedObject) target.deps.push(resolvedObject) - else { - this.logger.fileLog(ileObject.relativePath, { - message: `No object found for reference '${ref.lookup}'`, - type: `warning`, - line: ref.line - }); - } - }); - - scope.variables - .filter((struct: any) => struct.keyword[`DTAARA`]) - .map((ref): RpgLookup => { - const keyword = ref.keyword; - let fileName: string = ref.name; - const dtaara = keyword[`DTAARA`]; - if (dtaara) { - if (dtaara === true) fileName = ref.name; - else fileName = trimQuotes(dtaara); - } - - return { - lookup: fileName.toUpperCase(), - line: ref.position ? ref.position.range.line : undefined - }; - }) - .forEach((ref: RpgLookup) => { - const resolvedObject = this.searchForObject({ systemName: ref.lookup, type: `DTAARA` }); - if (resolvedObject) target.deps.push(resolvedObject) - else { - this.logger.fileLog(ileObject.relativePath, { - message: `No object found for reference '${ref.lookup}'`, - type: `warning`, - line: ref.line - }); - } - }); - } - - // TODO: did we duplicate this? - // We also look to see if there is a `.cmd` object with the same name - const resolvedObject = this.searchForObject({ systemName: ileObject.systemName, type: `CMD` }); - if (resolvedObject) this.createOrAppend(resolvedObject, target); - - if (target.deps.length > 0) - infoOut(`Depends on: ${target.deps.map(d => `${d.systemName}.${d.type}`).join(` `)}`); - - this.addNewTarget(target); - } - - getTarget(object: ILEObject): ILEObjectTarget | undefined { - return this.targets[`${object.systemName}.${object.type}`]; - } - - getTargets(): ILEObjectTarget[] { - return Object.values(this.targets).filter(x => x); - } - - // Generates targets for service programs and binding directories - public resolveBinder() { - // Right now, we really only support single module programs and service programs - - const allTargets = this.getTargets(); - - // We can simply check for any modules since we turn them into service programs - this.needsBinder = allTargets.some(d => d.type === `SRVPGM`); - - infoOut(``); - - // We need to loop through all the user-defined server programs (binder source) - // And resolve the service program program exports to module exports to bind them together nicely - const allSrvPgms = this.getTargetsOfType(`SRVPGM`); - const allModules = this.getTargetsOfType(`MODULE`); - - for (const target of allSrvPgms) { - if (target.exports) { - infoOut(`Resolving modules for ${target.systemName}.${target.type}`); - - target.deps = []; - - for (const exportName of target.exports) { - // We loop through each export of the service program and find the module that exports it - const foundModule = allModules.find(mod => mod.exports && mod.exports.includes(exportName.toUpperCase())); - if (foundModule) { - const alreadyBound = target.deps.some(dep => dep.systemName === foundModule.systemName && dep.type === `MODULE`); - if (!alreadyBound) { - infoOut(`Adding module ${foundModule.systemName}.${foundModule.type}`); - target.deps.push(foundModule); - } - } - } - - if (target.deps.length > 0) { - // Add this new service program to the project binding directory - this.createOrAppend(this.projectBindingDirectory, target); - - // Make sure we can resolve to this service program - for (const e of target.exports) { - this.resolvedExports[e.toUpperCase()] = target; - } - } else { - // This service program target doesn't have any deps... so, it's not used? - this.removeObject(target); - - if (target.relativePath) { - this.logger.fileLog(target.relativePath, { - message: `Removed as target because no modules were found with matching exports.`, - type: `info` - }); - } - } - - infoOut(``); - } - } - - // We loop through all programs and module and study their imports. - // We do this in case they depend on another service programs based on import - for (let currentTarget of allTargets) { - if ([`PGM`, `MODULE`].includes(currentTarget.type) && currentTarget.imports) { - let newImports: ILEObject[] = []; - - // Remove any service program deps so we can resolve them cleanly - currentTarget.deps = currentTarget.deps.filter(d => ![`SRVPGM`].includes(d.type)); - - for (const importName of currentTarget.imports) { - // Find if this import resolves to another object - const possibleSrvPgmDep = this.resolvedExports[importName.toUpperCase()]; - // We can't add a module as a dependency at this step. - if (possibleSrvPgmDep && possibleSrvPgmDep.type === `SRVPGM`) { - // Make sure we haven't imported it before! - if (!newImports.some(i => i.systemName === possibleSrvPgmDep.systemName && i.type === possibleSrvPgmDep.type)) { - newImports.push(possibleSrvPgmDep); - } - - } else if ([`PGM`, `MODULE`].includes(currentTarget.type)) { - // Perhaps we're looking at a program object, which actually should be a multi - // module program, so we do a lookup for additional modules. - const possibleModuleDep = allModules.find(mod => mod.exports && mod.exports.includes(importName.toUpperCase())) - if (possibleModuleDep) { - if (!newImports.some(i => i.systemName === possibleModuleDep.systemName && i.type === possibleModuleDep.type)) { - newImports.push(possibleModuleDep); - - // TODO: consider other IMPORTS that `possibleModuleDep` needs. - } - } - } - }; - - // If the program or module has imports that we ca resolve, then we add them as deps - if (newImports.length > 0) { - infoOut(`${currentTarget.systemName}.${currentTarget.type} has additional dependencies: ${newImports.map(i => `${i.systemName}.${i.type}`)}`); - currentTarget.deps.push(...newImports); - - if (currentTarget.type === `PGM`) { - // If this program has MODULE dependecies, that means we need to change the way it's compiled - // to be a program made up of many modules, usually done with CRTPGM - if (currentTarget.deps.some(d => d.type === `MODULE`)) { - this.convertBoundProgramToMultiModuleProgram(currentTarget); - - // Then, also include any of the modules dep modules into the currentTarget deps!! - const depTargets = currentTarget.deps - .filter(d => d.type === `MODULE`) - .map(m => this.getTarget(m)); - - // Confusing names, it means: dependencies of the dependencies that are modules - const depDeps = depTargets.map(m => m?.deps).flat().filter(d => d.type === `MODULE`); - - for (const newDep of depDeps) { - if (newDep && !currentTarget.deps.some(d => d.systemName === newDep.systemName && d.type === newDep.type)) { - currentTarget.deps.push(newDep); - } - } - } - } - } - } - } - - const commandObjects = this.getResolvedObjects(`CMD`); - for (let cmdObject of commandObjects) { - // Check if a program exists with the same name. - const programObject = this.getTarget({ systemName: cmdObject.systemName, type: `PGM` }); - if (programObject) { - const newTarget = { - ...cmdObject, - deps: [programObject] - } - - this.addNewTarget(newTarget); - } else { - - this.removeObject(cmdObject); - this.logger.fileLog(cmdObject.relativePath, { - message: `Removed as target because no program was found with a matching name.`, - type: `info` - }); - } - } - } - - private convertBoundProgramToMultiModuleProgram(currentTarget: ILEObjectTarget) { - const basePath = currentTarget.relativePath; - - // First, let's change this current target to be solely a program - // Change the extension so it's picked up correctly during the build process. - currentTarget.extension = `pgm`; - currentTarget.relativePath = undefined; - - // Store a fake path for this program object - this.storeResolved(path.join(this.cwd, `${currentTarget.systemName}.PGM`), currentTarget); - - // Then we can create the new module object from this path - const newModule: ILEObject = { - systemName: currentTarget.systemName, - imports: currentTarget.imports, - exports: [], - headers: currentTarget.headers, - type: `MODULE`, - relativePath: basePath, - extension: path.extname(basePath).substring(1) - }; - - // Replace the old resolved object with the module - this.storeResolved(path.join(this.cwd, basePath), newModule); - - // Create a new target for the module - const newModTarget = this.createOrAppend(newModule); - - // Clean up imports for module and program - newModTarget.imports = currentTarget.imports; - currentTarget.imports = undefined; - currentTarget.headers = undefined; - - this.createOrAppend(currentTarget, newModule); - } - - public createOrAppend(parentObject: ILEObject, newDep?: ILEObject) { - let existingTarget = this.targets[`${parentObject.systemName}.${parentObject.type}`]; - - if (!existingTarget) { - existingTarget = { - ...parentObject, - deps: [] - }; - - this.addNewTarget(existingTarget); - } - - if (newDep) - existingTarget.deps.push(newDep); - - return existingTarget; - } - - private addNewTarget(dep: ILEObjectTarget) { - this.targets[`${dep.systemName}.${dep.type}`] = dep; - } - - public binderRequired() { - return this.needsBinder; - } - - public getTargetsOfType(type: ObjectType): ILEObjectTarget[] { - return this.getTargets().filter(d => d && d.type === type); - } - - public getResolvedObjects(type?: ObjectType): ILEObject[] { - const objects = Object.values(this.resolvedObjects); - - return objects.filter(o => o && (type === undefined || o.type === type)); - } - - public getResolvedObject(fullPath: string): ILEObject { - return this.resolvedObjects[fullPath]; - } - - /** - * This API is a little trick. - * You can pass in a valid file extension, or if you pass - * solely just `pgm`, it will return all programs that - * have multiple modules. - */ - public getResolvedObjectsByFileExtension(ext: string): ILEObject[] { - const extensionParts = ext.split(`.`); - let extension = ext.toUpperCase(), shouldBeProgram = false, anyPrograms = false; - - if (extensionParts.length === 2 && extensionParts[0].toUpperCase() === `PGM`) { - extension = extensionParts[1].toUpperCase(); - shouldBeProgram = true; - } else if (extension === `PGM`) { - anyPrograms = true; - } - - return Object.values(this.resolvedObjects).filter(obj => - (obj.extension?.toUpperCase() === extension && (obj.type === `PGM`) === shouldBeProgram) || - (anyPrograms === true && obj.type === `PGM` && obj.extension.toUpperCase() === extension) - ); - } - - public getExports() { - return this.resolvedExports; - } - - /** - * Returns a list of objects that will be impacted if the given object is changed. - */ - public getImpactFor(theObject: ILEObject) { - const allDeps = this.getTargets(); - let currentTree: ILEObject[] = []; - - let currentItem: ImpactedObject = { ileObject: theObject, children: [] }; - - function lookupObject(currentItem: ImpactedObject) { - currentTree.push(currentItem.ileObject); - - for (const target of allDeps) { - const containsLookup = target.deps.some(d => d.systemName === currentItem.ileObject.systemName && d.type === currentItem.ileObject.type); - const circular = currentTree.some(d => d.systemName === target.systemName && d.type === target.type); - - if (containsLookup && !circular) { - let newDependant: ImpactedObject = { ileObject: target, children: [] }; - lookupObject(newDependant); - currentItem.children.push(newDependant); - } - } - - currentTree.pop(); - } - - lookupObject(currentItem); - - return currentItem; - } - - /** - * This is used when loading in all objects. - * SQL sources can have two object names: a system name and a long name - * Sadly the long name is not typically part of the path name, so we need to - * find the name inside of the source code. - */ - async sqlObjectDataFromPath(fullPath: string): Promise { - const relativePath = this.getRelative(fullPath); - - if (await this.fs.exists(fullPath)) { - const content = await this.fs.readFile(fullPath); - const document = new Document(content); - - const groups = document.getStatementGroups(); - - if (groups.length === 0) { - this.logger.fileLog(relativePath, { - message: `No SQL statements found in file.`, - type: `info` - }); - - return; - } - - const createCount = groups.filter(g => g.statements[0].type === StatementType.Create).length; - - if (createCount > 1) { - this.logger.fileLog(relativePath, { - message: `Includes multiple create statements. They should be in individual sources. This file will not be parsed.`, - type: `warning`, - }); - } - - const firstGroup = groups[0]; - const create = firstGroup.statements.find(s => s.type === StatementType.Create); - - if (create) { - const defs = create.getObjectReferences(); - const mainDef = defs.find(d => d.createType); - - return mainDef; - } - } - } -} - -function trimQuotes(input: string|boolean, value = `'`) { - if (typeof input === `string`) { - if (input[0] === value) input = input.substring(1); - if (input[input.length - 1] === value) input = input.substring(0, input.length - 1); - return input; - } else { - return ''; - } -} \ No newline at end of file diff --git a/cli/src/targets/index.ts b/cli/src/targets/index.ts new file mode 100644 index 0000000..c510cd9 --- /dev/null +++ b/cli/src/targets/index.ts @@ -0,0 +1,740 @@ +import path from 'path'; +import { infoOut } from '../cli'; +import Document from "vscode-db2i/src/language/sql/document"; +import { ObjectRef, StatementType } from 'vscode-db2i/src/language/sql/types'; +import { Logger } from '../logger'; +import { getReferenceObjectsFrom, getSystemNameFromPath, globalEntryIsValid, toLocalPath } from '../utils'; +import { ReadFileSystem } from '../readFileSystem'; +import { TargetsLanguageProvider } from './languages'; +import { sqlExtensions } from './languages/sql'; + +export type ObjectType = "PGM" | "SRVPGM" | "MODULE" | "FILE" | "BNDDIR" | "DTAARA" | "CMD" | "MENU" | "DTAQ"; + +const ignoredObjects = [`QSYSPRT`, `QCMDEXC`, `*LDA.DTAARA`, `QDCXLATE`, `QUSRJOBI`, `QTQCVRT`, `QWCRDTAA`, `QUSROBJD`, `QUSRMBRD`, `QUSROBJD`, `QUSLOBJ`, `QUSRTVUS`, `QUSCRTUS`]; + +const DEFAULT_BINDER_TARGET: ILEObject = { systemName: `$(APP_BNDDIR)`, type: `BNDDIR` }; + +const TextRegex = /\%TEXT.*(?=\n|\*)/gm + +export interface ILEObject { + systemName: string; + longName?: string; + type: ObjectType; + text?: string, + relativePath?: string; + extension?: string; + + reference?: boolean; + + /** exported functions */ + exports?: string[]; + /** each function import in the object */ + imports?: string[]; + + /** headers. only supports RPGLE and is not recursive */ + headers?: string[]; +} + +export interface ILEObjectTarget extends ILEObject { + deps: ILEObject[]; +} + +export interface TargetSuggestions { + renames?: boolean; + includes?: boolean; +} + +export interface ImpactedObject { + ileObject: ILEObject, + children: ImpactedObject[] +} + +export interface FileOptions { + isFree?: boolean; + text?: string; +} + +/** + * This class is responsible for storing all the targets + * and their dependencies. It also handles the parsing + * of files and the creation of targets. + * + * const files = getAllFilesInDir(`.`); + * const targets = new Targets(cwd); + * targets.handlePseudoFile(pseudoFilePath); + * targets.loadObjectsFromPaths(files); + * await Promise.all(files.map(f => targets.parseFile(f))); + * targets.resolveBinder(); + */ + +export class Targets { + private languageProvider: TargetsLanguageProvider; + + /* pathCache and resolvedSearches are used for file resolving. */ + private pathCache: { [path: string]: true | string[] } | undefined; + private resolvedSearches: { [query: string]: string } = {}; + + private assumePrograms = false; + + private resolvedObjects: { [localPath: string]: ILEObject } = {}; + private resolvedExports: { [name: string]: ILEObject } = {}; + private targets: { [name: string]: ILEObjectTarget } = {}; + + private needsBinder = false; + private projectBindingDirectory = DEFAULT_BINDER_TARGET; + + private actionSuggestions: TargetSuggestions = {}; + + public logger: Logger; + + constructor(private cwd: string, private fs: ReadFileSystem) { + this.logger = new Logger(); + this.languageProvider = new TargetsLanguageProvider(this); + } + + static get ignoredObjects() { + return ignoredObjects; + } + + getSearchGlob(): string { + return this.languageProvider.getGlob(); + } + + public getCwd() { + return this.cwd; + } + + get rfs() { + return this.fs; + } + + public get suggestions() { + return this.actionSuggestions; + } + + public setAssumePrograms(assumePrograms: boolean) { + this.assumePrograms = assumePrograms; + } + + public setSuggestions(newSuggestions: TargetSuggestions) { + this.actionSuggestions = newSuggestions; + } + + public getBinderTarget() { + return this.projectBindingDirectory; + } + + public getRelative(fullPath: string) { + return path.relative(this.cwd, fullPath); + } + + storeResolved(localPath: string, ileObject: ILEObject) { + this.resolvedObjects[localPath] = ileObject; + } + + public async loadProject(withRef?: string) { + if (withRef) { + await this.handleRefsFile(path.join(this.cwd, withRef)); + } + + const initialFiles = await this.fs.getFiles(this.cwd, this.getSearchGlob()); + await this.loadObjectsFromPaths(initialFiles); + await Promise.allSettled(initialFiles.map(f => this.parseFile(f))); + } + + private extCanBeProgram(ext: string): boolean { + return [`MODULE`, `PGM`].includes(this.languageProvider.getObjectType(ext)); + } + + public async resolvePathToObject(localPath: string, newText?: string) { + if (this.resolvedObjects[localPath]) { + if (newText) this.resolvedObjects[localPath].text = newText; + return this.resolvedObjects[localPath]; + } + + const detail = path.parse(localPath); + const relativePath = this.getRelative(localPath); + + const extension = detail.ext.length > 1 ? detail.ext.substring(1) : detail.ext; + const hasProgramAttribute = detail.name.toUpperCase().endsWith(`.PGM`); + const isProgram = this.assumePrograms ? this.extCanBeProgram(extension) : hasProgramAttribute; + const name = getSystemNameFromPath(hasProgramAttribute ? detail.name.substring(0, detail.name.length - 4) : detail.name); + const type: ObjectType = (isProgram ? "PGM" : this.getObjectType(relativePath, extension)); + + const theObject: ILEObject = { + systemName: name, + type: type, + text: newText, + relativePath, + extension + }; + + // If this file is an SQL file, we need to look to see if it has a long name as we need to resolve all names here + if (sqlExtensions.includes(extension.toLowerCase())) { + const ref = await this.sqlObjectDataFromPath(localPath); + if (ref) { + if (ref.object.system) theObject.systemName = ref.object.system.toUpperCase(); + if (ref.object.name) theObject.longName = ref.object.name; + // theObject.type = ref.type; + } + } + + if (type === `BNDDIR`) { + this.projectBindingDirectory = theObject; + } + + // This allows us to override the .objrefs if the source actually exists. + if (this.isReferenceObject(theObject, true)) { + this.logger.fileLog(relativePath, { + type: `info`, + message: `The object ${theObject.systemName}.${theObject.type} is defined in the references file even though the source exists for it.` + }); + } + + this.storeResolved(localPath, theObject); + + return theObject; + } + + /** + * This can be expensive. It should only be called: + * before loadObjectsFromPaths and parseFile are called. + * @param filePath Fully qualified path to the file. Assumed to exist. + */ + public async handleRefsFile(filePath: string) { + const content = await this.fs.readFile(filePath); + + const pseudoObjects = getReferenceObjectsFrom(content); + + for (const ileObject of pseudoObjects) { + if (!this.searchForObject(ileObject)) { + const key = `${ileObject.systemName}.${ileObject.type}`; + ileObject.reference = true; + this.resolvedObjects[key] = ileObject; + } + }; + } + + public isReferenceObject(ileObject: ILEObject, remove?: boolean) { + const key = `${ileObject.systemName}.${ileObject.type}`; + const existing = this.resolvedObjects[key]; + const isRef = Boolean(existing && existing.reference); + + if (isRef && remove) { + this.resolvedObjects[key] = undefined; + } + + return isRef; + } + + public removeObjectByPath(localPath: string) { + const resolvedObject = this.resolvedObjects[localPath]; + + if (resolvedObject) { + // First, delete the simple caches + this.resolvedObjects[localPath] = undefined; + + return this.removeObject(resolvedObject); + } + + return [] + } + + public removeObject(resolvedObject: ILEObject) { + let impactedTargets: ILEObject[] = []; + + for (const targetId in this.targets) { + const target = this.targets[targetId]; + + if (target) { + const depIndex = target.deps.findIndex(d => (d.systemName === resolvedObject.systemName && d.type === resolvedObject.type) || d.relativePath === resolvedObject.relativePath); + + if (depIndex >= 0) { + impactedTargets.push(target); + target.deps.splice(depIndex, 1); + + if (target.relativePath) { + this.logger.fileLog(target.relativePath, { + type: `info`, + message: `This object depended on ${resolvedObject.systemName}.${resolvedObject.type} before it was deleted.` + }) + } + } + } + } + + // Remove it as a global target + this.targets[`${resolvedObject.systemName}.${resolvedObject.type}`] = undefined; + this.resolvedSearches[`${resolvedObject.systemName}.${resolvedObject.type}`] = undefined; + + // Remove possible logs + if (resolvedObject.relativePath) { + this.logger.flush(resolvedObject.relativePath) + } + + return impactedTargets; + } + + /** + * Resolves a search to an object. Use `systemName` parameter for short and long name. + */ + public searchForObject(lookFor: ILEObject) { + return this.getResolvedObjects().find(o => (lookFor.systemName === o.systemName || (o.longName && lookFor.systemName === o.longName)) && o.type === lookFor.type); + } + + public searchForAnyObject(lookFor: { name: string, types?: ObjectType[] }) { + lookFor.name = lookFor.name.toUpperCase(); + return this.getResolvedObjects().find(o => (o.systemName === lookFor.name || o.longName?.toUpperCase() === lookFor.name) && (lookFor.types === undefined || lookFor.types.includes(o.type))); + } + + public async resolveLocalFile(name: string, baseFile?: string): Promise { + name = name.toUpperCase(); + + if (this.resolvedSearches[name]) return this.resolvedSearches[name]; + + if (!this.pathCache) { + this.pathCache = {}; + + (await this.fs.getFiles(this.getCwd(), `**/*`, { + cwd: this.cwd, + absolute: true, + nocase: true, + })).forEach(localPath => { + this.pathCache[localPath] = true; + }); + } + + 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; + } + } + } + } + + const result = searchCache(); + + if (result) { + // To local path is required because glob returns posix paths + const localPath = toLocalPath(result) + this.resolvedSearches[name] = localPath; + return localPath; + } + } + + // TODO: move this to language provider + getObjectType(relativePath: string, ext: string): ObjectType { + const objType = this.languageProvider.getObjectType(ext); + + if (!objType) { + this.logger.fileLog(relativePath, { + type: `warning`, + message: `'${ext}' not found a matching object type. Defaulting to '${ext}'` + }); + + return (ext.toUpperCase() as ObjectType); + } + + return objType; + } + + public loadObjectsFromPaths(paths: string[]) { + // optimiseFileList(paths); //Ensure we load SQL files first + return Promise.all(paths.map(p => this.resolvePathToObject(p))); + } + + public async parseFile(filePath: string) { + const pathDetail = path.parse(filePath); + const relative = this.getRelative(filePath); + + let success = true; + + if (pathDetail.ext.length > 1) { + if (!this.actionSuggestions.renames) { + // Don't clear the logs if we're suggestion renames. + this.logger.flush(relative); + } + + const ext = pathDetail.ext.substring(1).toLowerCase(); + + try { + const content = await this.fs.readFile(filePath); + + // Really only applied to rpg + const isFree = (content.length >= 6 ? content.substring(0, 6).toLowerCase() === `**free` : false); + + let textMatch; + try { + [textMatch] = content.match(TextRegex); + if (textMatch) { + if (textMatch.startsWith(`%TEXT`)) textMatch = textMatch.substring(5); + if (textMatch.endsWith(`*`)) textMatch = textMatch.substring(0, textMatch.length - 1); + textMatch = textMatch.trim(); + } + } catch (e) { } + + const options: FileOptions = { + isFree, + text: textMatch + }; + + await this.languageProvider.handleLanguage(filePath, content, options); + } catch (e) { + this.logger.fileLog(relative, { + message: `Failed to parse file.`, + type: `warning` + }); + + console.log(relative); + console.log(e); + + success = false; + } + + infoOut(``); + } else { + success = false; + } + + return success; + + } + + + getTarget(object: ILEObject): ILEObjectTarget | undefined { + return this.targets[`${object.systemName}.${object.type}`]; + } + + getTargets(): ILEObjectTarget[] { + return Object.values(this.targets).filter(x => x); + } + + // Generates targets for service programs and binding directories + public resolveBinder() { + // Right now, we really only support single module programs and service programs + + const allTargets = this.getTargets(); + + // We can simply check for any modules since we turn them into service programs + this.needsBinder = allTargets.some(d => d.type === `SRVPGM`); + + infoOut(``); + + // We need to loop through all the user-defined server programs (binder source) + // And resolve the service program program exports to module exports to bind them together nicely + const allSrvPgms = this.getTargetsOfType(`SRVPGM`); + const allModules = this.getTargetsOfType(`MODULE`); + + for (const target of allSrvPgms) { + if (target.exports) { + infoOut(`Resolving modules for ${target.systemName}.${target.type}`); + + target.deps = []; + + for (const exportName of target.exports) { + // We loop through each export of the service program and find the module that exports it + const foundModule = allModules.find(mod => mod.exports && mod.exports.includes(exportName.toUpperCase())); + if (foundModule) { + const alreadyBound = target.deps.some(dep => dep.systemName === foundModule.systemName && dep.type === `MODULE`); + if (!alreadyBound) { + infoOut(`Adding module ${foundModule.systemName}.${foundModule.type}`); + target.deps.push(foundModule); + } + } + } + + if (target.deps.length > 0) { + // Add this new service program to the project binding directory + this.createOrAppend(this.projectBindingDirectory, target); + + // Make sure we can resolve to this service program + for (const e of target.exports) { + this.resolvedExports[e.toUpperCase()] = target; + } + } else { + // This service program target doesn't have any deps... so, it's not used? + this.removeObject(target); + + if (target.relativePath) { + this.logger.fileLog(target.relativePath, { + message: `Removed as target because no modules were found with matching exports.`, + type: `info` + }); + } + } + + infoOut(``); + } + } + + // We loop through all programs and module and study their imports. + // We do this in case they depend on another service programs based on import + for (let currentTarget of allTargets) { + if ([`PGM`, `MODULE`].includes(currentTarget.type) && currentTarget.imports) { + let newImports: ILEObject[] = []; + + // Remove any service program deps so we can resolve them cleanly + currentTarget.deps = currentTarget.deps.filter(d => ![`SRVPGM`].includes(d.type)); + + for (const importName of currentTarget.imports) { + // Find if this import resolves to another object + const possibleSrvPgmDep = this.resolvedExports[importName.toUpperCase()]; + // We can't add a module as a dependency at this step. + if (possibleSrvPgmDep && possibleSrvPgmDep.type === `SRVPGM`) { + // Make sure we haven't imported it before! + if (!newImports.some(i => i.systemName === possibleSrvPgmDep.systemName && i.type === possibleSrvPgmDep.type)) { + newImports.push(possibleSrvPgmDep); + } + + } else if ([`PGM`, `MODULE`].includes(currentTarget.type)) { + // Perhaps we're looking at a program object, which actually should be a multi + // module program, so we do a lookup for additional modules. + const possibleModuleDep = allModules.find(mod => mod.exports && mod.exports.includes(importName.toUpperCase())) + if (possibleModuleDep) { + if (!newImports.some(i => i.systemName === possibleModuleDep.systemName && i.type === possibleModuleDep.type)) { + newImports.push(possibleModuleDep); + + // TODO: consider other IMPORTS that `possibleModuleDep` needs. + } + } + } + }; + + // If the program or module has imports that we ca resolve, then we add them as deps + if (newImports.length > 0) { + infoOut(`${currentTarget.systemName}.${currentTarget.type} has additional dependencies: ${newImports.map(i => `${i.systemName}.${i.type}`)}`); + currentTarget.deps.push(...newImports); + + if (currentTarget.type === `PGM`) { + // If this program has MODULE dependecies, that means we need to change the way it's compiled + // to be a program made up of many modules, usually done with CRTPGM + if (currentTarget.deps.some(d => d.type === `MODULE`)) { + this.convertBoundProgramToMultiModuleProgram(currentTarget); + + // Then, also include any of the modules dep modules into the currentTarget deps!! + const depTargets = currentTarget.deps + .filter(d => d.type === `MODULE`) + .map(m => this.getTarget(m)); + + // Confusing names, it means: dependencies of the dependencies that are modules + const depDeps = depTargets.map(m => m?.deps).flat().filter(d => d.type === `MODULE`); + + for (const newDep of depDeps) { + if (newDep && !currentTarget.deps.some(d => d.systemName === newDep.systemName && d.type === newDep.type)) { + currentTarget.deps.push(newDep); + } + } + } + } + } + } + } + + const commandObjects = this.getResolvedObjects(`CMD`); + for (let cmdObject of commandObjects) { + // Check if a program exists with the same name. + const programObject = this.getTarget({ systemName: cmdObject.systemName, type: `PGM` }); + if (programObject) { + const newTarget = { + ...cmdObject, + deps: [programObject] + } + + this.addNewTarget(newTarget); + } else { + + this.removeObject(cmdObject); + this.logger.fileLog(cmdObject.relativePath, { + message: `Removed as target because no program was found with a matching name.`, + type: `info` + }); + } + } + } + + private convertBoundProgramToMultiModuleProgram(currentTarget: ILEObjectTarget) { + const basePath = currentTarget.relativePath; + + // First, let's change this current target to be solely a program + // Change the extension so it's picked up correctly during the build process. + currentTarget.extension = `pgm`; + currentTarget.relativePath = undefined; + + // Store a fake path for this program object + this.storeResolved(path.join(this.cwd, `${currentTarget.systemName}.PGM`), currentTarget); + + // Then we can create the new module object from this path + const newModule: ILEObject = { + systemName: currentTarget.systemName, + imports: currentTarget.imports, + exports: [], + headers: currentTarget.headers, + type: `MODULE`, + relativePath: basePath, + extension: path.extname(basePath).substring(1) + }; + + // Replace the old resolved object with the module + this.storeResolved(path.join(this.cwd, basePath), newModule); + + // Create a new target for the module + const newModTarget = this.createOrAppend(newModule); + + // Clean up imports for module and program + newModTarget.imports = currentTarget.imports; + currentTarget.imports = undefined; + currentTarget.headers = undefined; + + this.createOrAppend(currentTarget, newModule); + } + + public createOrAppend(parentObject: ILEObject, newDep?: ILEObject) { + let existingTarget = this.targets[`${parentObject.systemName}.${parentObject.type}`]; + + if (!existingTarget) { + existingTarget = { + ...parentObject, + deps: [] + }; + + this.addNewTarget(existingTarget); + } + + if (newDep) + existingTarget.deps.push(newDep); + + return existingTarget; + } + + public addNewTarget(dep: ILEObjectTarget) { + this.targets[`${dep.systemName}.${dep.type}`] = dep; + } + + public binderRequired() { + return this.needsBinder; + } + + public getTargetsOfType(type: ObjectType): ILEObjectTarget[] { + return this.getTargets().filter(d => d && d.type === type); + } + + public getResolvedObjects(type?: ObjectType): ILEObject[] { + const objects = Object.values(this.resolvedObjects); + + return objects.filter(o => o && (type === undefined || o.type === type)); + } + + public getResolvedObject(fullPath: string): ILEObject { + return this.resolvedObjects[fullPath]; + } + + /** + * This API is a little trick. + * You can pass in a valid file extension, or if you pass + * solely just `pgm`, it will return all programs that + * have multiple modules. + */ + public getResolvedObjectsByFileExtension(ext: string): ILEObject[] { + const extensionParts = ext.split(`.`); + let extension = ext.toUpperCase(), shouldBeProgram = false, anyPrograms = false; + + if (extensionParts.length === 2 && extensionParts[0].toUpperCase() === `PGM`) { + extension = extensionParts[1].toUpperCase(); + shouldBeProgram = true; + } else if (extension === `PGM`) { + anyPrograms = true; + } + + return Object.values(this.resolvedObjects).filter(obj => + (obj.extension?.toUpperCase() === extension && (obj.type === `PGM`) === shouldBeProgram) || + (anyPrograms === true && obj.type === `PGM` && obj.extension.toUpperCase() === extension) + ); + } + + public getExports() { + return this.resolvedExports; + } + + /** + * Returns a list of objects that will be impacted if the given object is changed. + */ + public getImpactFor(theObject: ILEObject) { + const allDeps = this.getTargets(); + let currentTree: ILEObject[] = []; + + let currentItem: ImpactedObject = { ileObject: theObject, children: [] }; + + function lookupObject(currentItem: ImpactedObject) { + currentTree.push(currentItem.ileObject); + + for (const target of allDeps) { + const containsLookup = target.deps.some(d => d.systemName === currentItem.ileObject.systemName && d.type === currentItem.ileObject.type); + const circular = currentTree.some(d => d.systemName === target.systemName && d.type === target.type); + + if (containsLookup && !circular) { + let newDependant: ImpactedObject = { ileObject: target, children: [] }; + lookupObject(newDependant); + currentItem.children.push(newDependant); + } + } + + currentTree.pop(); + } + + lookupObject(currentItem); + + return currentItem; + } + + /** + * This is used when loading in all objects. + * SQL sources can have two object names: a system name and a long name + * Sadly the long name is not typically part of the path name, so we need to + * find the name inside of the source code. + */ + async sqlObjectDataFromPath(fullPath: string): Promise { + const relativePath = this.getRelative(fullPath); + + if (await this.fs.exists(fullPath)) { + const content = await this.fs.readFile(fullPath); + const document = new Document(content); + + const groups = document.getStatementGroups(); + + if (groups.length === 0) { + this.logger.fileLog(relativePath, { + message: `No SQL statements found in file.`, + type: `info` + }); + + return; + } + + const createCount = groups.filter(g => g.statements[0].type === StatementType.Create).length; + + if (createCount > 1) { + this.logger.fileLog(relativePath, { + message: `Includes multiple create statements. They should be in individual sources. This file will not be parsed.`, + type: `warning`, + }); + } + + const firstGroup = groups[0]; + const create = firstGroup.statements.find(s => s.type === StatementType.Create); + + if (create) { + const defs = create.getObjectReferences(); + const mainDef = defs.find(d => d.createType); + + return mainDef; + } + } + } +} \ No newline at end of file diff --git a/cli/src/targets/languages.ts b/cli/src/targets/languages.ts new file mode 100644 index 0000000..63014f2 --- /dev/null +++ b/cli/src/targets/languages.ts @@ -0,0 +1,69 @@ +import { FileOptions, ObjectType, Targets } from "."; +import { clExtensions, clleTargetCallback, clObjects } from "./languages/clle"; +import { ddsExtension, ddsObjects, ddsTargetCallback } from "./languages/dds"; +import { rpgExtensions, rpgleTargetParser, rpgObjects } from "./languages/rpgle"; +import { sqlExtensions, sqlObjects, sqlTargetCallback } from "./languages/sql"; +import { binderExtensions, binderObjects, binderTargetCallback } from "./languages/binder"; +import { cmdExtensions, cmdObjects, cmdTargetCallback } from "./languages/cmd"; +import { noSourceObjects, noSourceTargetCallback, noSourceTargetObjects } from "./languages/nosrc"; + +export type LanguageCallback = (targets: Targets, relativePath: string, content: string, options: FileOptions) => Promise +interface LanguageGroup { + extensions: string[]; + callback: LanguageCallback; +} + +export type ExtensionMap = {[ext: string]: ObjectType}; + +export class TargetsLanguageProvider { + private languageTargets: LanguageGroup[] = []; + private extensionMap: ExtensionMap = {}; + + constructor(private readonly targets: Targets) { + const rpgleTargets = new rpgleTargetParser(this.targets); + + this.registerLanguage(clExtensions, clleTargetCallback, clObjects); + this.registerLanguage(sqlExtensions, sqlTargetCallback, sqlObjects); + this.registerLanguage(ddsExtension, ddsTargetCallback, ddsObjects); + this.registerLanguage(binderExtensions, binderTargetCallback, binderObjects); + this.registerLanguage(cmdExtensions, cmdTargetCallback, cmdObjects); + this.registerLanguage(noSourceObjects, noSourceTargetCallback, noSourceTargetObjects); + + this.registerLanguage(rpgExtensions, (tazrgets, relativePath, content, options) => { + return rpgleTargets.rpgleTargetCallback(targets, relativePath, content, options); + }, rpgObjects); + } + + public getExtensions() { + return this.languageTargets.map(lang => lang.extensions).flat(); + } + + public getGlob() { + const allExtensions = this.getExtensions(); + return `**/*.{${allExtensions.join(`,`)},${allExtensions.map(e => e.toUpperCase()).join(`,`)}}`; + } + + public async handleLanguage(relativePath: string, content: string, options: FileOptions = {}) { + const ext = relativePath.split('.').pop()?.toLowerCase(); + const language = this.languageTargets.find(lang => lang.extensions.includes(ext)); + if (ext && language) { + await language.callback(this.targets, relativePath, content, options); + } + } + + public registerLanguage(extensions: string[], callback: LanguageCallback, objectTypes: ExtensionMap = {}) { + for (const ext of extensions) { + if (this.languageTargets.some(lang => lang.extensions.includes(ext))) { + throw new Error(`Language with extension '${ext}' is already registered.`); + } + } + + this.extensionMap = {...this.extensionMap, ...objectTypes}; + + this.languageTargets.push({extensions, callback}); + } + + public getObjectType(ext: string): ObjectType | undefined { + return this.extensionMap[ext.toLowerCase()]; + } +} \ No newline at end of file diff --git a/cli/src/targets/languages/binder.ts b/cli/src/targets/languages/binder.ts new file mode 100644 index 0000000..a8e38b9 --- /dev/null +++ b/cli/src/targets/languages/binder.ts @@ -0,0 +1,89 @@ +import path from "path"; +import { CLParser, DefinitionType, Module, File } from "vscode-clle/language"; +import { FileOptions, ILEObjectTarget, Targets } from ".."; +import { infoOut } from "../../cli"; +import { trimQuotes } from "../../utils"; +import { ExtensionMap } from "../languages"; + +export const binderExtensions = [`binder`, `bnd`]; +export const binderObjects: ExtensionMap = { + binder: `SRVPGM`, + bnd: `SRVPGM`, +} + +export async function binderTargetCallback(targets: Targets, localPath: string, content: string, options: FileOptions) { + const clDocs = new CLParser(); + const tokens = clDocs.parseDocument(content); + + const module = new Module(); + module.parseStatements(tokens); + + const ileObject = await targets.resolvePathToObject(localPath, options.text); + + const target: ILEObjectTarget = { + ...ileObject, + deps: [], + exports: [] + }; + + if (ileObject.extension === `binder`) { + const pathDetail = path.parse(localPath); + + if (targets.suggestions.renames) { + targets.logger.fileLog(ileObject.relativePath, { + message: `Rename suggestion`, + type: `rename`, + change: { + rename: { + path: localPath, + newName: pathDetail.name + `.bnd` + } + } + }); + } else { + targets.logger.fileLog(ileObject.relativePath, { + message: `Extension is '${ileObject.extension}'. Consolidate by using 'bnd'?`, + type: `warning`, + }); + } + } + + const validStatements = module.statements.filter(s => { + const possibleObject = s.getObject(); + return (possibleObject && possibleObject.name && [`STRPGMEXP`, `ENDPGMEXP`, `EXPORT`].includes(possibleObject.name.toUpperCase())); + }); + + for (const statement of validStatements) { + const currentCommand = statement.getObject().name.toUpperCase(); + if (currentCommand === `EXPORT`) { + const parms = statement.getParms(); + const symbolTokens = parms[`SYMBOL`]; + + if (symbolTokens.block && symbolTokens.block.length === 1 && symbolTokens.block[0].type === `string` && symbolTokens.block[0].value) { + target.exports.push(trimQuotes(symbolTokens.block[0].value)); + } else + if (symbolTokens.block && symbolTokens.block.length === 1 && symbolTokens.block[0].type === `word` && symbolTokens.block[0].value) { + target.exports.push(trimQuotes(symbolTokens.block[0].value, `"`)); + } else { + targets.logger.fileLog(ileObject.relativePath, { + message: `Invalid EXPORT found. Single quote string expected.`, + type: `warning`, + range: { + start: symbolTokens.range.start, + end: symbolTokens.range.end + } + }) + } + + } else + if (currentCommand === `ENDPGMEXP`) { + // Return, we only really care about the first export block + break; + } + } + + // Exports are always uppercase + target.exports = target.exports.map(e => e.toUpperCase()); + + targets.addNewTarget(target); +} \ No newline at end of file diff --git a/cli/src/targets/languages/clle.ts b/cli/src/targets/languages/clle.ts new file mode 100644 index 0000000..a8de2b7 --- /dev/null +++ b/cli/src/targets/languages/clle.ts @@ -0,0 +1,159 @@ +import path from "path"; +import { CLParser, DefinitionType, Module, File } from "vscode-clle/language"; +import { FileOptions, ILEObjectTarget, Targets } from ".."; +import { infoOut } from "../../cli"; +import { ExtensionMap } from "../languages"; + +export const clExtensions = [`clle`, `cl`, `clp`]; +export const clObjects: ExtensionMap = { + clle: `MODULE`, + cl: `MODULE`, + clp: `PGM` +} + +export async function clleTargetCallback(targets: Targets, filePath: string, content: string, options: FileOptions) { + const clDocs = new CLParser(); + const tokens = clDocs.parseDocument(content); + + const module = new Module(); + module.parseStatements(tokens); + + const ileObject = await targets.resolvePathToObject(filePath); + + const pathDetail = path.parse(filePath); + const target: ILEObjectTarget = { + ...ileObject, + deps: [] + }; + + infoOut(`${ileObject.systemName}.${ileObject.type}: ${ileObject.relativePath}`); + + if (ileObject.extension?.toLowerCase() === `clp`) { + if (targets.suggestions.renames) { + targets.logger.fileLog(ileObject.relativePath, { + message: `Rename suggestion`, + type: `rename`, + change: { + rename: { + path: filePath, + newName: pathDetail.name + `.pgm.clle` + } + } + }); + } else { + targets.logger.fileLog(ileObject.relativePath, { + message: `Extension is '${ileObject.extension}', but Source Orbit doesn't support CLP. Is it possible the extension should use '.pgm.clle'?`, + type: `warning`, + }); + } + + } else { + if (ileObject.type === `MODULE`) { + if (targets.suggestions.renames) { + targets.logger.fileLog(ileObject.relativePath, { + message: `Rename suggestion`, + type: `rename`, + change: { + rename: { + path: filePath, + newName: pathDetail.name + `.pgm` + pathDetail.ext + } + } + }); + } else { + targets.logger.fileLog(ileObject.relativePath, { + message: `Type detected as ${ileObject.type} but Source Orbit doesn't support CL modules. Is it possible the extension should include '.pgm'?`, + type: `warning`, + }); + } + } + } + + const files = module.getDefinitionsOfType(DefinitionType.File); + + // Loop through local file defs to find a possible dep + files.forEach(def => { + const possibleObject = def.file; + if (possibleObject) { + if (possibleObject.library?.toUpperCase() === `*LIBL`) { + possibleObject.library = undefined; // targets means lookup as normal + } + + if (possibleObject.library) { + targets.logger.fileLog(ileObject.relativePath, { + message: `Definition to ${possibleObject.library}/${possibleObject.name} ignored due to qualified path.`, + range: { + start: def.range.start, + end: def.range.end + }, + type: `info`, + }); + + } else { + if (Targets.ignoredObjects.includes(possibleObject.name.toUpperCase())) return; + + const resolvedPath = targets.searchForObject({ systemName: possibleObject.name.toUpperCase(), type: `FILE` }); + if (resolvedPath) target.deps.push(resolvedPath); + else { + targets.logger.fileLog(ileObject.relativePath, { + message: `no object found for reference '${possibleObject.name}'`, + range: { + start: def.range.start, + end: def.range.end + }, + type: `warning`, + }); + } + } + } + }); + + module.statements.filter(s => { + const possibleObject = s.getObject(); + return (possibleObject && possibleObject.name && possibleObject.name === `CALL`); + }).forEach(s => { + + const parms = s.getParms(); + const pgmParm = parms[`PGM`]; + + if (pgmParm && pgmParm.block) { + const block = pgmParm.block; + if (block.length === 1) { + const name = block[0].value!; + + if (Targets.ignoredObjects.includes(name.toUpperCase())) return; + + const resolvedPath = targets.searchForObject({ systemName: name.toUpperCase(), type: `PGM` }); + if (resolvedPath) target.deps.push(resolvedPath); + else { + targets.logger.fileLog(ileObject.relativePath, { + message: `no object found for reference '${name}'`, + range: { + start: pgmParm.range.start, + end: pgmParm.range.end + }, + type: `warning`, + }); + } + } else { + targets.logger.fileLog(ileObject.relativePath, { + message: `PGM call not included as possible reference to library.`, + range: { + start: pgmParm.range.start, + end: pgmParm.range.end + }, + type: `info`, + }); + } + } + }); + + // We also look to see if there is a `.cmd` object with the same name + const possibleCommandObject = targets.searchForObject({ systemName: ileObject.systemName, type: `CMD` }); + if (possibleCommandObject) targets.createOrAppend(possibleCommandObject, target); + + if (target.deps.length > 0) + infoOut(`Depends on: ${target.deps.map(d => `${d.systemName}.${d.type}`).join(` `)}`); + + targets.addNewTarget(target); +} \ No newline at end of file diff --git a/cli/src/targets/languages/cmd.ts b/cli/src/targets/languages/cmd.ts new file mode 100644 index 0000000..afcf555 --- /dev/null +++ b/cli/src/targets/languages/cmd.ts @@ -0,0 +1,11 @@ +import { FileOptions, Targets } from ".."; +import { ExtensionMap } from "../languages"; + +export const cmdExtensions = [`cmd`]; +export const cmdObjects: ExtensionMap = { + cmd: `CMD` +} + +export async function cmdTargetCallback(targets: Targets, localPath: string, content: string, options: FileOptions) { + targets.resolvePathToObject(localPath, options.text); +} \ No newline at end of file diff --git a/cli/src/targets/languages/dds.ts b/cli/src/targets/languages/dds.ts new file mode 100644 index 0000000..3601368 --- /dev/null +++ b/cli/src/targets/languages/dds.ts @@ -0,0 +1,114 @@ +import path from "path"; +import { DisplayFile as dds } from "vscode-displayfile/src/dspf"; +import { FileOptions, ILEObjectTarget, Targets } from ".."; +import { infoOut } from "../../cli"; +import { ExtensionMap } from "../languages"; + +export const ddsExtension = [`pf`, `lf`, `dspf`, `prtf`]; +export const ddsObjects: ExtensionMap = { + pf: `FILE`, + lf: `FILE`, + dspf: `FILE`, + prtf: `FILE` +} + +export async function ddsTargetCallback(targets: Targets, filePath: string, content: string, options: FileOptions) { + const eol = content.indexOf(`\r\n`) >= 0 ? `\r\n` : `\n`; + + const ddsFile = new dds(); + ddsFile.parse(content.split(eol)); + + const ileObject = await targets.resolvePathToObject(filePath, options.text); + + const target: ILEObjectTarget = { + ...ileObject, + deps: [] + }; + + infoOut(`${ileObject.systemName}.${ileObject.type}: ${ileObject.relativePath}`); + + // We have a local cache of refs found so we don't keep doing global lookups + // on objects we already know to depend on in this object. + + let alreadyFoundRefs: string[] = []; + + const handleObjectPath = (currentKeyword: string, recordFormat: any, value: string) => { + const qualified = value.split(`/`); + + let objectName: string | undefined; + if (qualified.length === 2 && qualified[0].toLowerCase() === `*libl`) { + objectName = qualified[1]; + } else if (qualified.length === 1) { + objectName = qualified[0]; + } + + if (objectName) { + const upperName = objectName.toUpperCase(); + if (alreadyFoundRefs.includes(upperName)) return; + + const resolvedPath = targets.searchForObject({ systemName: upperName, type: `FILE` }); + if (resolvedPath) { + target.deps.push(resolvedPath); + alreadyFoundRefs.push(upperName); + } + else { + targets.logger.fileLog(ileObject.relativePath, { + message: `no object found for reference '${objectName}'`, + type: `warning`, + line: recordFormat.range.start + }); + } + } else { + targets.logger.fileLog(ileObject.relativePath, { + message: `${currentKeyword} reference not included as possible reference to library found.`, + type: `info`, + line: recordFormat.range.start + }); + } + } + + // PFILE -> https://www.ibm.com/docs/en/i/7.5?topic=80-pfile-physical-file-keywordlogical-files-only + // REF -> https://www.ibm.com/docs/en/i/7.5?topic=80-ref-reference-keywordphysical-files-only + + const ddsRefKeywords = [`PFILE`, `REF`, `JFILE`]; + + for (const recordFormat of ddsFile.formats) { + + // Look through this record format keywords for the keyword we're looking for + for (const keyword of ddsRefKeywords) { + const keywordObj = recordFormat.keywords.find(k => k.name === keyword); + if (keywordObj) { + const wholeValue: string = keywordObj.value; + const parts = wholeValue.split(` `).filter(x => x.length > 0); + + // JFILE can have multiple files referenced in it, whereas + // REF and PFILE can only have one at the first element + const pathsToCheck = (keyword === `JFILE` ? parts.length : 1); + + for (let i = 0; i < pathsToCheck; i++) { + handleObjectPath(keyword, recordFormat, parts[i]); + } + } + } + + // REFFLD -> https://www.ibm.com/docs/en/i/7.5?topic=80-reffld-referenced-field-keywordphysical-files-only + + // Then, let's loop through the fields in this format and see if we can find REFFLD + for (const field of recordFormat.fields) { + const refFld = field.keywords.find(k => k.name === `REFFLD`); + + if (refFld) { + const [fieldRef, fileRef] = refFld.value.trim().split(` `); + + if (fileRef) { + handleObjectPath(`REFFLD`, recordFormat, fileRef); + } + } + } + } + + if (target.deps.length > 0) + infoOut(`Depends on: ${target.deps.map(d => `${d.systemName}.${d.type}`).join(` `)}`); + + targets.addNewTarget(target); +} \ No newline at end of file diff --git a/cli/src/targets/languages/nosrc.ts b/cli/src/targets/languages/nosrc.ts new file mode 100644 index 0000000..66536ba --- /dev/null +++ b/cli/src/targets/languages/nosrc.ts @@ -0,0 +1,15 @@ +import { Targets, FileOptions } from ".."; +import { ExtensionMap } from "../languages"; + +export const noSourceObjects = [`dtaara`, `mnucmd`, `msgf`, `dtaq`, `bnddir`]; +export const noSourceTargetObjects: ExtensionMap = { + dtaara: `DTAARA`, + mnucmd: `CMD`, + msgf: `FILE`, + dtaq: `DTAQ`, + bnddir: `BNDDIR` +} + +export async function noSourceTargetCallback(targets: Targets, localPath: string, content: string, options: FileOptions) { + targets.resolvePathToObject(localPath, options.text); +} \ No newline at end of file diff --git a/cli/src/targets/languages/rpgle.ts b/cli/src/targets/languages/rpgle.ts new file mode 100644 index 0000000..4afb709 --- /dev/null +++ b/cli/src/targets/languages/rpgle.ts @@ -0,0 +1,476 @@ +import path from "path"; +import { FileOptions, ILEObjectTarget, Targets } from ".."; +import { infoOut } from "../../cli"; +import Parser from "vscode-rpgle/language/parser"; +import { IncludeStatement } from "vscode-rpgle/language/parserTypes"; +import { asPosix, toLocalPath, trimQuotes } from "../../utils"; +import { isSqlFunction } from "../../languages/sql"; +import { ExtensionMap } from "../languages"; + +export const rpgExtensions = [`sqlrpgle`, `rpgle`]; +export const rpgObjects: ExtensionMap = { + sqlrpgle: `MODULE`, + rpgle: `MODULE`, +} + +interface RpgLookup { + lookup: string, + line?: number +} + +export class rpgleTargetParser { + private parser: Parser; + private includeFileCache: { [path: string]: string } = {}; + + constructor(private targets: Targets) { + this.parser = this.setupParser(targets); + } + + public async rpgleTargetCallback(targets: Targets, localPath: string, content: string, options: FileOptions) { + const cache = await this.parser.getDocs( + localPath, + content, + { + ignoreCache: true, + withIncludes: true + } + ); + + if (cache) { + const ileObject = await this.targets.resolvePathToObject(localPath, options.text); + + const pathDetail = path.parse(localPath); + // define internal imports + ileObject.imports = cache.procedures + .filter((proc: any) => proc.keyword[`EXTPROC`] && !proc.keyword[`EXPORT`]) + .map(ref => { + const keyword = ref.keyword; + let importName: string = ref.name; + const extproc: string | boolean = keyword[`EXTPROC`]; + if (extproc) { + if (extproc === true) importName = ref.name; + else importName = extproc; + } + + if (importName.includes(`:`)) { + const parmParms = importName.split(`:`); + importName = parmParms.filter(p => !p.startsWith(`*`)).join(``); + } + + if (importName.startsWith(`*`)) { + importName = ref.name; + } else { + importName = trimQuotes(importName); + } + + return importName; + }); + + // define exported functions + if (cache.keyword[`NOMAIN`]) { + ileObject.type = `MODULE`; + + // Note that we store exports as uppercase. + ileObject.exports = cache.procedures + .filter((proc: any) => proc.keyword[`EXPORT`]) + .map(ref => ref.name.toUpperCase()); + } + + infoOut(`${ileObject.systemName}.${ileObject.type}: ${ileObject.relativePath}`); + + if (cache.includes && cache.includes.length > 0) { + ileObject.headers = []; + + cache.includes.forEach((include: IncludeStatement) => { + // RPGLE includes are always returned as posix paths + // even on Windows. We need to do some magic to convert here for Windows systems + include.toPath = toLocalPath(include.toPath); + + const includeDetail = path.parse(include.toPath); + + if (includeDetail.ext.toLowerCase() !== `.rpgleinc`) { + const possibleName = includeDetail.name.toLowerCase().endsWith(`.pgm`) ? includeDetail.name.substring(0, includeDetail.name.length - 4) : includeDetail.name; + + if (this.targets.suggestions.renames) { + const renameLogPath = this.targets.getRelative(include.toPath); + + // We need to make sure the .rpgleinc rename is most important + if (this.targets.logger.exists(renameLogPath, `rename`)) { + this.targets.logger.flush(renameLogPath); + } + + this.targets.logger.fileLog(renameLogPath, { + message: `Rename suggestion`, + type: `rename`, + change: { + rename: { + path: include.toPath, + newName: `${possibleName}.rpgleinc` + } + } + }); + } else { + this.targets.logger.fileLog(this.targets.getRelative(include.toPath), { + message: `referenced as include, but should use the '.rpgleinc' extension.`, + type: `warning`, + }); + } + } + + const theIncludePath = asPosix(this.targets.getRelative(include.toPath)); + + ileObject.headers.push(theIncludePath); + + if (this.targets.suggestions.includes) { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `Will update to use unix style path.`, + type: `includeFix`, + line: include.line, + change: { + lineContent: (options.isFree ? `` : ``.padEnd(6)) + `/copy '${theIncludePath}'` + } + }); + } else { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `Include at line ${include.line} found, to path '${theIncludePath}'`, + type: `info`, + line: include.line, + }); + } + }); + } + + const target: ILEObjectTarget = { + ...ileObject, + deps: [] + }; + + // This usually means .pgm is in the name + if (ileObject.type === `PGM` && cache.keyword[`NOMAIN`]) { + const possibleName = pathDetail.name.toLowerCase().endsWith(`.pgm`) ? pathDetail.name.substring(0, pathDetail.name.length - 4) : pathDetail.name; + + if (this.targets.suggestions.renames) { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `Rename suggestion`, + type: `rename`, + change: { + rename: { + path: localPath, + newName: possibleName + pathDetail.ext + } + } + }) + } else { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `type detected as ${ileObject.type} but NOMAIN keyword found.`, + type: `warning`, + }); + } + } + + // This usually means it's source name is a module (no .pgm) but doesn't have NOMAIN. + // We need to do this for other language too down the line + if (ileObject.type === `MODULE` && !cache.keyword[`NOMAIN`]) { + if (this.targets.suggestions.renames) { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `Rename suggestion`, + type: `rename`, + change: { + rename: { + path: localPath, + newName: pathDetail.name + `.pgm` + pathDetail.ext + } + } + }); + } else { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `type detected as ${ileObject.type} but NOMAIN keyword was not found. Is it possible the extension should include '.pgm'?`, + type: `warning`, + }); + } + } + + if (cache.keyword[`BNDDIR`]) { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `has the BNDDIR keyword. 'binders' property in iproj.json should be used instead.`, + type: `info`, + }); + } + + // Find external programs + cache.procedures + .filter((proc: any) => proc.keyword[`EXTPGM`]) + .map((ref): RpgLookup => { + const keyword = ref.keyword; + let fileName = ref.name; + const extpgm = keyword[`EXTPGM`]; + if (extpgm) { + if (extpgm === true) fileName = ref.name; + else fileName = trimQuotes(extpgm); + } + + return { + lookup: fileName.toUpperCase(), + line: ref.position ? ref.position.range.line : undefined + }; + }) + .forEach((ref: RpgLookup) => { + // Don't add ignored objects (usually system APIs) + if (Targets.ignoredObjects.includes(ref.lookup)) return; + // Don't add itself + if (ref.lookup === ileObject.systemName) return; + + const resolvedObject = this.targets.searchForObject({ systemName: ref.lookup, type: `PGM` }); + if (resolvedObject) { + // because of legacy fixed CALL, there can be dupliicate EXTPGMs with the same name :( + if (!target.deps.some(d => d.systemName === resolvedObject.systemName && d.type && resolvedObject.type)) { + target.deps.push(resolvedObject) + } + } + + else { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `No object found for reference '${ref.lookup}'`, + type: `warning`, + line: ref.line + }); + } + }); + + // Scan the multiple scopes available in an RPGLE program + const scopes = [cache, ...cache.procedures.map(p => p.scope)].filter(s => s); + + for (const scope of scopes) { + + // Find external data structure sources + scope.structs + .filter((struct: any) => struct.keyword[`EXTNAME`]) + .map((struct): RpgLookup => { + const keyword = struct.keyword; + const value = trimQuotes(keyword[`EXTNAME`]); + + return { + lookup: value.split(`:`)[0].toUpperCase(), + line: struct.position ? struct.position.range.line : undefined + }; + }) + .forEach((ref: RpgLookup) => { + const resolvedObject = this.targets.searchForObject({ systemName: ref.lookup, type: `FILE` }); + if (resolvedObject) target.deps.push(resolvedObject) + else { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `No object found for reference '${ref.lookup}'`, + type: `warning`, + line: ref.line + }); + } + }); + + // Find external files + scope.files + .map((file): RpgLookup => { + let possibleName: string = file.name; + const keyword = file.keyword; + + const extNameValue = keyword[`EXTFILE`]; + if (extNameValue) { + possibleName = trimQuotes(extNameValue).split(`:`)[0] + } + + if (possibleName.toLowerCase() === `*extdesc`) { + const extDescValue = keyword[`EXTDESC`]; + if (extDescValue) { + possibleName = trimQuotes(extDescValue); + } else { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `*EXTDESC is used for '${file.name}' but EXTDESC keyword not found`, + type: `warning`, + }); + } + } + + return { + lookup: possibleName.toUpperCase(), + line: file.position ? file.position.range.line : undefined + }; + }) + .forEach((ref: RpgLookup) => { + if (Targets.ignoredObjects.includes(ref.lookup)) return; + + const previouslyScanned = target.deps.some((r => (ref.lookup === r.systemName || ref.lookup === r.longName?.toUpperCase()) && r.type === `FILE`)); + if (previouslyScanned) return; + + const resolvedObject = this.targets.searchForObject({ systemName: ref.lookup, type: `FILE` }); + if (resolvedObject) target.deps.push(resolvedObject) + else { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `No object found for reference '${ref.lookup}'`, + type: `warning`, + line: ref.line + }); + } + }); + + // We ignore anything with hardcoded schemas + scope.sqlReferences + .filter(ref => !ref.description) + .map((ref): RpgLookup => ({ + lookup: trimQuotes(ref.name, `"`).toUpperCase(), + line: ref.position ? ref.position.range.line : undefined + })) + .forEach((ref: RpgLookup) => { + const previouslyScanned = target.deps.some((r => (ref.lookup === r.systemName || ref.lookup === r.longName?.toUpperCase()) && r.type === `FILE`)); + if (previouslyScanned) return; + const resolvedObject = this.targets.searchForObject({ systemName: ref.lookup, type: `FILE` }); + if (resolvedObject) target.deps.push(resolvedObject) + else if (!isSqlFunction(ref.lookup)) { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `No object found for reference '${ref.lookup}'`, + type: `warning`, + line: ref.line + }); + } + }); + + // Find external data areas + scope.structs + .filter((struct: any) => struct.keyword[`DTAARA`]) + .map((ref): RpgLookup => { + const keyword = ref.keyword; + let fileName: string = ref.name; + const dtaara = keyword[`DTAARA`]; + if (dtaara) { + if (dtaara === true) fileName = ref.name; + else fileName = trimQuotes(dtaara); + } + + return { + lookup: fileName.toUpperCase(), + line: ref.position ? ref.position.range.line : undefined + }; + }) + .forEach((ref: RpgLookup) => { + if (Targets.ignoredObjects.includes(ref.lookup.toUpperCase())) return; + + const resolvedObject = this.targets.searchForObject({ systemName: ref.lookup, type: `DTAARA` }); + if (resolvedObject) target.deps.push(resolvedObject) + else { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `No object found for reference '${ref.lookup}'`, + type: `warning`, + line: ref.line + }); + } + }); + + scope.variables + .filter((struct: any) => struct.keyword[`DTAARA`]) + .map((ref): RpgLookup => { + const keyword = ref.keyword; + let fileName: string = ref.name; + const dtaara = keyword[`DTAARA`]; + if (dtaara) { + if (dtaara === true) fileName = ref.name; + else fileName = trimQuotes(dtaara); + } + + return { + lookup: fileName.toUpperCase(), + line: ref.position ? ref.position.range.line : undefined + }; + }) + .forEach((ref: RpgLookup) => { + const resolvedObject = this.targets.searchForObject({ systemName: ref.lookup, type: `DTAARA` }); + if (resolvedObject) target.deps.push(resolvedObject) + else { + this.targets.logger.fileLog(ileObject.relativePath, { + message: `No object found for reference '${ref.lookup}'`, + type: `warning`, + line: ref.line + }); + } + }); + } + + // TODO: did we duplicate this? + // We also look to see if there is a `.cmd` object with the same name + const resolvedObject = this.targets.searchForObject({ systemName: ileObject.systemName, type: `CMD` }); + if (resolvedObject) this.targets.createOrAppend(resolvedObject, target); + + if (target.deps.length > 0) + infoOut(`Depends on: ${target.deps.map(d => `${d.systemName}.${d.type}`).join(` `)}`); + + this.targets.addNewTarget(target); + } + } + + setupParser(targets: Targets): Parser { + const parser = new Parser(); + + parser.setIncludeFileFetch(async (baseFile: string, includeFile: string) => { + if (includeFile.startsWith(`'`) && includeFile.endsWith(`'`)) { + includeFile = includeFile.substring(1, includeFile.length - 1); + } + + let file: string; + + if (includeFile.includes(`,`)) { + // If the member include path is qualified with a source file + // then we should convert to be a unix style path so we can + // search the explicit directories. + includeFile = includeFile.replace(/,/g, `/`) + `.*`; + + // Keep making the path less specific until we find a possible include + let parts = includeFile.split(`/`); + while (!file && parts.length > 0) { + file = await targets.resolveLocalFile(includeFile); + + if (!file) { + parts.shift(); + includeFile = parts.join(`/`); + } + } + } else if (!includeFile.includes(`/`)) { + const parent = path.basename(path.dirname(baseFile)); + console.log(parent); + includeFile = `${parent}/${includeFile}`; + + + file = await targets.resolveLocalFile(includeFile); + } else { + file = await targets.resolveLocalFile(includeFile); + } + + if (file) { + if (this.includeFileCache[file]) { + return { + found: true, + uri: file, + content: this.includeFileCache[file] + } + + } else { + const content = await targets.rfs.readFile(file); + this.includeFileCache[file] = content; + + return { + found: true, + uri: file, + content: content + } + } + } + + return { + found: false + }; + }); + + parser.setTableFetch(async (table: string, aliases = false) => { + // Can't support tables in CLI mode I suppose? + return []; + }); + + return parser; + } +} \ No newline at end of file diff --git a/cli/src/targets/languages/sql.ts b/cli/src/targets/languages/sql.ts new file mode 100644 index 0000000..c205a87 --- /dev/null +++ b/cli/src/targets/languages/sql.ts @@ -0,0 +1,263 @@ +import path from "path"; +import { FileOptions, ILEObject, ILEObjectTarget, Targets } from ".."; +import { infoOut } from "../../cli"; + +import Document from "vscode-db2i/src/language/sql/document"; +import { StatementType } from 'vscode-db2i/src/language/sql/types'; +import { isSqlFunction } from "../../languages/sql"; +import { trimQuotes } from "../../utils"; +import { ExtensionMap } from "../languages"; + +const sqlTypeExtension = { + 'TABLE': `table`, + 'VIEW': `view`, + 'PROCEDURE': `sqlprc`, + 'FUNCTION': `sqludf`, + 'TRIGGER': `sqltrg`, + 'ALIAS': `sqlalias`, + 'SEQUENCE': `sqlseq` +}; + +export const sqlExtensions = [`sql`, `table`, `view`, `index`, `alias`, `sqlprc`, `sqludf`, `sqludt`, `sqltrg`, `sqlalias`, `sqlseq`]; +export const sqlObjects: ExtensionMap = { + 'sql': `FILE`, + 'table': `FILE`, + 'view': `FILE`, + 'index': `FILE`, + 'alias': `FILE`, + 'sqludf': `FILE`, + 'sqludt': `FILE`, + 'sqlalias': `FILE`, + 'sqlseq': `FILE`, + 'sequence': `FILE`, + 'function': `SRVPGM`, + 'procedure': `PGM`, + 'sqlprc': `PGM`, + 'trigger': `PGM`, + 'sqltrg': `PGM` +} + +export async function sqlTargetCallback(targets: Targets, localPath: string, content: string, options: FileOptions) { + const document = new Document(content); + + const pathDetail = path.parse(localPath); + const relativePath = targets.getRelative(localPath); + + const groups = document.getStatementGroups(); + + // TODO: Note, this returns high level definitions. + // If the index/view/etc specifies a table dep, + // they will not appear as a dependency + + const createCount = groups.filter(g => g.statements[0].type === StatementType.Create).length; + + if (createCount > 1) { + targets.logger.fileLog(relativePath, { + message: `Includes multiple create statements. They should be in individual sources. This file will not be parsed.`, + type: `warning`, + }); + + return; + } + + for (const group of groups) { + const statement = group.statements[0]; + const defs = statement.getObjectReferences(); + const mainDef = defs[0]; + + if (mainDef && mainDef.createType && mainDef.object.name) { + const tokens = mainDef.tokens; + if (mainDef.object.schema) { + targets.logger.fileLog(relativePath, { + message: `${mainDef.object.schema}/${mainDef.object.name} (${mainDef.createType}) reference not included as possible reference to library found.`, + range: { + start: tokens[0].range.start, + end: tokens[tokens.length - 1].range.end + }, + type: `warning`, + }); + + } else { + switch (statement.type) { + // Alters are a little weird in that they can exist + // in any file, so we can't assume the current source + // is the name of the object. Sad times + case StatementType.Alter: + // We don't do anything for alter currently + // because it's too easy to create circular deps. + // This is bad!! + targets.logger.fileLog(relativePath, { + message: `${mainDef.object.name} (${mainDef.createType}) alter not tracked due to possible circular dependency.`, + range: { + start: tokens[0].range.start, + end: tokens[tokens.length - 1].range.end + }, + type: `info`, + }); + + // let currentTarget: ILEObjectTarget|undefined; + // const resolvedPath = targets.resolveLocalObjectQuery(mainDef.object.name + `.*`); + // const currentRelative = path.basename(resolvedPath); + // if (resolvedPath) { + // currentTarget = { + // ...targets.resolveObject(resolvedPath), + // deps: [] + // }; + // } + + // if (currentTarget) { + // info(`${currentTarget.name}.${currentTarget.type}`); + // info(`\tSource: ${currentTarget.relativePath}`); + + // if (defs.length > 1) { + // for (const def of defs.slice(1)) { + // const subResolvedPath = targets.resolveLocalObjectQuery(def.object.name + `.*`, currentRelative); + // if (subResolvedPath) currentTarget.deps.push(targets.resolveObject(subResolvedPath)) + // else info(`\tNo object found for reference '${def.object.name}'`); + // } + // } + + // if (currentTarget.deps.length > 0) { + // info(`Depends on: ${currentTarget.deps.map(d => `${d.name}.${d.type}`).join(` `)}`); + + // targets.pushDep(currentTarget); + // } + // } + break; + + // Creates should be in their own unique file + case StatementType.Create: + let hasLongName = mainDef.object.name && mainDef.object.name.length > 10 ? mainDef.object.name : undefined; + let objectName = mainDef.object.system || trimQuotes(mainDef.object.name, `"`); + + const extension = pathDetail.ext.substring(1); + + let ileObject: ILEObject = { + systemName: objectName.toUpperCase(), + longName: hasLongName, + type: targets.getObjectType(relativePath, mainDef.createType), + text: options.text, + relativePath, + extension + } + + let suggestRename = false; + const sqlFileName = pathDetail.name; + + // First check the file name + if (ileObject.systemName.length <= 10) { + if (ileObject.systemName.toUpperCase() !== sqlFileName.toUpperCase() && ileObject.longName !== sqlFileName) { + suggestRename = true; + } + } + + // Then make an extension suggestion + if (extension.toUpperCase() === `SQL` && mainDef.createType) { + suggestRename = true; + } + + // Let them know to use a system name in the create statement if one is not present + if (ileObject.systemName.length > 10 && mainDef.object.system === undefined) { + targets.logger.fileLog(ileObject.relativePath, { + message: `${ileObject.systemName} (${ileObject.type}) name is longer than 10 characters. Consider using 'FOR SYSTEM NAME' in the CREATE statement.`, + type: `warning`, + range: { + start: tokens[0].range.start, + end: tokens[tokens.length - 1].range.end + }, + }); + + suggestRename = false; + } + + let newTarget: ILEObjectTarget = { + ...ileObject, + deps: [] + }; + + infoOut(`${newTarget.systemName}.${newTarget.type}: ${newTarget.relativePath}`); + + // Now, let's go through all the other statements in this group (BEGIN/END) + // and grab any references to other objects :eyes: + let otherDefs = defs.slice(1); + + for (let i = 1; i < group.statements.length; i++) { + const currentStatement = group.statements[i]; + if ([StatementType.Alter, StatementType.Insert, StatementType.Delete, StatementType.With, StatementType.Select, StatementType.Call].includes(currentStatement.type)) { + otherDefs.push(...group.statements[i].getObjectReferences()); + } + } + + for (const def of otherDefs) { + const refTokens = def.tokens; + const simpleName = trimQuotes(def.object.name, `"`); + // TODO: do we need to look for SRVPGM (function) or PGM (procedure) here? + const resolvedObject = targets.searchForAnyObject({ name: simpleName, types: [`FILE`, `SRVPGM`, `PGM`] }); + if (resolvedObject) { + if (!newTarget.deps.find(d => d.systemName === resolvedObject.systemName && d.type === resolvedObject.type)) { + newTarget.deps.push(resolvedObject); + } + } + else if (!isSqlFunction(def.object.name)) { + targets.logger.fileLog(newTarget.relativePath, { + message: `No object found for reference '${def.object.name}'`, + type: `warning`, + range: { + start: refTokens[0].range.start, + end: refTokens[refTokens.length - 1].range.end + }, + }); + } + } + + if (newTarget.deps.length > 0) { + infoOut(`Depends on: ${newTarget.deps.map(d => `${d.systemName}.${d.type}`).join(` `)}`); + } + + // So we can later resolve the path to the created object + targets.storeResolved(localPath, ileObject); + + targets.addNewTarget(newTarget); + + // If the extension is SQL, let's make better suggestions + // based on the create type in the CREATE statement + if (suggestRename) { + const newExtension = sqlTypeExtension[mainDef.createType.toUpperCase()]; + + if (newExtension) { + const possibleName = (ileObject.longName ? ileObject.longName : ileObject.systemName.toLowerCase()) + `.` + newExtension; + + if (targets.suggestions.renames) { + const renameLogPath = relativePath; + + // We need to make sure the .rpgleinc rename is most important + if (targets.logger.exists(renameLogPath, `rename`)) { + targets.logger.flush(renameLogPath); + } + + targets.logger.fileLog(renameLogPath, { + message: `Rename suggestion`, + type: `rename`, + change: { + rename: { + path: localPath, + newName: possibleName + } + } + }); + } else { + targets.logger.fileLog(relativePath, { + message: `Extension should be based on type. Suggested name is '${possibleName}'`, + type: `warning`, + }); + } + } + } + + break; + } + + } + } + } +} \ No newline at end of file diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 268643e..08fdaba 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -314,4 +314,14 @@ export function globalEntryIsValid(fullPath: string, search: string, ignoreBase? } return false; +} + +export function trimQuotes(input: string|boolean, value = `'`) { + if (typeof input === `string`) { + if (input[0] === value) input = input.substring(1); + if (input[input.length - 1] === value) input = input.substring(0, input.length - 1); + return input; + } else { + return ''; + } } \ No newline at end of file diff --git a/cli/test/autofix.test.ts b/cli/test/autofix.test.ts index daf6d9a..899e01d 100644 --- a/cli/test/autofix.test.ts +++ b/cli/test/autofix.test.ts @@ -3,7 +3,6 @@ import { setupFixture } from "./fixtures/projects"; import { Targets } from '../src/targets' import { renameFiles } from "../src/utils"; -import { scanGlob } from "../src/extensions"; import * as path from "path"; import { ReadFileSystem } from "../src/readFileSystem"; diff --git a/cli/test/autofix2.test.ts b/cli/test/autofix2.test.ts index 0e0c6bb..396fe1f 100644 --- a/cli/test/autofix2.test.ts +++ b/cli/test/autofix2.test.ts @@ -3,7 +3,6 @@ import { setupFixture } from "./fixtures/projects"; import { Targets } from '../src/targets' import { renameFiles } from "../src/utils"; -import { scanGlob } from "../src/extensions"; import * as path from "path"; import { ReadFileSystem } from "../src/readFileSystem"; diff --git a/cli/test/includeMismatchFix.test.ts b/cli/test/includeMismatchFix.test.ts index 78f8a19..141f3e4 100644 --- a/cli/test/includeMismatchFix.test.ts +++ b/cli/test/includeMismatchFix.test.ts @@ -24,7 +24,7 @@ describe(`include_mismatch_fix tests`, () => { test(`Ensure rename is against correct file`, async () => { const articlePf = targets.getTarget({systemName: `ARTICLE`, type: `FILE`}); - expect(articlePf).toBeDefined();articlePf.relativePath + expect(articlePf).toBeDefined(); const articlePfLogs = targets.logger.getLogsFor(path.join(`QDDSSRC`, `ARTICLE.PF`)); expect(articlePfLogs.length).toBe(1); diff --git a/cli/test/overrideObjRef.test.ts b/cli/test/overrideObjRef.test.ts index 67cafb9..205203f 100644 --- a/cli/test/overrideObjRef.test.ts +++ b/cli/test/overrideObjRef.test.ts @@ -5,7 +5,7 @@ import path from 'path'; import { MakeProject } from '../src/builders/make'; import { getFiles } from '../src/utils'; import { setupFixture } from './fixtures/projects'; -import { referencesFileName, scanGlob } from '../src/extensions'; +import { referencesFileName } from '../src/extensions'; import { writeFileSync } from 'fs'; import { BobProject } from '../src/builders/bob'; import { ReadFileSystem } from '../src/readFileSystem'; diff --git a/cli/test/project.test.ts b/cli/test/project.test.ts index b041858..1a6129a 100644 --- a/cli/test/project.test.ts +++ b/cli/test/project.test.ts @@ -4,7 +4,6 @@ import { Targets } from '../src/targets' import path from 'path'; import { MakeProject } from '../src/builders/make'; import { setupFixture } from './fixtures/projects'; -import { scanGlob } from '../src/extensions'; import { writeFileSync } from 'fs'; import { getDefaultCompiles } from '../src/builders/environment'; import { ReadFileSystem } from '../src/readFileSystem';