diff --git a/extension/client/src/language/columnAssist.ts b/extension/client/src/language/columnAssist.ts index 9268bd59..58d74678 100644 --- a/extension/client/src/language/columnAssist.ts +++ b/extension/client/src/language/columnAssist.ts @@ -20,11 +20,38 @@ let currentEditorLine = -1; import { SpecFieldDef, SpecFieldValue, SpecRulers, specs } from '../schemas/specs'; +/** + * Resolves the spec lookup key for a line, detecting O-spec sub-types + * (OAnd, OF, OFC, OXF) so they map to the correct SpecFieldDef array. + * All other spec letters are returned as-is. + */ +function resolveSpecKey(line: string): string { + const specLetter = line[5]?.toUpperCase() ?? ``; + if (specLetter !== `O`) return specLetter; + + const paddedLine = line.padEnd(80); + const andOrKeyword = paddedLine.substring(15, 20).trim().toUpperCase(); + if (andOrKeyword === `AND` || andOrKeyword === `OR`) return `OAnd`; + + const filename = paddedLine.substring(6, 16).trim(); + const type = paddedLine.substring(16, 17).trim(); + const fieldName = paddedLine.substring(29, 43).trim(); + const endPos = paddedLine.substring(46, 51).trim(); + const constant = paddedLine.substring(52).trim(); + + if (endPos && (fieldName || constant)) return `OF`; + if (filename || type) return `O`; + if (!fieldName && constant) return `OFC`; + if (fieldName) return `OXF`; + return `O`; +} + const getAreasForLine = (line: string, index: number) => { if (line.length < 6) return undefined; if (line[6] === `*` || line[6] === `/`) return undefined; - const specLetter = line[5].toUpperCase(); + const specLetter = resolveSpecKey(line); + const baseSpecLetter = line[5]?.toUpperCase() ?? ``; if (specs[specLetter]) { const specification = specs[specLetter]; @@ -33,13 +60,13 @@ const getAreasForLine = (line: string, index: number) => { return { specification, active, - outline: SpecRulers[specLetter] + outline: SpecRulers[specLetter] ?? SpecRulers[baseSpecLetter] }; - } else if (SpecRulers[specLetter]) { + } else if (SpecRulers[specLetter] ?? SpecRulers[baseSpecLetter]) { return { specification: [] as SpecFieldDef[], active: -1, - outline: SpecRulers[specLetter] + outline: SpecRulers[specLetter] ?? SpecRulers[baseSpecLetter] }; } } @@ -231,7 +258,7 @@ async function promptLine (line: string, _index: number): Promise output.position && output.position.path === currentPath && validRange(output)) + .forEach(output => { + const outputDef = DocumentSymbol.create( + output.name, + prettyKeywords(output.keyword), + SymbolKind.String, // or SymbolKind.Field + Range.create(output.range.start!, 0, output.range.end!, 0), + Range.create(output.range.start!, 0, output.range.end!, 0) + ); + currentScopeDefs.push(outputDef); + }); scope.structs .filter(struct => struct.position && struct.position.path === currentPath && validRange(struct)) diff --git a/language/models/cache.ts b/language/models/cache.ts index 9c5bcdce..a7d3f40f 100644 --- a/language/models/cache.ts +++ b/language/models/cache.ts @@ -141,6 +141,10 @@ export default class Cache { return this.symbols.filter(s => s.type === `parameter`); } + get outputs() { + return this.symbols.filter(s => s.type === `output`); + } + addSymbol(symbol: Declaration) { const name = symbol.name.toUpperCase(); if (this.symbolRegister.has(name)) { diff --git a/language/models/declaration.ts b/language/models/declaration.ts index 089ee1dd..525852f8 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"| "output"; export default class Declaration { name: string = ``; diff --git a/language/models/fixed.js b/language/models/fixed.ts similarity index 56% rename from language/models/fixed.js rename to language/models/fixed.ts index e706b30b..c878b0ca 100644 --- a/language/models/fixed.js +++ b/language/models/fixed.ts @@ -151,6 +151,210 @@ export function parsePLine(content, lineNumber, lineIndex) { }; } +/** + * Detect O-Spec line type + * Note: Comment lines (* in column 7) are filtered out before reaching this function + */ +function detectOSpecType(content: string): 'O' | 'OAnd' | 'OF' | 'OFC' | 'OXF' { + // Check for comment line (though this should already be filtered by parser) + const commentChar = content.substr(6, 1); + if (commentChar === '*' || commentChar === '/') { + return 'OF'; // Return default to avoid processing + } + + const filename = content.substr(6, 10).trim(); + const type = content.substr(16, 1).trim(); + const andOrKeyword = content.substr(15, 5).trim().toUpperCase(); + const fieldName = content.substr(29, 14).trim(); + const endPos = content.substr(46, 5).trim(); + const constant = content.substr(52).trim(); + + if (andOrKeyword === 'AND' || andOrKeyword === 'OR') { + return 'OAnd'; + } + + // Check if this line has field/constant data (endPos + fieldName/constant) + // If so, it's a field line (OF), even if it also has type/filename + if (endPos && (fieldName || constant)) { + if (!endPos) { + return 'OXF'; + } + return 'OF'; + } + + if (filename || type) { + return 'O'; + } + + if (!fieldName && constant) { + return 'OFC'; + } + + if (fieldName) { + const endPosCheck = content.substr(46, 5).trim(); + if (!endPosCheck) { + return 'OXF'; + } + return 'OF'; + } + + return 'OF'; +} + +/** + * Parse O-Spec (Output Specification) line + * @param {number} lineNumber + * @param {number} lineIndex + * @param {string} content + */ +export function parseOLine(lineNumber, lineIndex, content) { + content = content.padEnd(80); + + const lineType = detectOSpecType(content); + + const filename = content.substr(6, 10); + const type = content.substr(16, 1); + + if (lineType === 'O') { + const fetchOverflow = content.substr(17, 3); + const addDeleteIndicator = content.substr(20, 3); + const outputIndicator1 = content.substr(23, 3); + const outputIndicator2 = content.substr(26, 3); + const exceptName = content.substr(29, 10); + const spaceBefore = content.substr(39, 3); + const spaceAfter = content.substr(42, 3); + const skipBefore = content.substr(45, 3); + const skipAfter = content.substr(48, 3); + + return { + lineType: 'O', + filename: calculateToken(lineNumber, lineIndex+6, filename), + type: calculateToken(lineNumber, lineIndex+16, type), + fetchOverflow: calculateToken(lineNumber, lineIndex+17, fetchOverflow), + addDeleteIndicator: calculateToken(lineNumber, lineIndex+20, addDeleteIndicator), + outputIndicator1: calculateToken(lineNumber, lineIndex+23, outputIndicator1), + outputIndicator2: calculateToken(lineNumber, lineIndex+26, outputIndicator2), + exceptName: calculateToken(lineNumber, lineIndex+29, exceptName), + spaceBefore: calculateToken(lineNumber, lineIndex+39, spaceBefore), + spaceAfter: calculateToken(lineNumber, lineIndex+42, spaceAfter), + skipBefore: calculateToken(lineNumber, lineIndex+45, skipBefore), + skipAfter: calculateToken(lineNumber, lineIndex+48, skipAfter), + andOr: undefined, + fieldName: undefined, + blankAfter: undefined, + editCodes: undefined, + endPosition: undefined, + dataFormat: undefined, + constantOrEdit: undefined + }; + } else if (lineType === 'OAnd') { + const andOrKeyword = content.substr(15, 5); + const outputIndicator1 = content.substr(20, 3); + const outputIndicator2 = content.substr(23, 3); + const outputIndicator3 = content.substr(26, 3); + const exceptName = content.substr(29, 10); + + return { + lineType: 'OAnd', + andOrKeyword: calculateToken(lineNumber, lineIndex+15, andOrKeyword), + outputIndicator1: calculateToken(lineNumber, lineIndex+20, outputIndicator1), + outputIndicator2: calculateToken(lineNumber, lineIndex+23, outputIndicator2), + outputIndicator3: calculateToken(lineNumber, lineIndex+26, outputIndicator3), + exceptName: calculateToken(lineNumber, lineIndex+29, exceptName), + filename: undefined, + type: undefined, + fetchOverflow: undefined, + andOr: calculateToken(lineNumber, lineIndex+15, andOrKeyword), + fieldName: undefined, + blankAfter: undefined, + editCodes: undefined, + endPosition: undefined, + dataFormat: undefined, + constantOrEdit: undefined + }; + } else if (lineType === 'OF') { + const outputIndicator1 = content.substr(20, 3); + const outputIndicator2 = content.substr(23, 3); + const outputIndicator3 = content.substr(26, 3); + const fieldName = content.substr(29, 14); + const blankAfter = content.substr(43, 1); + const editCodes = content.substr(44, 2); + const endPosition = content.substr(46, 5); + const dataFormat = content.substr(51, 1); + const constantOrEdit = content.substr(52, 28); + + return { + lineType: 'OF', + outputIndicator1: calculateToken(lineNumber, lineIndex+20, outputIndicator1), + outputIndicator2: calculateToken(lineNumber, lineIndex+23, outputIndicator2), + outputIndicator3: calculateToken(lineNumber, lineIndex+26, outputIndicator3), + fieldName: calculateToken(lineNumber, lineIndex+29, fieldName), + blankAfter: calculateToken(lineNumber, lineIndex+43, blankAfter), + editCodes: calculateToken(lineNumber, lineIndex+44, editCodes), + endPosition: calculateToken(lineNumber, lineIndex+46, endPosition), + dataFormat: calculateToken(lineNumber, lineIndex+51, dataFormat), + constantOrEdit: calculateToken(lineNumber, lineIndex+52, constantOrEdit), + filename: undefined, + type: undefined, + fetchOverflow: undefined, + andOr: undefined + }; + } else if (lineType === 'OFC') { + const constantOrEdit = content.substr(52, 28); + + return { + lineType: 'OFC', + constantOrEdit: calculateToken(lineNumber, lineIndex+52, constantOrEdit), + filename: undefined, + type: undefined, + fetchOverflow: undefined, + andOr: undefined, + fieldName: undefined, + blankAfter: undefined, + editCodes: undefined, + endPosition: undefined, + dataFormat: undefined + }; + } else if (lineType === 'OXF') { + const outputIndicator1 = content.substr(20, 3); + const outputIndicator2 = content.substr(23, 3); + const outputIndicator3 = content.substr(26, 3); + const fieldName = content.substr(29, 14); + const blankAfter = content.substr(44, 1); + + return { + lineType: 'OXF', + outputIndicator1: calculateToken(lineNumber, lineIndex+20, outputIndicator1), + outputIndicator2: calculateToken(lineNumber, lineIndex+23, outputIndicator2), + outputIndicator3: calculateToken(lineNumber, lineIndex+26, outputIndicator3), + fieldName: calculateToken(lineNumber, lineIndex+29, fieldName), + blankAfter: calculateToken(lineNumber, lineIndex+44, blankAfter), + filename: undefined, + type: undefined, + fetchOverflow: undefined, + andOr: undefined, + editCodes: undefined, + endPosition: undefined, + dataFormat: undefined, + constantOrEdit: undefined + }; + } + + return { + lineType: 'OF', + filename: undefined, + type: undefined, + fetchOverflow: undefined, + andOr: undefined, + fieldName: undefined, + blankAfter: undefined, + editCodes: undefined, + endPosition: undefined, + dataFormat: undefined, + constantOrEdit: undefined + }; +} + export function prettyTypeFromToken(dSpec) { return getPrettyType({ type: dSpec.type ? dSpec.type.value : ``, diff --git a/language/parser.ts b/language/parser.ts index 08290afc..65f0f0ad 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 } from "./models/fixed"; +import { parseFLine, parseCLine, parsePLine, parseDLine, parseOLine, getPrettyType, prettyTypeFromToken } from "./models/fixed"; import { Token } from "./types"; import { Keywords } from "./parserTypes"; import { NO_NAME } from "./statement"; @@ -689,7 +689,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`, `P`, `C`, `F`, `H`, `O`].includes(spec)) { continue; } } @@ -1889,6 +1889,159 @@ export default class Parser { potentialName = undefined; } break; + + case `O`: + const oSpec = parseOLine(lineNumber, lineIndex, line); + + // Handle based on line type + if (oSpec.lineType === 'O') { + // Record ID Line + if (oSpec.filename || oSpec.exceptName || oSpec.type) { + currentItem = new Declaration(`output`); + currentItem.name = oSpec.filename?.value || oSpec.exceptName?.value || oSpec.type?.value || ''; + + currentItem.keyword = { + lineType: 'O', + filename: oSpec.filename?.value || '', + type: oSpec.type?.value || '', + fetchOverflow: oSpec.fetchOverflow?.value || '', + addDeleteIndicator: oSpec.addDeleteIndicator?.value || '', + outputIndicator1: oSpec.outputIndicator1?.value || '', + outputIndicator2: oSpec.outputIndicator2?.value || '', + exceptName: oSpec.exceptName?.value || '', + spaceBefore: oSpec.spaceBefore?.value || '', + spaceAfter: oSpec.spaceAfter?.value || '', + skipBefore: oSpec.skipBefore?.value || '', + skipAfter: oSpec.skipAfter?.value || '' + }; + + currentItem.position = { + path: fileUri, + range: oSpec.filename?.range || oSpec.exceptName?.range || oSpec.type?.range + }; + + currentItem.range = { + start: lineNumber, + end: lineNumber + }; + + scope.addSymbol(currentItem); + resetDefinition = true; + } + } else if (oSpec.lineType === 'OAnd') { + // AND/OR Continuation Line + if (oSpec.andOrKeyword || oSpec.exceptName) { + currentItem = new Declaration(`output`); + currentItem.name = oSpec.andOrKeyword?.value || oSpec.exceptName?.value || ''; + + currentItem.keyword = { + lineType: 'OAnd', + andOrKeyword: oSpec.andOrKeyword?.value || '', + outputIndicator1: oSpec.outputIndicator1?.value || '', + outputIndicator2: oSpec.outputIndicator2?.value || '', + outputIndicator3: oSpec.outputIndicator3?.value || '', + exceptName: oSpec.exceptName?.value || '' + }; + + currentItem.position = { + path: fileUri, + range: oSpec.andOrKeyword?.range || oSpec.exceptName?.range + }; + + currentItem.range = { + start: lineNumber, + end: lineNumber + }; + + scope.addSymbol(currentItem); + resetDefinition = true; + } + } else if (oSpec.lineType === 'OF') { + // Program-Described Field Line + if (oSpec.fieldName || oSpec.constantOrEdit) { + currentItem = new Declaration(`output`); + currentItem.name = oSpec.fieldName?.value || oSpec.constantOrEdit?.value || ''; + + currentItem.keyword = { + lineType: 'OF', + outputIndicator1: oSpec.outputIndicator1?.value || '', + outputIndicator2: oSpec.outputIndicator2?.value || '', + outputIndicator3: oSpec.outputIndicator3?.value || '', + fieldName: oSpec.fieldName?.value || '', + blankAfter: oSpec.blankAfter?.value || '', + editCodes: oSpec.editCodes?.value || '', + endPosition: oSpec.endPosition?.value || '', + dataFormat: oSpec.dataFormat?.value || '', + constantOrEdit: oSpec.constantOrEdit?.value || '' + }; + + currentItem.position = { + path: fileUri, + range: oSpec.fieldName?.range || oSpec.constantOrEdit?.range + }; + + currentItem.range = { + start: lineNumber, + end: lineNumber + }; + + scope.addSymbol(currentItem); + resetDefinition = true; + } + } else if (oSpec.lineType === 'OFC') { + // Field Constant Continuation Line + if (oSpec.constantOrEdit) { + currentItem = new Declaration(`output`); + currentItem.name = oSpec.constantOrEdit?.value || ''; + + currentItem.keyword = { + lineType: 'OFC', + constantOrEdit: oSpec.constantOrEdit?.value || '' + }; + + currentItem.position = { + path: fileUri, + range: oSpec.constantOrEdit?.range + }; + + currentItem.range = { + start: lineNumber, + end: lineNumber + }; + + scope.addSymbol(currentItem); + resetDefinition = true; + } + } else if (oSpec.lineType === 'OXF') { + // Externally-Described Field Line + if (oSpec.fieldName) { + currentItem = new Declaration(`output`); + currentItem.name = oSpec.fieldName?.value || ''; + + currentItem.keyword = { + lineType: 'OXF', + outputIndicator1: oSpec.outputIndicator1?.value || '', + outputIndicator2: oSpec.outputIndicator2?.value || '', + outputIndicator3: oSpec.outputIndicator3?.value || '', + fieldName: oSpec.fieldName?.value || '', + blankAfter: oSpec.blankAfter?.value || '' + }; + + currentItem.position = { + path: fileUri, + range: oSpec.fieldName?.range + }; + + currentItem.range = { + start: lineNumber, + end: lineNumber + }; + + scope.addSymbol(currentItem); + resetDefinition = true; + } + } + break; } } diff --git a/tests/suite/ospec.test.ts b/tests/suite/ospec.test.ts new file mode 100644 index 00000000..de7c7d23 --- /dev/null +++ b/tests/suite/ospec.test.ts @@ -0,0 +1,457 @@ +import { expect, test } from "vitest"; +import setupParser from "../parserSetup"; + +const parser = setupParser(); +const uri = `source.rpgle`; + +test('ospec1 - basic output specification', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER OFLIND(*INOF)`, + ` O 10 'CUSTOMER'`, + ` O CUSTNO 20`, + ` O CUSTNAME 50`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.files.length).toBe(1); + + // Check that output specifications are parsed + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec2 - output with type indicators and edit codes', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D TITLE S 30`, + ` D AMOUNT S 10 2`, + ` O H 10 'HEADER'`, + ` O TITLE 40`, + ` O D 10 'DETAIL'`, + ` O AMOUNT 1 30`, + ` O T 10 'TOTAL'`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec3 - complex output specification', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER OFLIND(*INOF)`, + ` D CUSTNO S 5 0`, + ` D CUSTNAME S 30`, + ` D AMOUNT S 10 2`, + ` O H 1P 10 'CUSTOMER REPORT'`, + ` O 30 'DATE:'`, + ` O *DATE Y 40`, + ` O D 01 10 'CUSTOMER:'`, + ` O CUSTNO Z 20`, + ` O CUSTNAME 55`, + ` O AMOUNT 1 70`, + ` O T LR 10 'TOTAL RECORDS:'`, + ` O *COUNT Z 30`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.files.length).toBe(1); + expect(cache.variables.length).toBeGreaterThan(0); + + // Verify output specifications are parsed + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec4 - output with fetch overflow', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER OFLIND(*INOF)`, + ` D FIELD1 S 20`, + ` O H 1P 10 'HEADER'`, + ` O D OF 10 'OVERFLOW'`, + ` O FIELD1 30`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + + // Check output with fetch overflow - at least FIELD1 should be parsed + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec5 - output with data format', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D DATE1 S D`, + ` D TIME1 S T`, + ` D AMOUNT S 10 2`, + ` O DATE1 Y 20`, + ` O TIME1 Y 35`, + ` O AMOUNT 1 50`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + + // Verify output with data format (Y for date/time) + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec6 - output with blank after (B)', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D FIELD1 S 20`, + ` D FIELD2 S 15`, + ` O FIELD1 B 25`, + ` O FIELD2 45`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec7 - output with multiple edit codes', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D AMT1 S 10 2`, + ` D AMT2 S 10 2`, + ` D AMT3 S 10 2`, + ` O AMT1 1 20`, + ` O AMT2 2 40`, + ` O AMT3 Z 60`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec8 - output with AND/OR indicators', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D FIELD1 S 20`, + ` O D 01 10 'LINE 1'`, + ` O FIELD1 30`, + ` O D 02 AND 03 10 'LINE 2'`, + ` O FIELD1 30`, + ` O D 04 OR 05 10 'LINE 3'`, + ` O FIELD1 30`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec9 - output with exception lines (E)', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D FIELD1 S 20`, + ` O E EXCEPT1 10 'EXCEPTION'`, + ` O FIELD1 30`, + ` O E EXCEPT2 10 'ANOTHER'`, + ` O FIELD1 30`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec10 - output with space/skip before/after', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D FIELD1 S 20`, + ` O H 2 10 'HEADER'`, + ` O 2 30 'SPACED'`, + ` O D 1 10 'DETAIL'`, + ` O FIELD1 30`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec11 - output with constants and field names mixed', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D CUSTNO S 5 0`, + ` D CUSTNAME S 30`, + ` O 10 'CUSTOMER #:'`, + ` O CUSTNO 25`, + ` O 30 'NAME:'`, + ` O CUSTNAME 60`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec12 - output with reserved words (*DATE, *TIME, *PAGE)', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` O H 10 'REPORT'`, + ` O *DATE Y 30`, + ` O *TIME 45`, + ` O 60 'PAGE:'`, + ` O *PAGE Z 70`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec13 - output with all record types (H, D, T, E)', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D FIELD1 S 20`, + ` D TOTAL S 10 2`, + ` O H 10 'HEADER LINE'`, + ` O *DATE Y 30`, + ` O D 10 'DETAIL LINE'`, + ` O FIELD1 30`, + ` O T LR 10 'TOTAL:'`, + ` O TOTAL 1 30`, + ` O E ERROR1 10 'ERROR'`, + ` O FIELD1 30`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec14 - output with numeric edit codes (1-4, A-D, J-Q)', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D AMT1 S 10 2`, + ` D AMT2 S 10 2`, + ` D AMT3 S 10 2`, + ` D AMT4 S 10 2`, + ` O AMT1 A 20`, + ` O AMT2 B 40`, + ` O AMT3 J 60`, + ` O AMT4 K 80`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec15 - output with zero suppression (Z)', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D AMOUNT S 10 2`, + ` D QUANTITY S 5 0`, + ` O AMOUNT Z 20`, + ` O QUANTITY Z 40`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec16 - output with multiple files', async () => { + const lines = [ + ` FQPRINT1 O F 132 PRINTER`, + ` FQPRINT2 O F 132 PRINTER`, + ` D FIELD1 S 20`, + ` OQPRINT1 H 10 'FILE 1'`, + ` O FIELD1 30`, + ` OQPRINT2 H 10 'FILE 2'`, + ` O FIELD1 30`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.files.length).toBe(2); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec17 - output with conditioning indicators (01-99, H1-H9, L1-L9, LR)', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D FIELD1 S 20`, + ` O H 1P 10 'FIRST PAGE'`, + ` O D 01 10 'INDICATOR 01'`, + ` O FIELD1 30`, + ` O D L1 10 'LEVEL BREAK'`, + ` O FIELD1 30`, + ` O T LR 10 'LAST RECORD'`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec18 - output with empty lines and comments', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D FIELD1 S 20`, + ` * This is a comment`, + ` O H 10 'HEADER'`, + ` *`, + ` O FIELD1 30`, + ` * Another comment`, + ` O D 10 'DETAIL'`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec19 - output with long constants (up to 28 characters)', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` O H 40 'THIS IS A VERY LONG CONST'`, + ` O 80 'ANOTHER LONG CONSTANT HE'`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec20 - output with field names at various positions', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D FLD1 S 10`, + ` D FLD2 S 10`, + ` D FLD3 S 10`, + ` O FLD1 10`, + ` O FLD2 50`, + ` O FLD3 132`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec21 - output with negative indicators (N01-N99)', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D FIELD1 S 20`, + ` O D N01 10 'NOT 01'`, + ` O FIELD1 30`, + ` O D N99 10 'NOT 99'`, + ` O FIELD1 30`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec22 - output with complex AND/OR combinations', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D FIELD1 S 20`, + ` O D 01 AND 02 AND 03 10 'THREE AND'`, + ` O FIELD1 30`, + ` O D 04 OR 05 OR 06 10 'THREE OR'`, + ` O FIELD1 30`, + ` O D N01 ANDN02 10 'NOT AND NOT'`, + ` O FIELD1 30`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec23 - output with all data formats (Y, blank)', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` D DATE1 S D`, + ` D TIME1 S T`, + ` D CHAR1 S 10`, + ` O DATE1 Y 20`, + ` O TIME1 Y 35`, + ` O CHAR1 50`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec24 - output with minimum specification', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER`, + ` O H 10 'X'`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +test('ospec25 - output with maximum complexity', async () => { + const lines = [ + ` FQPRINT O F 132 PRINTER OFLIND(*INOF)`, + ` D CUSTNO S 5 0`, + ` D CUSTNAME S 30`, + ` D AMOUNT S 10 2`, + ` D TOTAL S 12 2`, + ` OQPRINT H 1P 10 'CUSTOMER REPORT'`, + ` O 30 'DATE:'`, + ` O *DATE Y 40`, + ` O 50 'PAGE:'`, + ` O *PAGE Z 60`, + ` O H OF 10 'CONTINUED...'`, + ` O D 01 AND 02 10 'CUSTOMER:'`, + ` O CUSTNO Z 25`, + ` O CUSTNAME B 60`, + ` O AMOUNT 1 80`, + ` O D N01 10 'NO CUSTOMER'`, + ` O T L1 10 'SUBTOTAL:'`, + ` O TOTAL 1 80`, + ` O T LR 10 'GRAND TOTAL:'`, + ` O TOTAL 1 80`, + ` O *COUNT Z 100`, + ` O E ERROR1 10 'ERROR OCCURRED'`, + ].join(`\n`); + + const cache = await parser.getDocs(uri, lines, { ignoreCache: true, withIncludes: true }); + + expect(cache).toBeDefined(); + expect(cache.files.length).toBe(1); + expect(cache.variables.length).toBeGreaterThan(0); + expect(cache.outputs.length).toBeGreaterThan(0); +}); + +// Made with Bob