diff --git a/extension/server/src/connection.ts b/extension/server/src/connection.ts index 37549187..c5e5114f 100644 --- a/extension/server/src/connection.ts +++ b/extension/server/src/connection.ts @@ -158,6 +158,7 @@ export function handleClientRequests() { indicators: doc.indicators, tags: doc.tags, includes: doc.includes, + outputs: doc.outputs } }); } diff --git a/extension/server/src/providers/documentSymbols.ts b/extension/server/src/providers/documentSymbols.ts index f54de824..68804eb1 100644 --- a/extension/server/src/providers/documentSymbols.ts +++ b/extension/server/src/providers/documentSymbols.ts @@ -30,7 +30,7 @@ export default async function documentSymbolProvider(handler: DocumentSymbolPara .filter(subitem => subitem.position && subitem.position.path === currentPath) .map(subitem => expandStruct(subitem)); } - + return parent; } @@ -65,7 +65,7 @@ export default async function documentSymbolProvider(handler: DocumentSymbolPara Range.create(subitem.range.start!, 0, subitem.range.end!, 0), Range.create(subitem.range.start!, 0, subitem.range.end!, 0) )); - + procDef.children.push(...getScopeVars(proc.scope)); } @@ -163,6 +163,18 @@ export default async function documentSymbolProvider(handler: DocumentSymbolPara currentScopeDefs.push(fileDef); }); + scope.outputs + .filter(output => 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.js index 99148fdf..f7f391ed 100644 --- a/language/models/fixed.js +++ b/language/models/fixed.js @@ -150,7 +150,39 @@ export function parsePLine(content, lineNumber, lineIndex) { start }; } - +/** + * 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 filename = content.substr(6, 10); // Columns 7-16 + const type = content.substr(16, 1); // Column 17 + const fetchOverflow = content.substr(17, 3); // Columns 18-20 + const andOr = content.substr(20, 9); // Columns 21-29 + const fieldName = content.substr(29, 14); // Columns 30-43 + const blankAfter = content.substr(43, 1); // Column 44 + const editCodes = content.substr(44, 2); // Columns 45-46 + const endPosition = content.substr(46, 5); // Columns 47-51 + const dataFormat = content.substr(51, 1); // Column 52 + const constantOrEdit = content.substr(52, 28); // Columns 53-80 + + return { + filename: calculateToken(lineNumber, lineIndex+6, filename), + type: calculateToken(lineNumber, lineIndex+16, type), + fetchOverflow: calculateToken(lineNumber, lineIndex+17, fetchOverflow), + andOr: calculateToken(lineNumber, lineIndex+20, andOr), + 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) + }; +} export function prettyTypeFromToken(dSpec) { return getPrettyType({ type: dSpec.type ? dSpec.type.value : ``, diff --git a/language/parser.ts b/language/parser.ts index 52a06d6a..fba1c716 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"; @@ -41,7 +41,7 @@ export default class Parser { console.log(`Clearing cache of these files: ${Object.keys(this.tables).join(`, `)}`) this.tables = {}; } - + async fetchTable(name: string, keyVersion = ``, aliases?: boolean): Promise { if (name === undefined || (name && name.trim() === ``)) return []; if (!this.tableFetch) return []; @@ -103,16 +103,16 @@ export default class Parser { } /** - * @param {string} line - * @returns {string|undefined} - */ + * @param {string} line + * @returns {string|undefined} + */ static getIncludeFromDirective(line: string): string|undefined { if (line.indexOf(`*`) !== line.toLowerCase().indexOf(`*libl`)) return; // Likely comment if (line.trim().startsWith(`//`)) return; // Likely comment const upperLine = line.toUpperCase(); let comment = -1; - + let directivePosition = upperLine.indexOf(`/COPY `); // Search comment AFTER the directive comment = upperLine.indexOf(`//`, directivePosition); @@ -126,7 +126,7 @@ export default class Parser { }; let directiveValue: string|undefined; - + if (directivePosition >= 0) { if (comment >= 0) { directiveValue = line.substring(directivePosition+directiveLength, comment).trim(); @@ -183,8 +183,8 @@ export default class Parser { let lastToken: number; while ( - tokens[checkNextToken] && - [`block`, `word`, `dot`, `builtin`].includes(tokens[checkNextToken].type) && + tokens[checkNextToken] && + [`block`, `word`, `dot`, `builtin`].includes(tokens[checkNextToken].type) && tokens[lastToken]?.type !== tokens[checkNextToken].type && checkNextToken >= 0 @@ -223,7 +223,7 @@ export default class Parser { scopes.push(new Cache()); const getObjectName = (objectName: string, keywords: Keywords): string => { - + // Check for external object const extFile = keywords[`EXTFILE`]; if (extFile && typeof extFile === `string`) { @@ -471,7 +471,7 @@ export default class Parser { break; } } - + return inputLine; } @@ -504,7 +504,7 @@ export default class Parser { unique: string }) => { const objectName = getObjectName(currentItem.name, fOptions.keyword); - + // ======== // First we do the work to set the subfields // ======== @@ -637,7 +637,7 @@ export default class Parser { if (li >= 1) { lineIndex += lines[li-1].length + EOL.length; } - + const scope = scopes[scopes.length - 1]; let baseLine = lines[li]; @@ -656,7 +656,7 @@ export default class Parser { // But it can be put on any other line and ignored. continue; } - + // If it's something else, we assume it's compile time data else break; } @@ -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; } } @@ -710,7 +710,7 @@ export default class Parser { let tokens: Token[] = []; let parts: string[]; let partsLower: string[]; - + if (isFullyFree || lineIsFree) { // Free format! if (line.trim() === ``) { @@ -771,79 +771,79 @@ export default class Parser { return; } else { switch (parts[0]) { - case `/COPY`: - case `/INCLUDE`: - if (options.withIncludes && this.includeFileFetch && lineCanRun()) { - const includePath = Parser.getIncludeFromDirective(line); - - if (includePath) { - const include = await this.includeFileFetch(workingUri, includePath); - if (include.found && include.uri) { - if (!scopes[0].includes.some(inc => inc.toPath === include.uri)) { - scopes[0].includes.push({ - fromPath: fileUri, - toPath: include.uri, - line: lineNumber - }); - - try { - await parseContent(include.uri, include.content); - } catch (e) { - console.log(`Error parsing include: ${include.uri}`); - console.log(e); + case `/COPY`: + case `/INCLUDE`: + if (options.withIncludes && this.includeFileFetch && lineCanRun()) { + const includePath = Parser.getIncludeFromDirective(line); + + if (includePath) { + const include = await this.includeFileFetch(workingUri, includePath); + if (include.found && include.uri) { + if (!scopes[0].includes.some(inc => inc.toPath === include.uri)) { + scopes[0].includes.push({ + fromPath: fileUri, + toPath: include.uri, + line: lineNumber + }); + + try { + await parseContent(include.uri, include.content); + } catch (e) { + console.log(`Error parsing include: ${include.uri}`); + console.log(e); + } } } } } - } - continue; - case `/IF`: - // Not conditions can run - let condition = false; - let hasNot = (parts[1] === `NOT`); - let expr = tokens.slice(hasNot ? 2 : 1); - let keywords = Parser.expandKeywords(expr); - - if (typeof keywords[`DEFINED`] === `string`) { - condition = definedMacros.includes(keywords[`DEFINED`]); - } + continue; + case `/IF`: + // Not conditions can run + let condition = false; + let hasNot = (parts[1] === `NOT`); + let expr = tokens.slice(hasNot ? 2 : 1); + let keywords = Parser.expandKeywords(expr); + + if (typeof keywords[`DEFINED`] === `string`) { + condition = definedMacros.includes(keywords[`DEFINED`]); + } - if (hasNot) condition = !condition; + if (hasNot) condition = !condition; directIfScope.push({condition: condition}); - continue; - case `/ELSE`: - if (directIfScope.length > 0) { - directIfScope[directIfScope.length - 1].condition = !directIfScope[directIfScope.length - 1].condition; - } - continue; - case `/ELSEIF`: - if (directIfScope.length > 0) { - directIfScope.pop(); + continue; + case `/ELSE`: + if (directIfScope.length > 0) { + directIfScope[directIfScope.length - 1].condition = !directIfScope[directIfScope.length - 1].condition; + } + continue; + case `/ELSEIF`: + if (directIfScope.length > 0) { + directIfScope.pop(); directIfScope.push({condition: false}); - } - continue; - case `/ENDIF`: - if (directIfScope.length > 0) { - directIfScope.pop(); - } - continue; + } + continue; + case `/ENDIF`: + if (directIfScope.length > 0) { + directIfScope.pop(); + } + continue; - case `/DEFINE`: - if (lineCanRun()) { - definedMacros.push(parts[1]); - } + case `/DEFINE`: + if (lineCanRun()) { + definedMacros.push(parts[1]); + } - continue; - default: - if (line.startsWith(`/`)) { - continue; - } - if (!lineCanRun()) { - // Ignore lines inside the IF scope. continue; - } - break; + default: + if (line.startsWith(`/`)) { + continue; + } + if (!lineCanRun()) { + // Ignore lines inside the IF scope. + continue; + } + break; } } } @@ -875,8 +875,8 @@ export default class Parser { } else if (!line.endsWith(`;`)) { currentStmtStart.content = (currentStmtStart.content || ``) + baseLine; - - if (currentStmtStart.content.endsWith(`-`)) + + if (currentStmtStart.content.endsWith(`-`)) currentStmtStart.content = currentStmtStart.content.substring(0, currentStmtStart.content.length - 1) + ` `; currentStmtStart.content += EOL; @@ -886,16 +886,67 @@ export default class Parser { } switch (parts[0]) { - case `CTL-OPT`: - globalKeyword.push(...parts.slice(1)); - break; + case `CTL-OPT`: + globalKeyword.push(...parts.slice(1)); + break; + + case `DCL-F`: + if (currentItem === undefined) { + if (parts.length > 1) { + currentItem = new Declaration(`file`); + currentItem.name = partsLower[1]; + currentItem.keyword = Parser.expandKeywords(tokens.slice(2)); + + currentItem.position = { + path: fileUri, + range: tokens[1].range + }; + + currentItem.range = { + start: currentStmtStart.line, + end: lineNumber + } + + await handleFSpec(currentItem, { + keyword: currentItem.keyword, + unique: parts.length.toString() + }); + + scope.addSymbol(currentItem); + resetDefinition = true; + } + } + break; + + case `DCL-C`: + if (currentItem === undefined) { + if (parts.length > 1) { + currentItem = new Declaration(`constant`); + currentItem.name = partsLower[1]; + currentItem.keyword = Parser.expandKeywords(tokens.slice(2), true); + + currentItem.position = { + path: fileUri, + range: tokens[1].range + }; + + currentItem.range = { + start: currentStmtStart.line, + end: lineNumber + }; + + scope.addSymbol(currentItem); + resetDefinition = true; + } + } + break; - case `DCL-F`: - if (currentItem === undefined) { + case `DCL-S`: if (parts.length > 1) { - currentItem = new Declaration(`file`); + currentItem = new Declaration(`variable`); currentItem.name = partsLower[1]; currentItem.keyword = Parser.expandKeywords(tokens.slice(2)); + currentItem.tags = currentTags; currentItem.position = { path: fileUri, @@ -905,25 +956,18 @@ export default class Parser { currentItem.range = { start: currentStmtStart.line, end: lineNumber - } - - await handleFSpec(currentItem, { - keyword: currentItem.keyword, - unique: parts.length.toString() - }); + }; scope.addSymbol(currentItem); resetDefinition = true; } - } - break; + break; - case `DCL-C`: - if (currentItem === undefined) { + case `DCL-ENUM`: if (parts.length > 1) { currentItem = new Declaration(`constant`); currentItem.name = partsLower[1]; - currentItem.keyword = Parser.expandKeywords(tokens.slice(2), true); + currentItem.keyword = Parser.expandKeywords(tokens.slice(2)); currentItem.position = { path: fileUri, @@ -932,139 +976,143 @@ export default class Parser { currentItem.range = { start: currentStmtStart.line, - end: lineNumber + end: currentStmtStart.line }; + currentItem.readParms = true; + + currentGroup = `constants`; + + currentDescription = []; + } + break; + + case `END-ENUM`: + if (currentItem && currentItem.type === `constant`) { + currentItem.range.end = currentStmtStart.line; + scope.addSymbol(currentItem); + resetDefinition = true; } - } - break; - - case `DCL-S`: - if (parts.length > 1) { - currentItem = new Declaration(`variable`); - currentItem.name = partsLower[1]; - currentItem.keyword = Parser.expandKeywords(tokens.slice(2)); - currentItem.tags = currentTags; - - currentItem.position = { - path: fileUri, - range: tokens[1].range - }; - - currentItem.range = { - start: currentStmtStart.line, - end: lineNumber - }; + break; - scope.addSymbol(currentItem); - resetDefinition = true; - } - break; + case `DCL-DS`: + if (currentItem === undefined) { + if (parts.length > 1) { + currentItem = new Declaration(`struct`); + currentItem.name = partsLower[1]; + currentItem.keyword = Parser.expandKeywords(tokens.slice(2)); + currentItem.tags = currentTags; - case `DCL-ENUM`: - if (parts.length > 1) { - currentItem = new Declaration(`constant`); - currentItem.name = partsLower[1]; - currentItem.keyword = Parser.expandKeywords(tokens.slice(2)); + currentItem.position = { + path: fileUri, + range: tokens[1].range + }; - currentItem.position = { - path: fileUri, - range: tokens[1].range - }; + currentItem.range = { + start: currentStmtStart.line, + end: currentStmtStart.line + }; - currentItem.range = { - start: currentStmtStart.line, - end: currentStmtStart.line - }; + currentGroup = `structs`; - currentItem.readParms = true; + // Expand the LIKEDS value if there is one. + await expandDs(fileUri, tokens[1], currentItem); - currentGroup = `constants`; + // Does the keywords include a keyword that makes end-ds useless? + const singleLineDef = Object.keys(currentItem.keyword).some(keyword => oneLineTriggers[`DCL-DS`].some(trigger => keyword.startsWith(trigger))); + if (singleLineDef) { + if (dsScopes.length > 0) { + // If we're already inside a dsScope, that means we need to add this item to the current definition + let lastItem = dsScopes[dsScopes.length - 1]; + lastItem.subItems.push(currentItem); + } else { + // Otherwise, we push as a new item + currentItem.range.end = tokens[tokens.length-1].range.line; + scope.addSymbol(currentItem); + } + } else { + // If it's not a single line defintion, flag the item to keep adding new fields + currentItem.readParms = true; + dsScopes.push(currentItem); + } - currentDescription = []; - } - break; + resetDefinition = true; - case `END-ENUM`: - if (currentItem && currentItem.type === `constant`) { - currentItem.range.end = currentStmtStart.line; - - scope.addSymbol(currentItem); + currentDescription = []; + } + } + break; - resetDefinition = true; - } - break; + case `END-DS`: + if (dsScopes.length > 0) { + const currentDs = dsScopes[dsScopes.length - 1]; + currentDs.range.end = currentStmtStart.line; + } - case `DCL-DS`: - if (currentItem === undefined) { - if (parts.length > 1) { - currentItem = new Declaration(`struct`); - currentItem.name = partsLower[1]; - currentItem.keyword = Parser.expandKeywords(tokens.slice(2)); - currentItem.tags = currentTags; + if (dsScopes.length === 1) { + scope.addSymbol(dsScopes.pop()); + } else + if (dsScopes.length > 1) { + dsScopes[dsScopes.length - 2].subItems.push(dsScopes.pop()); + } + break; - currentItem.position = { - path: fileUri, - range: tokens[1].range - }; + case `DCL-PR`: + if (currentItem === undefined) { + if (parts.length > 1) { + currentGroup = `procedures`; + currentItem = new Declaration(`procedure`); + currentItem.name = partsLower[1]; + currentItem.keyword = Parser.expandKeywords(tokens.slice(2)); + currentItem.tags = currentTags; - currentItem.range = { - start: currentStmtStart.line, - end: currentStmtStart.line - }; + currentItem.position = { + path: fileUri, + range: tokens[1].range + }; - currentGroup = `structs`; + currentItem.readParms = true; - // Expand the LIKEDS value if there is one. - await expandDs(fileUri, tokens[1], currentItem); + currentItem.range = { + start: currentStmtStart.line, + end: currentStmtStart.line + }; - // Does the keywords include a keyword that makes end-ds useless? - const singleLineDef = Object.keys(currentItem.keyword).some(keyword => oneLineTriggers[`DCL-DS`].some(trigger => keyword.startsWith(trigger))); - if (singleLineDef) { - if (dsScopes.length > 0) { - // If we're already inside a dsScope, that means we need to add this item to the current definition - let lastItem = dsScopes[dsScopes.length - 1]; - lastItem.subItems.push(currentItem); - } else { - // Otherwise, we push as a new item - currentItem.range.end = tokens[tokens.length-1].range.line; + // Does the keywords include a keyword that makes end-ds useless? + if (Object.keys(currentItem.keyword).some(keyword => oneLineTriggers[`DCL-PR`].some(trigger => keyword.startsWith(trigger)))) { + currentItem.range.end = currentStmtStart.line; scope.addSymbol(currentItem); + resetDefinition = true; } - } else { - // If it's not a single line defintion, flag the item to keep adding new fields - currentItem.readParms = true; - dsScopes.push(currentItem); + + currentDescription = []; } + } + break; - resetDefinition = true; + case `END-PR`: + if (currentItem && currentItem.type === `procedure`) { + currentItem.range.end = currentStmtStart.line; - currentDescription = []; - } - } - break; + const isDefinedGlobally = scopes[0].findAll(currentItem.name).find(proc => proc.scope); - case `END-DS`: - if (dsScopes.length > 0) { - const currentDs = dsScopes[dsScopes.length - 1]; - currentDs.range.end = currentStmtStart.line; - } + // Don't re-add self. This can happens when `END-PR` is used in the wrong place. + if (!isDefinedGlobally) { + scope.addSymbol(currentItem); + } - if (dsScopes.length === 1) { - scope.addSymbol(dsScopes.pop()); - } else - if (dsScopes.length > 1) { - dsScopes[dsScopes.length - 2].subItems.push(dsScopes.pop()); + resetDefinition = true; } - break; - - case `DCL-PR`: - if (currentItem === undefined) { + break; + + case `DCL-PROC`: if (parts.length > 1) { - currentGroup = `procedures`; currentItem = new Declaration(`procedure`); - currentItem.name = partsLower[1]; + + currentProcName = partsLower[1]; + currentItem.name = currentProcName; currentItem.keyword = Parser.expandKeywords(tokens.slice(2)); currentItem.tags = currentTags; @@ -1073,109 +1121,71 @@ export default class Parser { range: tokens[1].range }; - currentItem.readParms = true; + currentItem.readParms = false; currentItem.range = { start: currentStmtStart.line, end: currentStmtStart.line }; - // Does the keywords include a keyword that makes end-ds useless? - if (Object.keys(currentItem.keyword).some(keyword => oneLineTriggers[`DCL-PR`].some(trigger => keyword.startsWith(trigger)))) { - currentItem.range.end = currentStmtStart.line; - scope.addSymbol(currentItem); - resetDefinition = true; - } - - currentDescription = []; - } - } - break; - - case `END-PR`: - if (currentItem && currentItem.type === `procedure`) { - currentItem.range.end = currentStmtStart.line; + currentItem.scope = new Cache(undefined, true); - const isDefinedGlobally = scopes[0].findAll(currentItem.name).find(proc => proc.scope); - - // Don't re-add self. This can happens when `END-PR` is used in the wrong place. - if (!isDefinedGlobally) { scope.addSymbol(currentItem); - } + resetDefinition = true; - resetDefinition = true; - } - break; - - case `DCL-PROC`: - if (parts.length > 1) { - currentItem = new Declaration(`procedure`); - - currentProcName = partsLower[1]; - currentItem.name = currentProcName; - currentItem.keyword = Parser.expandKeywords(tokens.slice(2)); - currentItem.tags = currentTags; - - currentItem.position = { - path: fileUri, - range: tokens[1].range - }; + scopes.push(currentItem.scope); + } + break; - currentItem.readParms = false; + case `DCL-PI`: + //Procedures can only exist in the global scope. + if (parts.length > 0) { + if (currentProcName) { + currentGroup = `procedures`; + currentItem = scopes[0].findAll(currentProcName).find(proc => !proc.prototype && proc.scope); + } else { + currentItem = new Declaration(`struct`); + currentItem.name = PROGRAMPARMS_NAME; + } - currentItem.range = { - start: currentStmtStart.line, - end: currentStmtStart.line - }; + if (currentItem) { + const endInline = tokens.findIndex(part => part.value.toUpperCase() === `END-PI`); - currentItem.scope = new Cache(undefined, true); + // Indicates that the PI starts and ends on the same line + if (endInline >= 0) { + tokens.splice(endInline, 1); + currentItem.readParms = false; + resetDefinition = true; + } - scope.addSymbol(currentItem); - resetDefinition = true; + currentItem.keyword = { + ...currentItem.keyword, + ...Parser.expandKeywords(tokens.slice(2)) + } + currentItem.readParms = true; - scopes.push(currentItem.scope); - } - break; + currentDescription = []; + } + } + break; - case `DCL-PI`: - //Procedures can only exist in the global scope. - if (parts.length > 0) { + case `END-PI`: + //Procedures can only exist in the global scope. if (currentProcName) { - currentGroup = `procedures`; currentItem = scopes[0].findAll(currentProcName).find(proc => !proc.prototype && proc.scope); - } else { - currentItem = new Declaration(`struct`); - currentItem.name = PROGRAMPARMS_NAME; - } - if (currentItem) { - const endInline = tokens.findIndex(part => part.value.toUpperCase() === `END-PI`); - - // Indicates that the PI starts and ends on the same line - if (endInline >= 0) { - tokens.splice(endInline, 1); + if (currentItem && currentItem.type === `procedure`) { currentItem.readParms = false; - resetDefinition = true; - } + currentItem.subItems.forEach(subItem => { + subItem.type = `parameter`; + scope.addSymbol(subItem); + }); - currentItem.keyword = { - ...currentItem.keyword, - ...Parser.expandKeywords(tokens.slice(2)) + resetDefinition = true; } - currentItem.readParms = true; - - currentDescription = []; - } - } - break; - - case `END-PI`: - //Procedures can only exist in the global scope. - if (currentProcName) { - currentItem = scopes[0].findAll(currentProcName).find(proc => !proc.prototype && proc.scope); + } else if (currentItem && currentItem.name === PROGRAMPARMS_NAME) { + // Assign this scopes parameters to the subitems of the program parameters struct - if (currentItem && currentItem.type === `procedure`) { - currentItem.readParms = false; currentItem.subItems.forEach(subItem => { subItem.type = `parameter`; scope.addSymbol(subItem); @@ -1183,257 +1193,247 @@ export default class Parser { resetDefinition = true; } - } else if (currentItem && currentItem.name === PROGRAMPARMS_NAME) { - // Assign this scopes parameters to the subitems of the program parameters struct - - currentItem.subItems.forEach(subItem => { - subItem.type = `parameter`; - scope.addSymbol(subItem); - }); - - resetDefinition = true; - } - break; + break; - case `END-PROC`: - //Procedures can only exist in the global scope. - if (scopes.length > 1) { - // currentItem = scopes[0].symbols.find(proc => !proc.prototype && proc.name === currentProcName); - currentItem = scopes[0].findAll(currentProcName).find(proc => !proc.prototype && proc.scope); + case `END-PROC`: + //Procedures can only exist in the global scope. + if (scopes.length > 1) { + // currentItem = scopes[0].symbols.find(proc => !proc.prototype && proc.name === currentProcName); + currentItem = scopes[0].findAll(currentProcName).find(proc => !proc.prototype && proc.scope); - if (currentItem && currentItem.type === `procedure`) { - scopes.pop(); - currentItem.range.end = currentStmtStart.line; - resetDefinition = true; + if (currentItem && currentItem.type === `procedure`) { + scopes.pop(); + currentItem.range.end = currentStmtStart.line; + resetDefinition = true; + } } - } - break; + break; - case `BEGSR`: - if (parts.length > 1) { - if (!scope.find(parts[1], `subroutine`)) { - currentItem = new Declaration(`subroutine`); - currentItem.name = partsLower[1]; + case `BEGSR`: + if (parts.length > 1) { + if (!scope.find(parts[1], `subroutine`)) { + currentItem = new Declaration(`subroutine`); + currentItem.name = partsLower[1]; currentItem.keyword = {'Subroutine': true}; - currentItem.position = { - path: fileUri, - range: tokens[1].range - }; + currentItem.position = { + path: fileUri, + range: tokens[1].range + }; - currentItem.range = { - start: currentStmtStart.line, - end: currentStmtStart.line - }; + currentItem.range = { + start: currentStmtStart.line, + end: currentStmtStart.line + }; - currentDescription = []; + currentDescription = []; + } } - } - break; - - case `ENDSR`: - if (currentItem && currentItem.type === `subroutine`) { - currentItem.range.end = currentStmtStart.line; - scope.addSymbol(currentItem); - resetDefinition = true; - } - break; - - case `EXEC`: - const pIncludes = (value) => { - return parts.some(p => p === value); - } + break; - const pIs = (part, value) => { - return part && part === value; - } - - if (tokens.length > 2 && !pIncludes(`FETCH`)) { - // insert into XX.XX - // delete from xx.xx - // update xx.xx set - // select * into :x from xx.xx - // call xx.xx() - const preFileWords = [`INTO`, `FROM`, `UPDATE`, `CALL`, `JOIN`]; - const ignoredWords = [`FINAL`, `SET`]; - - const cleanupObjectRef = (index) => { - let nameIndex = index; - const result = { - schema: undefined, - name: partsLower[index], - length: 1, - nameToken: tokens[index] - } + case `ENDSR`: + if (currentItem && currentItem.type === `subroutine`) { + currentItem.range.end = currentStmtStart.line; + scope.addSymbol(currentItem); + resetDefinition = true; + } + break; - const schemaSplit = [`.`, `/`].includes(parts[nameIndex + 1]); - if (schemaSplit) { - result.schema = partsLower[index] - result.name = partsLower[index + 2] - result.length = 3; - result.nameToken = tokens[index + 2]; - nameIndex += 2; - } + case `EXEC`: + const pIncludes = (value) => { + return parts.some(p => p === value); + } - return result; + const pIs = (part, value) => { + return part && part === value; } - let isContinued = false; + if (tokens.length > 2 && !pIncludes(`FETCH`)) { + // insert into XX.XX + // delete from xx.xx + // update xx.xx set + // select * into :x from xx.xx + // call xx.xx() + const preFileWords = [`INTO`, `FROM`, `UPDATE`, `CALL`, `JOIN`]; + const ignoredWords = [`FINAL`, `SET`]; + + const cleanupObjectRef = (index) => { + let nameIndex = index; + const result = { + schema: undefined, + name: partsLower[index], + length: 1, + nameToken: tokens[index] + } + + const schemaSplit = [`.`, `/`].includes(parts[nameIndex + 1]); + if (schemaSplit) { + result.schema = partsLower[index] + result.name = partsLower[index + 2] + result.length = 3; + result.nameToken = tokens[index + 2]; + nameIndex += 2; + } + + return result; + } + + let isContinued = false; - let ignoreCtes: string[] = []; + let ignoreCtes: string[] = []; - if (pIncludes(`WITH`)) { - for (let index = 4; index < tokens.length; index++) { + if (pIncludes(`WITH`)) { + for (let index = 4; index < tokens.length; index++) { if (pIs(parts[index], `AS`) && pIs(parts[index+1], `(`)) { ignoreCtes.push(parts[index-1].toUpperCase()); + } } } - } - for (let index = 0; index < parts.length; index++) { - const part = parts[index]; - let inBlock = preFileWords.includes(part); + for (let index = 0; index < parts.length; index++) { + const part = parts[index]; + let inBlock = preFileWords.includes(part); - if ( - (inBlock || isContinued) && // If this is true, usually means next word is the object + if ( + (inBlock || isContinued) && // If this is true, usually means next word is the object (part === `INTO` ? parts[index-1] === `INSERT` : true) // INTO is special, as it can be used in both SELECT and INSERT - ) { + ) { if (index >= 0 && (index+1) < parts.length && tokens[index+1].type === `word` && !ignoredWords.includes(parts[index+1])) { const qualifiedObjectPath = cleanupObjectRef(index+1); - index += (qualifiedObjectPath.length); + index += (qualifiedObjectPath.length); isContinued = (parts[index+1] === `,`); - if (qualifiedObjectPath.name && !ignoreCtes.includes(qualifiedObjectPath.name.toUpperCase())) { - const currentSqlItem = new Declaration(`file`); - currentSqlItem.name = qualifiedObjectPath.name; + if (qualifiedObjectPath.name && !ignoreCtes.includes(qualifiedObjectPath.name.toUpperCase())) { + const currentSqlItem = new Declaration(`file`); + currentSqlItem.name = qualifiedObjectPath.name; - if (currentSqlItem.name) - currentSqlItem.keyword = {}; - - if (qualifiedObjectPath.schema) { - currentSqlItem.tags.push({ - tag: `description`, - content: qualifiedObjectPath.schema - }) + if (currentSqlItem.name) + currentSqlItem.keyword = {}; + + if (qualifiedObjectPath.schema) { + currentSqlItem.tags.push({ + tag: `description`, + content: qualifiedObjectPath.schema + }) + } + + currentSqlItem.position = { + path: fileUri, + range: qualifiedObjectPath.nameToken.range + }; + + scope.sqlReferences.push(currentSqlItem); } - - currentSqlItem.position = { - path: fileUri, - range: qualifiedObjectPath.nameToken.range - }; - - scope.sqlReferences.push(currentSqlItem); } } - } - }; - } - break; - - case `///`: - docs = !docs; - - // When enabled - if (docs === true) { - currentTitle = undefined; - currentDescription = []; - currentTags = []; - } - break; - - default: - if (lineIsComment) { - if (docs) { - const content = line.substring(2).trim(); - if (content.length > 0) { - if (content.startsWith(`@`)) { - const lineData = content.substring(1).split(` `); - currentTags.push({ - tag: lineData[0], - content: lineData.slice(1).join(` `) - }); - } else { - if (currentTags.length > 0) { - const lastTag = currentTags[currentTags.length - 1]; - lastTag.content += (lastTag.content.length === 0 ? `` : ` `) + content; + }; + } + break; - } else if (!currentTags.some(tag => tag.tag === `title`)) { - currentTags.push({ - tag: `title`, - content - }); + case `///`: + docs = !docs; + + // When enabled + if (docs === true) { + currentTitle = undefined; + currentDescription = []; + currentTags = []; + } + break; + default: + if (lineIsComment) { + if (docs) { + const content = line.substring(2).trim(); + if (content.length > 0) { + if (content.startsWith(`@`)) { + const lineData = content.substring(1).split(` `); currentTags.push({ - tag: `description`, - content: `` + tag: lineData[0], + content: lineData.slice(1).join(` `) }); + } else { + if (currentTags.length > 0) { + const lastTag = currentTags[currentTags.length - 1]; + lastTag.content += (lastTag.content.length === 0 ? `` : ` `) + content; + + } else if (!currentTags.some(tag => tag.tag === `title`)) { + currentTags.push({ + tag: `title`, + content + }); + + currentTags.push({ + tag: `description`, + content: `` + }); + } } } + + } else { + //Do nothing because it's a regular comment } } else { - //Do nothing because it's a regular comment - } - - } else { - if (!currentItem) { - if (dsScopes.length >= 1) { - // We do this as there can be many levels to data structures in free format - currentItem = dsScopes[dsScopes.length - 1]; + if (!currentItem) { + if (dsScopes.length >= 1) { + // We do this as there can be many levels to data structures in free format + currentItem = dsScopes[dsScopes.length - 1]; + } } - } - if (currentItem && [`procedure`, `struct`, `constant`].includes(currentItem.type)) { - if (currentItem.readParms && parts.length > 0) { - if (parts[0].startsWith(`DCL`)) { - parts.slice(1); - partsLower = partsLower.splice(1); - tokens.splice(1); - } + if (currentItem && [`procedure`, `struct`, `constant`].includes(currentItem.type)) { + if (currentItem.readParms && parts.length > 0) { + if (parts[0].startsWith(`DCL`)) { + parts.slice(1); + partsLower = partsLower.splice(1); + tokens.splice(1); + } - currentSub = new Declaration(`subitem`); - currentSub.name = (parts[0] === NO_NAME ? NO_NAME : partsLower[0]); - currentSub.keyword = Parser.expandKeywords(tokens.slice(1)); + currentSub = new Declaration(`subitem`); + currentSub.name = (parts[0] === NO_NAME ? NO_NAME : partsLower[0]); + currentSub.keyword = Parser.expandKeywords(tokens.slice(1)); - currentSub.position = { - path: fileUri, - range: tokens[0].range - }; + currentSub.position = { + path: fileUri, + range: tokens[0].range + }; - currentSub.range = { - start: currentStmtStart.line, - end: lineNumber - }; + currentSub.range = { + start: currentStmtStart.line, + end: lineNumber + }; - // Add comments from the tags - if (currentItem.type === `procedure`) { - const paramTags = currentItem.tags.filter(tag => tag.tag === `param`); - const paramTag = paramTags.length > currentItem.subItems.length ? paramTags[currentItem.subItems.length] : undefined; - if (paramTag) { - currentSub.tags = [{ - tag: `description`, - content: paramTag.content - }]; + // Add comments from the tags + if (currentItem.type === `procedure`) { + const paramTags = currentItem.tags.filter(tag => tag.tag === `param`); + const paramTag = paramTags.length > currentItem.subItems.length ? paramTags[currentItem.subItems.length] : undefined; + if (paramTag) { + currentSub.tags = [{ + tag: `description`, + content: paramTag.content + }]; + } } - } - // If the parameter has likeds, add the subitems to make it a struct. - await expandDs(fileUri, tokens[0], currentSub); + // If the parameter has likeds, add the subitems to make it a struct. + await expandDs(fileUri, tokens[0], currentSub); - currentItem.subItems.push(currentSub); - currentSub = undefined; + currentItem.subItems.push(currentSub); + currentSub = undefined; - if (currentItem.type === `struct` && currentItem.name !== PROGRAMPARMS_NAME) { - resetDefinition = true; + if (currentItem.type === `struct` && currentItem.name !== PROGRAMPARMS_NAME) { + resetDefinition = true; + } } } } - } - break; + break; } } else { @@ -1444,450 +1444,489 @@ export default class Parser { } switch (spec) { - case `H`: - globalKeyword.push(line.substring(6)); - break; - - case `F`: - const fSpec = parseFLine(lineNumber, lineIndex, line); + case `H`: + globalKeyword.push(line.substring(6)); + break; - if (fSpec.name) { - currentItem = new Declaration(`file`); - currentItem.name = fSpec.name.value; - currentItem.keyword = fSpec.keywords; + case `F`: + const fSpec = parseFLine(lineNumber, lineIndex, line); - currentItem.position = { - path: fileUri, - range: fSpec.name.range - }; + if (fSpec.name) { + currentItem = new Declaration(`file`); + currentItem.name = fSpec.name.value; + currentItem.keyword = fSpec.keywords; - currentItem.range.start = lineNumber; - currentItem.range.end = lineNumber; - - await handleFSpec(currentItem, { - keyword: fSpec.keywords, - unique: line.length.toString() - }); + currentItem.position = { + path: fileUri, + range: fSpec.name.range + }; - scope.addSymbol(currentItem); - } else { - // currentItem = scope.symbols[scope.symbols.length-1]; - if (currentItem) { + currentItem.range.start = lineNumber; currentItem.range.end = lineNumber; - currentItem.keyword = { - ...fSpec.keywords, - ...currentItem.keyword, - }; - // We have to run this after the keyword is set await handleFSpec(currentItem, { keyword: fSpec.keywords, unique: line.length.toString() }); - } - } - - break; - - case `C`: - const cSpec = parseCLine(lineNumber, lineIndex, line); - - tokens = [cSpec.clIndicator, cSpec.indicator, cSpec.ind1, cSpec.ind2, cSpec.ind3]; - - const fromToken = (token?: Token) => { - return token ? Parser.lineTokens(token.value, lineNumber, token.range.start) : []; - }; - if (cSpec.opcode && ALLOWS_EXTENDED.includes(cSpec.opcode.value) && !cSpec.factor1 && cSpec.extended) { - tokens.push(...fromToken(cSpec.extended)); - } else if (!cSpec.factor1 && !cSpec.opcode && cSpec.extended) { - tokens.push(...fromToken(cSpec.extended)); - } else { - tokens.push( - ...fromToken(cSpec.factor1), - ...fromToken(cSpec.factor2), - ...fromToken(cSpec.result), - ); - - if (cSpec.result && cSpec.fieldLength) { - // This means we need to dynamically define this field - const fieldName = cSpec.result.value; - // Don't redefine this field. - if (!scopes[0].findDefinition(lineNumber, fieldName)) { - const fieldLength = parseInt(cSpec.fieldLength.value); - const decimals = cSpec.fieldDecimals ? parseInt(cSpec.fieldDecimals.value) : undefined; - const type = decimals !== undefined ? `PACKED`: `CHAR`; - - currentSub = new Declaration(`variable`); - currentSub.name = fieldName; - currentSub.keyword = {[type]: `${fieldLength}${decimals !== undefined ? `:${decimals}` : ``}`}; - currentSub.position = { - path: fileUri, - range: cSpec.result.range - }; - currentSub.range = { - start: lineNumber, - end: lineNumber + scope.addSymbol(currentItem); + } else { + // currentItem = scope.symbols[scope.symbols.length-1]; + if (currentItem) { + currentItem.range.end = lineNumber; + currentItem.keyword = { + ...fSpec.keywords, + ...currentItem.keyword, }; - scope.addSymbol(currentSub); + // We have to run this after the keyword is set + await handleFSpec(currentItem, { + keyword: fSpec.keywords, + unique: line.length.toString() + }); } } - } - switch (cSpec.opcode && cSpec.opcode.value) { - case `BEGSR`: - if (cSpec.factor1 && !scope.find(cSpec.factor1.value, `subroutine`)) { - currentItem = new Declaration(`subroutine`); - currentItem.name = cSpec.factor1.value; - currentItem.keyword = {'Subroutine': true}; - - currentItem.position = { - path: fileUri, - range: cSpec.factor1.range - }; - - currentItem.range = { - start: lineNumber, - end: lineNumber - }; - - currentDescription = []; - } break; - case `ENDSR`: - if (currentItem && currentItem.type === `subroutine`) { - currentItem.range.end = lineNumber; - scope.addSymbol(currentItem); - resetDefinition = true; - } - break; - - case `CALL`: - const callItem = new Declaration(`procedure`); - if (cSpec.factor2) { - const f2Value = cSpec.factor2.value; - callItem.name = (f2Value.startsWith(`'`) && f2Value.endsWith(`'`) ? f2Value.substring(1, f2Value.length-1) : f2Value); - callItem.keyword = {'CALL': true} - callItem.tags = currentTags; + case `C`: + const cSpec = parseCLine(lineNumber, lineIndex, line); - callItem.position = { - path: fileUri, - range: cSpec.factor2.range - }; + tokens = [cSpec.clIndicator, cSpec.indicator, cSpec.ind1, cSpec.ind2, cSpec.ind3]; - callItem.range = { - start: lineNumber, - end: lineNumber - }; - - scope.addSymbol(callItem); - } - break; + const fromToken = (token?: Token) => { + return token ? Parser.lineTokens(token.value, lineNumber, token.range.start) : []; + }; - case `TAG`: - const tagItem = new Declaration(`tag`); - if (cSpec.factor1) { - tagItem.name = cSpec.factor1.value; - tagItem.position = { - path: fileUri, - range: cSpec.factor1.range - }; + if (cSpec.opcode && ALLOWS_EXTENDED.includes(cSpec.opcode.value) && !cSpec.factor1 && cSpec.extended) { + tokens.push(...fromToken(cSpec.extended)); + } else if (!cSpec.factor1 && !cSpec.opcode && cSpec.extended) { + tokens.push(...fromToken(cSpec.extended)); + } else { + tokens.push( + ...fromToken(cSpec.factor1), + ...fromToken(cSpec.factor2), + ...fromToken(cSpec.result), + ); + + if (cSpec.result && cSpec.fieldLength) { + // This means we need to dynamically define this field + const fieldName = cSpec.result.value; + // Don't redefine this field. + if (!scopes[0].findDefinition(lineNumber, fieldName)) { + const fieldLength = parseInt(cSpec.fieldLength.value); + const decimals = cSpec.fieldDecimals ? parseInt(cSpec.fieldDecimals.value) : undefined; + const type = decimals !== undefined ? `PACKED`: `CHAR`; - tagItem.range = { - start: lineNumber, - end: lineNumber - }; + currentSub = new Declaration(`variable`); + currentSub.name = fieldName; + currentSub.keyword = {[type]: `${fieldLength}${decimals !== undefined ? `:${decimals}` : ``}`}; + currentSub.position = { + path: fileUri, + range: cSpec.result.range + }; + currentSub.range = { + start: lineNumber, + end: lineNumber + }; - scope.addSymbol(tagItem); + scope.addSymbol(currentSub); + } + } } - break; - } - break; - case `P`: - const pSpec = parsePLine(line, lineNumber, lineIndex); + switch (cSpec.opcode && cSpec.opcode.value) { + case `BEGSR`: + if (cSpec.factor1 && !scope.find(cSpec.factor1.value, `subroutine`)) { + currentItem = new Declaration(`subroutine`); + currentItem.name = cSpec.factor1.value; + currentItem.keyword = {'Subroutine': true}; - if (pSpec.potentialName) { - pushPotentialNameToken(pSpec.potentialName); - - tokens = [pSpec.potentialName]; - } else { - if (pSpec.start) { - tokens = [...pSpec.keywordsRaw, pSpec.name]; + currentItem.position = { + path: fileUri, + range: cSpec.factor1.range + }; - if (pSpec.name && pSpec.name.value.length > 0) { - pushPotentialNameToken(pSpec.name); - } + currentItem.range = { + start: lineNumber, + end: lineNumber + }; - const currentNameToken = getPotentialNameToken(); + currentDescription = []; + } + break; - if (currentNameToken) { - currentItem = new Declaration(`procedure`); + case `ENDSR`: + if (currentItem && currentItem.type === `subroutine`) { + currentItem.range.end = lineNumber; + scope.addSymbol(currentItem); + resetDefinition = true; + } + break; - currentProcName = currentNameToken.value; - currentItem.name = currentProcName; - currentItem.keyword = pSpec.keywords; + case `CALL`: + const callItem = new Declaration(`procedure`); + if (cSpec.factor2) { + const f2Value = cSpec.factor2.value; + callItem.name = (f2Value.startsWith(`'`) && f2Value.endsWith(`'`) ? f2Value.substring(1, f2Value.length-1) : f2Value); + callItem.keyword = {'CALL': true} + callItem.tags = currentTags; - currentItem.position = { - path: fileUri, - range: currentNameToken.range - }; + callItem.position = { + path: fileUri, + range: cSpec.factor2.range + }; - currentItem.range = { - start: currentNameToken.range.line, - end: currentNameToken.range.line - }; + callItem.range = { + start: lineNumber, + end: lineNumber + }; - currentItem.scope = new Cache(undefined, true); + scope.addSymbol(callItem); + } + break; - scope.addSymbol(currentItem); - resetDefinition = true; + case `TAG`: + const tagItem = new Declaration(`tag`); + if (cSpec.factor1) { + tagItem.name = cSpec.factor1.value; + tagItem.position = { + path: fileUri, + range: cSpec.factor1.range + }; - scopes.push(currentItem.scope); - } - } else { - if (scopes.length > 1) { - //Procedures can only exist in the global scope. - currentItem = scopes[0].find(currentProcName); + tagItem.range = { + start: lineNumber, + end: lineNumber + }; - if (currentItem && currentItem.type === `procedure`) { - scopes.pop(); - currentItem.range.end = lineNumber; - resetDefinition = true; + scope.addSymbol(tagItem); } - } + break; } - } - break; - case `D`: - const dSpec = parseDLine(lineNumber, lineIndex, line); + break; + case `P`: + const pSpec = parsePLine(line, lineNumber, lineIndex); - if (dSpec.potentialName) { - pushPotentialNameToken(dSpec.potentialName); - tokens = [dSpec.potentialName]; - continue; - } else { + if (pSpec.potentialName) { + pushPotentialNameToken(pSpec.potentialName); - if (dSpec.name && dSpec.name.value.length > 0) { - pushPotentialNameToken(dSpec.name); - } + tokens = [pSpec.potentialName]; + } else { + if (pSpec.start) { + tokens = [...pSpec.keywordsRaw, pSpec.name]; - tokens = [dSpec.field, ...dSpec.keywordsRaw, dSpec.name]; + if (pSpec.name && pSpec.name.value.length > 0) { + pushPotentialNameToken(pSpec.name); + } - const currentNameToken = getPotentialNameToken(); + const currentNameToken = getPotentialNameToken(); - switch (dSpec.field && dSpec.field.value) { - case `C`: - currentItem = new Declaration(`constant`); - currentItem.name = currentNameToken?.value || NO_NAME; - currentItem.keyword = dSpec.keywords || {}; - - // TODO: line number might be different with ...? - currentItem.position = { - path: fileUri, - range: dSpec.field.range - }; + if (currentNameToken) { + currentItem = new Declaration(`procedure`); - currentItem.range = { - start: currentNameToken.range.line, - end: currentItem.position.range.line - }; - - scope.addSymbol(currentItem); - resetDefinition = true; - break; - case `S`: + currentProcName = currentNameToken.value; + currentItem.name = currentProcName; + currentItem.keyword = pSpec.keywords; - if (!currentNameToken) { - // If we don't have a current name token - // we cannot create a variable declaration (RNF3316) - break; - } + currentItem.position = { + path: fileUri, + range: currentNameToken.range + }; - currentItem = new Declaration(`variable`); - currentItem.name = currentNameToken.value; - currentItem.keyword = { - ...dSpec.keywords, - ...prettyTypeFromToken(dSpec), - } + currentItem.range = { + start: currentNameToken.range.line, + end: currentNameToken.range.line + }; - // TODO: line number might be different with ...? - currentItem.position = { - path: fileUri, - range: currentNameToken.range - }; + currentItem.scope = new Cache(undefined, true); - currentItem.range = { - start: currentNameToken.range.line, - end: currentItem.position.range.line - }; + scope.addSymbol(currentItem); + resetDefinition = true; - scope.addSymbol(currentItem); - resetDefinition = true; - break; + scopes.push(currentItem.scope); + } + } else { + if (scopes.length > 1) { + //Procedures can only exist in the global scope. + currentItem = scopes[0].find(currentProcName); + + if (currentItem && currentItem.type === `procedure`) { + scopes.pop(); + currentItem.range.end = lineNumber; + resetDefinition = true; + } + } + } + } + break; - case `DS`: - currentItem = new Declaration(`struct`); - currentItem.name = currentNameToken?.value || NO_NAME; - currentItem.keyword = dSpec.keywords; + case `D`: + const dSpec = parseDLine(lineNumber, lineIndex, line); - currentItem.position = { - path: fileUri, - range: currentNameToken?.range || dSpec.field.range - }; + if (dSpec.potentialName) { + pushPotentialNameToken(dSpec.potentialName); + tokens = [dSpec.potentialName]; + continue; + } else { - currentItem.range = { - start: currentItem.position.range.line, - end: currentItem.position.range.line - }; + if (dSpec.name && dSpec.name.value.length > 0) { + pushPotentialNameToken(dSpec.name); + } - expandDs(fileUri, currentNameToken, currentItem); + tokens = [dSpec.field, ...dSpec.keywordsRaw, dSpec.name]; - currentGroup = `structs`; - scope.addSymbol(currentItem); - resetDefinition = true; - break; + const currentNameToken = getPotentialNameToken(); - case `PR`: - currentItem = new Declaration(`procedure`); - currentItem.name = currentNameToken?.value || NO_NAME; - currentItem.keyword = { - ...prettyTypeFromToken(dSpec), - ...dSpec.keywords - } + switch (dSpec.field && dSpec.field.value) { + case `C`: + currentItem = new Declaration(`constant`); + currentItem.name = currentNameToken?.value || NO_NAME; + currentItem.keyword = dSpec.keywords || {}; - currentItem.position = { - path: fileUri, - range: getPotentialNameToken().range - }; + // TODO: line number might be different with ...? + currentItem.position = { + path: fileUri, + range: dSpec.field.range + }; - currentItem.range = { - start: currentItem.position.range.line, - end: currentItem.position.range.line - }; + currentItem.range = { + start: currentNameToken.range.line, + end: currentItem.position.range.line + }; - currentGroup = `procedures`; - scope.addSymbol(currentItem); - currentDescription = []; - break; + scope.addSymbol(currentItem); + resetDefinition = true; + break; + case `S`: - case `PI`: - //Procedures can only exist in the global scope. - if (currentProcName) { - currentItem = scopes[0].findAll(currentProcName).find(proc => !proc.prototype && proc.scope); + if (!currentNameToken) { + // If we don't have a current name token + // we cannot create a variable declaration (RNF3316) + break; + } - currentGroup = `procedures`; - if (currentItem) { + currentItem = new Declaration(`variable`); + currentItem.name = currentNameToken.value; currentItem.keyword = { - ...currentItem.keyword, + ...dSpec.keywords, ...prettyTypeFromToken(dSpec), - ...dSpec.keywords } - } - } else { - currentGroup = `parameters`; - } - break; - default: - // No type, must be either a struct subfield OR a parameter - if (!currentItem) { - switch (currentGroup) { - case `structs`: - case `procedures`: - // We have to do this backwards lookup to find the definition - // because in fixed format, currentItem is not defined. So - // we go find the latest procedure/structure defined - let validScope; - for (let i = scopes.length - 1; i >= 0; i--) { - validScope = scopes[i]; - if (validScope[currentGroup].length > 0) break; - } - - currentItem = validScope[currentGroup][validScope[currentGroup].length - 1]; + // TODO: line number might be different with ...? + currentItem.position = { + path: fileUri, + range: currentNameToken.range + }; + + currentItem.range = { + start: currentNameToken.range.line, + end: currentItem.position.range.line + }; + + scope.addSymbol(currentItem); + resetDefinition = true; break; - case `parameters`: + case `DS`: currentItem = new Declaration(`struct`); - currentItem.name = PROGRAMPARMS_NAME; - break; - } - } + currentItem.name = currentNameToken?.value || NO_NAME; + currentItem.keyword = dSpec.keywords; - if (currentItem) { - const isProgramParameter = currentItem.name === PROGRAMPARMS_NAME; - - // This happens when it's a blank parm. - const baseToken = dSpec.type || dSpec.len; - if (!potentialName && baseToken) { - pushPotentialNameToken({ - ...baseToken, - value: NO_NAME - }); - } + currentItem.position = { + path: fileUri, + range: currentNameToken?.range || dSpec.field.range + }; - const currentNameToken = getPotentialNameToken(); + currentItem.range = { + start: currentItem.position.range.line, + end: currentItem.position.range.line + }; - if (potentialName) { - currentSub = new Declaration(isProgramParameter ? `parameter` : `subitem`); - currentSub.name = currentNameToken?.value || NO_NAME; - currentSub.keyword = { + expandDs(fileUri, currentNameToken, currentItem); + + currentGroup = `structs`; + scope.addSymbol(currentItem); + resetDefinition = true; + break; + + case `PR`: + currentItem = new Declaration(`procedure`); + currentItem.name = currentNameToken?.value || NO_NAME; + currentItem.keyword = { ...prettyTypeFromToken(dSpec), ...dSpec.keywords } - currentSub.position = { + currentItem.position = { path: fileUri, - range: currentNameToken?.range + range: getPotentialNameToken().range }; - currentSub.range = { - start: lineNumber, - end: lineNumber - } + currentItem.range = { + start: currentItem.position.range.line, + end: currentItem.position.range.line + }; - // If the parameter has likeds, add the subitems to make it a struct. - await expandDs(fileUri, currentNameToken, currentSub); + currentGroup = `procedures`; + scope.addSymbol(currentItem); + currentDescription = []; + break; - if (isProgramParameter) { - scope.addSymbol(currentSub); + case `PI`: + //Procedures can only exist in the global scope. + if (currentProcName) { + currentItem = scopes[0].findAll(currentProcName).find(proc => !proc.prototype && proc.scope); + + currentGroup = `procedures`; + if (currentItem) { + currentItem.keyword = { + ...currentItem.keyword, + ...prettyTypeFromToken(dSpec), + ...dSpec.keywords + } + } } else { - currentItem.subItems.push(currentSub); + currentGroup = `parameters`; + } + break; + + default: + // No type, must be either a struct subfield OR a parameter + if (!currentItem) { + switch (currentGroup) { + case `structs`: + case `procedures`: + // We have to do this backwards lookup to find the definition + // because in fixed format, currentItem is not defined. So + // we go find the latest procedure/structure defined + let validScope; + for (let i = scopes.length - 1; i >= 0; i--) { + validScope = scopes[i]; + if (validScope[currentGroup].length > 0) break; + } + + currentItem = validScope[currentGroup][validScope[currentGroup].length - 1]; + break; + + case `parameters`: + currentItem = new Declaration(`struct`); + currentItem.name = PROGRAMPARMS_NAME; + break; + } } - currentSub = undefined; - resetDefinition = true; - } else { if (currentItem) { - if (currentItem.subItems.length > 0) { - currentItem.subItems[currentItem.subItems.length - 1].keyword = { - ...currentItem.subItems[currentItem.subItems.length - 1].keyword, + const isProgramParameter = currentItem.name === PROGRAMPARMS_NAME; + + // This happens when it's a blank parm. + const baseToken = dSpec.type || dSpec.len; + if (!potentialName && baseToken) { + pushPotentialNameToken({ + ...baseToken, + value: NO_NAME + }); + } + + const currentNameToken = getPotentialNameToken(); + + if (potentialName) { + currentSub = new Declaration(isProgramParameter ? `parameter` : `subitem`); + currentSub.name = currentNameToken?.value || NO_NAME; + currentSub.keyword = { ...prettyTypeFromToken(dSpec), ...dSpec.keywords + } + + currentSub.position = { + path: fileUri, + range: currentNameToken?.range }; - currentItem.subItems[currentItem.subItems.length - 1].range.end = lineNumber; - } else { - currentItem.keyword = { - ...currentItem.keyword, - ...dSpec.keywords + currentSub.range = { + start: lineNumber, + end: lineNumber } + // If the parameter has likeds, add the subitems to make it a struct. + await expandDs(fileUri, currentNameToken, currentSub); + + if (isProgramParameter) { + scope.addSymbol(currentSub); + } else { + currentItem.subItems.push(currentSub); + } + currentSub = undefined; + + resetDefinition = true; + } else { + if (currentItem) { + if (currentItem.subItems.length > 0) { + currentItem.subItems[currentItem.subItems.length - 1].keyword = { + ...currentItem.subItems[currentItem.subItems.length - 1].keyword, + ...prettyTypeFromToken(dSpec), + ...dSpec.keywords + }; + + currentItem.subItems[currentItem.subItems.length - 1].range.end = lineNumber; + } else { + currentItem.keyword = { + ...currentItem.keyword, + ...dSpec.keywords + } + + } + } } - } - } - currentItem.range.end = lineNumber; + currentItem.range.end = lineNumber; + } + break; } - break; + + potentialName = undefined; } - - potentialName = undefined; - } - break; + break; + + case `O`: + const oSpec = parseOLine(lineNumber, lineIndex, line); + + // Create output specification declaration + if (oSpec.filename || oSpec.fieldName || oSpec.constantOrEdit) { + currentItem = new Declaration(`output`); + + // Set the name to fieldName if available, otherwise use filename or constant + currentItem.name = oSpec.fieldName?.value || oSpec.filename?.value || oSpec.constantOrEdit?.value || ''; + + // Store O-Spec details + currentItem.keyword = { + filename: oSpec.filename?.value || '', + type: oSpec.type?.value || '', + fetchOverflow: oSpec.fetchOverflow?.value || '', + andOr: oSpec.andOr?.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.filename?.range || oSpec.constantOrEdit?.range + }; + + currentItem.range = { + start: lineNumber, + end: lineNumber + }; + + scope.addSymbol(currentItem); + resetDefinition = true; + } + break; } } @@ -1899,7 +1938,7 @@ export default class Parser { if (resetDefinition) { potentialName = undefined; - + currentItem = undefined; currentTitle = undefined; currentDescription = []; @@ -1929,7 +1968,7 @@ export default class Parser { static getTokens(content: string|string[]|Token[], lineNumber?: number, baseIndex?: number): Token[] { if (Array.isArray(content) && typeof content[0] === `string`) { return Parser.lineTokens(content.join(` `), lineNumber, baseIndex); - } else + } else if (typeof content === `string`) { return Parser.lineTokens(content, lineNumber, baseIndex); } else { @@ -1953,7 +1992,7 @@ export default class Parser { if (!keyvalues[`CONST`]) { keyvalues[`CONST`] = ``; } - + keyvalues[`CONST`] += keywordParts[i].value; } else { keyvalues[keywordParts[i].value.toUpperCase()] = true; diff --git a/tests/suite/fixed.test.ts b/tests/suite/fixed.test.ts index 8a7a2d20..2130779d 100644 --- a/tests/suite/fixed.test.ts +++ b/tests/suite/fixed.test.ts @@ -1450,4 +1450,4 @@ test('missing subroutines #443', async () => { expect(subroutines.length).toBe(2); expect(subroutines[0].name).toBe('PROCRC'); expect(subroutines[1].name).toBe('ADDSUB'); -}); \ No newline at end of file +}); diff --git a/tests/suite/ospec.test.ts b/tests/suite/ospec.test.ts new file mode 100644 index 00000000..600da72a --- /dev/null +++ b/tests/suite/ospec.test.ts @@ -0,0 +1,458 @@ +import path from "path"; +import setupParser from "../parserSetup"; +import { test, expect } from "vitest"; + +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