diff --git a/extension/server/src/providers/documentSymbols.ts b/extension/server/src/providers/documentSymbols.ts index 5b749b89..ca26341e 100644 --- a/extension/server/src/providers/documentSymbols.ts +++ b/extension/server/src/providers/documentSymbols.ts @@ -74,7 +74,17 @@ export default async function documentSymbolProvider(handler: DocumentSymbolPara SymbolKind.Constant, Range.create(def.position.line, 0, def.position.line, 0), Range.create(def.position.line, 0, def.position.line, 0) - )) + )), + + ...scope.cursors + .filter(cursor => cursor.position && cursor.position.path === currentPath) + .map(def => DocumentSymbol.create( + def.name, + def.keywords.join(` `).trim(), + SymbolKind.Interface, + Range.create(def.position.line, 0, def.position.line, 0), + Range.create(def.position.line, 0, def.position.line, 0) + )) ); scope.files diff --git a/language/models/cache.js b/language/models/cache.js index 74834307..12872ba0 100644 --- a/language/models/cache.js +++ b/language/models/cache.js @@ -45,6 +45,10 @@ export default class Cache { /** @type {import("../parserTypes").IncludeStatement[]} */ this.includes = cache.includes || []; + + /** @type {Declaration[]} */ + this.cursors = cache.cursors || []; + } /** @@ -61,6 +65,7 @@ export default class Cache { files: [...this.files, ...second.files], structs: [...this.structs, ...second.structs], constants: [...this.constants, ...second.constants], + cursors: [...this.cursors, ...second.cursors], sqlReferences: [...this.sqlReferences, ...second.sqlReferences], indicators: [...this.indicators, ...second.indicators] }); @@ -83,6 +88,7 @@ export default class Cache { ...this.subroutines.map(def => def.name), ...this.variables.map(def => def.name), ...this.structs.map(def => def.name), + ...this.cursors.map(def => def.name), ].filter(name => name); } @@ -97,7 +103,8 @@ export default class Cache { this.structs.filter(d => d.position.path === fsPath).pop(), this.variables.filter(d => d.position.path === fsPath).pop(), this.constants.filter(d => d.position.path === fsPath).pop(), - this.files.filter(d => d.position.path === fsPath).pop() + this.files.filter(d => d.position.path === fsPath).pop(), + this.cursors.filter(d => d.position.path === fsPath).pop() ].filter(d => d !== undefined); const lines = lasts.map(d => d.range && d.range.end ? d.range.end : d.position.line).sort((a, b) => b - a); @@ -124,6 +131,7 @@ export default class Cache { ...this.subroutines.filter(def => def.name.toUpperCase() === name), ...this.variables.filter(def => def.name.toUpperCase() === name), ...this.indicators.filter(def => def.name.toUpperCase() === name), + ...this.cursors.filter(def => def.name.toUpperCase() === name), ]; if (allStructs.length > 0 && possibles.length === 0) { @@ -140,7 +148,7 @@ export default class Cache { } clearReferences() { - [...this.parameters, ...this.constants, ...this.files, ...this.procedures, ...this.subroutines, ...this.variables, ...this.structs].forEach(def => { + [...this.parameters, ...this.constants, ...this.files, ...this.procedures, ...this.subroutines, ...this.variables, ...this.structs, ...this.cursors].forEach(def => { def.references = []; }); diff --git a/language/models/declaration.js b/language/models/declaration.js index 6766c777..ec9030f9 100644 --- a/language/models/declaration.js +++ b/language/models/declaration.js @@ -4,7 +4,7 @@ import Cache from "./cache"; export default class Declaration { /** * - * @param {"procedure"|"subroutine"|"file"|"struct"|"subitem"|"variable"|"constant"} type + * @param {"procedure"|"subroutine"|"file"|"struct"|"subitem"|"variable"|"constant"|"cursor"} type */ constructor(type) { this.type = type; diff --git a/language/parser.js b/language/parser.js index a924f64d..59fb2186 100644 --- a/language/parser.js +++ b/language/parser.js @@ -47,8 +47,8 @@ export default class Parser { } /** - * @param {includeFilePromise} promise - */ + * @param {includeFilePromise} promise + */ setIncludeFileFetch(promise) { this.includeFileFetch = promise; } @@ -121,9 +121,9 @@ export default class Parser { } /** - * @param {string} line - * @returns {string|undefined} - */ + * @param {string} line + * @returns {string|undefined} + */ static getIncludeFromDirective(line) { if (line.includes(`*`)) return; // Likely comment @@ -138,7 +138,7 @@ export default class Parser { }; if (directivePosition >= 0) { - return line.substring(directivePosition+directiveLength).trim(); + return line.substring(directivePosition + directiveLength).trim(); } } @@ -148,7 +148,7 @@ export default class Parser { * @param {{withIncludes?: boolean, ignoreCache?: boolean}} options * @returns {Promise} */ - async getDocs(workingUri, content, options = {withIncludes: true}) { + async getDocs(workingUri, content, options = { withIncludes: true }) { const existingCache = this.getParsedCache(workingUri); if (options.ignoreCache !== true && existingCache) { return existingCache; @@ -170,6 +170,8 @@ export default class Parser { /** @type {Declaration} */ let currentSub; let currentProcName; + /** @type {Declaration} */ + let currentCursor; let resetDefinition = false; //Set to true when you're done defining a new item let docs = false; // If section is for ILEDocs @@ -197,12 +199,12 @@ export default class Parser { let objectName = defaultName; const extObjKeywords = [`EXTFILE`]; const extObjKeywordsDesc = [`EXTDESC`]; - + // Check for external object extObjKeywords.forEach(keyword => { const keywordValue = keywords.find(part => part.startsWith(`${keyword}(`) && part.endsWith(`)`)); if (keywordValue) { - objectName = keywordValue.substring(keyword.length+1, keywordValue.length - 1).toUpperCase(); + objectName = keywordValue.substring(keyword.length + 1, keywordValue.length - 1).toUpperCase(); if (objectName.startsWith(`'`) && objectName.endsWith(`'`)) { objectName = objectName.substring(1, objectName.length - 1); @@ -210,12 +212,12 @@ export default class Parser { } }); - if(objectName === `*EXTDESC`){ + if (objectName === `*EXTDESC`) { // Check for external object extObjKeywordsDesc.forEach(keyword => { const keywordValue = keywords.find(part => part.startsWith(`${keyword}(`) && part.endsWith(`)`)); if (keywordValue) { - objectName = keywordValue.substring(keyword.length+1, keywordValue.length - 1).toUpperCase(); + objectName = keywordValue.substring(keyword.length + 1, keywordValue.length - 1).toUpperCase(); if (objectName.startsWith(`'`) && objectName.endsWith(`'`)) { objectName = objectName.substring(1, objectName.length - 1); @@ -237,7 +239,7 @@ export default class Parser { for (const tag of tags) { const keyword = ds.keywords.find(keyword => keyword.startsWith(`${tag}(`) && keyword.endsWith(`)`)); if (keyword) { - let keywordValue = keyword.substring(tag.length+1, keyword.length - 1).toUpperCase(); + let keywordValue = keyword.substring(tag.length + 1, keyword.length - 1).toUpperCase(); if (keywordValue.includes(`:`)) { const parms = keywordValue.split(`:`).filter(part => part.trim().startsWith(`*`) === false); @@ -281,7 +283,7 @@ export default class Parser { const valuePointer = scopes[i].structs.find(struct => struct.name.toUpperCase() === keywordValue); if (valuePointer) { ds.subItems = valuePointer.subItems; - + // We need to add qualified as it is qualified by default. if (!ds.keywords.includes(`QUALIFIED`)) ds.keywords.push(`QUALIFIED`); @@ -294,7 +296,7 @@ export default class Parser { }; if (options.withIncludes && this.includeFileFetch) { - //First loop is for copy/include statements + //First loop is for copy/include statements for (let i = baseLines.length - 1; i >= 0; i--) { let line = baseLines[i]; //Paths are case insensitive so it's okay if (line === ``) continue; @@ -366,7 +368,7 @@ export default class Parser { lineIsFree = true; } else { if (spec === ` `) { - //Clear out stupid comments + //Clear out stupid comments line = line.substring(7); lineIsFree = true; @@ -377,7 +379,7 @@ export default class Parser { // We don't want to waste precious time parsing all C specs, so we make sure it's got // BEGSR or ENDSR in it first. const upperLine = line.toUpperCase(); - if ([`BEGSR`, `ENDSR`, `CALL`].some(v => upperLine.includes(v)) === false) { + if ([`BEGSR`, `ENDSR`, `CURSOR`, `CALL`].some(v => upperLine.includes(v)) === false) { continue; } } @@ -392,7 +394,7 @@ export default class Parser { pieces = []; parts = []; - + if (isFullyFree || lineIsFree) { // Free format! line = line.trim(); @@ -410,23 +412,23 @@ export default class Parser { // End of parsing for this file break; } else - if (parts[0] === `/IF`) { - // Directive IF - directIfScope += 1; - continue; - } else - if (parts[0] === `/ENDIF`) { - // Directive ENDIF - directIfScope -= 1; - continue; - } else - if (directIfScope > 0) { - // Ignore lines inside the IF scope. - continue; - } else - if (line.startsWith(`/`)) { - continue; - } + if (parts[0] === `/IF`) { + // Directive IF + directIfScope += 1; + continue; + } else + if (parts[0] === `/ENDIF`) { + // Directive ENDIF + directIfScope -= 1; + continue; + } else + if (directIfScope > 0) { + // Ignore lines inside the IF scope. + continue; + } else + if (line.startsWith(`/`)) { + continue; + } } if (pieces.length > 1 && pieces[1].includes(`//`)) line = pieces[0] + `;`; @@ -448,7 +450,7 @@ export default class Parser { } else if (!line.endsWith(`;`)) { currentStatement = (currentStatement || ``) + line.trim(); - if (currentStatement.endsWith(`-`)) + if (currentStatement.endsWith(`-`)) currentStatement = currentStatement.substring(0, currentStatement.length - 1); else currentStatement += ` `; @@ -458,162 +460,106 @@ export default class Parser { } switch (parts[0]) { - case `CTL-OPT`: - keywords.push(...parts.slice(1)); - break; - - case `DCL-F`: - if (currentItem === undefined) { - if (parts.length > 1) { - currentItem = new Declaration(`file`); - currentItem.name = partsLower[1]; - currentItem.keywords = parts.slice(2); - currentItem.description = currentDescription.join(`\n`); - - currentItem.position = { - path: file, - line: lineNumber - }; + case `CTL-OPT`: + keywords.push(...parts.slice(1)); + break; - const objectName = getObjectName(parts[1], parts); - let prefix = ``; + case `DCL-F`: + if (currentItem === undefined) { + if (parts.length > 1) { + currentItem = new Declaration(`file`); + currentItem.name = partsLower[1]; + currentItem.keywords = parts.slice(2); + currentItem.description = currentDescription.join(`\n`); - parts.find(element => { - if (element.toUpperCase().includes(`PREFIX`)) { - prefix = element.trim().substring(7, element.indexOf(`)`)) - return true; - } - }); - - const recordFormats = await this.fetchTable(objectName, parts.length.toString(), parts.includes(`ALIAS`)); + currentItem.position = { + path: file, + line: lineNumber + }; - if (recordFormats.length > 0) { - const qualified = parts.includes(`QUALIFIED`); + const objectName = getObjectName(parts[1], parts); + let prefix = ``; - // Got to fix the positions for the defintions to be the declare. - recordFormats.forEach(recordFormat => { - recordFormat.keywords = [parts[1]]; - if (qualified) recordFormat.keywords.push(`QUALIFIED`); + parts.find(element => { + if (element.toUpperCase().includes(`PREFIX`)) { + prefix = element.trim().substring(7, element.indexOf(`)`)) + return true; + } + }); - recordFormat.position = currentItem.position; + const recordFormats = await this.fetchTable(objectName, parts.length.toString(), parts.includes(`ALIAS`)); - recordFormat.subItems.forEach(subItem => { - // We put the prefix here because in 'fetchTable' we use cached version. So if the user change the prefix, it will not refresh the variable name - if(prefix) { - subItem.name = prefix + subItem.name; - } - subItem.position = currentItem.position; - }); - }); + if (recordFormats.length > 0) { + const qualified = parts.includes(`QUALIFIED`); - currentItem.subItems.push(...recordFormats); - } + // Got to fix the positions for the defintions to be the declare. + recordFormats.forEach(recordFormat => { + recordFormat.keywords = [parts[1]]; + if (qualified) recordFormat.keywords.push(`QUALIFIED`); - scope.files.push(currentItem); - resetDefinition = true; - } - } - break; + recordFormat.position = currentItem.position; - case `DCL-C`: - if (currentItem === undefined) { - if (parts.length > 1) { - currentItem = new Declaration(`constant`); - currentItem.name = partsLower[1]; - currentItem.keywords = parts.slice(2); - currentItem.description = currentDescription.join(`\n`); + recordFormat.subItems.forEach(subItem => { + // We put the prefix here because in 'fetchTable' we use cached version. So if the user change the prefix, it will not refresh the variable name + if (prefix) { + subItem.name = prefix + subItem.name; + } + subItem.position = currentItem.position; + }); + }); - currentItem.position = { - path: file, - line: statementStartingLine - }; + currentItem.subItems.push(...recordFormats); + } - scope.constants.push(currentItem); - resetDefinition = true; + scope.files.push(currentItem); + resetDefinition = true; + } } - } - break; + break; - case `DCL-S`: - if (parts.length > 1) { + case `DCL-C`: if (currentItem === undefined) { - currentItem = new Declaration(`variable`); - currentItem.name = partsLower[1]; - currentItem.keywords = parts.slice(2); - currentItem.description = currentDescription.join(`\n`); - currentItem.tags = currentTags; + if (parts.length > 1) { + currentItem = new Declaration(`constant`); + currentItem.name = partsLower[1]; + currentItem.keywords = parts.slice(2); + currentItem.description = currentDescription.join(`\n`); - currentItem.position = { - path: file, - line: statementStartingLine - }; + currentItem.position = { + path: file, + line: statementStartingLine + }; - scope.variables.push(currentItem); - resetDefinition = true; + scope.constants.push(currentItem); + resetDefinition = true; + } } - } - break; + break; - case `DCL-DS`: - if (currentItem === undefined) { + case `DCL-S`: if (parts.length > 1) { - currentItem = new Declaration(`struct`); - currentItem.name = partsLower[1]; - currentItem.keywords = parts.slice(2); - currentItem.description = currentDescription.join(`\n`); - currentItem.tags = currentTags; - - currentItem.position = { - path: file, - line: statementStartingLine - }; - - currentItem.range = { - start: statementStartingLine, - end: statementStartingLine - }; - - currentGroup = `structs`; + if (currentItem === undefined) { + currentItem = new Declaration(`variable`); + currentItem.name = partsLower[1]; + currentItem.keywords = parts.slice(2); + currentItem.description = currentDescription.join(`\n`); + currentItem.tags = currentTags; - // Expand the LIKEDS value if there is one. - await expandDs(file, currentItem); + currentItem.position = { + path: file, + line: statementStartingLine + }; - // Does the keywords include a keyword that makes end-ds useless? - if (currentItem.keywords.some(keyword => oneLineTriggers[`DCL-DS`].some(trigger => keyword.startsWith(trigger)))) { - currentItem.range.end = statementStartingLine; - scope.structs.push(currentItem); - } else { - currentItem.readParms = true; - dsScopes.push(currentItem); + scope.variables.push(currentItem); + resetDefinition = true; } - - resetDefinition = true; - - currentDescription = []; } - } - break; - - case `END-DS`: - if (dsScopes.length > 0) { - const currentDs = dsScopes[dsScopes.length - 1]; - currentDs.range.end = statementStartingLine; - } + break; - if (dsScopes.length === 1) { - scope.structs.push(dsScopes.pop()); - } else - if (dsScopes.length > 1) { - dsScopes[dsScopes.length - 2].subItems.push(dsScopes.pop()); - } - break; - - case `DCL-PR`: - if (currentItem === undefined) { - if (parts.length > 1) { - if (!scope.procedures.find(proc => proc.name && proc.name.toUpperCase() === parts[1])) { - currentGroup = `procedures`; - currentItem = new Declaration(`procedure`); + case `DCL-DS`: + if (currentItem === undefined) { + if (parts.length > 1) { + currentItem = new Declaration(`struct`); currentItem.name = partsLower[1]; currentItem.keywords = parts.slice(2); currentItem.description = currentDescription.join(`\n`); @@ -624,303 +570,381 @@ export default class Parser { line: statementStartingLine }; - currentItem.readParms = true; - currentItem.range = { start: statementStartingLine, end: statementStartingLine }; + currentGroup = `structs`; + + // Expand the LIKEDS value if there is one. + await expandDs(file, currentItem); + // Does the keywords include a keyword that makes end-ds useless? - if (currentItem.keywords.some(keyword => oneLineTriggers[`DCL-PR`].some(trigger => keyword.startsWith(trigger)))) { + if (currentItem.keywords.some(keyword => oneLineTriggers[`DCL-DS`].some(trigger => keyword.startsWith(trigger)))) { currentItem.range.end = statementStartingLine; - scope.procedures.push(currentItem); - resetDefinition = true; + scope.structs.push(currentItem); + } else { + currentItem.readParms = true; + dsScopes.push(currentItem); } + resetDefinition = true; + currentDescription = []; } } - } - break; - - case `END-PR`: - if (currentItem && currentItem.type === `procedure`) { - currentItem.range.end = statementStartingLine; - - const isDefinedGlobally = scopes[0].procedures.some(proc => proc.name.toUpperCase() === currentItem.name.toUpperCase()); + break; - // Don't re-add self. This can happens when `END-PR` is used in the wrong place. - if (!isDefinedGlobally) { - scope.procedures.push(currentItem); + case `END-DS`: + if (dsScopes.length > 0) { + const currentDs = dsScopes[dsScopes.length - 1]; + currentDs.range.end = statementStartingLine; } - resetDefinition = true; - } - break; - - case `DCL-PROC`: - if (parts.length > 1) { - //We can overwrite it.. it might have been a PR before. - // eslint-disable-next-line no-case-declarations - const existingProc = scope.procedures.findIndex(proc => proc.name && proc.name.toUpperCase() === parts[1]); - - // We found the PR... so we can overwrite it - if (existingProc >= 0) scope.procedures.splice(existingProc, 1); - - currentItem = new Declaration(`procedure`); - - currentProcName = partsLower[1]; - currentItem.name = currentProcName; - currentItem.keywords = parts.slice(2); - currentItem.description = currentDescription.join(`\n`); - currentItem.tags = currentTags; - - currentItem.position = { - path: file, - line: statementStartingLine - }; - - currentItem.readParms = false; - - currentItem.range = { - start: statementStartingLine, - end: statementStartingLine - }; - - scope.procedures.push(currentItem); - resetDefinition = true; + if (dsScopes.length === 1) { + scope.structs.push(dsScopes.pop()); + } else + if (dsScopes.length > 1) { + dsScopes[dsScopes.length - 2].subItems.push(dsScopes.pop()); + } + break; - scopes.push(new Cache()); - } - break; + case `DCL-PR`: + if (currentItem === undefined) { + if (parts.length > 1) { + if (!scope.procedures.find(proc => proc.name && proc.name.toUpperCase() === parts[1])) { + currentGroup = `procedures`; + currentItem = new Declaration(`procedure`); + currentItem.name = partsLower[1]; + currentItem.keywords = parts.slice(2); + currentItem.description = currentDescription.join(`\n`); + currentItem.tags = currentTags; + + currentItem.position = { + path: file, + line: statementStartingLine + }; - case `DCL-PI`: - //Procedures can only exist in the global scope. - if (currentProcName) { - if (parts.length > 0) { - currentGroup = `procedures`; - currentItem = scopes[0].procedures.find(proc => proc.name === currentProcName); + currentItem.readParms = true; - const endInline = parts.findIndex(part => part === `END-PI`); + currentItem.range = { + start: statementStartingLine, + end: statementStartingLine + }; - if (currentItem) { + // Does the keywords include a keyword that makes end-ds useless? + if (currentItem.keywords.some(keyword => oneLineTriggers[`DCL-PR`].some(trigger => keyword.startsWith(trigger)))) { + currentItem.range.end = statementStartingLine; + scope.procedures.push(currentItem); + resetDefinition = true; + } - // Indicates that the PI starts and ends on the same line - if (endInline >= 0) { - parts.splice(endInline, 1); - currentItem.readParms = false; - resetDefinition = true; + currentDescription = []; } - - currentItem.keywords.push(...parts.slice(2)); - currentItem.readParms = true; - - currentDescription = []; } } - } - break; + break; - case `END-PI`: - //Procedures can only exist in the global scope. - currentItem = scopes[0].procedures.find(proc => proc.name === currentProcName); + case `END-PR`: + if (currentItem && currentItem.type === `procedure`) { + currentItem.range.end = statementStartingLine; - if (currentItem && currentItem.type === `procedure`) { - currentItem.readParms = false; - resetDefinition = true; - } - break; + const isDefinedGlobally = scopes[0].procedures.some(proc => proc.name.toUpperCase() === currentItem.name.toUpperCase()); - case `END-PROC`: - //Procedures can only exist in the global scope. - if (scopes.length > 1) { - currentItem = scopes[0].procedures.find(proc => proc.name === currentProcName); + // Don't re-add self. This can happens when `END-PR` is used in the wrong place. + if (!isDefinedGlobally) { + scope.procedures.push(currentItem); + } - if (currentItem && currentItem.type === `procedure`) { - currentItem.scope = scopes.pop(); - currentItem.range.end = statementStartingLine; resetDefinition = true; } - } - break; + break; + + case `DCL-PROC`: + if (parts.length > 1) { + //We can overwrite it.. it might have been a PR before. + // eslint-disable-next-line no-case-declarations + const existingProc = scope.procedures.findIndex(proc => proc.name && proc.name.toUpperCase() === parts[1]); + + // We found the PR... so we can overwrite it + if (existingProc >= 0) scope.procedures.splice(existingProc, 1); - case `BEGSR`: - if (parts.length > 1) { - if (!scope.subroutines.find(sub => sub.name && sub.name.toUpperCase() === parts[1])) { - currentItem = new Declaration(`subroutine`); - currentItem.name = partsLower[1]; + currentItem = new Declaration(`procedure`); + + currentProcName = partsLower[1]; + currentItem.name = currentProcName; + currentItem.keywords = parts.slice(2); currentItem.description = currentDescription.join(`\n`); - currentItem.keywords = [`Subroutine`]; + currentItem.tags = currentTags; currentItem.position = { path: file, line: statementStartingLine }; + currentItem.readParms = false; + currentItem.range = { start: statementStartingLine, end: statementStartingLine }; - currentDescription = []; + scope.procedures.push(currentItem); + resetDefinition = true; + + scopes.push(new Cache()); } - } - break; - - case `ENDSR`: - if (currentItem && currentItem.type === `subroutine`) { - currentItem.range.end = statementStartingLine; - scope.subroutines.push(currentItem); - resetDefinition = true; - } - break; - - case `EXEC`: - if (parts.length > 2 && !parts.includes(`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 cleanupObjectRef = (content = ``) => { - const result = { - schema: undefined, - name: content + break; + + case `DCL-PI`: + //Procedures can only exist in the global scope. + if (currentProcName) { + if (parts.length > 0) { + currentGroup = `procedures`; + currentItem = scopes[0].procedures.find(proc => proc.name === currentProcName); + + const endInline = parts.findIndex(part => part === `END-PI`); + + if (currentItem) { + + // Indicates that the PI starts and ends on the same line + if (endInline >= 0) { + parts.splice(endInline, 1); + currentItem.readParms = false; + resetDefinition = true; + } + + currentItem.keywords.push(...parts.slice(2)); + currentItem.readParms = true; + + currentDescription = []; + } } + } + break; + + case `END-PI`: + //Procedures can only exist in the global scope. + currentItem = scopes[0].procedures.find(proc => proc.name === currentProcName); - const schemaSplit = Math.max(result.name.indexOf(`.`), result.name.indexOf(`/`)); - if (schemaSplit >= 0) { - result.schema = result.name.substring(0, schemaSplit); - result.name = result.name.substring(schemaSplit+1); + if (currentItem && currentItem.type === `procedure`) { + currentItem.readParms = false; + resetDefinition = true; + } + break; + + case `END-PROC`: + //Procedures can only exist in the global scope. + if (scopes.length > 1) { + currentItem = scopes[0].procedures.find(proc => proc.name === currentProcName); + + if (currentItem && currentItem.type === `procedure`) { + currentItem.scope = scopes.pop(); + currentItem.range.end = statementStartingLine; + resetDefinition = true; } + } + break; - // For procedures or functions? - const openBracket = result.name.indexOf(`(`); - if (openBracket >= 0) { - result.name = result.name.substring(0, openBracket); + case `BEGSR`: + if (parts.length > 1) { + if (!scope.subroutines.find(sub => sub.name && sub.name.toUpperCase() === parts[1])) { + currentItem = new Declaration(`subroutine`); + currentItem.name = partsLower[1]; + currentItem.description = currentDescription.join(`\n`); + currentItem.keywords = [`Subroutine`]; + + currentItem.position = { + path: file, + line: statementStartingLine + }; + + currentItem.range = { + start: statementStartingLine, + end: statementStartingLine + }; + + currentDescription = []; } + } + break; - return result; + case `ENDSR`: + if (currentItem && currentItem.type === `subroutine`) { + currentItem.range.end = statementStartingLine; + scope.subroutines.push(currentItem); + resetDefinition = true; } + break; - parts.forEach((part, index) => { - if ( - preFileWords.includes(part) && // 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) { - const possibleFileName = partsLower[index+1]; - const qualifiedObjectPath = cleanupObjectRef(possibleFileName); - - const currentSqlItem = new Declaration(`file`); - currentSqlItem.name = qualifiedObjectPath.name; - currentSqlItem.keywords = []; - currentSqlItem.description = qualifiedObjectPath.schema || ``; - - currentSqlItem.position = { - path: file, - line: statementStartingLine - }; - - scope.sqlReferences.push(currentSqlItem); + case `EXEC`: + if (parts[1] === `SQL`) { + const indexDeclare = parts.findIndex(el => el === `DECLARE`); + const indexCursor = parts.findIndex(el => el === `CURSOR`); + let indexCursorName = 0; + if (indexCursor - 1 == indexDeclare + 1) { + indexCursorName = indexCursor - 1; + } + if (currentCursor === undefined + && indexDeclare >= 0 + && indexCursor >= 0) { + currentCursor = new Declaration(`cursor`); + currentCursor.name = parts[indexCursorName]; + currentCursor.position = { + path: file, + line: statementStartingLine + }; + + scope.cursors.push(currentCursor); + currentCursor = undefined; + } + } + + if (parts.length > 2 && !parts.includes(`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 cleanupObjectRef = (content = ``) => { + const result = { + schema: undefined, + name: content + } + + const schemaSplit = Math.max(result.name.indexOf(`.`), result.name.indexOf(`/`)); + if (schemaSplit >= 0) { + result.schema = result.name.substring(0, schemaSplit); + result.name = result.name.substring(schemaSplit + 1); + } + + // For procedures or functions? + const openBracket = result.name.indexOf(`(`); + if (openBracket >= 0) { + result.name = result.name.substring(0, openBracket); } + + return result; } - - resetDefinition = true; - }); - } - 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) { - currentTags[currentTags.length - 1].content += ` ${content}`; + parts.forEach((part, index) => { + if ( + preFileWords.includes(part) && // 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) { + const possibleFileName = partsLower[index + 1]; + const qualifiedObjectPath = cleanupObjectRef(possibleFileName); + + const currentSqlItem = new Declaration(`file`); + currentSqlItem.name = qualifiedObjectPath.name; + currentSqlItem.keywords = []; + currentSqlItem.description = qualifiedObjectPath.schema || ``; + + currentSqlItem.position = { + path: file, + line: statementStartingLine + }; + + scope.sqlReferences.push(currentSqlItem); + } + } + + resetDefinition = true; + }); + } + 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 (currentTitle === undefined) { - currentTitle = content; + if (currentTags.length > 0) { + currentTags[currentTags.length - 1].content += ` ${content}`; + } else { - currentDescription.push(content); + if (currentTitle === undefined) { + currentTitle = content; + } else { + currentDescription.push(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`].includes(currentItem.type)) { - if (currentItem.readParms && parts.length > 0) { - if (parts[0].startsWith(`DCL`)) { - parts.slice(1); - partsLower = partsLower.splice(1); - } + if (currentItem && [`procedure`, `struct`].includes(currentItem.type)) { + if (currentItem.readParms && parts.length > 0) { + if (parts[0].startsWith(`DCL`)) { + parts.slice(1); + partsLower = partsLower.splice(1); + } - currentSub = new Declaration(`subitem`); - currentSub.name = (parts[0] === `*N` ? `parm${currentItem.subItems.length+1}` : partsLower[0]) ; - currentSub.keywords = parts.slice(1); + currentSub = new Declaration(`subitem`); + currentSub.name = (parts[0] === `*N` ? `parm${currentItem.subItems.length + 1}` : partsLower[0]); + currentSub.keywords = parts.slice(1); - currentSub.position = { - path: file, - line: statementStartingLine - }; + currentSub.position = { + path: file, + line: statementStartingLine + }; - // 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.description = 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.description = paramTag.content; + } } - } - // If the parameter has likeds, add the subitems to make it a struct. - await expandDs(file, currentSub); - currentSub.keyword = Parser.expandKeywords(currentSub.keywords); + // If the parameter has likeds, add the subitems to make it a struct. + await expandDs(file, currentSub); + currentSub.keyword = Parser.expandKeywords(currentSub.keywords); - currentItem.subItems.push(currentSub); - currentSub = undefined; + currentItem.subItems.push(currentSub); + currentSub = undefined; - if (currentItem.type === `struct`) { - resetDefinition = true; + if (currentItem.type === `struct`) { + resetDefinition = true; + } } } } - } - break; + break; } } else { @@ -931,344 +955,372 @@ export default class Parser { } switch (spec) { - case `H`: - keywords.push(line.substring(6)); - break; - - case `F`: - const fSpec = parseFLine(line); - potentialName = getObjectName(fSpec.name, fSpec.keywords); - - if (fSpec.name) { - currentItem = new Declaration(`file`); - currentItem.name = potentialName; - currentItem.keywords = fSpec.keywords; - - currentItem.position = { - path: file, - line: lineNumber - }; - - let prefix = ``; - - fSpec.keywords.find(element => { - if (element.toUpperCase().includes(`PREFIX`)) { - prefix = element.substring(7, element.indexOf(`)`)) - return true; - } - }); + case `H`: + keywords.push(line.substring(6)); + break; - const recordFormats = await this.fetchTable(potentialName, line.length.toString(), fSpec.keywords.includes(`ALIAS`)); + case `F`: + const fSpec = parseFLine(line); + potentialName = getObjectName(fSpec.name, fSpec.keywords); - if (recordFormats.length > 0) { - const qualified = fSpec.keywords.includes(`QUALIFIED`); + if (fSpec.name) { + currentItem = new Declaration(`file`); + currentItem.name = potentialName; + currentItem.keywords = fSpec.keywords; - // Got to fix the positions for the defintions to be the declare. - recordFormats.forEach(recordFormat => { - recordFormat.keywords = [potentialName]; - if (qualified) recordFormat.keywords.push(`QUALIFIED`); + currentItem.position = { + path: file, + line: lineNumber + }; - recordFormat.position = currentItem.position; + let prefix = ``; - recordFormat.subItems.forEach(subItem => { - // We put the prefix here because in 'fetchTable' we use cached version. So if the user change the prefix, it will not refresh the variable name - if(prefix) { - subItem.name = prefix.toUpperCase() + subItem.name; - } - subItem.position = currentItem.position; - }); + fSpec.keywords.find(element => { + if (element.toUpperCase().includes(`PREFIX`)) { + prefix = element.substring(7, element.indexOf(`)`)) + return true; + } }); - currentGroup = `structs`; - currentItem.subItems.push(...recordFormats); - } + const recordFormats = await this.fetchTable(potentialName, line.length.toString(), fSpec.keywords.includes(`ALIAS`)); - scope.files.push(currentItem); - } else { - currentItem = scope.files[scope.files.length-1]; - currentItem.keywords = [ - ...(currentItem.keywords ? currentItem.keywords : []), - ...fSpec.keywords - ] - } - - resetDefinition = true; - break; + if (recordFormats.length > 0) { + const qualified = fSpec.keywords.includes(`QUALIFIED`); - case `C`: - const cSpec = parseCLine(line); + // Got to fix the positions for the defintions to be the declare. + recordFormats.forEach(recordFormat => { + recordFormat.keywords = [potentialName]; + if (qualified) recordFormat.keywords.push(`QUALIFIED`); - potentialName = cSpec.factor1; + recordFormat.position = currentItem.position; - switch (cSpec.opcode) { - case `BEGSR`: - if (!scope.subroutines.find(sub => sub.name && sub.name.toUpperCase() === potentialName)) { - currentItem = new Declaration(`subroutine`); - currentItem.name = potentialName; - currentItem.keywords = [`Subroutine`]; - - currentItem.position = { - path: file, - line: lineNumber - }; - - currentItem.range = { - start: lineNumber, - end: lineNumber - }; - - currentDescription = []; - } - break; + recordFormat.subItems.forEach(subItem => { + // We put the prefix here because in 'fetchTable' we use cached version. So if the user change the prefix, it will not refresh the variable name + if (prefix) { + subItem.name = prefix.toUpperCase() + subItem.name; + } + subItem.position = currentItem.position; + }); + }); - case `ENDSR`: - if (currentItem && currentItem.type === `subroutine`) { - currentItem.range.end = lineNumber; - scope.subroutines.push(currentItem); - resetDefinition = true; + currentGroup = `structs`; + currentItem.subItems.push(...recordFormats); + } + + scope.files.push(currentItem); + } else { + currentItem = scope.files[scope.files.length - 1]; + currentItem.keywords = [ + ...(currentItem.keywords ? currentItem.keywords : []), + ...fSpec.keywords + ] } + + resetDefinition = true; break; - - case `CALL`: - const callItem = new Declaration(`procedure`); - callItem.name = (cSpec.factor2.startsWith(`'`) && cSpec.factor2.endsWith(`'`) ? cSpec.factor2.substring(1, cSpec.factor2.length-1) : cSpec.factor2); - callItem.keywords = [`EXTPGM`]; - callItem.description = currentDescription.join(`\n`); - callItem.tags = currentTags; - - callItem.position = { - path: file, - line: lineNumber - }; - - callItem.range = { - start: lineNumber, - end: lineNumber - }; - - callItem.keyword = Parser.expandKeywords(callItem.keywords); - - scope.procedures.push(callItem); - break; - } - break; - case `P`: - const pSpec = parsePLine(line); + case `C`: + if (line.substring(5, 7) === `C+`) { + pieces = line.split(`;`); + parts = pieces[0].toUpperCase().split(` `).filter(piece => piece !== ``); + const indexDeclare = parts.findIndex(el => el === `DECLARE`); + const indexCursor = parts.findIndex(el => el === `CURSOR`); + let indexCursorName = 0; + if (indexCursor - 1 == indexDeclare + 1) { + indexCursorName = indexCursor - 1; + } + if (currentCursor === undefined + && indexDeclare >= 0 + && indexCursor >= 0) { + currentCursor = new Declaration(`cursor`); + currentCursor.name = parts[indexCursorName]; + currentCursor.position = { + path: file, + line: lineNumber + }; - if (pSpec.potentialName === ``) continue; + scope.cursors.push(currentCursor); + currentCursor = undefined; + } + } else { + const cSpec = parseCLine(line); - if (pSpec.potentialName.endsWith(`...`)) { - potentialName = pSpec.potentialName.substring(0, pSpec.potentialName.length - 3); - potentialNameUsed = true; - } else { - if (pSpec.start) { - potentialName = pSpec.name.length > 0 ? pSpec.name : potentialName; + potentialName = cSpec.factor1; - if (potentialName) { - //We can overwrite it.. it might have been a PR before. - const existingProc = scope.procedures.findIndex(proc => proc.name && proc.name.toUpperCase() === potentialName.toUpperCase()); + switch (cSpec.opcode) { + case `BEGSR`: + if (!scope.subroutines.find(sub => sub.name && sub.name.toUpperCase() === potentialName)) { + currentItem = new Declaration(`subroutine`); + currentItem.name = potentialName; + currentItem.keywords = [`Subroutine`]; - // We found the PR... so we can overwrite it - if (existingProc >= 0) scope.procedures.splice(existingProc, 1); + currentItem.position = { + path: file, + line: lineNumber + }; - currentItem = new Declaration(`procedure`); + currentItem.range = { + start: lineNumber, + end: lineNumber + }; - currentProcName = potentialName; - currentItem.name = currentProcName; - currentItem.keywords = pSpec.keywords; + currentDescription = []; + } + break; + case `ENDSR`: + if (currentItem && currentItem.type === `subroutine`) { + currentItem.range.end = lineNumber; + scope.subroutines.push(currentItem); + resetDefinition = true; + } + break; - currentItem.position = { - path: file, - line: lineNumber - (potentialNameUsed ? 1 : 0) // Account that name is on line before - }; + case `CALL`: + const callItem = new Declaration(`procedure`); + callItem.name = (cSpec.factor2.startsWith(`'`) && cSpec.factor2.endsWith(`'`) ? cSpec.factor2.substring(1, cSpec.factor2.length - 1) : cSpec.factor2); + callItem.keywords = [`EXTPGM`]; + callItem.description = currentDescription.join(`\n`); + callItem.tags = currentTags; - currentItem.range = { - start: currentItem.position.line, - end: currentItem.position.line - }; + callItem.position = { + path: file, + line: lineNumber + }; - scope.procedures.push(currentItem); - resetDefinition = true; + callItem.range = { + start: lineNumber, + end: lineNumber + }; - scopes.push(new Cache()); - } - } else { - if (scopes.length > 1) { - //Procedures can only exist in the global scope. - currentItem = scopes[0].procedures.find(proc => proc.name === currentProcName); + callItem.keyword = Parser.expandKeywords(callItem.keywords); + + scope.procedures.push(callItem); + break; - if (currentItem && currentItem.type === `procedure`) { - currentItem.scope = scopes.pop(); - currentItem.range.end = lineNumber; - resetDefinition = true; - } } + break; + } - } - break; - case `D`: - const dSpec = parseDLine(line); + break; - if (dSpec.potentialName === ``) continue; + case `P`: + const pSpec = parsePLine(line); - if (dSpec.potentialName.endsWith(`...`)) { - potentialName = dSpec.potentialName.substring(0, dSpec.potentialName.length - 3); - potentialNameUsed = true; - continue; - } else { - potentialName = dSpec.name.length > 0 ? dSpec.name : potentialName ? potentialName : ``; - - switch (dSpec.field) { - case `C`: - currentItem = new Declaration(`constant`); - currentItem.name = potentialName || `*N`; - currentItem.keywords = [...dSpec.keywords]; - - // TODO: line number might be different with ...? - currentItem.position = { - path: file, - line: lineNumber - (potentialNameUsed ? 1 : 0) // Account that name is on line before - }; - - scope.constants.push(currentItem); - resetDefinition = true; - break; - case `S`: - currentItem = new Declaration(`variable`); - currentItem.name = potentialName || `*N`; - currentItem.keywords = [getPrettyType(dSpec), ...dSpec.keywords]; + if (pSpec.potentialName === ``) continue; - // TODO: line number might be different with ...? - currentItem.position = { - path: file, - line: lineNumber - (potentialNameUsed ? 1 : 0) // Account that name is on line before - }; + if (pSpec.potentialName.endsWith(`...`)) { + potentialName = pSpec.potentialName.substring(0, pSpec.potentialName.length - 3); + potentialNameUsed = true; + } else { + if (pSpec.start) { + potentialName = pSpec.name.length > 0 ? pSpec.name : potentialName; - scope.variables.push(currentItem); - resetDefinition = true; - break; + if (potentialName) { + //We can overwrite it.. it might have been a PR before. + const existingProc = scope.procedures.findIndex(proc => proc.name && proc.name.toUpperCase() === potentialName.toUpperCase()); - case `DS`: - currentItem = new Declaration(`struct`); - currentItem.name = potentialName || `*N`; - currentItem.keywords = dSpec.keywords; + // We found the PR... so we can overwrite it + if (existingProc >= 0) scope.procedures.splice(existingProc, 1); - currentItem.position = { - path: file, - line: lineNumber - (potentialNameUsed ? 1 : 0) // Account that name is on line before - }; + currentItem = new Declaration(`procedure`); - currentItem.range = { - start: currentItem.position.line, - end: currentItem.position.line - }; + currentProcName = potentialName; + currentItem.name = currentProcName; + currentItem.keywords = pSpec.keywords; - expandDs(file, currentItem); + currentItem.position = { + path: file, + line: lineNumber - (potentialNameUsed ? 1 : 0) // Account that name is on line before + }; - currentGroup = `structs`; - scope.structs.push(currentItem); - resetDefinition = true; - break; + currentItem.range = { + start: currentItem.position.line, + end: currentItem.position.line + }; - case `PR`: - // Only add a PR if it's not been defined - if (!scope.procedures.find(proc => proc.name && proc.name.toUpperCase() === potentialName.toUpperCase())) { - currentItem = new Declaration(`procedure`); - currentItem.name = potentialName || `*N`; - currentItem.keywords = [getPrettyType(dSpec), ...dSpec.keywords]; - - currentItem.position = { - path: file, - line: lineNumber - (potentialNameUsed ? 1 : 0) // Account that name is on line before - }; + scope.procedures.push(currentItem); + resetDefinition = true; - currentItem.range = { - start: currentItem.position.line, - end: currentItem.position.line - }; - - currentGroup = `procedures`; - scope.procedures.push(currentItem); - currentDescription = []; + scopes.push(new Cache()); + } + } else { + if (scopes.length > 1) { + //Procedures can only exist in the global scope. + currentItem = scopes[0].procedures.find(proc => proc.name === currentProcName); + + if (currentItem && currentItem.type === `procedure`) { + currentItem.scope = scopes.pop(); + currentItem.range.end = lineNumber; + resetDefinition = true; + } + } } - break; + } + break; - case `PI`: - //Procedures can only exist in the global scope. - if (currentProcName) { - currentItem = scopes[0].procedures.find(proc => proc.name === currentProcName); + case `D`: + const dSpec = parseDLine(line); - currentGroup = `procedures`; - if (currentItem) { - currentItem.keywords.push(getPrettyType(dSpec), ...dSpec.keywords); - } - } - break; + if (dSpec.potentialName === ``) continue; - 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]; + if (dSpec.potentialName.endsWith(`...`)) { + potentialName = dSpec.potentialName.substring(0, dSpec.potentialName.length - 3); + potentialNameUsed = true; + continue; + } else { + potentialName = dSpec.name.length > 0 ? dSpec.name : potentialName ? potentialName : ``; + + switch (dSpec.field) { + case `C`: + currentItem = new Declaration(`constant`); + currentItem.name = potentialName || `*N`; + currentItem.keywords = [...dSpec.keywords]; + + // TODO: line number might be different with ...? + currentItem.position = { + path: file, + line: lineNumber - (potentialNameUsed ? 1 : 0) // Account that name is on line before + }; + + scope.constants.push(currentItem); + resetDefinition = true; break; - } - } + case `S`: + currentItem = new Declaration(`variable`); + currentItem.name = potentialName || `*N`; + currentItem.keywords = [getPrettyType(dSpec), ...dSpec.keywords]; - if (currentItem) { + // TODO: line number might be different with ...? + currentItem.position = { + path: file, + line: lineNumber - (potentialNameUsed ? 1 : 0) // Account that name is on line before + }; - // This happens when it's a blank parm. - if (potentialName === `` && (dSpec.type || dSpec.len)) - potentialName = (potentialName === `` ? `parm${currentItem.subItems.length+1}` : potentialName); + scope.variables.push(currentItem); + resetDefinition = true; + break; - if (potentialName) { - currentSub = new Declaration(`subitem`); - currentSub.name = potentialName; - currentSub.keywords = [getPrettyType(dSpec), ...dSpec.keywords]; + case `DS`: + currentItem = new Declaration(`struct`); + currentItem.name = potentialName || `*N`; + currentItem.keywords = dSpec.keywords; - currentSub.position = { + currentItem.position = { path: file, - line: lineNumber + line: lineNumber - (potentialNameUsed ? 1 : 0) // Account that name is on line before }; - // If the parameter has likeds, add the subitems to make it a struct. - await expandDs(file, currentSub); - currentSub.keyword = Parser.expandKeywords(currentSub.keywords); + currentItem.range = { + start: currentItem.position.line, + end: currentItem.position.line + }; - currentItem.subItems.push(currentSub); - currentSub = undefined; + expandDs(file, currentItem); + currentGroup = `structs`; + scope.structs.push(currentItem); resetDefinition = true; - } else { - if (currentItem) { - if (currentItem.subItems.length > 0) - currentItem.subItems[currentItem.subItems.length - 1].keywords.push(getPrettyType(dSpec), ...dSpec.keywords); - else - currentItem.keywords.push(...dSpec.keywords); + break; + + case `PR`: + // Only add a PR if it's not been defined + if (!scope.procedures.find(proc => proc.name && proc.name.toUpperCase() === potentialName.toUpperCase())) { + currentItem = new Declaration(`procedure`); + currentItem.name = potentialName || `*N`; + currentItem.keywords = [getPrettyType(dSpec), ...dSpec.keywords]; + + currentItem.position = { + path: file, + line: lineNumber - (potentialNameUsed ? 1 : 0) // Account that name is on line before + }; + + currentItem.range = { + start: currentItem.position.line, + end: currentItem.position.line + }; + + currentGroup = `procedures`; + scope.procedures.push(currentItem); + currentDescription = []; } - } + break; + + case `PI`: + //Procedures can only exist in the global scope. + if (currentProcName) { + currentItem = scopes[0].procedures.find(proc => proc.name === currentProcName); + + currentGroup = `procedures`; + if (currentItem) { + currentItem.keywords.push(getPrettyType(dSpec), ...dSpec.keywords); + } + } + 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; + } + } + + if (currentItem) { + + // This happens when it's a blank parm. + if (potentialName === `` && (dSpec.type || dSpec.len)) + potentialName = (potentialName === `` ? `parm${currentItem.subItems.length + 1}` : potentialName); - currentItem.range.end = lineNumber; + if (potentialName) { + currentSub = new Declaration(`subitem`); + currentSub.name = potentialName; + currentSub.keywords = [getPrettyType(dSpec), ...dSpec.keywords]; + + currentSub.position = { + path: file, + line: lineNumber + }; + + // If the parameter has likeds, add the subitems to make it a struct. + await expandDs(file, currentSub); + currentSub.keyword = Parser.expandKeywords(currentSub.keywords); + + currentItem.subItems.push(currentSub); + currentSub = undefined; + + resetDefinition = true; + } else { + if (currentItem) { + if (currentItem.subItems.length > 0) + currentItem.subItems[currentItem.subItems.length - 1].keywords.push(getPrettyType(dSpec), ...dSpec.keywords); + else + currentItem.keywords.push(...dSpec.keywords); + } + } + + currentItem.range.end = lineNumber; + } + break; } - break; + + potentialName = undefined; } - - potentialName = undefined; - } - break; + break; + } } @@ -1280,7 +1332,7 @@ export default class Parser { potentialName = undefined; potentialNameUsed = false; - + currentItem = undefined; currentTitle = undefined; currentDescription = []; @@ -1294,7 +1346,7 @@ export default class Parser { scopes[0].keyword = Parser.expandKeywords(keywords); } - scopes[0].fixProcedures(); + scopes[0].fixProcedures(); const parsedData = scopes[0]; @@ -1315,8 +1367,8 @@ export default class Parser { for (let i = 0; i < keywordParts.length; i++) { if (keywordParts[i].value) { - if (keywordParts[i+1] && keywordParts[i+1].type === `block`) { - keyvalues[keywordParts[i].value.toUpperCase()] = keywordParts[i+1].block.map(part => part.value).join(``); + if (keywordParts[i + 1] && keywordParts[i + 1].type === `block`) { + keyvalues[keywordParts[i].value.toUpperCase()] = keywordParts[i + 1].block.map(part => part.value).join(``); i++; // Skip one for the block. } else { keyvalues[keywordParts[i].value.toUpperCase()] = true; diff --git a/language/parserTypes.ts b/language/parserTypes.ts index 0b86b1dc..2e9e21e1 100644 --- a/language/parserTypes.ts +++ b/language/parserTypes.ts @@ -22,6 +22,7 @@ export interface CacheProps { sqlReferences?: Declaration[]; indicators?: Declaration[]; includes?: IncludeStatement[]; + cursors?: Declaration[]; } export interface Rules { diff --git a/tests/suite/directives.js b/tests/suite/directives.js index ff5f91f3..fc96ca05 100644 --- a/tests/suite/directives.js +++ b/tests/suite/directives.js @@ -463,5 +463,33 @@ module.exports = { const someDs = cache.find(`someDs`); assert.strictEqual(someDs.keyword[`BASED`], undefined); + }, + + exec_sql: async () => { + const lines = [ + `**FREE`, + ``, + `EXEC SQL`, + ` select count(*) into :NB_LIG`, + ` from DEPARTMENT`, + ` where DEPTNAME = 'A'`, + `EXEC SQL`, + ` DECLARE C1 CURSOR FOR`, + ` select *`, + ` from DEPARTMENT`, + ` where DEPTNAME = 'A';`, + `exec sql`, + ` OPEN C1`, + `exec sql`, + ` Fetch C1 INTO :DEPARTMENT`, + `exec sql`, + ` CLOSE C1`, + ].join(`\n`); + + const parser = parserSetup(); + const cache = await parser.getDocs(uri, lines); + + assert.strictEqual(cache.cursors.length, 1); + assert.strictEqual(cache.cursors[0].name, `C1`); } } \ No newline at end of file diff --git a/tests/suite/fixed.js b/tests/suite/fixed.js index ae2ecd53..2f1a290d 100644 --- a/tests/suite/fixed.js +++ b/tests/suite/fixed.js @@ -1025,6 +1025,36 @@ exports.ctl_opt_fixed = async () => { assert.strictEqual(cache.keyword[`COPYRIGHT`], `'(C) Copyright ABC Programming - 1995'`); }; +exports.fixedfree2 = async () => { + const lines = [ + ` C/exec sql`, + ` C+ select count(*) into :NB_LIG`, + ` C+ from DEPARTMENT`, + ` C+ where DEPTNAME = 'A'`, + ` C/end-exec`, + ` C/exec sql`, + ` C+ DECLARE C1 CURSOR FOR`, + ` C+ select *`, + ` C+ from DEPARTMENT`, + ` C+ where DEPTNAME = 'A'`, + ` C/end-exec`, + ` C/exec sql`, + ` C+ OPEN C1`, + ` C/end-exec`, + ` C/exec sql`, + ` C+ Fetch C1 INTO :DEPARTMENT`, + ` C/end-exec`, + ` C/exec sql`, + ` C+ CLOSE C1`, + ` C/end-exec` + ].join(`\n`); + + const parser = parserSetup(); + const cache = await parser.getDocs(uri, lines); + + assert.strictEqual(cache.cursors.length, 1); + assert.strictEqual(cache.cursors[0].name, `C1`); +}; exports.call_opcode = async () => { const lines = [ ` C CreateNewBoardBEGSR`, @@ -1088,4 +1118,4 @@ exports.file_keywords = async () => { assert.strictEqual(ord100d.keyword[`INDDS`], `indds`); assert.strictEqual(ord100d.keyword[`SFILE`], `sfl01:rrn01`); assert.strictEqual(ord100d.keyword[`INFDS`], `Info`); -} \ No newline at end of file +} diff --git a/tests/suite/linter.js b/tests/suite/linter.js index 7e8d0f72..73d66c64 100644 --- a/tests/suite/linter.js +++ b/tests/suite/linter.js @@ -2177,8 +2177,47 @@ exports.linter34 = async () => { SQLHostVarCheck: true }, cache); - assert.strictEqual(errors.length, 1); + assert.strictEqual(errors.length, 3); + assert.deepStrictEqual(errors, [ + { + range: new Range( + new Position(7, 0), + new Position(10, 28), + ), + offset: { + position: 17, + end: 24 + }, + type: `SQLHostVarCheck`, + newValue: `:empCurA` + }, + { + type: `SQLHostVarCheck`, + newValue: `:Deptnum`, + range: new Range( + new Position(7, 0), + new Position(10, 28), + ), + offset: { + position: 117, + end: 124 + }, + }, + { + range: new Range( + new Position(12, 0), + new Position(16, 29), + ), + offset: { + position: 17, + end: 24 + }, + type: `SQLHostVarCheck`, + newValue: `:empCurB` + } + ]); + assert.deepStrictEqual(errors[0], { offset: { position: 183, end: 190 }, type: `SQLHostVarCheck`,