From 2eb5ecfd6b527746d314a2e8b22c0571e483e9ea Mon Sep 17 00:00:00 2001 From: worksofliam Date: Tue, 29 Jul 2025 10:08:36 -0400 Subject: [PATCH 1/8] Initial work on parsing I specifications Signed-off-by: worksofliam --- language/models/{fixed.js => fixed.ts} | 120 ++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 13 deletions(-) rename language/models/{fixed.js => fixed.ts} (70%) diff --git a/language/models/fixed.js b/language/models/fixed.ts similarity index 70% rename from language/models/fixed.js rename to language/models/fixed.ts index 99148fdf..e00bacd2 100644 --- a/language/models/fixed.js +++ b/language/models/fixed.ts @@ -1,4 +1,7 @@ + import Parser from "../parser"; +import { Keywords } from "../parserTypes"; +import { Token } from "../types"; /** * @param {number} lineNumber @@ -7,7 +10,7 @@ import Parser from "../parser"; * @param {string} [type] * @returns {import("../types").Token|undefined} */ -function calculateToken(lineNumber, startingPos, value, type) { +function calculateToken(lineNumber: number, startingPos: number, value: string, type?: string): Token | undefined { let resultValue = value.trim(); if (resultValue === ``) { @@ -62,7 +65,7 @@ export function parseFLine(lineNumber, lineIndex, content) { /** * @param {string} content */ -export function parseCLine(lineNumber, lineIndex, content) { +export function parseCLine(lineNumber: number, lineIndex: number, content: string) { content = content.padEnd(80); const clIndicator = content.substr(7, 8).toUpperCase(); const indicator = content.substr(9, 11); @@ -97,10 +100,7 @@ export function parseCLine(lineNumber, lineIndex, content) { }; } -/** - * @param {string} content - */ -export function parseDLine(lineNumber, lineIndex, content) { +export function parseDLine(lineNumber: number, lineIndex: number, content: string) { content = content.padEnd(80); const longForm = content.substring(6).trimEnd(); const potentialName = longForm.endsWith(`...`) ? calculateToken(lineNumber, lineIndex+6, longForm.substring(0, longForm.length - 3)) : undefined; @@ -129,7 +129,7 @@ export function parseDLine(lineNumber, lineIndex, content) { /** * @param {string} content */ -export function parsePLine(content, lineNumber, lineIndex) { +export function parsePLine(content: string, lineNumber: number, lineIndex: number) { content = content.padEnd(80); let name = content.substr(6, 16).trimEnd(); if (name.endsWith(`...`)) { @@ -162,12 +162,7 @@ export function prettyTypeFromToken(dSpec) { }) } -/** - * - * @param {{type: string, keywords: import("../parserTypes").Keywords, len: string, pos: string, decimals: string, field: string}} lineData - * @returns {import("../parserTypes").Keywords} - */ -export function getPrettyType(lineData) { +export function getPrettyType(lineData: {type: string, keywords: Keywords, len: string, pos: string, decimals: string, field: string}): Keywords { let outType = ``; let length = Number(lineData.len); @@ -304,4 +299,103 @@ export function getPrettyType(lineData) { } return Parser.expandKeywords(Parser.getTokens(outType)); +} + +// https://www.ibm.com/docs/fr/i/7.4.0?topic=specifications-input +export function parseILine(lineNumber: number, lineIndex: number, content: string) { + content = content.padEnd(80); + let iType: "programRecord"|"programField"|"externalRecord"|"externalField"|undefined; + + const name = content.substring(6, 16).trimEnd(); + + if (name) { + // RECORD + const externalReserved = content.substring(15, 20).trim(); + if (externalReserved) { + // If this reserved area is not empty, then it is a program record + iType = `programRecord`; + } else { + iType = `externalRecord`; + } + + } else { + // FIELD + const externalName = content.substring(20, 30).trim(); + + if (externalName) { + iType = `externalField`; + } else { + iType = `programField`; + } + } + + const getPart = (start: number, end: number, type?: string) => { + return calculateToken(lineNumber, lineIndex + start, content.substring(start-1, end).trimEnd(), type); + } + + switch (iType) { + case `programRecord`: + // Handle program record + // https://www.ibm.com/docs/fr/i/7.4.0?topic=specifications-record-identification-entries#iri + + return { + iType, + name: getPart(7, 16), + logicalRelationship: getPart(16, 18), + sequence: getPart(18, 19), + number: getPart(19, 20), + option: getPart(20, 21), + recordIdentifyingIndicator: getPart(21, 23, `special-ind`) // 2 characters + } + break; + case `programField`: + // Handle program field + // https://www.ibm.com/docs/fr/i/7.4.0?topic=specifications-field-description-entries#ifd + + return { + iType, + dataAttributes: getPart(31, 34), + dateTimeSeparator: getPart(35, 36), + dataFormat: getPart(36, 37), + fieldLocation: getPart(37, 46), + decimalPositions: getPart(47, 48), + fieldName: getPart(49, 62), + controlLevel: getPart(63, 64), + matchingFields: getPart(65, 66), + fieldRecordRelation: getPart(67, 68), + fieldIndicators: [ + getPart(69, 70, `special-ind`), + getPart(71, 72, `special-ind`), + getPart(73, 74, `special-ind`) + ] + } + + case `externalRecord`: + // Handle external record + // https://www.ibm.com/docs/fr/i/7.4.0?topic=is-record-identification-entries#ier + + return { + iType, + name: getPart(7, 16), + recordIdentifyingIndicator: getPart(21, 22, `special-ind`), // 2 characters + }; + break; + case `externalField`: + // Handle external field + // https://www.ibm.com/docs/fr/i/7.4.0?topic=is-field-description-entries#ied + + return { + iType, + externalName: getPart(21, 30), + fieldName: getPart(49, 62), + controlLevel: getPart(63, 64), + matchingFields: getPart(65, 66), + fieldIndicators: [ + getPart(69, 70, `special-ind`), + getPart(71, 72, `special-ind`), + getPart(73, 74, `special-ind`) + ] + }; + break; + } } \ No newline at end of file From 79bb8a9405a87b53d557f9baeeef124ba2628e27 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Tue, 29 Jul 2025 16:30:42 -0400 Subject: [PATCH 2/8] Initial work on collecting input symbols Signed-off-by: worksofliam --- language/models/cache.ts | 77 ++++++-- language/models/declaration.ts | 2 +- language/models/fixed.ts | 2 +- language/parser.ts | 68 ++++++- language/tokens.ts | 6 + tests/suite/ispec.test.ts | 60 +++++++ tests/tables/index.ts | 6 +- tests/tables/qcbblesrc.ts | 314 +++++++++++++++++++++++++++++++++ tests/tables/qrpglesrc.ts | 314 +++++++++++++++++++++++++++++++++ 9 files changed, 831 insertions(+), 18 deletions(-) create mode 100644 tests/suite/ispec.test.ts create mode 100644 tests/tables/qcbblesrc.ts create mode 100644 tests/tables/qrpglesrc.ts diff --git a/language/models/cache.ts b/language/models/cache.ts index f0385cbb..87b27935 100644 --- a/language/models/cache.ts +++ b/language/models/cache.ts @@ -1,4 +1,5 @@ import { CacheProps, IncludeStatement, Keywords } from "../parserTypes"; +import { trimQuotes } from "../tokens"; import { IRange } from "../types"; import Declaration, { DeclarationType } from "./declaration"; @@ -132,7 +133,7 @@ export default class Cache { return (lines.length >= 1 ? lines[0] : 0); } - find(name: string, specificType?: DeclarationType): Declaration | undefined { + find(name: string, specificType?: DeclarationType, ignorePrefix?: boolean): Declaration | undefined { name = name.toUpperCase(); const existing = this.symbolRegister.get(name); @@ -152,32 +153,86 @@ export default class Cache { } } + // If we didn't find it, let's check for subfields + const [subfield] = this.findSubfields(name, ignorePrefix, true); + + return subfield; + } + + findAll(name: string, ignorePrefix?: boolean): Declaration[] { + name = name.toUpperCase(); + const symbols = this.symbolRegister.get(name) || []; + + symbols.push(...this.findSubfields(name, ignorePrefix)); + + return symbols || []; + } + + private findSubfields(name: string, ignorePrefix: boolean, onlyOne?: boolean): Declaration[] { + let symbols: Declaration[] = []; + // Additional logic to check for subItems in symbols const symbolsWithSubs = [...this.structs, ...this.files]; + const subNameIsValid = (sub: Declaration, name: string, prefix?: string) => { + if (prefix) { + name = `${prefix}${name}`; + } + + return sub.name.toUpperCase() === name; + } + + // First we do a loop to check all names without changing the prefix for (const struct of symbolsWithSubs) { if (struct.keyword[`QUALIFIED`] !== true) { // If the symbol is qualified, we need to check the subItems - const subItem = struct.subItems.find(sub => sub.name.toUpperCase() === name); - if (subItem) return subItem; + const subItem = struct.subItems.find(sub => subNameIsValid(sub, name)); + if (subItem) { + symbols.push(subItem); + if (onlyOne) return symbols; + } if (struct.type === `file`) { // If it's a file, we also need to check the subItems of the file's recordformats for (const subFile of struct.subItems) { - const subSubItem = subFile.subItems.find(sub => sub.name.toUpperCase() === name); - if (subSubItem) return subSubItem; + const subSubItem = subFile.subItems.find(sub => subNameIsValid(sub, name)); + if (subSubItem) { + symbols.push(subSubItem); + if (onlyOne) return symbols; + } } } } } - return; - } + // Then we check the names, ignoring the prefix + if (ignorePrefix) { + for (const struct of symbolsWithSubs) { + if (struct.keyword[`QUALIFIED`] !== true) { + // If the symbol is qualified, we need to check the subItems + const subItem = struct.subItems.find(sub => subNameIsValid(sub, name)); + if (subItem) { + symbols.push(subItem); + if (onlyOne) return symbols; + } - findAll(name: string): Declaration[] { - name = name.toUpperCase(); - const symbols = this.symbolRegister.get(name); - return symbols || []; + if (struct.type === `file`) { + const prefix = ignorePrefix && struct.keyword[`PREFIX`] && typeof struct.keyword[`PREFIX`] === `string` ? trimQuotes(struct.keyword[`PREFIX`].toUpperCase()) : ``; + + // If it's a file, we also need to check the subItems of the file's recordformats + for (const subFile of struct.subItems) { + const subSubItem = subFile.subItems.find(sub => subNameIsValid(sub, name, prefix)); + if (subSubItem) { + symbols.push(subSubItem); + if (onlyOne) return symbols; + } + } + } + } + } + } + + return symbols; } public findProcedurebyLine(lineNumber: number): Declaration | undefined { diff --git a/language/models/declaration.ts b/language/models/declaration.ts index 089ee1dd..11e502d3 100644 --- a/language/models/declaration.ts +++ b/language/models/declaration.ts @@ -3,7 +3,7 @@ import { Keywords, Reference } from "../parserTypes"; import { IRangeWithLine } from "../types"; import Cache from "./cache"; -export type DeclarationType = "parameter"|"procedure"|"subroutine"|"file"|"struct"|"subitem"|"variable"|"constant"|"tag"|"indicator"; +export type DeclarationType = "parameter"|"procedure"|"subroutine"|"file"|"struct"|"subitem"|"variable"|"constant"|"tag"|"indicator"|"input"; export default class Declaration { name: string = ``; diff --git a/language/models/fixed.ts b/language/models/fixed.ts index e00bacd2..fc87e5d1 100644 --- a/language/models/fixed.ts +++ b/language/models/fixed.ts @@ -302,7 +302,7 @@ export function getPrettyType(lineData: {type: string, keywords: Keywords, len: } // https://www.ibm.com/docs/fr/i/7.4.0?topic=specifications-input -export function parseILine(lineNumber: number, lineIndex: number, content: string) { +export function parseISpec(lineNumber: number, lineIndex: number, content: string) { content = content.padEnd(80); let iType: "programRecord"|"programField"|"externalRecord"|"externalField"|undefined; diff --git a/language/parser.ts b/language/parser.ts index 4f7130d8..4da63acc 100644 --- a/language/parser.ts +++ b/language/parser.ts @@ -1,12 +1,12 @@ /* eslint-disable no-case-declarations */ -import { ALLOWS_EXTENDED, createBlocks, tokenise } from "./tokens"; +import { ALLOWS_EXTENDED, createBlocks, tokenise, trimQuotes } from "./tokens"; import Cache from "./models/cache"; import Declaration from "./models/declaration"; import oneLineTriggers from "./models/oneLineTriggers"; -import { parseFLine, parseCLine, parsePLine, parseDLine, getPrettyType, prettyTypeFromToken } from "./models/fixed"; +import { parseFLine, parseCLine, parsePLine, parseDLine, getPrettyType, prettyTypeFromToken, parseISpec } from "./models/fixed"; import { Token } from "./types"; import { Keywords } from "./parserTypes"; import { NO_NAME } from "./statement"; @@ -498,7 +498,7 @@ export default class Parser { const qualified = fOptions.keyword[`QUALIFIED`] === true; const prefixKeyword = fOptions.keyword[`PREFIX`]; - const prefix = prefixKeyword && typeof prefixKeyword === `string` ? prefixKeyword.toUpperCase() : ``; + const prefix = prefixKeyword && typeof prefixKeyword === `string` ? trimQuotes(prefixKeyword.toUpperCase()) : ``; for (const recordFormat of currentItem.subItems) { if (renames[recordFormat.name.toUpperCase()]) { @@ -637,7 +637,7 @@ export default class Parser { baseLine = ``.padEnd(7) + baseLine.substring(7); lineIsFree = true; - } else if (![`D`, `P`, `C`, `F`, `H`].includes(spec)) { + } else if (![`D`, `I`, `P`, `C`, `F`, `H`].includes(spec)) { continue; } } @@ -1426,6 +1426,66 @@ export default class Parser { break; + case `I`: + const iSpec = parseISpec(lineNumber, lineIndex, line); + + switch (iSpec.iType) { + case `programRecord`: + case `externalRecord`: + tokens = [iSpec.name]; + currentItem = new Declaration(`input`); + currentItem.name = iSpec.name.value; + currentItem.keyword = { + type: iSpec.iType === `programRecord` ? `program` : `external` + }; + + currentItem.position = { + path: fileUri, + range: iSpec.name.range + }; + + currentItem.range = { + start: lineNumber, + end: lineNumber + }; + break; + + case `programField`: + if (!currentItem) { + break; + } + + tokens = [iSpec.fieldName, ...iSpec.fieldIndicators]; + + // TODO: generate a type for this + + break; + + case `externalField`: + if (!currentItem) { + break; + } + + tokens = [iSpec.externalName, iSpec.fieldName, ...iSpec.fieldIndicators]; + if (iSpec.externalName) { + // Generate a type for this + let lookup = scope.find(iSpec.externalName.value, undefined, true); + if (lookup) { + lookup.name = iSpec.fieldName.value; + currentItem.subItems.push(lookup); + currentItem.range.end = lineNumber; + } + } else { + let lookup = scope.find(iSpec.fieldName.value, undefined, true); + if (lookup) { + currentItem.subItems.push(lookup); + currentItem.range.end = lineNumber; + } + } + break; + } + break; + case `C`: const cSpec = parseCLine(lineNumber, lineIndex, line); diff --git a/language/tokens.ts b/language/tokens.ts index 57e854c6..344c9a92 100644 --- a/language/tokens.ts +++ b/language/tokens.ts @@ -483,4 +483,10 @@ export function createBlocks(tokens: Token[]) { } return tokens; +} + +export function trimQuotes(input: string) { + if (input[0] === `'`) input = input.substring(1); + if (input[input.length - 1] === `'`) input = input.substring(0, input.length - 1); + return input; } \ No newline at end of file diff --git a/tests/suite/ispec.test.ts b/tests/suite/ispec.test.ts new file mode 100644 index 00000000..6252c612 --- /dev/null +++ b/tests/suite/ispec.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from "vitest"; +import setupParser from "../parserSetup"; + +const parser = setupParser(); +const uri = `source.rpgle`; + +test('ispec rename 1', async () => { + const lines =[ + ` dcl-f qrpglesrc prefix('RPG_');`, + ` dcl-f qcbblesrc;`, + ``, + ` * Use the prefixed name if an I spec is needed`, + ` Iqarpglesrc`, + ` I RPG_SRCSEQ 10`, + ` I RPG_SRCDTA 11`, + ` * Renaming a prefixed field uses the external name. ALL_SRCDAT`, + ` * is also one of the internal fields for QACBLLESRC`, + ` I SRCDAT ALL_SRCDAT 12`, + ``, + ` * Renaming I spec fields`, + ` * External name Internal name`, + ` Iqacbllesrc`, + ` I SRCSEQ CBL_SRCSEQ`, + ` I SRCDTA CBL_SRCDTA`, + ` I SRCDAT ALL_SRCDAT`, + ].join('\n'); + + const cache = await parser.getDocs(uri, lines, {ignoreCache: true}); + + const symbols = cache.symbols; + expect(symbols.length).toBeGreaterThan(0); + + const files = cache.files; + expect(files.length).toBe(2); + + const rpgSrc = files.find(f => f.name === `qrpglesrc`); + expect(rpgSrc).toBeDefined(); + + const rpgExpectedColumns = [`ALL_SRCDAT`, `RPG_SRCSEQ`, `RPG_SRCDTA`]; + expect(rpgSrc.subItems[0].subItems.map(s => s.name)).toEqual(rpgExpectedColumns); + + const cblSrc = files.find(f => f.name === `qcbblesrc`); + expect(cblSrc).toBeDefined(); + expect(cblSrc.subItems[0].subItems.length).toBe(3); + + const cblExpectedColumns = [`ALL_SRCDAT`, `CBL_SRCSEQ`, `CBL_SRCDTA`]; + expect(cblSrc.subItems[0].subItems.map(s => s.name)).toEqual(cblExpectedColumns); + + // TODO: add check for input symbols in cache + + // for (const file of [rpgSrc, cblSrc]) { + // console.log(`File: ${file.name}`); + // for (const subItem of file.subItems) { + // console.log(` Record format: ${subItem.name}`); + // for (const subSubItem of subItem.subItems) { + // console.log(` Field: ${subSubItem.name}`); + // } + // } + // } +}) \ No newline at end of file diff --git a/tests/tables/index.ts b/tests/tables/index.ts index 192898e5..34febb65 100644 --- a/tests/tables/index.ts +++ b/tests/tables/index.ts @@ -2,10 +2,14 @@ import emps from './emps'; import employee from './employee'; import department from './department'; import display from './display'; +import qrpglesrc from './qrpglesrc'; +import qcbblesrc from './qcbblesrc'; export default { 'EMPS': emps, 'EMPLOYEE': employee, 'DEPARTMENT': department, - 'DISPLAY': display + 'DISPLAY': display, + 'QRPGLESRC': qrpglesrc, + 'QCBBLESRC': qcbblesrc }; \ No newline at end of file diff --git a/tests/tables/qcbblesrc.ts b/tests/tables/qcbblesrc.ts new file mode 100644 index 00000000..69248ecd --- /dev/null +++ b/tests/tables/qcbblesrc.ts @@ -0,0 +1,314 @@ +export default [ + { + "WHFILE": "QCBBLESRC", + "WHLIB": "TESTLIB", + "WHCRTD": "1220707", + "WHFTYP": "P", + "WHCNT": 1, + "WHDTTM": "1221025213000", + "WHNAME": "QCBBLESRCR", + "WHSEQ": "3E34F5148594B", + "WHTEXT": "", + "WHFLDN": 6, + "WHRLEN": 112, + "WHFLDI": "SRCDAT", + "WHFLDE": "SRCDAT", + "WHFOBO": 1, + "WHIBO": 1, + "WHFLDB": 3, + "WHFLDD": 0, + "WHFLDP": 0, + "WHFTXT": "", + "WHRCDE": 0, + "WHRFIL": "", + "WHRLIB": "", + "WHRFMT": "", + "WHRFLD": "", + "WHCHD1": "SRCDAT", + "WHCHD2": "", + "WHCHD3": "", + "WHFLDT": "A", + "WHFIOB": "B", + "WHECDE": "", + "WHEWRD": "", + "WHVCNE": 0, + "WHNFLD": 5, + "WHNIND": 0, + "WHSHFT": "", + "WHALTY": "N", + "WHALIS": "", + "WHJREF": 0, + "WHDFTL": 0, + "WHDFT": "", + "WHCHRI": "N", + "WHCTNT": "N", + "WHFONT": "", + "WHCSWD": 0, + "WHCSHI": 0, + "WHBCNM": "", + "WHBCHI": 0, + "WHMAP": "N", + "WHMAPS": 0, + "WHMAPL": 0, + "WHSYSN": "UT25BP18", + "WHRES1": "", + "WHSQLT": "T", + "WHHEX": "N", + "WHPNTS": 0, + "WHCSID": 37, + "WHFMT": "", + "WHSEP": "", + "WHVARL": "N", + "WHALLC": 0, + "WHNULL": "N", + "WHFCSN": "", + "WHFCSL": "", + "WHFCPN": "", + "WHFCPL": "", + "WHCDFN": "", + "WHCDFL": "", + "WHDCDF": "", + "WHDCDL": "", + "WHTXRT": "- 1", + "WHFLDG": 0, + "WHFDSL": 0, + "WHFSPS": 0, + "WHCFPS": 0, + "WHIFPS": 0, + "WHDBLL": 0, + "WHDBUN": "", + "WHDBUL": "", + "WHDBFC": "", + "WHDBFI": "", + "WHDBRP": "", + "WHDBWP": "", + "WHDBRC": "", + "WHDBOU": "", + "WHPSUD": "", + "WHBCUH": 0, + "WHFPSW": 0, + "WHFSPW": 0, + "WHCFPW": 0, + "WHIFPW": 0, + "WHRWID": "N", + "WHIDC": "N", + "WHDROW": 0, + "WHDCOL": 0, + "WHALI2": "", + "WHALCH": "", + "WHNRML": "0", + "WHJRF2": 0, + "WHHDNCOL": "0", + "WHRCTS": "", + "WHFPPN": "", + "WHFPLN": "" + }, + { + "WHFILE": "QCBBLESRC", + "WHLIB": "TESTLIB", + "WHCRTD": "1220707", + "WHFTYP": "P", + "WHCNT": 1, + "WHDTTM": "1221025213000", + "WHNAME": "QCBBLESRCR", + "WHSEQ": "3E34F5148594B", + "WHTEXT": "", + "WHFLDN": 6, + "WHRLEN": 112, + "WHFLDI": "SRCSEQ", + "WHFLDE": "SRCSEQ", + "WHFOBO": 1, + "WHIBO": 1, + "WHFLDB": 3, + "WHFLDD": 0, + "WHFLDP": 0, + "WHFTXT": "", + "WHRCDE": 0, + "WHRFIL": "", + "WHRLIB": "", + "WHRFMT": "", + "WHRFLD": "", + "WHCHD1": "SRCSEQ", + "WHCHD2": "", + "WHCHD3": "", + "WHFLDT": "A", + "WHFIOB": "B", + "WHECDE": "", + "WHEWRD": "", + "WHVCNE": 0, + "WHNFLD": 5, + "WHNIND": 0, + "WHSHFT": "", + "WHALTY": "N", + "WHALIS": "", + "WHJREF": 0, + "WHDFTL": 0, + "WHDFT": "", + "WHCHRI": "N", + "WHCTNT": "N", + "WHFONT": "", + "WHCSWD": 0, + "WHCSHI": 0, + "WHBCNM": "", + "WHBCHI": 0, + "WHMAP": "N", + "WHMAPS": 0, + "WHMAPL": 0, + "WHSYSN": "UT25BP18", + "WHRES1": "", + "WHSQLT": "T", + "WHHEX": "N", + "WHPNTS": 0, + "WHCSID": 37, + "WHFMT": "", + "WHSEP": "", + "WHVARL": "N", + "WHALLC": 0, + "WHNULL": "N", + "WHFCSN": "", + "WHFCSL": "", + "WHFCPN": "", + "WHFCPL": "", + "WHCDFN": "", + "WHCDFL": "", + "WHDCDF": "", + "WHDCDL": "", + "WHTXRT": "- 1", + "WHFLDG": 0, + "WHFDSL": 0, + "WHFSPS": 0, + "WHCFPS": 0, + "WHIFPS": 0, + "WHDBLL": 0, + "WHDBUN": "", + "WHDBUL": "", + "WHDBFC": "", + "WHDBFI": "", + "WHDBRP": "", + "WHDBWP": "", + "WHDBRC": "", + "WHDBOU": "", + "WHPSUD": "", + "WHBCUH": 0, + "WHFPSW": 0, + "WHFSPW": 0, + "WHCFPW": 0, + "WHIFPW": 0, + "WHRWID": "N", + "WHIDC": "N", + "WHDROW": 0, + "WHDCOL": 0, + "WHALI2": "", + "WHALCH": "", + "WHNRML": "0", + "WHJRF2": 0, + "WHHDNCOL": "0", + "WHRCTS": "", + "WHFPPN": "", + "WHFPLN": "" + }, + { + "WHFILE": "QCBBLESRC", + "WHLIB": "TESTLIB", + "WHCRTD": "1220707", + "WHFTYP": "P", + "WHCNT": 1, + "WHDTTM": "1221025213000", + "WHNAME": "QCBBLESRCR", + "WHSEQ": "3E34F5148594B", + "WHTEXT": "", + "WHFLDN": 100, + "WHRLEN": 112, + "WHFLDI": "SRCDTA", + "WHFLDE": "SRCDTA", + "WHFOBO": 1, + "WHIBO": 1, + "WHFLDB": 3, + "WHFLDD": 0, + "WHFLDP": 0, + "WHFTXT": "", + "WHRCDE": 0, + "WHRFIL": "", + "WHRLIB": "", + "WHRFMT": "", + "WHRFLD": "", + "WHCHD1": "SRCDTA", + "WHCHD2": "", + "WHCHD3": "", + "WHFLDT": "A", + "WHFIOB": "B", + "WHECDE": "", + "WHEWRD": "", + "WHVCNE": 0, + "WHNFLD": 5, + "WHNIND": 0, + "WHSHFT": "", + "WHALTY": "N", + "WHALIS": "", + "WHJREF": 0, + "WHDFTL": 0, + "WHDFT": "", + "WHCHRI": "N", + "WHCTNT": "N", + "WHFONT": "", + "WHCSWD": 0, + "WHCSHI": 0, + "WHBCNM": "", + "WHBCHI": 0, + "WHMAP": "N", + "WHMAPS": 0, + "WHMAPL": 0, + "WHSYSN": "UT25BP18", + "WHRES1": "", + "WHSQLT": "T", + "WHHEX": "N", + "WHPNTS": 0, + "WHCSID": 37, + "WHFMT": "", + "WHSEP": "", + "WHVARL": "N", + "WHALLC": 0, + "WHNULL": "N", + "WHFCSN": "", + "WHFCSL": "", + "WHFCPN": "", + "WHFCPL": "", + "WHCDFN": "", + "WHCDFL": "", + "WHDCDF": "", + "WHDCDL": "", + "WHTXRT": "- 1", + "WHFLDG": 0, + "WHFDSL": 0, + "WHFSPS": 0, + "WHCFPS": 0, + "WHIFPS": 0, + "WHDBLL": 0, + "WHDBUN": "", + "WHDBUL": "", + "WHDBFC": "", + "WHDBFI": "", + "WHDBRP": "", + "WHDBWP": "", + "WHDBRC": "", + "WHDBOU": "", + "WHPSUD": "", + "WHBCUH": 0, + "WHFPSW": 0, + "WHFSPW": 0, + "WHCFPW": 0, + "WHIFPW": 0, + "WHRWID": "N", + "WHIDC": "N", + "WHDROW": 0, + "WHDCOL": 0, + "WHALI2": "", + "WHALCH": "", + "WHNRML": "0", + "WHJRF2": 0, + "WHHDNCOL": "0", + "WHRCTS": "", + "WHFPPN": "", + "WHFPLN": "" + }, +] \ No newline at end of file diff --git a/tests/tables/qrpglesrc.ts b/tests/tables/qrpglesrc.ts new file mode 100644 index 00000000..e562d061 --- /dev/null +++ b/tests/tables/qrpglesrc.ts @@ -0,0 +1,314 @@ +export default [ + { + "WHFILE": "QRPGLESRC", + "WHLIB": "TESTLIB", + "WHCRTD": "1220707", + "WHFTYP": "P", + "WHCNT": 1, + "WHDTTM": "1221025213000", + "WHNAME": "QRPGLESRCR", + "WHSEQ": "3E34F5148594B", + "WHTEXT": "", + "WHFLDN": 6, + "WHRLEN": 112, + "WHFLDI": "SRCDAT", + "WHFLDE": "SRCDAT", + "WHFOBO": 1, + "WHIBO": 1, + "WHFLDB": 3, + "WHFLDD": 0, + "WHFLDP": 0, + "WHFTXT": "", + "WHRCDE": 0, + "WHRFIL": "", + "WHRLIB": "", + "WHRFMT": "", + "WHRFLD": "", + "WHCHD1": "SRCDAT", + "WHCHD2": "", + "WHCHD3": "", + "WHFLDT": "A", + "WHFIOB": "B", + "WHECDE": "", + "WHEWRD": "", + "WHVCNE": 0, + "WHNFLD": 5, + "WHNIND": 0, + "WHSHFT": "", + "WHALTY": "N", + "WHALIS": "", + "WHJREF": 0, + "WHDFTL": 0, + "WHDFT": "", + "WHCHRI": "N", + "WHCTNT": "N", + "WHFONT": "", + "WHCSWD": 0, + "WHCSHI": 0, + "WHBCNM": "", + "WHBCHI": 0, + "WHMAP": "N", + "WHMAPS": 0, + "WHMAPL": 0, + "WHSYSN": "UT25BP18", + "WHRES1": "", + "WHSQLT": "T", + "WHHEX": "N", + "WHPNTS": 0, + "WHCSID": 37, + "WHFMT": "", + "WHSEP": "", + "WHVARL": "N", + "WHALLC": 0, + "WHNULL": "N", + "WHFCSN": "", + "WHFCSL": "", + "WHFCPN": "", + "WHFCPL": "", + "WHCDFN": "", + "WHCDFL": "", + "WHDCDF": "", + "WHDCDL": "", + "WHTXRT": "- 1", + "WHFLDG": 0, + "WHFDSL": 0, + "WHFSPS": 0, + "WHCFPS": 0, + "WHIFPS": 0, + "WHDBLL": 0, + "WHDBUN": "", + "WHDBUL": "", + "WHDBFC": "", + "WHDBFI": "", + "WHDBRP": "", + "WHDBWP": "", + "WHDBRC": "", + "WHDBOU": "", + "WHPSUD": "", + "WHBCUH": 0, + "WHFPSW": 0, + "WHFSPW": 0, + "WHCFPW": 0, + "WHIFPW": 0, + "WHRWID": "N", + "WHIDC": "N", + "WHDROW": 0, + "WHDCOL": 0, + "WHALI2": "", + "WHALCH": "", + "WHNRML": "0", + "WHJRF2": 0, + "WHHDNCOL": "0", + "WHRCTS": "", + "WHFPPN": "", + "WHFPLN": "" + }, + { + "WHFILE": "QRPGLESRC", + "WHLIB": "TESTLIB", + "WHCRTD": "1220707", + "WHFTYP": "P", + "WHCNT": 1, + "WHDTTM": "1221025213000", + "WHNAME": "QRPGLESRCR", + "WHSEQ": "3E34F5148594B", + "WHTEXT": "", + "WHFLDN": 6, + "WHRLEN": 112, + "WHFLDI": "SRCSEQ", + "WHFLDE": "SRCSEQ", + "WHFOBO": 1, + "WHIBO": 1, + "WHFLDB": 3, + "WHFLDD": 0, + "WHFLDP": 0, + "WHFTXT": "", + "WHRCDE": 0, + "WHRFIL": "", + "WHRLIB": "", + "WHRFMT": "", + "WHRFLD": "", + "WHCHD1": "SRCSEQ", + "WHCHD2": "", + "WHCHD3": "", + "WHFLDT": "A", + "WHFIOB": "B", + "WHECDE": "", + "WHEWRD": "", + "WHVCNE": 0, + "WHNFLD": 5, + "WHNIND": 0, + "WHSHFT": "", + "WHALTY": "N", + "WHALIS": "", + "WHJREF": 0, + "WHDFTL": 0, + "WHDFT": "", + "WHCHRI": "N", + "WHCTNT": "N", + "WHFONT": "", + "WHCSWD": 0, + "WHCSHI": 0, + "WHBCNM": "", + "WHBCHI": 0, + "WHMAP": "N", + "WHMAPS": 0, + "WHMAPL": 0, + "WHSYSN": "UT25BP18", + "WHRES1": "", + "WHSQLT": "T", + "WHHEX": "N", + "WHPNTS": 0, + "WHCSID": 37, + "WHFMT": "", + "WHSEP": "", + "WHVARL": "N", + "WHALLC": 0, + "WHNULL": "N", + "WHFCSN": "", + "WHFCSL": "", + "WHFCPN": "", + "WHFCPL": "", + "WHCDFN": "", + "WHCDFL": "", + "WHDCDF": "", + "WHDCDL": "", + "WHTXRT": "- 1", + "WHFLDG": 0, + "WHFDSL": 0, + "WHFSPS": 0, + "WHCFPS": 0, + "WHIFPS": 0, + "WHDBLL": 0, + "WHDBUN": "", + "WHDBUL": "", + "WHDBFC": "", + "WHDBFI": "", + "WHDBRP": "", + "WHDBWP": "", + "WHDBRC": "", + "WHDBOU": "", + "WHPSUD": "", + "WHBCUH": 0, + "WHFPSW": 0, + "WHFSPW": 0, + "WHCFPW": 0, + "WHIFPW": 0, + "WHRWID": "N", + "WHIDC": "N", + "WHDROW": 0, + "WHDCOL": 0, + "WHALI2": "", + "WHALCH": "", + "WHNRML": "0", + "WHJRF2": 0, + "WHHDNCOL": "0", + "WHRCTS": "", + "WHFPPN": "", + "WHFPLN": "" + }, + { + "WHFILE": "QRPGLESRC", + "WHLIB": "TESTLIB", + "WHCRTD": "1220707", + "WHFTYP": "P", + "WHCNT": 1, + "WHDTTM": "1221025213000", + "WHNAME": "QRPGLESRCR", + "WHSEQ": "3E34F5148594B", + "WHTEXT": "", + "WHFLDN": 100, + "WHRLEN": 112, + "WHFLDI": "SRCDTA", + "WHFLDE": "SRCDTA", + "WHFOBO": 1, + "WHIBO": 1, + "WHFLDB": 3, + "WHFLDD": 0, + "WHFLDP": 0, + "WHFTXT": "", + "WHRCDE": 0, + "WHRFIL": "", + "WHRLIB": "", + "WHRFMT": "", + "WHRFLD": "", + "WHCHD1": "SRCDTA", + "WHCHD2": "", + "WHCHD3": "", + "WHFLDT": "A", + "WHFIOB": "B", + "WHECDE": "", + "WHEWRD": "", + "WHVCNE": 0, + "WHNFLD": 5, + "WHNIND": 0, + "WHSHFT": "", + "WHALTY": "N", + "WHALIS": "", + "WHJREF": 0, + "WHDFTL": 0, + "WHDFT": "", + "WHCHRI": "N", + "WHCTNT": "N", + "WHFONT": "", + "WHCSWD": 0, + "WHCSHI": 0, + "WHBCNM": "", + "WHBCHI": 0, + "WHMAP": "N", + "WHMAPS": 0, + "WHMAPL": 0, + "WHSYSN": "UT25BP18", + "WHRES1": "", + "WHSQLT": "T", + "WHHEX": "N", + "WHPNTS": 0, + "WHCSID": 37, + "WHFMT": "", + "WHSEP": "", + "WHVARL": "N", + "WHALLC": 0, + "WHNULL": "N", + "WHFCSN": "", + "WHFCSL": "", + "WHFCPN": "", + "WHFCPL": "", + "WHCDFN": "", + "WHCDFL": "", + "WHDCDF": "", + "WHDCDL": "", + "WHTXRT": "- 1", + "WHFLDG": 0, + "WHFDSL": 0, + "WHFSPS": 0, + "WHCFPS": 0, + "WHIFPS": 0, + "WHDBLL": 0, + "WHDBUN": "", + "WHDBUL": "", + "WHDBFC": "", + "WHDBFI": "", + "WHDBRP": "", + "WHDBWP": "", + "WHDBRC": "", + "WHDBOU": "", + "WHPSUD": "", + "WHBCUH": 0, + "WHFPSW": 0, + "WHFSPW": 0, + "WHCFPW": 0, + "WHIFPW": 0, + "WHRWID": "N", + "WHIDC": "N", + "WHDROW": 0, + "WHDCOL": 0, + "WHALI2": "", + "WHALCH": "", + "WHNRML": "0", + "WHJRF2": 0, + "WHHDNCOL": "0", + "WHRCTS": "", + "WHFPPN": "", + "WHFPLN": "" + }, +] \ No newline at end of file From 6c0194b3dab63c1260265aa0fd4a38de252df510 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Wed, 30 Jul 2025 09:57:44 -0400 Subject: [PATCH 3/8] Capture fields of I specs correctly Signed-off-by: worksofliam --- language/models/cache.ts | 26 +++++++++----------------- language/parser.ts | 12 ++++++++++++ tests/suite/ispec.test.ts | 10 ++++++++++ 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/language/models/cache.ts b/language/models/cache.ts index 87b27935..f1f15100 100644 --- a/language/models/cache.ts +++ b/language/models/cache.ts @@ -182,24 +182,16 @@ export default class Cache { return sub.name.toUpperCase() === name; } - // First we do a loop to check all names without changing the prefix + // First we do a loop to check all names without changing the prefix. + // This only applied to files for (const struct of symbolsWithSubs) { - if (struct.keyword[`QUALIFIED`] !== true) { - // If the symbol is qualified, we need to check the subItems - const subItem = struct.subItems.find(sub => subNameIsValid(sub, name)); - if (subItem) { - symbols.push(subItem); - if (onlyOne) return symbols; - } - - if (struct.type === `file`) { - // If it's a file, we also need to check the subItems of the file's recordformats - for (const subFile of struct.subItems) { - const subSubItem = subFile.subItems.find(sub => subNameIsValid(sub, name)); - if (subSubItem) { - symbols.push(subSubItem); - if (onlyOne) return symbols; - } + if (struct.type === `file` && struct.keyword[`QUALIFIED`] !== true) { + // If it's a file, we also need to check the subItems of the file's recordformats + for (const subFile of struct.subItems) { + const subSubItem = subFile.subItems.find(sub => subNameIsValid(sub, name)); + if (subSubItem) { + symbols.push(subSubItem); + if (onlyOne) return symbols; } } } diff --git a/language/parser.ts b/language/parser.ts index 4da63acc..098d046f 100644 --- a/language/parser.ts +++ b/language/parser.ts @@ -1448,6 +1448,9 @@ export default class Parser { start: lineNumber, end: lineNumber }; + + scope.addSymbol(currentItem); + break; case `programField`: @@ -1458,6 +1461,15 @@ export default class Parser { tokens = [iSpec.fieldName, ...iSpec.fieldIndicators]; // TODO: generate a type for this + let lookup = scope.find(iSpec.fieldName.value, undefined); + + if (iSpec.dataFormat) { + + } else if (lookup) { + currentItem.subItems.push(lookup); + } + + currentItem.range.end = lineNumber; break; diff --git a/tests/suite/ispec.test.ts b/tests/suite/ispec.test.ts index 6252c612..890a53e9 100644 --- a/tests/suite/ispec.test.ts +++ b/tests/suite/ispec.test.ts @@ -48,6 +48,16 @@ test('ispec rename 1', async () => { // TODO: add check for input symbols in cache + const inputs = cache.symbols.filter(s => s.type === `input`); + + expect(inputs.length).toBe(2); + + const qarpg = inputs.find(s => s.name === `qarpglesrc`); + expect(qarpg).toBeDefined(); + expect(qarpg.subItems.length).toBe(3); + + // TODO: more testing + // for (const file of [rpgSrc, cblSrc]) { // console.log(`File: ${file.name}`); // for (const subItem of file.subItems) { From f987f0bafadb3dbe9182969809758521e95bf363 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Wed, 30 Jul 2025 11:36:37 -0400 Subject: [PATCH 4/8] Finish first ispec test, prepare for I-spec definitions Signed-off-by: worksofliam --- language/models/fixed.ts | 26 +++++++++++++++++++------- language/parser.ts | 33 +++++++++++++++++++++++---------- tests/suite/ispec.test.ts | 26 ++++++++++++++------------ 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/language/models/fixed.ts b/language/models/fixed.ts index fc87e5d1..7e6bf632 100644 --- a/language/models/fixed.ts +++ b/language/models/fixed.ts @@ -151,7 +151,7 @@ export function parsePLine(content: string, lineNumber: number, lineIndex: numbe }; } -export function prettyTypeFromToken(dSpec) { +export function prettyTypeFromDSpecTokens(dSpec) { return getPrettyType({ type: dSpec.type ? dSpec.type.value : ``, keywords: dSpec.keywords, @@ -162,6 +162,17 @@ export function prettyTypeFromToken(dSpec) { }) } +export function prettyTypeFromISpecTokens(iSpec) { + return getPrettyType({ + type: iSpec.dataFormat ? iSpec.dataFormat.value : ``, + keywords: {}, + len: iSpec.length ? iSpec.length.value : ``, + pos: iSpec.fieldLocation ? iSpec.fieldLocation.value : ``, + decimals: iSpec.decimalPositions ? iSpec.decimalPositions.value : ``, + field: `` + }) +} + export function getPrettyType(lineData: {type: string, keywords: Keywords, len: string, pos: string, decimals: string, field: string}): Keywords { let outType = ``; let length = Number(lineData.len); @@ -357,12 +368,13 @@ export function parseISpec(lineNumber: number, lineIndex: number, content: strin dataAttributes: getPart(31, 34), dateTimeSeparator: getPart(35, 36), dataFormat: getPart(36, 37), - fieldLocation: getPart(37, 46), + fieldLocation: getPart(37, 41), + length: getPart(42, 46), decimalPositions: getPart(47, 48), fieldName: getPart(49, 62), - controlLevel: getPart(63, 64), - matchingFields: getPart(65, 66), - fieldRecordRelation: getPart(67, 68), + controlLevel: getPart(63, 64, `special-ind`), + matchingFields: getPart(65, 66, `special-ind`), + fieldRecordRelation: getPart(67, 68, `special-ind`), fieldIndicators: [ getPart(69, 70, `special-ind`), getPart(71, 72, `special-ind`), @@ -388,8 +400,8 @@ export function parseISpec(lineNumber: number, lineIndex: number, content: strin iType, externalName: getPart(21, 30), fieldName: getPart(49, 62), - controlLevel: getPart(63, 64), - matchingFields: getPart(65, 66), + controlLevel: getPart(63, 64, `special-ind`), + matchingFields: getPart(65, 66, `special-ind`), fieldIndicators: [ getPart(69, 70, `special-ind`), getPart(71, 72, `special-ind`), diff --git a/language/parser.ts b/language/parser.ts index 098d046f..c53b7e77 100644 --- a/language/parser.ts +++ b/language/parser.ts @@ -6,7 +6,7 @@ import Cache from "./models/cache"; import Declaration from "./models/declaration"; import oneLineTriggers from "./models/oneLineTriggers"; -import { parseFLine, parseCLine, parsePLine, parseDLine, getPrettyType, prettyTypeFromToken, parseISpec } from "./models/fixed"; +import { parseFLine, parseCLine, parsePLine, parseDLine, getPrettyType, prettyTypeFromDSpecTokens, parseISpec, prettyTypeFromISpecTokens } from "./models/fixed"; import { Token } from "./types"; import { Keywords } from "./parserTypes"; import { NO_NAME } from "./statement"; @@ -1432,7 +1432,7 @@ export default class Parser { switch (iSpec.iType) { case `programRecord`: case `externalRecord`: - tokens = [iSpec.name]; + tokens = [iSpec.name, iSpec.recordIdentifyingIndicator]; currentItem = new Declaration(`input`); currentItem.name = iSpec.name.value; currentItem.keyword = { @@ -1458,13 +1458,20 @@ export default class Parser { break; } - tokens = [iSpec.fieldName, ...iSpec.fieldIndicators]; + tokens = [ + iSpec.fieldName, + iSpec.controlLevel, + iSpec.matchingFields, + iSpec.fieldRecordRelation, + ...iSpec.fieldIndicators + ]; // TODO: generate a type for this let lookup = scope.find(iSpec.fieldName.value, undefined); if (iSpec.dataFormat) { - + const definedDataType = prettyTypeFromISpecTokens(iSpec); + console.log(definedDataType); } else if (lookup) { currentItem.subItems.push(lookup); } @@ -1478,7 +1485,13 @@ export default class Parser { break; } - tokens = [iSpec.externalName, iSpec.fieldName, ...iSpec.fieldIndicators]; + tokens = [ + iSpec.externalName, + iSpec.fieldName, + iSpec.controlLevel, + iSpec.matchingFields, + ...iSpec.fieldIndicators + ]; if (iSpec.externalName) { // Generate a type for this let lookup = scope.find(iSpec.externalName.value, undefined, true); @@ -1709,7 +1722,7 @@ export default class Parser { currentItem.name = currentNameToken?.value || NO_NAME; currentItem.keyword = { ...dSpec.keywords, - ...prettyTypeFromToken(dSpec), + ...prettyTypeFromDSpecTokens(dSpec), } // TODO: line number might be different with ...? @@ -1748,7 +1761,7 @@ export default class Parser { currentItem = new Declaration(`procedure`); currentItem.name = currentNameToken?.value || NO_NAME; currentItem.keyword = { - ...prettyTypeFromToken(dSpec), + ...prettyTypeFromDSpecTokens(dSpec), ...dSpec.keywords } @@ -1776,7 +1789,7 @@ export default class Parser { if (currentItem) { currentItem.keyword = { ...currentItem.keyword, - ...prettyTypeFromToken(dSpec), + ...prettyTypeFromDSpecTokens(dSpec), ...dSpec.keywords } } @@ -1821,7 +1834,7 @@ export default class Parser { currentSub = new Declaration(`subitem`); currentSub.name = currentNameToken?.value || NO_NAME; currentSub.keyword = { - ...prettyTypeFromToken(dSpec), + ...prettyTypeFromDSpecTokens(dSpec), ...dSpec.keywords } @@ -1842,7 +1855,7 @@ export default class Parser { if (currentItem.subItems.length > 0) { currentItem.subItems[currentItem.subItems.length - 1].keyword = { ...currentItem.subItems[currentItem.subItems.length - 1].keyword, - ...prettyTypeFromToken(dSpec), + ...prettyTypeFromDSpecTokens(dSpec), ...dSpec.keywords } } else { diff --git a/tests/suite/ispec.test.ts b/tests/suite/ispec.test.ts index 890a53e9..3b037560 100644 --- a/tests/suite/ispec.test.ts +++ b/tests/suite/ispec.test.ts @@ -55,16 +55,18 @@ test('ispec rename 1', async () => { const qarpg = inputs.find(s => s.name === `qarpglesrc`); expect(qarpg).toBeDefined(); expect(qarpg.subItems.length).toBe(3); + const qaRpgNames = [`RPG_SRCSEQ`, `RPG_SRCDTA`, `ALL_SRCDAT`]; + expect(qarpg.subItems.map(s => s.name)).toEqual(qaRpgNames); - // TODO: more testing - - // for (const file of [rpgSrc, cblSrc]) { - // console.log(`File: ${file.name}`); - // for (const subItem of file.subItems) { - // console.log(` Record format: ${subItem.name}`); - // for (const subSubItem of subItem.subItems) { - // console.log(` Field: ${subSubItem.name}`); - // } - // } - // } -}) \ No newline at end of file + const qacbl = inputs.find(s => s.name === `qacbllesrc`); + expect(qacbl).toBeDefined(); + expect(qacbl.subItems.length).toBe(3); + const qaCblNames = [`CBL_SRCSEQ`, `CBL_SRCDTA`, `ALL_SRCDAT`]; + expect(qacbl.subItems.map(s => s.name)).toEqual(qaCblNames); + + const allSrcDat = cache.find(`ALL_SRCDAT`); + expect(allSrcDat).toBeDefined(); + + const cblSrcSeq = cache.find(`CBL_SRCSEQ`); + expect(cblSrcSeq).toBeDefined(); +}); \ No newline at end of file From 7ebeaa8f958354754419bb9c373f6f99a840aa11 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Wed, 30 Jul 2025 12:38:35 -0400 Subject: [PATCH 5/8] Working I spec for fields, prove spec parser is broken Signed-off-by: worksofliam --- language/models/cache.ts | 50 ++++++++------- language/parser.ts | 39 ++++++++++-- tests/suite/ispec.test.ts | 126 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 25 deletions(-) diff --git a/language/models/cache.ts b/language/models/cache.ts index f1f15100..82b2d137 100644 --- a/language/models/cache.ts +++ b/language/models/cache.ts @@ -80,6 +80,10 @@ export default class Cache { return this.symbols.filter(s => s.type === `file`); } + get inputs() { + return this.symbols.filter(s => s.type === `input`); + } + get constants() { return this.symbols.filter(s => s.type === `constant`); } @@ -161,10 +165,15 @@ export default class Cache { findAll(name: string, ignorePrefix?: boolean): Declaration[] { name = name.toUpperCase(); - const symbols = this.symbolRegister.get(name) || []; + let symbols = this.symbolRegister.get(name) || []; symbols.push(...this.findSubfields(name, ignorePrefix)); + // Remove duplicates by position, since we can have the same reference to symbols in structures due to I-spec + symbols = symbols.filter((s, index, self) => { + return self.findIndex(item => item.position.path === s.position.path && s.position.range.line === item.position.range.line) === index; + }); + return symbols || []; } @@ -172,7 +181,7 @@ export default class Cache { let symbols: Declaration[] = []; // Additional logic to check for subItems in symbols - const symbolsWithSubs = [...this.structs, ...this.files]; + const symbolsWithSubs = [...this.structs, ...this.files, ...this.inputs]; const subNameIsValid = (sub: Declaration, name: string, prefix?: string) => { if (prefix) { @@ -185,7 +194,15 @@ export default class Cache { // First we do a loop to check all names without changing the prefix. // This only applied to files for (const struct of symbolsWithSubs) { - if (struct.type === `file` && struct.keyword[`QUALIFIED`] !== true) { + if (struct.keyword[`QUALIFIED`] !== true) { + + // If the symbol is qualified, we need to check the subItems + const subItem = struct.subItems.find(sub => subNameIsValid(sub, name)); + if (subItem) { + symbols.push(subItem); + if (onlyOne) return symbols; + } + // If it's a file, we also need to check the subItems of the file's recordformats for (const subFile of struct.subItems) { const subSubItem = subFile.subItems.find(sub => subNameIsValid(sub, name)); @@ -200,24 +217,15 @@ export default class Cache { // Then we check the names, ignoring the prefix if (ignorePrefix) { for (const struct of symbolsWithSubs) { - if (struct.keyword[`QUALIFIED`] !== true) { - // If the symbol is qualified, we need to check the subItems - const subItem = struct.subItems.find(sub => subNameIsValid(sub, name)); - if (subItem) { - symbols.push(subItem); - if (onlyOne) return symbols; - } - - if (struct.type === `file`) { - const prefix = ignorePrefix && struct.keyword[`PREFIX`] && typeof struct.keyword[`PREFIX`] === `string` ? trimQuotes(struct.keyword[`PREFIX`].toUpperCase()) : ``; - - // If it's a file, we also need to check the subItems of the file's recordformats - for (const subFile of struct.subItems) { - const subSubItem = subFile.subItems.find(sub => subNameIsValid(sub, name, prefix)); - if (subSubItem) { - symbols.push(subSubItem); - if (onlyOne) return symbols; - } + if (struct.type === `file` && struct.keyword[`QUALIFIED`] !== true) { + const prefix = ignorePrefix && struct.keyword[`PREFIX`] && typeof struct.keyword[`PREFIX`] === `string` ? trimQuotes(struct.keyword[`PREFIX`].toUpperCase()) : ``; + + // If it's a file, we also need to check the subItems of the file's recordformats + for (const subFile of struct.subItems) { + const subSubItem = subFile.subItems.find(sub => subNameIsValid(sub, name, prefix)); + if (subSubItem) { + symbols.push(subSubItem); + if (onlyOne) return symbols; } } } diff --git a/language/parser.ts b/language/parser.ts index c53b7e77..1986ba13 100644 --- a/language/parser.ts +++ b/language/parser.ts @@ -1469,11 +1469,42 @@ export default class Parser { // TODO: generate a type for this let lookup = scope.find(iSpec.fieldName.value, undefined); - if (iSpec.dataFormat) { - const definedDataType = prettyTypeFromISpecTokens(iSpec); - console.log(definedDataType); - } else if (lookup) { + // This means the lookup is part of a struct + if (lookup && lookup.type === `subitem` && iSpec.dataFormat === undefined) { + // So we assign it a default type if there isn't one + iSpec.dataFormat = { + type: `word`, + value: iSpec.decimalPositions ? `S` : `A`, + range: {start: 35, end: 37, line: lineNumber} + }; + } + + const definedDataType = prettyTypeFromISpecTokens(iSpec); + + if (lookup) { + // TODO: does definedDataType match to lookup? + if (Object.keys(lookup.keyword).length === 0) { + lookup.keyword = definedDataType; + // console.log({name: lookup.name, definedDataType}); + } else { + // console.log({name: lookup.name, lookupKeyword: lookup.keyword, definedDataType}); + } + currentItem.subItems.push(lookup); + } else { + currentSub = new Declaration(`subitem`); + currentSub.name = iSpec.fieldName.value; + currentSub.keyword = definedDataType; + currentSub.position = { + path: fileUri, + range: iSpec.fieldName.range + }; + currentSub.range = { + start: lineNumber, + end: lineNumber + }; + + currentItem.subItems.push(currentSub); } currentItem.range.end = lineNumber; diff --git a/tests/suite/ispec.test.ts b/tests/suite/ispec.test.ts index 3b037560..692bcbc5 100644 --- a/tests/suite/ispec.test.ts +++ b/tests/suite/ispec.test.ts @@ -1,5 +1,7 @@ import { expect, test } from "vitest"; import setupParser from "../parserSetup"; +import exp from "constants"; +import { parseISpec } from "../../language/models/fixed"; const parser = setupParser(); const uri = `source.rpgle`; @@ -69,4 +71,128 @@ test('ispec rename 1', async () => { const cblSrcSeq = cache.find(`CBL_SRCSEQ`); expect(cblSrcSeq).toBeDefined(); +}); + +test('ispec range tests', async () => { + const lines = [ + ` ISRCPF NS`, + ` I 13 90 WKLINE` + ]; + + const a = parseISpec(1, 0, lines[0]); + expect(a).toBeDefined(); + + console.log(a); + console.log(lines[0].substring(a.name.range.start, a.name.range.end)); + + expect(lines[0].substring(a.name.range.start, a.name.range.end)).toBe(`SRCPF`); + + const b = parseISpec(1, 0, lines[1]); + expect(b).toBeDefined(); +}) + +test('ispec file fields definitions', async () => { + const lines = [ + ``, + ` dcl-f file1 disk(100);`, + ``, + ` dcl-s B_S_DTYP_P5_0 packed(5:0);`, + ` dcl-s B_P_DTYP_P5_0 packed(5:0);`, + ` dcl-s B_B_DTYP_P9_0 packed(9:0);`, + ` dcl-s B_I_DTYP_P10_0 packed(10:0);`, + ` dcl-s B_U_DTYP_P3_0 packed(3:0);`, + ``, + ` dcl-s C_P_DTYP_P4_0 packed(4:0);`, + ` dcl-s C_B_DTYP_P1_0 packed(1:0);`, + ``, + ` // Coding numeric fields in a DS with no type causes them`, + ` // to default to zoned`, + ` dcl-ds *n;`, + ` D_S_DTYP_S5_0;`, + ` D_P_DTYP_S5_0;`, + // ` D_P_DTYP_S5_0;`, + ` D_B_DTYP_S9_0;`, + ` D_I_DTYP_S10_0;`, + ` D_U_DTYP_S3_0;`, + ` end-ds;`, + ``, + ` dcl-ds *n;`, + ` E_S_DTYP_S5_0 zoned(5:0);`, + ` E_P_DTYP_P5_0 packed(5:0);`, + ` E_B_DTYP_B9_0 bindec(9:0);`, + ` E_I_DTYP_I10_0 int(10);`, + ` E_U_DTYP_U3_0 uns(3);`, + ` end-ds;`, + ``, + ``, + ` Ifile1 ns 01`, + ` * A. These are not coded in any D specs. All packed`, + ` * A. These are not coded in any D specs. All packed`, + ` I S 1 5 0A_S_TYP_P5_0`, + ` I P 1 3 0A_P_TYP_P5_0`, + ` I B 1 4 0A_B_TYP_P9_0`, + ` I I 1 4 0A_I_TYP_P10_0`, + ` I U 1 1 0A_U_TYP_P3_0`, + ` * B. These are also coded in D specs with same type as the defaul`, + ` I S 1 5 0B_S_DTYP_P5_0`, + ` I P 1 3 0B_P_DTYP_P5_0`, + ` I B 1 4 0B_B_DTYP_P9_0`, + ` I I 1 4 0B_I_DTYP_P10_0`, + ` I U 1 1 0B_U_DTYP_P3_0`, + ` * C. These are coded in D or C specs with a lower number of digit`, + ` I P 1 3 0C_P_DTYP_P4_0`, + ` I P 1 3 0C_P_CTYP_P4_0`, + ` I B 1 2 0C_B_DTYP_P1_0`, + ` I B 1 4 0C_B_CTYP_P5_0`, + ` * D. These are coded in a data structure with no types. All zoned`, + ` I S 1 5 0D_S_DTYP_S5_0`, + ` I P 1 3 0D_P_DTYP_S5_0`, + ` I B 1 4 0D_B_DTYP_S9_0`, + ` I I 1 4 0D_I_DTYP_S10_0`, + ` I U 1 1 0D_U_DTYP_S3_0`, + ` * E. These are coded in a DS with the same types as the I specs`, + ` I S 1 5 0E_S_DTYP_S5_0`, + ` I P 1 3 0E_P_DTYP_P5_0`, + ` I B 1 4 0E_B_DTYP_B9_0`, + ` I I 1 4 0E_I_DTYP_I10_0`, + ` I U 1 1 0E_U_DTYP_U3_0`, + ``, + ` C z-add 0 C_P_CTYP_P4_0 4 0`, + ` C z-add 0 C_B_CTYP_P5_0 5 0`, + ``, + ` C return`, + ]; + + const cache = await parser.getDocs(uri, lines.join('\n'), {ignoreCache: true, collectReferences: true}); + + const files = cache.files; + expect(files.length).toBe(1); + + const inputs = cache.symbols.filter(s => s.type === `input`); + expect(inputs.length).toBe(1); + + const structs = cache.structs; + expect(structs.length).toBe(2); + expect(structs.every(s => s.name.startsWith(`*n`))).toBeTruthy(); + + const blankA = structs[0]; + expect(blankA.subItems.length).toBe(5); + + const blankATwo = blankA.subItems[1]; + expect(blankATwo.name).toBe(`D_P_DTYP_S5_0`); + expect(blankATwo.keyword[`PACKED`]).toBe(`3:0`); + + const blankB = structs[1]; + expect(blankB.subItems.length).toBe(5); + + const file1input = inputs[0]; + expect(file1input.name).toBe(`file1`); + expect(file1input.subItems.length).toBe(24); + + const multipleDefinitionVar = cache.findAll(`D_P_DTYP_S5_0`); + expect(multipleDefinitionVar.length).toBe(1); + + const someVar = cache.find(`C_P_CTYP_P4_0`); + expect(someVar).toBeDefined(); + expect(someVar.references.length).toBe(2); }); \ No newline at end of file From 41a6103ab209cd5cccdd39995560c9aadd62ca22 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Wed, 30 Jul 2025 13:50:57 -0400 Subject: [PATCH 6/8] Fix position error on I-spec Signed-off-by: worksofliam --- language/models/fixed.ts | 2 +- tests/suite/ispec.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/language/models/fixed.ts b/language/models/fixed.ts index 7e6bf632..a4a383dd 100644 --- a/language/models/fixed.ts +++ b/language/models/fixed.ts @@ -341,7 +341,7 @@ export function parseISpec(lineNumber: number, lineIndex: number, content: strin } const getPart = (start: number, end: number, type?: string) => { - return calculateToken(lineNumber, lineIndex + start, content.substring(start-1, end).trimEnd(), type); + return calculateToken(lineNumber, lineIndex + (start-1), content.substring(start-1, end).trimEnd(), type); } switch (iType) { diff --git a/tests/suite/ispec.test.ts b/tests/suite/ispec.test.ts index 692bcbc5..47b7a1bc 100644 --- a/tests/suite/ispec.test.ts +++ b/tests/suite/ispec.test.ts @@ -81,14 +81,14 @@ test('ispec range tests', async () => { const a = parseISpec(1, 0, lines[0]); expect(a).toBeDefined(); - - console.log(a); - console.log(lines[0].substring(a.name.range.start, a.name.range.end)); + expect(a.name.value).toBe(`SRCPF`); expect(lines[0].substring(a.name.range.start, a.name.range.end)).toBe(`SRCPF`); const b = parseISpec(1, 0, lines[1]); expect(b).toBeDefined(); + expect(b.fieldName.value).toBe(`WKLINE`); + expect(lines[1].substring(b.fieldName.range.start, b.fieldName.range.end)).toBe(`WKLINE`); }) test('ispec file fields definitions', async () => { From 5646892d5609167fcca551365a0432a7886682f3 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Wed, 30 Jul 2025 13:59:01 -0400 Subject: [PATCH 7/8] Show enums correctly in symbol provider Signed-off-by: worksofliam --- extension/server/src/providers/documentSymbols.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/extension/server/src/providers/documentSymbols.ts b/extension/server/src/providers/documentSymbols.ts index f54de824..67173807 100644 --- a/extension/server/src/providers/documentSymbols.ts +++ b/extension/server/src/providers/documentSymbols.ts @@ -97,21 +97,22 @@ export default async function documentSymbolProvider(handler: DocumentSymbolPara scope.constants .filter(constant => constant.position && constant.position.path === currentPath) .forEach(def => { + const isEnum = def.subItems && def.subItems.length > 0; const constantDef = DocumentSymbol.create( def.name, prettyKeywords(def.keyword), - SymbolKind.Constant, + isEnum ? SymbolKind.Enum : SymbolKind.Constant, Range.create(def.range.start!, 0, def.range.end!, 0), Range.create(def.range.start!, 0, def.range.end!, 0) ); - if (def.subItems.length > 0) { + if (isEnum) { constantDef.children = def.subItems .filter(subitem => subitem.position && subitem.position.path === currentPath) .map(subitem => DocumentSymbol.create( subitem.name, prettyKeywords(subitem.keyword), - SymbolKind.Property, + SymbolKind.EnumMember, Range.create(subitem.range.start!, 0, subitem.range.start!, 0), Range.create(subitem.range.end!, 0, subitem.range.end!, 0) )); From e09d2519e444e279c6605763fe3a41895658e1e0 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Wed, 30 Jul 2025 14:11:07 -0400 Subject: [PATCH 8/8] Show input specs in outline view Signed-off-by: worksofliam --- .../server/src/providers/documentSymbols.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/extension/server/src/providers/documentSymbols.ts b/extension/server/src/providers/documentSymbols.ts index 67173807..04f9394a 100644 --- a/extension/server/src/providers/documentSymbols.ts +++ b/extension/server/src/providers/documentSymbols.ts @@ -171,6 +171,30 @@ export default async function documentSymbolProvider(handler: DocumentSymbolPara currentScopeDefs.push(expandStruct(struct)); }); + scope.inputs + .filter(input => input.position && input.position.path === currentPath && validRange(input)) + .forEach(input => { + const inputSymbol = DocumentSymbol.create( + input.name, + prettyKeywords(input.keyword), + SymbolKind.Interface, + Range.create(input.range.start!, 0, input.range.end!, 0), + Range.create(input.range.start!, 0, input.range.end!, 0) + ); + + inputSymbol.children = input.subItems + .filter(subitem => subitem.position && subitem.position.path === currentPath && validRange(subitem)) + .map(subitem => DocumentSymbol.create( + subitem.name, + prettyKeywords(subitem.keyword), + SymbolKind.Property, + Range.create(subitem.range.start!, 0, subitem.range.end!, 0), + Range.create(subitem.range.start!, 0, subitem.range.end!, 0) + )); + + currentScopeDefs.push(inputSymbol); + }); + return currentScopeDefs; };