├── .eslintignore ├── .eslintrc.json ├── .github ├── pr_testing_template.md ├── pull_request_template.md └── workflows │ ├── ci.yaml │ ├── pr.yaml │ └── test.yaml ├── .gitignore ├── .gitmodules ├── .nojekyll ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── ColAssist_01.png ├── ColAssist_02.png ├── DirectiveAfter.png ├── DirectiveBefore.png ├── LennonFigure1.png ├── LintFix_01.png ├── LintFix_02.png ├── OpenLintConfig.png ├── OpenLintConfig_02.png ├── Outline_01.png ├── Settings_01.png ├── lintopt_01.png └── lintopt_02.png ├── cli └── rpglint │ ├── .npmignore │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── tsconfig.json │ └── webpack.config.js ├── extension ├── client │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── base.ts │ │ ├── commands.ts │ │ ├── configuration.ts │ │ ├── extension.ts │ │ ├── language │ │ │ ├── columnAssist.ts │ │ │ ├── config.ts │ │ │ └── serverReferences.ts │ │ ├── linter.ts │ │ ├── requests.ts │ │ └── schemas │ │ │ ├── linter.ts │ │ │ └── specs.ts │ ├── testFixture │ │ ├── completion.txt │ │ └── diagnostics.txt │ ├── tsconfig.json │ └── webpack.config.js ├── server │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── connection.ts │ │ ├── data.ts │ │ ├── providers │ │ │ ├── apis │ │ │ │ └── index.ts │ │ │ ├── codeActions.ts │ │ │ ├── completionItem.ts │ │ │ ├── definition.ts │ │ │ ├── documentSymbols.ts │ │ │ ├── hover.ts │ │ │ ├── implementation.ts │ │ │ ├── index.ts │ │ │ ├── language.ts │ │ │ ├── linter │ │ │ │ ├── codeActions.ts │ │ │ │ ├── documentFormatting.ts │ │ │ │ ├── index.ts │ │ │ │ └── skipRules.ts │ │ │ ├── project │ │ │ │ ├── exportInterfaces.ts │ │ │ │ ├── index.ts │ │ │ │ ├── references.ts │ │ │ │ └── workspaceSymbol.ts │ │ │ ├── reference.ts │ │ │ └── rename.ts │ │ └── server.ts │ ├── tsconfig.json │ └── webpack.config.js └── tsconfig.base.json ├── index.html ├── jsconfig.json ├── language ├── document.ts ├── linter.ts ├── models │ ├── DataPoints.ts │ ├── cache.ts │ ├── declaration.ts │ ├── fixed.js │ ├── oneLineTriggers.js │ ├── opcodes.ts │ └── tags.js ├── parser.ts ├── parserTypes.ts ├── statement.ts ├── tokens.ts └── types.ts ├── media └── logo.png ├── package-lock.json ├── package.json ├── schemas ├── rpgle.code-snippets └── rpglint.json ├── shared.webpack.config.js └── tests ├── eof4.rpgle ├── parserSetup.ts ├── rpgle ├── CBKDTAARA.rpgle ├── CBKHEADER.rpgle ├── CBKOPTIMIZ.rpgle ├── CBKPCFGDCL.rpgle ├── CBKPCFGREA.rpgle ├── apival01s.rpgleinc ├── copy1.rpgle ├── copy2.rpgle ├── copy3.rpgle ├── copy4.rpgleinc ├── copy5.rpgleinc ├── db00030s_h.rpgleinc ├── db00040s_h.rpgleinc ├── depth1.rpgleinc ├── eof4.rpgle ├── file1.rpgleinc ├── fixed1.rpgleinc └── stat.rpgleinc ├── sources └── random │ └── hello.test.rpgle ├── suite ├── basics.test.ts ├── directives.test.ts ├── docs.test.ts ├── editing.test.ts ├── files.test.ts ├── fixed.test.ts ├── keywords.test.ts ├── linter.test.ts ├── partial.test.ts ├── references.test.ts └── sources.test.ts └── tables ├── department.ts ├── display.ts ├── employee.ts ├── emps.ts └── index.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "ecmaFeatures": { 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-const-assign": "warn", 18 | "no-this-before-super": "warn", 19 | "no-undef": "warn", 20 | "no-unreachable": "warn", 21 | "no-var": "error", 22 | "quotes": ["warn", "backtick"], 23 | "constructor-super": "warn", 24 | "valid-typeof": "warn", 25 | "indent": ["error", 2] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/pr_testing_template.md: -------------------------------------------------------------------------------- 1 | # Want to help test a PR? 2 | 3 | If you got a zip file from a build, extract it. Inside of it is a 'vsix'. 4 | 5 | Install it, test out the feature, and post some feedback. 6 | 7 | ### How to install 8 | 9 | 1. Uninstall your current version of the extension from VS Code. 10 | * you can do this from the extension panel 11 | * your configuration will not be lost 12 | 2. Download the `.vsix` to your machine 13 | 3. Press F1 / Command + Shift + P and select 'Install from VSIX...' 14 | 4. Select the `.vsix` from this PR. 15 | 5. Test out the feature! 16 | 17 | ### When you're done 18 | 19 | * post the feedback on the PR 20 | * uninstall the extension from the extension panel 21 | * re-install the extension from the Marketplace 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ### Changes 3 | 4 | Description of change here. 5 | 6 | ### Checklist 7 | 8 | * [ ] have tested my change 9 | * [ ] updated relevant documentation 10 | * [ ] Remove any/all `console.log`s I added 11 | * [ ] eslint is not complaining 12 | * [ ] have added myself to the contributors' list in the README 13 | * [ ] **for feature PRs**: PR only includes one feature enhancement. 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | release: 7 | name: Release and publish 8 | runs-on: ubuntu-latest 9 | steps: 10 | 11 | # Setup prereqs 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: '20' 15 | - uses: actions/checkout@v3 16 | - run: npm install 17 | - run: npm install -g vsce 18 | 19 | # Create and publish build to Marketplace 20 | - name: Publish to Marketplace 21 | run: vsce publish -p $PUBLISHER_TOKEN 22 | env: 23 | PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }} 24 | 25 | # Create and publish build to OpenVSX 26 | - name: Publish to Open VSX 27 | run: npx ovsx publish -p ${{ secrets.OPENVSX_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | 3 | jobs: 4 | release: 5 | name: Create PR build 6 | runs-on: ubuntu-latest 7 | if: contains(github.event.pull_request.labels.*.name, 'build') 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 1 12 | - run: npm install 13 | - run: node ./node_modules/eslint/bin/eslint src/** --no-error-on-unmatched-pattern 14 | - run: npm run test 15 | 16 | - run: npm install -g vsce 17 | - name: Create build 18 | run: npm run package 19 | 20 | - name: Upload build 21 | uses: actions/upload-artifact@v2 22 | with: 23 | name: code-for-ibmi-pr-build 24 | path: ./*.vsix 25 | 26 | - name: Post comment 27 | uses: actions/github-script@v5 28 | with: 29 | script: | 30 | github.rest.issues.createComment({ 31 | issue_number: context.issue.number, 32 | owner: context.repo.owner, 33 | repo: context.repo.repo, 34 | body: '👋 A new build is available for this PR based on ${{ github.event.pull_request.head.sha }}.\n * [Download here.](https://github.com/halcyon-tech/vscode-rpgle/actions/runs/${{ github.run_id }})\n* [Read more about how to test](https://github.com/halcyon-tech/vscode-rpgle/blob/master/.github/pr_testing_template.md)' 35 | }) 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | paths: 4 | - 'language/**' 5 | - 'tests/**' 6 | 7 | jobs: 8 | release: 9 | name: Test runner 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 1 15 | submodules: true 16 | - run: npm install 17 | - run: node ./node_modules/eslint/bin/eslint src/** --no-error-on-unmatched-pattern 18 | - run: npm run test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test/ 3 | *.vsix 4 | dist 5 | .DS_Store 6 | .vscode/settings.json 7 | out 8 | tsconfig.tsbuildinfo 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/sources/bob-recursive-example"] 2 | path = tests/sources/bob-recursive-example 3 | url = git@github.com:IBM/bob-recursive-example.git 4 | [submodule "tests/sources/BBS400"] 5 | path = tests/sources/BBS400 6 | url = git@github.com:worksofliam/BBS400.git 7 | [submodule "tests/sources/noxDB"] 8 | path = tests/sources/noxDB 9 | url = git@github.com:sitemule/noxDB.git 10 | [submodule "tests/sources/xmlservice"] 11 | path = tests/sources/xmlservice 12 | url = git@github.com:IBM/xmlservice.git 13 | [submodule "tests/sources/I_builder"] 14 | path = tests/sources/I_builder 15 | url = git@github.com:ibmiiste/I_builder.git 16 | [submodule "tests/sources/ibmi-company_system"] 17 | path = tests/sources/ibmi-company_system 18 | url = git@github.com:IBM/ibmi-company_system.git 19 | [submodule "tests/sources/httpapi"] 20 | path = tests/sources/httpapi 21 | url = git@github.com:ScottKlement/httpapi.git 22 | [submodule "tests/sources/rpgle-repl"] 23 | path = tests/sources/rpgle-repl 24 | url = git@github.com:tom-writes-code/rpgle-repl.git 25 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/.nojekyll -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "dbaeumer.vscode-eslint" 8 | ] 9 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "name": "Launch Client", 9 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 10 | "outFiles": ["${workspaceRoot}/out/**/*.js"], 11 | "sourceMaps": true, 12 | "sourceMapPathOverrides": { 13 | "webpack://client/./*": "${workspaceFolder}/extension/client/*" 14 | }, 15 | "preLaunchTask": { 16 | "type": "npm", 17 | "script": "webpack:dev" 18 | } 19 | }, 20 | { 21 | "type": "node", 22 | "request": "attach", 23 | "name": "Attach to Server", 24 | "port": 8789, 25 | "restart": true, 26 | "sourceMaps": true, 27 | "outFiles": ["${workspaceRoot}/out/**/*.js"], 28 | "sourceMapPathOverrides": { 29 | "webpack://server/./*": "${workspaceFolder}/extension/server/*" 30 | }, 31 | }, 32 | { 33 | "name": "Debug Tests", 34 | "type": "node", 35 | "request": "launch", 36 | "program": "${workspaceRoot}/out/tests/tests/index.js", 37 | "sourceMaps": true, 38 | "preLaunchTask": { 39 | "type": "npm", 40 | "script": "compile:tests" 41 | }, 42 | "args": ["sqlRunner1"], 43 | "env": { 44 | "INCLUDE_DIR": "${workspaceFolder}" 45 | } 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "webpack:dev", 7 | "group": "build", 8 | "presentation": { 9 | "panel": "dedicated", 10 | "reveal": "never" 11 | }, 12 | "problemMatcher": "$ts-webpack-watch" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .github/** 2 | assets/** 3 | **/node_modules 4 | node_modules 5 | scripts 6 | .vscode/** 7 | .vscode-test/** 8 | .gitignore 9 | .yarnrc 10 | vsc-extension-quickstart.md 11 | **/jsconfig.json 12 | **/*.map 13 | **/.eslintrc.json 14 | tests 15 | cli 16 | language 17 | **/cli 18 | cli/** 19 | cli 20 | extension -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "vscode-rpgle" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Halcyon Tech Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-rpgle README 2 | 3 | 4 | 5 | Adds functionality to assist in writing accurate, readable and consistent RPGLE, including: 6 | 7 | - Content assist 8 | - Outline view 9 | - Linter, including indentation checking and reformatting (`**FREE` only) 10 | - Column assist for fixed-format RPGLE. 11 | 12 | Depends on the Code for IBM i extension due to source code living on the remote system when developing with source members. 13 | 14 | ## Documentation 15 | 16 | Check out the [official documentation](https://codefori.github.io/docs/#/pages/extensions/rpgle/linter) for a usage guide. 17 | 18 | # Developing 19 | 20 | 1. Fork & clone 21 | 2. `npm i` 22 | 3. Run 23 | 24 | ## Debugging 25 | 26 | To run the tests, you have two options: 27 | 28 | 1. `npm run test` from the command line 29 | 2. 'Debug Tests' from the VS Code debugger 30 | 31 | To run debug the extension and server, from the VS Code debugger: 32 | 33 | 1. Debug 'Launch Client' 34 | 2. Debug 'Attach to Server' 35 | 36 | # Previous contributors 37 | 38 | Thanks so much to everyone [who has contributed](https://github.com/codefori/vscode-rpgle/graphs/contributors). 39 | 40 | - [@worksofliam](https://github.com/worksofliam) 41 | - [@SJLennon](https://github.com/SJLennon) 42 | - [@sebCIL](https://github.com/sebCIL) 43 | - [@p-behr](https://github.com/p-behr) 44 | - [@chrjorgensen](https://github.com/chrjorgensen) 45 | - [@sebjulliand](https://github.com/sebjulliand) 46 | - [@richardm90](https://github.com/richardm90) 47 | - [@wright4i](https://github.com/wright4i) 48 | - [@SanjulaGanepola](https://github.com/SanjulaGanepola) -------------------------------------------------------------------------------- /assets/ColAssist_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/ColAssist_01.png -------------------------------------------------------------------------------- /assets/ColAssist_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/ColAssist_02.png -------------------------------------------------------------------------------- /assets/DirectiveAfter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/DirectiveAfter.png -------------------------------------------------------------------------------- /assets/DirectiveBefore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/DirectiveBefore.png -------------------------------------------------------------------------------- /assets/LennonFigure1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/LennonFigure1.png -------------------------------------------------------------------------------- /assets/LintFix_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/LintFix_01.png -------------------------------------------------------------------------------- /assets/LintFix_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/LintFix_02.png -------------------------------------------------------------------------------- /assets/OpenLintConfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/OpenLintConfig.png -------------------------------------------------------------------------------- /assets/OpenLintConfig_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/OpenLintConfig_02.png -------------------------------------------------------------------------------- /assets/Outline_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/Outline_01.png -------------------------------------------------------------------------------- /assets/Settings_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/Settings_01.png -------------------------------------------------------------------------------- /assets/lintopt_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/lintopt_01.png -------------------------------------------------------------------------------- /assets/lintopt_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/lintopt_02.png -------------------------------------------------------------------------------- /cli/rpglint/.npmignore: -------------------------------------------------------------------------------- 1 | index.ts 2 | tsconfig.json 3 | webpack.config.js -------------------------------------------------------------------------------- /cli/rpglint/index.ts: -------------------------------------------------------------------------------- 1 | // rpglint CLI 2 | // --files [glob] 3 | // --cwd "[path]" 4 | 5 | import glob from "glob"; 6 | import { readFileSync } from 'fs'; 7 | 8 | import Parser from '../../language/parser'; 9 | import Linter from '../../language/linter'; 10 | import { Rules } from '../../language/parserTypes'; 11 | import path from 'path'; 12 | 13 | type FormatTypes = "standard" | "flc"; 14 | 15 | main(); 16 | 17 | async function main() { 18 | const parms = process.argv.slice(2); 19 | 20 | let cwd = process.cwd(); 21 | let scanGlob = `**/*.{SQLRPGLE,sqlrpgle,RPGLE,rpgle}`; 22 | let maxErrors: number|undefined; 23 | let outputType: FormatTypes; 24 | 25 | for (let i = 0; i < parms.length; i++) { 26 | switch (parms[0]) { 27 | case `-f`: 28 | case `--files`: 29 | scanGlob = parms[i + 1]; 30 | i++; 31 | break; 32 | 33 | case `-d`: 34 | case `--cwd`: 35 | cwd = parms[i + 1]; 36 | i++; 37 | break; 38 | 39 | case `-m`: 40 | case `--max`: 41 | maxErrors = Number(parms[i + 1]); 42 | i++; 43 | break; 44 | 45 | case `-o`: 46 | case `--output`: 47 | outputType = (parms[i+1] as FormatTypes); 48 | i++; 49 | break; 50 | 51 | case `-h`: 52 | case `--help`: 53 | console.log(`rpglint (derived from vscode-rpgle).`); 54 | console.log(``); 55 | console.log(`See who's contributed: https://github.com/halcyon-tech/vscode-rpgle/graphs/contributors`); 56 | console.log(); 57 | console.log(`Rules are inherited from the 'rpglint.json' found in the working directory.`); 58 | console.log(`This configuration file usually lives in '.vscode/rpglint.json'.`); 59 | console.log(); 60 | console.log(`\t-d`) 61 | console.log(`\t--cwd\t\tTo see the directory of where source code lives.`); 62 | console.log(`\t\t\tThe default is the current working directory.`); 63 | console.log(); 64 | console.log(`\t-f`); 65 | console.log(`\t--files\t\tGlob used to search for sources in the working directory.`); 66 | console.log(`\t\t\tDefaults to '${scanGlob}'`); 67 | console.log(); 68 | console.log(`\t-m`); 69 | console.log(`\t--max\t\tThe max limit of errored files before the process ends itself.`); 70 | console.log(); 71 | console.log(`\t-o`); 72 | console.log(`\t--output\tFormat of the lint errors in standard out.`); 73 | console.log(`\t\t\tDefaults to standard. Available: standard, flc`); 74 | console.log(); 75 | process.exit(0); 76 | } 77 | } 78 | 79 | let rules: Rules; 80 | let parser: Parser; 81 | let files: string[]; 82 | 83 | try { 84 | rules = getLintConfig(cwd); 85 | parser = setupParser(cwd, scanGlob); 86 | files = getFiles(cwd, scanGlob); 87 | } catch (e) { 88 | error(e.message || e); 89 | process.exit(1); 90 | } 91 | 92 | const ruleCount = Object.keys(rules).length; 93 | 94 | if (ruleCount === 0) { 95 | error(`rpglint.json does not have any rules. Exiting.`); 96 | process.exit(); 97 | } 98 | 99 | let totalFailures = 0; 100 | 101 | console.log(`Linting ${files.length} file${files.length !== 1 ? `s` : ``}.`); 102 | 103 | if (files.length > 500) { 104 | console.log(`Looks like you are linting a lot of files! It might be worth checking out the '--max' parameter to limit the amount of errors that will be produced. This is useful to end the lint process after so many issues.`); 105 | } 106 | 107 | console.log(); 108 | 109 | for (const filePath of files) { 110 | if (maxErrors && totalFailures >= maxErrors) { 111 | console.log(); 112 | console.log(`Max errors of ${maxErrors} has been reached. Ending.`); 113 | break; 114 | } 115 | 116 | try { 117 | const content = readFileSync(filePath, { encoding: `utf-8` }); 118 | const eol = content.includes(`\r\n`) ? `\r\n` : `\n`; 119 | const eolIndexes: number[] = []; 120 | 121 | for (let i = 0; i < content.length; i++) { 122 | if (content.substring(i, i + eol.length) === eol) { 123 | eolIndexes.push(i); 124 | } 125 | } 126 | 127 | if (content.length > 6 && content.substring(0, 6).toLowerCase() === `**free`) { 128 | const docs = await parser.getDocs( 129 | filePath, 130 | content, 131 | { 132 | withIncludes: true 133 | } 134 | ); 135 | 136 | const lintResult = Linter.getErrors({ 137 | uri: filePath, 138 | content 139 | }, rules, docs); 140 | 141 | const totalErrors = lintResult.errors.length + lintResult.indentErrors.length; 142 | 143 | if (totalErrors) { 144 | totalFailures += 1; 145 | 146 | switch (outputType) { 147 | case `flc`: 148 | if (lintResult.indentErrors.length) { 149 | lintResult.indentErrors.forEach(indentError => { 150 | console.log(`${filePath}:${indentError.line + 1}:${indentError.currentIndent}:Expected indent of ${indentError.expectedIndent}`); 151 | }); 152 | } 153 | 154 | if (lintResult.errors.length) { 155 | lintResult.errors.forEach(error => { 156 | const line = eolIndexes.findIndex(index => index > error.offset.position); 157 | const offset = error.offset.position - (eolIndexes[line-1] || 0); 158 | console.log(`${filePath}:${line+1}:${offset}:${Linter.getErrorText(error.type)}`); 159 | }); 160 | } 161 | break; 162 | 163 | case `standard`: 164 | default: 165 | const relative = path.relative(cwd, filePath); 166 | console.log(`${relative}: ${totalErrors} error${totalErrors !== 1 ? `s` : ``}.`); 167 | if (lintResult.indentErrors.length) { 168 | lintResult.indentErrors.forEach(indentError => { 169 | console.log(`\tLine ${indentError.line + 1}: expected indent of ${indentError.expectedIndent}, got ${indentError.currentIndent}`); 170 | }); 171 | } 172 | 173 | if (lintResult.errors.length) { 174 | lintResult.errors.forEach(error => { 175 | const line = eolIndexes.findIndex(index => index > error.offset.position); 176 | const offset = error.offset.position - (eolIndexes[line-1] || 0); 177 | console.log(`\tLine ${line+1}, column ${offset}: ${Linter.getErrorText(error.type)}`); 178 | }); 179 | } 180 | break; 181 | } 182 | } 183 | } 184 | 185 | } catch (e) { 186 | error(`Failed to lint ${filePath}: ${e.message || e}`); 187 | error(`Report this issue to us with an example: github.com/halcyon-tech/vscode-rpgle/issues`); 188 | } 189 | } 190 | 191 | process.exit(totalFailures > 0 ? 1 : 0); 192 | } 193 | 194 | function getLintConfig(cwd: string): Rules { 195 | const files = glob.sync(`**/rpglint.json`, { 196 | cwd, 197 | absolute: true, 198 | nocase: true, 199 | dot: true 200 | }); 201 | 202 | if (files.length >= 1) { 203 | const file = files[0]; 204 | 205 | const content = readFileSync(file, { encoding: `utf-8` }); 206 | const result = JSON.parse(content); 207 | return result; 208 | } 209 | 210 | throw new Error(`Unable to locate rpglint.json`); 211 | } 212 | 213 | function getFiles(cwd: string, globPath: string): string[] { 214 | return glob.sync(globPath, { 215 | cwd, 216 | absolute: true, 217 | nocase: true, 218 | }); 219 | } 220 | 221 | function error(line: string) { 222 | process.stdout.write(line + `\n`); 223 | } 224 | 225 | function setupParser(cwd: string, globPath: string): Parser { 226 | const parser = new Parser(); 227 | 228 | parser.setIncludeFileFetch(async (baseFile: string, includeFile: string) => { 229 | if (includeFile.startsWith(`'`) && includeFile.endsWith(`'`)) { 230 | includeFile = includeFile.substring(1, includeFile.length - 1); 231 | } 232 | 233 | if (includeFile.includes(`,`)) { 234 | includeFile = includeFile.split(`,`).join(`/`) + `.*`; 235 | } 236 | 237 | includeFile = path.join(`**`, includeFile); 238 | const files = glob.sync(includeFile, { 239 | cwd, 240 | absolute: true, 241 | nocase: true, 242 | }); 243 | 244 | if (files.length >= 1) { 245 | const file = files[0]; 246 | 247 | const content = readFileSync(file, { encoding: `utf-8` }); 248 | return { 249 | found: true, 250 | uri: file, 251 | lines: content.split(`\n`) 252 | } 253 | } 254 | 255 | return { 256 | found: false 257 | }; 258 | }); 259 | 260 | parser.setTableFetch(async (table: string, aliases = false) => { 261 | // Can't support tables in CLI mode I suppose? 262 | return []; 263 | }); 264 | 265 | return parser; 266 | } -------------------------------------------------------------------------------- /cli/rpglint/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@halcyontech/rpglint", 3 | "version": "0.27.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@halcyontech/rpglint", 9 | "version": "0.27.0", 10 | "license": "MIT", 11 | "bin": { 12 | "rpglint": "dist/index.js" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cli/rpglint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@halcyontech/rpglint", 3 | "version": "0.27.0", 4 | "description": "rpglint CLI tool", 5 | "bin": { 6 | "rpglint": "./dist/index.js" 7 | }, 8 | "scripts": { 9 | "webpack:dev": "webpack --mode none --config ./webpack.config.js", 10 | "webpack": "webpack --mode production --config ./webpack.config.js", 11 | "local": "npm run webpack:dev && npm i -g", 12 | "deploy": "npm run webpack && npm i && npm publish --access public" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/halcyon-tech/vscode-rpgle.git" 17 | }, 18 | "keywords": [ 19 | "rpgle", 20 | "ibmi" 21 | ], 22 | "author": "Halcyon Tech", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/halcyon-tech/vscode-rpgle/issues" 26 | }, 27 | "homepage": "https://github.com/halcyon-tech/vscode-rpgle#readme" 28 | } 29 | -------------------------------------------------------------------------------- /cli/rpglint/readme.md: -------------------------------------------------------------------------------- 1 | # RPG Linter CLI 2 | 3 | This is a command-line interface (CLI) for the RPG Linter, derived from the vscode-rpgle extension. It allows you to lint your RPG code from the command line, using the same rules and configuration as the vscode-rpgle extension. 4 | 5 | ## Installation 6 | 7 | `rpglint` can be installed through npm. You can see the package on npmjs.com! `rpglint` is intended to be installed globally and not at a project level. To do that, you can simply run: 8 | 9 | ```bash 10 | npm i @halcyontech/rpglint -g 11 | ``` 12 | 13 | ## Usage 14 | 15 | You can use the following command to see the available parameters: 16 | 17 | ```bash 18 | rpglint -h 19 | ``` 20 | 21 | The CLI accepts several command-line arguments to customize its behavior: 22 | 23 | - `-f` or `--files`: A glob pattern used to search for source files in the working directory. Defaults to `**/*.{SQLRPGLE,sqlrpgle,RPGLE,rpgle}`. 24 | - `-d` or `--cwd`: The directory where the source code lives. Defaults to the current working directory. 25 | - `-m` or `--max`: The maximum number of errored files before the process ends itself. 26 | - `-o` or `--output`: The format of the lint errors in standard out. Defaults to `standard`. Available options are `standard` and `flc`. 27 | - `-h` or `--help`: Displays help information. 28 | 29 | The CLI uses a configuration file named `rpglint.json` located in the working directory (usually in `.vscode/rpglint.json`) to determine the linting rules. 30 | 31 | ## Example 32 | 33 | Let's test `rpglint` with the IBM sample repository, Company System! `ibmi-company_system` is available on GitHub and anybody can clone it. For this example, we will clone it to our local device and then run `rpglint` against it. 34 | 35 | ```bash 36 | # cd to where the repo will be created 37 | cd ~/Downloads/ 38 | 39 | # clone the repository. HTTPS clone should work for everyone 40 | git clone git@github.com:IBM/ibmi-company_system.git 41 | 42 | # cd into the repository 43 | cd ibmi-company_system 44 | ``` 45 | 46 | By default, there are a bunch of errors in this repository. We do this to show off that `rpglint` works as expected! You can just use `rpglint` inside of this directory for the linter to run! 47 | 48 | ```bash 49 | barry$ pwd 50 | ~/Downloads/ibmi-company_system 51 | 52 | barry$ rpglint 53 | Linting 3 files. 54 | 55 | /Users/barry/Downloads/ibmi-company_system/qrpglesrc/employees.pgm.sqlrpgle: 22 errors. 56 | Line 8: expected indent of 0, got 6 57 | Line 12: expected indent of 0, got 6 58 | Line 38: expected indent of 0, got 6 59 | Line 39: expected indent of 0, got 6 60 | Line 45: expected indent of 0, got 8 61 | Line 64: expected indent of 0, got 8 62 | Line 4, column 0: Variable name casing does not match definition. 63 | Line 8, column 6: Comments must be correctly formatted. 64 | Line 10, column 0: Directives must be in uppercase. 65 | Line 12, column 6: Comments must be correctly formatted. 66 | Line 14, column 0: Variable name casing does not match definition. 67 | Line 38, column 6: Comments must be correctly formatted. 68 | Line 39, column 6: Comments must be correctly formatted. 69 | Line 45, column 8: Comments must be correctly formatted. 70 | Line 64, column 8: Comments must be correctly formatted. 71 | Line 74, column 2: Variable name casing does not match definition. 72 | Line 107, column 8: Variable name casing does not match definition. 73 | Line 116, column 2: Variable name casing does not match definition. 74 | Line 141, column 6: Variable name casing does not match definition. 75 | Line 91, column 2: Same string literal used more than once. Consider using a constant instead. 76 | Line 93, column 4: Same string literal used more than once. Consider using a constant instead. 77 | Line 101, column 6: Same string literal used more than once. Consider using a constant instead. 78 | rpglint simply sp 79 | ``` 80 | 81 | `rpglint` simply spits out all the issues it finds with the RPGLE code in the repository folder. The intention of a linter is to enforce code standards in a code base. When the linter finds errors, it will have an exit code of `1` and `0` if there are none. 82 | 83 | ## rpglint in your CI process 84 | 85 | Because we are able to use `rpglint` from the command line, this means we've opened up a totally new world to ourselves. We can now run `rpglint` as part of a CI (Continuous Integration) process. For example, here's a GitHub Action .yml which configures `rpglint` to run when a PR request is created or updated against the main branch: 86 | 87 | ```yaml 88 | name: rpglint CI 89 | 90 | on: 91 | pull_request: 92 | # Set your workflow to run on pull_request events that target the main branch 93 | branches: [ "main" ] 94 | 95 | jobs: 96 | build: 97 | runs-on: ubuntu-latest 98 | 99 | strategy: 100 | matrix: 101 | node-version: [18.x] 102 | 103 | steps: 104 | - uses: actions/checkout@v3 105 | - name: Use Node.js ${{ matrix.node-version }} 106 | uses: actions/setup-node@v3 107 | with: 108 | node-version: ${{ matrix.node-version }} 109 | cache: 'npm' 110 | - run: npm i -g rpglint 111 | - run: rpglint 112 | ``` 113 | 114 | The best part about the `rpglint` command is that it returns exit code `1` if there are errors. CI tools (like GitHub Actions, Jenkins, GitLab Runner, etc) can use this status code to determine if the CI was successful. It makes sense that the CI fails if there are lint errors, because it doesn't match the code standard defined in the `rpglint.json`. 115 | -------------------------------------------------------------------------------- /cli/rpglint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2019", 5 | "noImplicitAny": false, 6 | "noUnusedParameters": false, 7 | "strict": false, 8 | "allowJs": true, 9 | "outDir": "./dist", 10 | "esModuleInterop": true, 11 | "sourceMap": true 12 | } 13 | } -------------------------------------------------------------------------------- /cli/rpglint/webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | // @ts-nocheck 7 | 8 | 'use strict'; 9 | 10 | const withDefaults = require(`../../shared.webpack.config`); 11 | const path = require(`path`); 12 | const webpack = require(`webpack`); 13 | 14 | module.exports = withDefaults({ 15 | context: path.join(__dirname), 16 | entry: { 17 | extension: `./index.ts`, 18 | }, 19 | output: { 20 | filename: `index.js`, 21 | path: path.join(__dirname, `dist`) 22 | }, 23 | // Other stuff 24 | plugins: [ 25 | new webpack.BannerPlugin({banner: `#! /usr/bin/env node`, raw: true}) 26 | ] 27 | }); -------------------------------------------------------------------------------- /extension/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "description": "VSCode client for RPGLE LSP", 4 | "author": "Liam Allan", 5 | "license": "MIT", 6 | "version": "0.0.1", 7 | "publisher": "vscode", 8 | "engines": { 9 | "vscode": "^1.63.0" 10 | }, 11 | "dependencies": { 12 | "vscode-languageclient": "^7.0.0" 13 | }, 14 | "devDependencies": { 15 | "@halcyontech/vscode-ibmi-types": "^2.15.3", 16 | "@types/vscode": "^1.63.0", 17 | "@vscode/test-electron": "^2.1.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /extension/client/src/base.ts: -------------------------------------------------------------------------------- 1 | import { CodeForIBMi } from "@halcyontech/vscode-ibmi-types"; 2 | import Instance from "@halcyontech/vscode-ibmi-types/Instance"; 3 | import { ConfigurationChangeEvent, Extension, extensions, workspace } from "vscode"; 4 | 5 | let baseExtension: Extension|undefined; 6 | 7 | export async function checkAndWait() { 8 | baseExtension = extensions.getExtension(`halcyontechltd.code-for-ibmi`); 9 | 10 | if (baseExtension) { 11 | if (!baseExtension.isActive) { 12 | await baseExtension.activate(); 13 | } 14 | } 15 | 16 | return getInstance(); 17 | } 18 | 19 | /** 20 | * This should be used on your extension activation. 21 | */ 22 | export function loadBase(): CodeForIBMi|undefined { 23 | if (!baseExtension) { 24 | baseExtension = (extensions ? extensions.getExtension(`halcyontechltd.code-for-ibmi`) : undefined); 25 | } 26 | 27 | return (baseExtension && baseExtension.isActive && baseExtension.exports ? baseExtension.exports : undefined); 28 | } 29 | 30 | /** 31 | * Used when you want to fetch the extension 'instance' (the connection) 32 | */ 33 | export function getInstance(): Instance|undefined { 34 | const base = loadBase(); 35 | return (base ? base.instance : undefined); 36 | } 37 | 38 | // Stolen directly from vscode-ibmi 39 | export function onCodeForIBMiConfigurationChange(props: string | string[], todo: (value: ConfigurationChangeEvent) => void) { 40 | const keys = (Array.isArray(props) ? props : Array.of(props)).map(key => `code-for-ibmi.${key}`); 41 | return workspace.onDidChangeConfiguration(async event => { 42 | if (keys.some(key => event.affectsConfiguration(key))) { 43 | todo(event); 44 | } 45 | }) 46 | } -------------------------------------------------------------------------------- /extension/client/src/commands.ts: -------------------------------------------------------------------------------- 1 | import { commands, ExtensionContext, window } from "vscode"; 2 | import { clearTableCache } from "./requests"; 3 | import { LanguageClient } from "vscode-languageclient/node"; 4 | 5 | export function registerCommands(context: ExtensionContext, client: LanguageClient) { 6 | context.subscriptions.push( 7 | commands.registerCommand(`vscode-rpgle.server.reloadCache`, () => { 8 | clearTableCache(client); 9 | }) 10 | ) 11 | } -------------------------------------------------------------------------------- /extension/client/src/configuration.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurationTarget, workspace } from 'vscode'; 2 | 3 | export function get(prop: string) { 4 | const globalData = workspace.getConfiguration(`vscode-rpgle`); 5 | return globalData.get(prop); 6 | } 7 | 8 | export const RULER_ENABLED_BY_DEFAULT = `rulerEnabledByDefault`; 9 | export const projectFilesGlob = `**/*.{rpgle,RPGLE,sqlrpgle,SQLRPGLE,rpgleinc,RPGLEINC}`; -------------------------------------------------------------------------------- /extension/client/src/extension.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import * as path from 'path'; 7 | import { workspace, ExtensionContext } from 'vscode'; 8 | 9 | import * as Linter from "./linter"; 10 | import * as columnAssist from "./language/columnAssist"; 11 | 12 | 13 | import { 14 | LanguageClient, 15 | LanguageClientOptions, 16 | ServerOptions, 17 | TransportKind 18 | } from 'vscode-languageclient/node'; 19 | 20 | import { projectFilesGlob } from './configuration'; 21 | import { clearTableCache, buildRequestHandlers } from './requests'; 22 | import { getServerImplementationProvider, getServerSymbolProvider } from './language/serverReferences'; 23 | import { checkAndWait, loadBase, onCodeForIBMiConfigurationChange } from './base'; 24 | import { registerCommands } from './commands'; 25 | import { setLanguageSettings } from './language/config'; 26 | 27 | let client: LanguageClient; 28 | 29 | export function activate(context: ExtensionContext) { 30 | // The server is implemented in node 31 | const serverModule = context.asAbsolutePath( 32 | path.join('out', 'server.js') 33 | ); 34 | // The debug options for the server 35 | // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging 36 | const debugOptions = { execArgv: ['--nolazy', '--inspect=8789'] }; 37 | 38 | // If the extension is launched in debug mode then the debug server options are used 39 | // Otherwise the run options are used 40 | const serverOptions: ServerOptions = { 41 | run: { module: serverModule, transport: TransportKind.ipc }, 42 | debug: { 43 | module: serverModule, 44 | transport: TransportKind.ipc, 45 | options: debugOptions 46 | } 47 | }; 48 | 49 | loadBase(); 50 | 51 | // Options to control the language client 52 | const clientOptions: LanguageClientOptions = { 53 | // Register the server for plain text documents 54 | documentSelector: [ 55 | { language: 'rpgle' }, 56 | ], 57 | synchronize: { 58 | fileEvents: [ 59 | workspace.createFileSystemWatcher('**/iproj.json'), 60 | workspace.createFileSystemWatcher('**/rpglint.json'), 61 | workspace.createFileSystemWatcher(projectFilesGlob), 62 | ] 63 | } 64 | }; 65 | 66 | // Create the language client and start the client. 67 | client = new LanguageClient( 68 | 'lsp-rpgle-client', 69 | 'RPGLE language client', 70 | serverOptions, 71 | clientOptions 72 | ); 73 | 74 | client.onReady().then(async () => { 75 | buildRequestHandlers(client); 76 | 77 | const instance = await checkAndWait(); 78 | 79 | // We need to clear table caches when the connection changes 80 | if (instance) { 81 | // When the connection is established 82 | instance.subscribe(context, "connected", "vscode-rpgle", () => { 83 | clearTableCache(client); 84 | }); 85 | 86 | // When the library list changes 87 | context.subscriptions.push( 88 | onCodeForIBMiConfigurationChange("connectionSettings", async () => { 89 | clearTableCache(client); 90 | }), 91 | ); 92 | } 93 | }); 94 | 95 | // Start the client. This will also launch the server 96 | client.start(); 97 | 98 | Linter.initialise(context); 99 | columnAssist.registerColumnAssist(context); 100 | 101 | registerCommands(context, client); 102 | 103 | context.subscriptions.push(getServerSymbolProvider()); 104 | context.subscriptions.push(getServerImplementationProvider()); 105 | context.subscriptions.push(setLanguageSettings()); 106 | // context.subscriptions.push(...initBuilder(client)); 107 | 108 | 109 | console.log(`started`); 110 | } 111 | 112 | export function deactivate(): Thenable | undefined { 113 | if (!client) { 114 | return undefined; 115 | } 116 | return client.stop(); 117 | } 118 | -------------------------------------------------------------------------------- /extension/client/src/language/columnAssist.ts: -------------------------------------------------------------------------------- 1 | 2 | import { commands, DecorationOptions, ExtensionContext, Range, Selection, TextDocument, ThemeColor, window } from 'vscode'; 3 | import * as Configuration from "../configuration"; 4 | import { loadBase } from '../base'; 5 | 6 | const currentArea = window.createTextEditorDecorationType({ 7 | backgroundColor: `rgba(242, 242, 109, 0.3)`, 8 | border: `1px solid grey`, 9 | }); 10 | 11 | const notCurrentArea = window.createTextEditorDecorationType({ 12 | backgroundColor: `rgba(242, 242, 109, 0.1)`, 13 | border: `1px solid grey`, 14 | }); 15 | 16 | const outlineBar = window.createTextEditorDecorationType({ 17 | backgroundColor: new ThemeColor(`editor.background`), 18 | isWholeLine: true, 19 | opacity: `0`, 20 | }); 21 | 22 | let rulerEnabled = Configuration.get(Configuration.RULER_ENABLED_BY_DEFAULT) || false 23 | let currentEditorLine = -1; 24 | 25 | import { SpecFieldDef, SpecFieldValue, SpecRulers, specs } from '../schemas/specs'; 26 | 27 | const getAreasForLine = (line: string, index: number) => { 28 | if (line.length < 6) return undefined; 29 | if (line[6] === `*`) return undefined; 30 | 31 | const specLetter = line[5].toUpperCase(); 32 | if (specs[specLetter]) { 33 | const specification = specs[specLetter]; 34 | 35 | const active = specification.findIndex((box: any) => index >= box.start && index <= box.end); 36 | 37 | return { 38 | specification, 39 | active, 40 | outline: SpecRulers[specLetter] 41 | }; 42 | } 43 | } 44 | 45 | function documentIsFree(document: TextDocument) { 46 | if (document.languageId === `rpgle`) { 47 | const line = document.getText(new Range(0, 0, 0, 6)).toUpperCase(); 48 | return line === `**FREE`; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | export function registerColumnAssist(context: ExtensionContext) { 55 | context.subscriptions.push( 56 | commands.registerCommand(`vscode-rpgle.assist.launchUI`, async () => { 57 | const editor = window.activeTextEditor; 58 | if (editor) { 59 | const document = editor.document; 60 | 61 | if (document.languageId === `rpgle`) { 62 | if (!documentIsFree(document)) { 63 | const lineNumber = editor.selection.start.line; 64 | const positionIndex = editor.selection.start.character; 65 | 66 | const positionsData = await promptLine( 67 | document.getText(new Range(lineNumber, 0, lineNumber, 100)), 68 | positionIndex 69 | ); 70 | 71 | if (positionsData) { 72 | window.showTextDocument(document).then(newEditor => { 73 | newEditor.edit(editBuilder => { 74 | editBuilder.replace(new Range(lineNumber, 0, lineNumber, 80), positionsData); 75 | }); 76 | }) 77 | } 78 | } 79 | } 80 | } 81 | }), 82 | 83 | commands.registerCommand(`vscode-rpgle.assist.toggleFixedRuler`, async () => { 84 | rulerEnabled = !rulerEnabled; 85 | 86 | if (rulerEnabled) { 87 | updateRuler(); 88 | } else { 89 | clearRulers(); 90 | } 91 | }), 92 | 93 | commands.registerCommand(`vscode-rpgle.assist.moveLeft`, () => { 94 | moveFromPosition(`left`); 95 | }), 96 | commands.registerCommand(`vscode-rpgle.assist.moveRight`, () => { 97 | moveFromPosition(`right`); 98 | }), 99 | 100 | window.onDidChangeTextEditorSelection(e => { 101 | const editor = e.textEditor; 102 | if (rulerEnabled) { 103 | updateRuler(editor); 104 | } else { 105 | clearRulers(editor); 106 | } 107 | }), 108 | ) 109 | } 110 | 111 | function moveFromPosition(direction: "left"|"right", editor = window.activeTextEditor) { 112 | if (editor && editor.document.languageId === `rpgle` && !documentIsFree(editor.document)) { 113 | const document = editor.document; 114 | const lineNumber = editor.selection.start.line; 115 | const positionIndex = editor.selection.start.character; 116 | 117 | const positionsData = getAreasForLine( 118 | document.getText(new Range(lineNumber, 0, lineNumber, 100)), 119 | positionIndex 120 | ); 121 | 122 | if (positionsData) { 123 | let newIndex: number|undefined; 124 | if (direction === `left`) { 125 | newIndex = positionsData.active - 1; 126 | } else 127 | if (direction === `right`) { 128 | newIndex = positionsData.active + 1; 129 | } 130 | 131 | if (newIndex !== undefined && newIndex >= 0 && newIndex < positionsData.specification.length) { 132 | const box = positionsData.specification[newIndex]; 133 | if (box) { 134 | editor.selection = new Selection(lineNumber, box.start, lineNumber, box.start); 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | function updateRuler(editor = window.activeTextEditor) { 142 | let clear = true; 143 | 144 | if (editor) { 145 | const document = editor.document; 146 | if (document.languageId === `rpgle`) { 147 | if (!documentIsFree(document)) { 148 | const lineNumber = editor.selection.start.line; 149 | const positionIndex = editor.selection.start.character; 150 | 151 | const positionsData = getAreasForLine( 152 | document.getText(new Range(lineNumber, 0, lineNumber, 100)), 153 | positionIndex 154 | ); 155 | 156 | if (positionsData) { 157 | let decorations: DecorationOptions[] = []; 158 | 159 | positionsData.specification.forEach((box: any, index: number) => { 160 | if (index === positionsData.active) { 161 | //There should only be one current. 162 | editor.setDecorations(currentArea, [{ 163 | hoverMessage: box.name, 164 | range: new Range(lineNumber, box.start, lineNumber, box.end+1) 165 | }]); 166 | 167 | } else { 168 | decorations.push({ 169 | hoverMessage: box.name, 170 | range: new Range(lineNumber, box.start, lineNumber, box.end+1) 171 | }) 172 | } 173 | }); 174 | 175 | editor.setDecorations(notCurrentArea, decorations); 176 | 177 | if (currentEditorLine !== lineNumber && lineNumber >= 1) { 178 | editor.setDecorations(outlineBar, [ 179 | { 180 | range: new Range(lineNumber-1, 0, lineNumber-1, 80), 181 | renderOptions: { 182 | before: { 183 | contentText: positionsData.outline, 184 | color: new ThemeColor(`editorLineNumber.foreground`), 185 | } 186 | } 187 | }, 188 | ]); 189 | } 190 | 191 | clear = false; 192 | } 193 | 194 | currentEditorLine = lineNumber; 195 | } 196 | } 197 | } 198 | 199 | if (clear) { 200 | clearRulers(editor); 201 | } 202 | } 203 | 204 | function clearRulers(editor = window.activeTextEditor) { 205 | if (editor) { 206 | editor.setDecorations(currentArea, []); 207 | editor.setDecorations(notCurrentArea, []); 208 | editor.setDecorations(outlineBar, []); 209 | } 210 | } 211 | 212 | interface FieldBox { 213 | id: string, 214 | text: string, 215 | content: string 216 | values?: SpecFieldValue[], 217 | maxLength?: number 218 | } 219 | 220 | async function promptLine (line: string, _index: number): Promise { 221 | const base = loadBase(); 222 | 223 | if (!base) { 224 | window.showErrorMessage(`Code for IBM i is not installed. It is required due to required UI tools.`); 225 | return undefined; 226 | }; 227 | 228 | 229 | if (line.length < 6) return undefined; 230 | if (line[6] === `*`) return undefined; 231 | line = line.padEnd(80); 232 | 233 | const specLetter = line[5].toUpperCase(); 234 | if (specs[specLetter]) { 235 | const specification = specs[specLetter]; 236 | 237 | let parts: FieldBox[] = []; 238 | 239 | specification.forEach(box => { 240 | parts.push({ 241 | id: box.id, 242 | text: box.name, 243 | content: line.substring(box.start, box.end+1).trimEnd(), 244 | values: box.values, 245 | maxLength: box.values ? undefined : (box.end+1)-box.start 246 | }); 247 | }); 248 | 249 | const ui = base.customUI(); 250 | 251 | parts.forEach((box, index) => { 252 | if (box.values) { 253 | //Select box 254 | ui.addSelect(box.id, box.text, box.values.map(item => ({ 255 | selected: item.value.toUpperCase() === box.content.toUpperCase(), 256 | value: item.value, 257 | description: item.value, 258 | text: item.text 259 | }))) 260 | 261 | } else { 262 | //Input field 263 | ui.addInput(box.id, box.text); 264 | ui.fields[index].default = box.content; 265 | ui.fields[index].maxlength = box.maxLength; 266 | } 267 | }); 268 | 269 | ui.addButtons( 270 | { id: `apply`, label: `Apply changes` }, 271 | { id: `cancel`, label: `Cancel` } 272 | ); 273 | 274 | const result = await ui.loadPage<{[key: string]: string}>(`Column Assistant`); 275 | 276 | if (result && result.data) { 277 | result.panel.dispose(); 278 | const data = result.data; 279 | 280 | if (data.buttons !== `cancel`) { 281 | let spot: SpecFieldDef|undefined, length: number; 282 | for (const key in data) { 283 | spot = specification.find(box => box.id === key); 284 | if (spot) { 285 | length = (spot.end+1)-spot.start; 286 | 287 | if (data[key].length > length) data[key] = data[key].substr(0, length); 288 | 289 | line = line.substring(0, spot.start) + (spot.padStart ? data[key].padStart(length) : data[key].padEnd(length)) + line.substring(spot.end+1); 290 | } 291 | } 292 | 293 | return line.trimEnd(); 294 | } 295 | } 296 | 297 | return undefined; 298 | } else { 299 | return undefined; 300 | } 301 | } -------------------------------------------------------------------------------- /extension/client/src/language/config.ts: -------------------------------------------------------------------------------- 1 | import { languages } from "vscode"; 2 | 3 | export function setLanguageSettings() { 4 | return languages.setLanguageConfiguration(`rpgle`, { 5 | wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g 6 | }); 7 | } -------------------------------------------------------------------------------- /extension/client/src/language/serverReferences.ts: -------------------------------------------------------------------------------- 1 | import { commands, Definition, DocumentSymbol, languages, Location, ProgressLocation, Range, SymbolInformation, SymbolKind, TextDocument, Uri, window, workspace } from "vscode"; 2 | import { getInstance } from "../base"; 3 | import IBMi from "@halcyontech/vscode-ibmi-types/api/IBMi"; 4 | import { ConnectionConfig, IBMiMember } from "@halcyontech/vscode-ibmi-types"; 5 | 6 | export function getServerSymbolProvider() { 7 | let latestFetch: ExportInfo[]|undefined; 8 | 9 | return languages.registerWorkspaceSymbolProvider({ 10 | provideWorkspaceSymbols: async (query, token): Promise => { 11 | const instance = getInstance(); 12 | const editor = window.activeTextEditor; 13 | 14 | // Since this is a members only resolve. 15 | const documentIsValid = (doc: TextDocument) => { 16 | return doc.uri.scheme === `member` && doc.languageId === `rpgle`; 17 | } 18 | 19 | if (instance && instance.getConnection()) { 20 | const connection = instance.getConnection(); 21 | const config = connection.config! //TODO in vscode-ibmi 3.0.0 - change to getConfig() 22 | 23 | let member: IBMiMember|undefined; 24 | 25 | if (editor && documentIsValid(editor.document)) { 26 | const uriPath = editor.document.uri.path; 27 | member = connection.parserMemberPath(uriPath); 28 | } 29 | 30 | const libraryList = getLibraryList(config, member); 31 | 32 | if (query.length === 0 || !latestFetch) { 33 | latestFetch = await binderLookup(connection, libraryList, {generic: query}); 34 | } 35 | 36 | return latestFetch.map(e => { 37 | return new SymbolInformation( 38 | e.symbolName, 39 | SymbolKind.Function, 40 | e.moduleName, 41 | new Location(e.assumedUri, new Range(0, 0, 0, 0)) 42 | ) 43 | }) 44 | } 45 | 46 | return []; 47 | }, 48 | 49 | resolveWorkspaceSymbol: async (symbol, token): Promise => { 50 | const matchingSymbol = await window.withProgress({location: ProgressLocation.Window, title: `Fetching symbol ${symbol.name}`}, () => getSymbolFromDocument(symbol.location.uri, symbol.name)); 51 | 52 | if (matchingSymbol) { 53 | return new SymbolInformation( 54 | matchingSymbol.name, 55 | matchingSymbol.kind, 56 | symbol.containerName, 57 | new Location(symbol.location.uri, matchingSymbol.selectionRange) 58 | ) 59 | } 60 | 61 | return symbol; 62 | } 63 | }) 64 | } 65 | 66 | export function getServerImplementationProvider() { 67 | return languages.registerImplementationProvider({language: `rpgle`, scheme: `member`}, { 68 | async provideImplementation(document, position, token): Promise { 69 | const instance = getInstance(); 70 | const connection = instance?.getConnection(); 71 | 72 | if (connection) { 73 | const word = document.getText(document.getWordRangeAtPosition(position)); 74 | const config = connection.getConfig() //TODO in vscode-ibmi 3.0.0 - change to getConfig() 75 | 76 | const uriPath = document.uri.path; 77 | const member = connection.parserMemberPath(uriPath); 78 | 79 | const libraryList = getLibraryList(config, member); 80 | 81 | const exports = await binderLookup(connection, libraryList, {specific: word}); 82 | 83 | if (exports.length) { 84 | const exportsInLibraryListOrder = libraryList.map(lib => exports.find(e => e.sourceLibrary === lib)).filter(e => e) as ExportInfo[]; 85 | 86 | const resultingLocation = await window.withProgress({location: ProgressLocation.Window, title: `Resolving ${word}`}, async (progress) => { 87 | for (const exportInfo of exportsInLibraryListOrder) { 88 | progress.report({message: `checking ${exportInfo.moduleLibrary}/${exportInfo.moduleName}`}); 89 | const uri = exportInfo.assumedUri; 90 | 91 | const possibleSymbol = await getSymbolFromDocument(uri, word); 92 | if (possibleSymbol) { 93 | return new Location(uri, possibleSymbol.selectionRange); 94 | } 95 | } 96 | }); 97 | 98 | return resultingLocation; 99 | } 100 | 101 | return; 102 | } 103 | }, 104 | }); 105 | } 106 | 107 | async function getSymbolFromDocument(docUri: Uri, name: string): Promise { 108 | try { 109 | const openedDocument = await workspace.openTextDocument(docUri); 110 | const symbols = await getDocumentSymbols(openedDocument.uri); 111 | return symbols.find(s => s.name.toUpperCase() === name.toUpperCase()); 112 | } catch (e) { 113 | console.log(e); 114 | } 115 | 116 | return; 117 | } 118 | 119 | function getLibraryList(config: ConnectionConfig, member?: IBMiMember): string[] { 120 | let libraryList = [config.currentLibrary, ...config.libraryList]; 121 | 122 | if (member) { 123 | const editorLibrary = member.library; 124 | if (editorLibrary) { 125 | if (!libraryList.includes(editorLibrary)) { 126 | libraryList.unshift(editorLibrary); 127 | } 128 | } 129 | } 130 | 131 | return libraryList; 132 | } 133 | 134 | function getDocumentSymbols(uri: Uri) { 135 | return commands.executeCommand(`vscode.executeDocumentSymbolProvider`, uri) || []; 136 | } 137 | 138 | interface ExportInfo { 139 | symbolName: string; 140 | programLibrary: string; 141 | programName: string; 142 | moduleLibrary: string; 143 | moduleName: string; 144 | sourceLibrary: string; 145 | sourceFile: string; 146 | sourceMember: string; 147 | attribute: string; 148 | assumedUri: Uri; 149 | } 150 | 151 | async function binderLookup(connection: IBMi, libraryList: string[], filter: {specific?: string, generic?: string} = {}) { 152 | let symbolClause = ``; 153 | 154 | if (filter.generic) { 155 | symbolClause = filter.generic ? `UPPER(b.SYMBOL_NAME) like '%${filter.generic.toUpperCase()}%' and` : ``; 156 | } else if (filter.specific) { 157 | symbolClause = filter.specific ? `UPPER(b.SYMBOL_NAME) = '${filter.specific.toUpperCase()}' and` : ``; 158 | } 159 | 160 | const libraryInList = libraryList.map(lib => `'${lib.toUpperCase()}'`).join(`, `); 161 | 162 | const statement = [ 163 | `select`, 164 | ` b.SYMBOL_NAME,`, 165 | ` b.PROGRAM_LIBRARY as PGM_LIB,`, 166 | ` c.ENTRY as PGM_NAME,`, 167 | ` a.BOUND_MODULE_LIBRARY as MOD_LIB, `, 168 | ` a.BOUND_MODULE as MOD_NAME, `, 169 | // ...(streamFileSupported ? [`a.SOURCE_STREAM_FILE_PATH as PATH,`] : []), 170 | ` a.SOURCE_FILE_LIBRARY as LIB, `, 171 | ` a.SOURCE_FILE as SPF, `, 172 | ` a.SOURCE_FILE_MEMBER as MBR,`, 173 | ` a.MODULE_ATTRIBUTE as ATTR`, 174 | `from QSYS2.BOUND_MODULE_INFO as a`, 175 | `right join QSYS2.PROGRAM_EXPORT_IMPORT_INFO as b`, 176 | ` on a.PROGRAM_LIBRARY = b.PROGRAM_LIBRARY and a.PROGRAM_NAME = b.PROGRAM_NAME`, 177 | `right join qsys2.BINDING_DIRECTORY_INFO as c`, 178 | ` on c.ENTRY = b.PROGRAM_NAME`, 179 | `where ${symbolClause}`, 180 | ` (c.BINDING_DIRECTORY_LIBRARY in (${libraryInList})) and`, 181 | ` ((c.ENTRY_LIBRARY = b.PROGRAM_LIBRARY) or (c.ENTRY_LIBRARY = '*LIBL' and b.PROGRAM_LIBRARY in (${libraryInList}))) and`, 182 | ` (a.SOURCE_FILE_MEMBER is not null)`, 183 | // ` (${streamFileSupported ? `a.SOURCE_STREAM_FILE_PATH is not null or` : ``} a.SOURCE_FILE_MEMBER is not null)` 184 | ].join(` `); 185 | 186 | let exports: ExportInfo[] = []; 187 | 188 | try { 189 | const results = await connection.runSQL(statement); 190 | 191 | exports = results.map((r): ExportInfo => { 192 | return { 193 | symbolName: r.SYMBOL_NAME as string, 194 | programLibrary: r.PGM_LIB as string, 195 | programName: r.PGM_NAME as string, 196 | moduleLibrary: r.MOD_LIB as string, 197 | moduleName: r.MOD_NAME as string, 198 | sourceLibrary: r.LIB as string, 199 | sourceFile: r.SPF as string, 200 | sourceMember: r.MBR as string, 201 | attribute: r.ATTR as string, 202 | assumedUri: Uri.from({ 203 | scheme: `member`, 204 | path: [``, r.LIB, r.SPF, `${r.MBR}.${r.ATTR}`].join(`/`) 205 | }) 206 | } 207 | }) 208 | 209 | } catch (e) { 210 | console.log(e); 211 | } 212 | 213 | return exports; 214 | } -------------------------------------------------------------------------------- /extension/client/src/linter.ts: -------------------------------------------------------------------------------- 1 | import path = require('path'); 2 | import { commands, ExtensionContext, Uri, ViewColumn, window, workspace } from 'vscode'; 3 | import {getInstance} from './base'; 4 | 5 | import {DEFAULT_SCHEMA} from "./schemas/linter" 6 | 7 | export function initialise(context: ExtensionContext) { 8 | context.subscriptions.push( 9 | commands.registerCommand(`vscode-rpgle.openLintConfig`, async (filter) => { 10 | const instance = getInstance(); 11 | const editor = window.activeTextEditor; 12 | 13 | let exists = false; 14 | 15 | if (editor && ![`member`, `streamfile`].includes(editor.document.uri.scheme)) { 16 | const workspaces = workspace.workspaceFolders; 17 | if (workspaces && workspaces.length > 0) { 18 | const linter = await workspace.findFiles(`**/.vscode/rpglint.json`, `**/.git`, 1); 19 | let uri; 20 | if (linter && linter.length > 0) { 21 | uri = linter[0]; 22 | 23 | console.log(`Uri path: ${JSON.stringify(uri)}`); 24 | 25 | } else { 26 | console.log(`String path: ${path.join(workspaces[0].uri.fsPath, `.vscode`, `rpglint.json`)}`); 27 | 28 | uri = Uri.from({ 29 | scheme: `file`, 30 | path: path.join(workspaces[0].uri.fsPath, `.vscode`, `rpglint.json`) 31 | }); 32 | 33 | console.log(`Creating Uri path: ${JSON.stringify(uri)}`); 34 | 35 | await workspace.fs.writeFile( 36 | uri, 37 | Buffer.from(JSON.stringify(DEFAULT_SCHEMA, null, 2), `utf8`) 38 | ); 39 | } 40 | 41 | workspace.openTextDocument(uri).then(doc => { 42 | window.showTextDocument(doc, { 43 | viewColumn: ViewColumn.One 44 | }); 45 | }); 46 | } 47 | 48 | } else if (instance && instance.getConnection()) { 49 | const connection = instance.getConnection(); 50 | const content = instance.getContent(); 51 | 52 | /** @type {"member"|"streamfile"} */ 53 | let type = `member`; 54 | let configPath: string | undefined; 55 | 56 | if (filter && filter.description) { 57 | // Bad way to get the library for the filter .. 58 | const library: string = (filter.description.split(`/`)[0]).toLocaleUpperCase(); 59 | 60 | if (library.includes(`*`)) { 61 | window.showErrorMessage(`Cannot show lint config for a library filter.`); 62 | return; 63 | } 64 | 65 | configPath = `${library}/VSCODE/RPGLINT.JSON`; 66 | 67 | exists = (await connection.runCommand({ 68 | command: `CHKOBJ OBJ(${library}/VSCODE) OBJTYPE(*FILE) MBR(RPGLINT)`, 69 | noLibList: true 70 | })).code === 0; 71 | 72 | } else if (editor) { 73 | //@ts-ignore 74 | type = editor.document.uri.scheme; 75 | 76 | console.log(`Uri remote path: ${JSON.stringify(editor.document.uri)}`); 77 | 78 | switch (type) { 79 | case `member`: 80 | const memberPath = parseMemberUri(editor.document.uri.path); 81 | const cleanString = [ 82 | memberPath.library, 83 | `VSCODE`, 84 | `RPGLINT.JSON` 85 | ].join(`/`); 86 | 87 | const memberUri = Uri.from({ 88 | scheme: `member`, 89 | path: cleanString 90 | }); 91 | 92 | configPath = memberUri.path; 93 | 94 | exists = (await connection.runCommand({ 95 | command: `CHKOBJ OBJ(${memberPath.library!.toLocaleUpperCase()}/VSCODE) OBJTYPE(*FILE) MBR(RPGLINT)`, 96 | noLibList: true 97 | })).code === 0; 98 | break; 99 | 100 | case `streamfile`: 101 | const config = instance.getConfig(); 102 | if (config.homeDirectory) { 103 | configPath = path.posix.join(config.homeDirectory, `.vscode`, `rpglint.json`) 104 | exists = await content.testStreamFile(configPath, `r`); 105 | } 106 | break; 107 | } 108 | } else { 109 | window.showErrorMessage(`No active editor found.`); 110 | } 111 | 112 | if (configPath) { 113 | console.log(`Current path: ${configPath}`); 114 | 115 | if (exists) { 116 | await commands.executeCommand(`code-for-ibmi.openEditable`, configPath); 117 | } else { 118 | window.showErrorMessage(`RPGLE linter config doesn't exist for this file. Would you like to create a default at ${configPath}?`, `Yes`, `No`).then 119 | (async (value) => { 120 | if (value === `Yes`) { 121 | const jsonString = JSON.stringify(DEFAULT_SCHEMA, null, 2); 122 | 123 | switch (type) { 124 | case `member`: 125 | if (configPath) { 126 | const memberPath = configPath.split(`/`); 127 | 128 | // Will not crash, even if it fails 129 | await connection.runCommand( 130 | { 131 | 'command': `CRTSRCPF FILE(${memberPath[0]}/VSCODE) RCDLEN(112)` 132 | } 133 | ); 134 | 135 | // Will not crash, even if it fails 136 | await connection.runCommand( 137 | { 138 | command: `ADDPFM FILE(${memberPath[0]}/VSCODE) MBR(RPGLINT) SRCTYPE(JSON)` 139 | } 140 | ); 141 | 142 | try { 143 | console.log(`Member path: ${[memberPath[0], `VSCODE`, `RPGLINT`].join(`/`)}`); 144 | 145 | await content.uploadMemberContent(undefined, memberPath[0], `VSCODE`, `RPGLINT`, jsonString); 146 | await commands.executeCommand(`code-for-ibmi.openEditable`, configPath); 147 | } catch (e) { 148 | console.log(e); 149 | window.showErrorMessage(`Failed to create and open new lint configuration file: ${configPath}`); 150 | } 151 | } 152 | break; 153 | 154 | case `streamfile`: 155 | console.log(`IFS path: ${configPath}`); 156 | 157 | try { 158 | await content.writeStreamfile(configPath, jsonString); 159 | await commands.executeCommand(`code-for-ibmi.openEditable`, configPath); 160 | } catch (e) { 161 | console.log(e); 162 | window.showErrorMessage(`Failed to create and open new lint configuration file: ${configPath}`); 163 | } 164 | break; 165 | } 166 | } 167 | }); 168 | } 169 | } else { 170 | window.showErrorMessage(`No lint config path for this file. File must either be a member or a streamfile on the host IBM i.`); 171 | } 172 | } else { 173 | window.showErrorMessage(`Not connected to a system.`); 174 | } 175 | }), 176 | ) 177 | } 178 | 179 | function parseMemberUri(fullPath: string): {asp?: string, library?: string, file?: string, name: string} { 180 | const parts = fullPath.split(`/`).map(s => s.split(`,`)).flat().filter(s => s.length >= 1); 181 | return { 182 | name: path.parse(parts[parts.length - 1]).name, 183 | file: parts[parts.length - 2], 184 | library: parts[parts.length - 3], 185 | asp: parts[parts.length - 4] 186 | } 187 | }; -------------------------------------------------------------------------------- /extension/client/src/requests.ts: -------------------------------------------------------------------------------- 1 | import path = require('path'); 2 | import { Uri, workspace, RelativePattern, commands } from 'vscode'; 3 | import { LanguageClient } from 'vscode-languageclient/node'; 4 | import { getInstance } from './base'; 5 | import { IBMiMember } from '@halcyontech/vscode-ibmi-types'; 6 | 7 | export function buildRequestHandlers(client: LanguageClient) { 8 | /** 9 | * Validates a URI. 10 | * 1. Attemps to open a valid full path 11 | * 2. If running in a workspace, will search for the file by basename 12 | */ 13 | client.onRequest("getUri", async (stringUri: string): Promise => { 14 | const uri = Uri.parse(stringUri); 15 | let doc; 16 | try { 17 | doc = await workspace.openTextDocument(uri); 18 | } catch (e: any) { 19 | doc = undefined; 20 | } 21 | 22 | if (doc) { 23 | return doc.uri.toString(); 24 | } else 25 | if (uri.scheme === `file`) { 26 | const basename = path.basename(uri.path); 27 | const [possibleFile] = await workspace.findFiles(`**/${basename}`, `**/.git`, 1); 28 | if (possibleFile) { 29 | return possibleFile.toString(); 30 | } 31 | } 32 | 33 | return; 34 | }); 35 | 36 | /** 37 | * Returns the working directory from Code for IBM i. 38 | */ 39 | client.onRequest("getWorkingDirectory", async (): Promise => { 40 | const instance = getInstance(); 41 | if (instance) { 42 | const connection = instance.getConnection(); 43 | const config = connection?.getConfig(); 44 | if (config) { 45 | return config.homeDirectory; 46 | } 47 | } 48 | }) 49 | 50 | /** 51 | * Gets the text content for a provided Uri 52 | */ 53 | client.onRequest("getFile", async (stringUri: string): Promise => { 54 | // Always assumes URI is valid. Use getUri first 55 | const uri = Uri.parse(stringUri); 56 | try { 57 | const doc = await workspace.openTextDocument(uri); 58 | 59 | if (doc) { 60 | return doc.getText(); 61 | } 62 | } catch (e) { } 63 | 64 | return; 65 | }); 66 | 67 | /** 68 | * Resolves member paths 69 | */ 70 | client.onRequest("memberResolve", async (parms: string[]): Promise => { 71 | let memberName = parms[0], sourceFile = parms[1]; 72 | 73 | const instance = getInstance(); 74 | const connection = instance?.getConnection(); 75 | 76 | if (connection) { 77 | const config = connection.getConfig(); 78 | const content = connection.getContent(); 79 | 80 | if (config && content) { 81 | const files = [config?.currentLibrary, ...config?.libraryList!] 82 | .filter(l => l !== undefined) 83 | .map(l => ({ name: sourceFile, library: l! })); 84 | 85 | try { 86 | const member = await content?.memberResolve(memberName, files); 87 | 88 | return member; 89 | } catch (e) { 90 | console.log(e); 91 | return undefined; 92 | } 93 | } 94 | } 95 | }); 96 | 97 | client.onRequest("streamfileResolve", async (parms: any[]): Promise => { 98 | const bases: string[] = parms[0]; 99 | const includePaths: string[] = parms[1]; 100 | 101 | const instance = getInstance(); 102 | const connection = instance?.getConnection(); 103 | 104 | if (connection) { 105 | const content = connection.getContent(); 106 | const config = connection.getConfig()!; 107 | 108 | if (instance && content && config) { 109 | if (includePaths.length === 0) { 110 | includePaths.push(config.homeDirectory); 111 | } 112 | 113 | const resolvedPath = await content?.streamfileResolve(bases, includePaths); 114 | 115 | return resolvedPath; 116 | } 117 | } 118 | }); 119 | 120 | /** 121 | * Gets the column information for a provided file 122 | */ 123 | client.onRequest(`getObject`, async (table: string) => { 124 | const instance = getInstance(); 125 | 126 | if (instance) { 127 | console.log(`Fetching table: ${table}`); 128 | 129 | const connection = instance.getConnection(); 130 | if (connection) { 131 | const content = connection.getContent(); 132 | const config = connection.getConfig(); 133 | 134 | const dateStr = Date.now().toString().substr(-6); 135 | const randomFile = `R${table.substring(0, 3)}${dateStr}`.substring(0, 10); 136 | const fullPath = `${config.tempLibrary}/${randomFile}`; 137 | 138 | console.log(`Temp OUTFILE: ${fullPath}`); 139 | 140 | const parts = { 141 | schema: `*LIBL`, 142 | table: ``, 143 | }; 144 | 145 | if (table.includes(`/`)) { 146 | const splitName = table.split(`/`); 147 | if (splitName.length >= 2) parts.schema = splitName[splitName.length - 2]; 148 | if (splitName.length >= 1) parts.table = splitName[splitName.length - 1]; 149 | } else { 150 | parts.table = table; 151 | } 152 | 153 | // TODO: handle .env file here? 154 | 155 | const outfileRes: any = await connection.runCommand({ 156 | environment: `ile`, 157 | command: `DSPFFD FILE(${parts.schema}/${parts.table}) OUTPUT(*OUTFILE) OUTFILE(${fullPath})` 158 | }); 159 | 160 | console.log(outfileRes); 161 | const resultCode = outfileRes.code || 0; 162 | 163 | if (resultCode === 0) { 164 | const data: any[] = await content.getTable(config.tempLibrary, randomFile, randomFile, true); 165 | 166 | console.log(`Temp OUTFILE read. ${data.length} rows.`); 167 | 168 | connection.runCommand({ 169 | environment: `ile`, 170 | command: `DLTOBJ OBJ(${fullPath}) OBJTYPE(*FILE)` 171 | }); 172 | 173 | return data; 174 | } 175 | } 176 | } 177 | 178 | return []; 179 | }); 180 | } 181 | 182 | export function clearTableCache(client: LanguageClient) { 183 | client.sendRequest(`clearTableCache`); 184 | } -------------------------------------------------------------------------------- /extension/client/src/schemas/linter.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SCHEMA = { 2 | indent: 2, 3 | PrototypeCheck: true, 4 | NoOCCURS: true, 5 | NoSELECTAll: true, 6 | UppercaseConstants: true, 7 | IncorrectVariableCase: true, 8 | NoSQLJoins: true, 9 | PrettyComments: true, 10 | NoGlobalSubroutines: true, 11 | NoExternalTo: [ 12 | `QCMD`, 13 | `QP2TERM`, 14 | `QSH`, 15 | `SYSTEM`, 16 | `QCMDEXC`, 17 | ] 18 | } -------------------------------------------------------------------------------- /extension/client/src/schemas/specs.ts: -------------------------------------------------------------------------------- 1 | export type SpecFieldValue = {value: string, text: string}; 2 | export type SpecFieldDef = {id: string, name: string, start: number, end: number, values?: SpecFieldValue[], padStart?: boolean} 3 | 4 | export const SpecRulers: {[spec: string]: string} = { 5 | C: `.....CL0N01Factor1+++++++Opcode&ExtFactor2+++++++Result++++++++Len++D+HiLoEq`, 6 | D: `.....DName+++++++++++ETDsFrom+++To/L+++IDc.Keywords++++++++++++++++++++`, 7 | F: `.....FFilename++IPEASFRlen+LKlen+AIDevice+.Keywords++++++++++++++++++++`, 8 | P: `.....PName+++++++++++..T...................Keywords++++++++++++++++++++` 9 | } 10 | 11 | export const specs: {[spec: string]: SpecFieldDef[]} = { 12 | C: [ 13 | { 14 | id: `controlLevel`, 15 | name: `Control Level`, 16 | start: 6, 17 | end: 7, 18 | }, 19 | { 20 | id: `indicators`, 21 | name: `Indicators`, 22 | start: 8, 23 | end: 10, 24 | }, 25 | { 26 | id: `factor1`, 27 | name: `Factor 1`, 28 | start: 11, 29 | end: 24 30 | }, 31 | { 32 | id: `operation`, 33 | name: `Operation and Extender`, 34 | start: 25, 35 | end: 34 36 | }, 37 | { 38 | id: `factor2`, 39 | name: `Factor 2`, 40 | start: 35, 41 | end: 48 42 | }, 43 | { 44 | id: `result`, 45 | name: `Result Field`, 46 | start: 49, 47 | end: 62 48 | }, 49 | { 50 | id: `fieldLength`, 51 | name: `Field Length`, 52 | start: 63, 53 | end: 67 54 | }, 55 | { 56 | id: `decimalPositions`, 57 | name: `Decimal Positions`, 58 | start: 68, 59 | end: 69 60 | }, 61 | { 62 | id: `resultingIndicatorsA`, 63 | name: `Resulting Indicator`, 64 | start: 70, 65 | end: 71 66 | }, 67 | { 68 | id: `resultingIndicatorsB`, 69 | name: `Resulting Indicator`, 70 | start: 72, 71 | end: 73 72 | }, 73 | { 74 | id: `resultingIndicatorsC`, 75 | name: `Resulting Indicator`, 76 | start: 74, 77 | end: 75 78 | } 79 | ], 80 | 81 | D: [ 82 | {start: 6, end: 20, name: `Name`, id: `name`}, 83 | {start: 21, end: 21, name: `External Description`, id: `externalDescription`}, 84 | {start: 22, end: 22, name: `Type of Data Structure`, id: `typeOfDs`}, 85 | {start: 23, end: 24, name: `Definition Type`, id: `definitionType`, values: [ 86 | { value: ``, 87 | text: `The specification defines either a data structure subfield or a parameter within a prototype or procedure interface definition.`}, 88 | { value: `C`, 89 | text: `The specification defines a constant. Position 25 must be blank.`}, 90 | { value: `DS`, 91 | text: `The specification defines a data structure.`}, 92 | { value: `PR`, 93 | text: `The specification defines a prototype and the return value, if any.`}, 94 | { value: `PI`, 95 | text: `The specification defines a procedure interface, and the return value if any.`}, 96 | { value: `S`, 97 | text: `The specification defines a standalone field, array or table. Position 25 must be blank.`}, 98 | ]}, 99 | {start: 25, end: 31, name: `From Position`, id: `fromPosition`, padStart: true}, 100 | {start: 32, end: 38, name: `To Position / Length`, id: `toPosition`, padStart: true}, 101 | {start: 39, end: 39, name: `Internal Data Type`, id: `internalDataType`, values: [ 102 | { value: `A`, 103 | text: `Character (Fixed or Variable-length format)`}, 104 | { value: `B`, 105 | text: `Numeric (Binary format)`}, 106 | { value: `C`, 107 | text: `UCS-2 (Fixed or Variable-length format)`}, 108 | { value: `D`, 109 | text: `Date`}, 110 | { value: `F`, 111 | text: `Numeric (Float format)`}, 112 | { value: `G`, 113 | text: `Graphic (Fixed or Variable-length format)`}, 114 | { value: `I`, 115 | text: `Numeric (Integer format)`}, 116 | { value: `N`, 117 | text: `Character (Indicator format)`}, 118 | { value: `O`, 119 | text: `Object`}, 120 | { value: `P`, 121 | text: `Numeric (Packed decimal format)`}, 122 | { value: `S`, 123 | text: `Numeric (Zoned format)`}, 124 | { value: `T`, 125 | text: `Time`}, 126 | { value: `U`, 127 | text: `Numeric (Unsigned format)`}, 128 | { value: `Z`, 129 | text: `Timestamp`}, 130 | { value: `*`, 131 | text: `Basing pointer or procedure pointer`}, 132 | { value: ``, text: `Blank (Character, Packed or Zoned)`} 133 | ]}, 134 | {start: 40, end: 41, name: `Decimal Positions`, id: `decimalPositions`, padStart: true}, 135 | {start: 43, end: 79, name: `Keywords`, id: `keywords`} 136 | ], 137 | F: [ 138 | {start: 6, end: 15, name: `File Name`, id: `fileName`}, 139 | {start: 16, end: 16, name: `File Type`, id: `fileType`}, 140 | {start: 17, end: 17, name: `File Designation`, id: `fileDesignation`}, 141 | {start: 18, end: 18, name: `End of File`, id: `endOfFile`}, 142 | {start: 19, end: 19, name: `File Addition`, id: `fileAddition`}, 143 | {start: 20, end: 20, name: `Sequence`, id: `sequence`}, 144 | {start: 21, end: 21, name: `File Format`, id: `fileFormat`}, 145 | {start: 22, end: 26, name: `Record Length`, id: `recordLength`}, 146 | {start: 27, end: 27, name: `Limits Processing`, id: `limitsProcessing`}, 147 | {start: 28, end: 32, name: `Length of Key or Record Address`, id: `keyLength`}, 148 | {start: 33, end: 33, name: `Record Address Type`, id: `addressType`}, 149 | {start: 34, end: 34, name: `File Organization`, id: `fileOrg`}, 150 | {start: 35, end: 41, name: `Device`, id: `device`}, 151 | {start: 43, end: 79, name: `Keywords`, id: `keywords`} 152 | ], 153 | P: [ 154 | {start: 6, end: 20, name: `Name`, id: `name`}, 155 | {start: 23, end: 23, name: `Begin/End Procedure`, id: `proc`}, 156 | {start: 43, end: 79, name: `Keywords`, id: `keywords`} 157 | ] 158 | }; -------------------------------------------------------------------------------- /extension/client/testFixture/completion.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/extension/client/testFixture/completion.txt -------------------------------------------------------------------------------- /extension/client/testFixture/diagnostics.txt: -------------------------------------------------------------------------------- 1 | ANY browsers, ANY OS. -------------------------------------------------------------------------------- /extension/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es2020", 6 | "outDir": "out", 7 | "rootDir": "src", 8 | "lib": [ "es2020" ], 9 | "sourceMap": true, 10 | "composite": true 11 | }, 12 | "include": [ 13 | "src" 14 | ], 15 | "exclude": [ 16 | "node_modules", ".vscode-test", "src/test" 17 | ] 18 | } -------------------------------------------------------------------------------- /extension/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | //@ts-check 7 | 8 | 'use strict'; 9 | 10 | const withDefaults = require(`../../shared.webpack.config`); 11 | const path = require(`path`); 12 | 13 | module.exports = withDefaults({ 14 | context: path.join(__dirname), 15 | entry: { 16 | extension: `./src/extension.ts`, 17 | }, 18 | output: { 19 | filename: `extension.js`, 20 | path: path.join(__dirname, `..`, `..`, `out`) 21 | } 22 | }); -------------------------------------------------------------------------------- /extension/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "description": "RPGLE language server", 4 | "version": "1.0.0", 5 | "author": "Liam Allan", 6 | "license": "MIT", 7 | "engines": { 8 | "node": "*" 9 | }, 10 | "dependencies": { 11 | "glob": "^7.2.0", 12 | "minimatch": "^5.1.0", 13 | "p-queue": "^7.3.4", 14 | "vscode-languageserver": "^8.0.2", 15 | "vscode-languageserver-textdocument": "^1.0.7", 16 | "vscode-uri": "^3.0.6" 17 | }, 18 | "devDependencies": { 19 | "@halcyontech/vscode-ibmi-types": "^2.14.0", 20 | "@types/glob": "^8.0.0", 21 | "webpack": "^5.24.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /extension/server/src/connection.ts: -------------------------------------------------------------------------------- 1 | import { IBMiMember } from '@halcyontech/vscode-ibmi-types'; 2 | 3 | import { 4 | createConnection, 5 | DidChangeWatchedFilesParams, 6 | ProposedFeatures, 7 | _Connection, 8 | WorkspaceFolder 9 | } from 'vscode-languageserver/node'; 10 | 11 | import PQueue from 'p-queue'; 12 | 13 | import { documents, findFile, parser } from './providers'; 14 | import { includePath } from './providers/project'; 15 | 16 | // Create a connection for the server, using Node's IPC as a transport. 17 | // Also include all preview / proposed LSP features. 18 | export const connection: _Connection = createConnection(ProposedFeatures.all); 19 | 20 | const queue = new PQueue(); 21 | 22 | export let watchedFilesChangeEvent: ((params: DidChangeWatchedFilesParams) => void)[] = []; 23 | connection.onDidChangeWatchedFiles((params: DidChangeWatchedFilesParams) => { 24 | watchedFilesChangeEvent.forEach(editEvent => editEvent(params)); 25 | }) 26 | 27 | export async function validateUri(stringUri: string, scheme = ``) { 28 | // First, check local cache 29 | const possibleCachedFile = findFile(stringUri, scheme); 30 | if (possibleCachedFile) return possibleCachedFile; 31 | 32 | console.log(`Validating file from server: ${stringUri}`); 33 | 34 | // Then reach out to the extension to find it 35 | const uri: string|undefined = await connection.sendRequest("getUri", stringUri); 36 | if (uri) return uri; 37 | 38 | return; 39 | } 40 | 41 | export async function getFileRequest(uri: string) { 42 | // First, check if it's local 43 | const localCacheDoc = documents.get(uri); 44 | if (localCacheDoc) return localCacheDoc.getText(); 45 | 46 | console.log(`Fetching file from server: ${uri}`); 47 | 48 | // If not, then grab it from remote 49 | const body: string|undefined = await connection.sendRequest("getFile", uri); 50 | if (body) { 51 | // TODO.. cache it? 52 | return body; 53 | } 54 | 55 | return; 56 | } 57 | 58 | export let resolvedMembers: {[baseUri: string]: {[fileKey: string]: IBMiMember}} = {}; 59 | export let resolvedStreamfiles: {[baseUri: string]: {[fileKey: string]: string}} = {}; 60 | 61 | export async function memberResolve(baseUri: string, member: string, file: string): Promise { 62 | const fileKey = file+member; 63 | 64 | if (resolvedMembers[baseUri] && resolvedMembers[baseUri][fileKey]) return resolvedMembers[baseUri][fileKey]; 65 | 66 | try { 67 | const resolvedMember = await queue.add(() => {return connection.sendRequest("memberResolve", [member, file])}) as IBMiMember|undefined; 68 | // const resolvedMember = await connection.sendRequest("memberResolve", [member, file]) as IBMiMember|undefined; 69 | 70 | if (resolvedMember) { 71 | if (!resolvedMembers[baseUri]) resolvedMembers[baseUri] = {}; 72 | resolvedMembers[baseUri][fileKey] = resolvedMember; 73 | } 74 | 75 | return resolvedMember; 76 | } catch (e) { 77 | console.log(`Member resolve failed.`); 78 | console.log(JSON.stringify({baseUri, member, file})); 79 | console.log(e); 80 | } 81 | 82 | return undefined; 83 | } 84 | 85 | export async function streamfileResolve(baseUri: string, base: string[]): Promise { 86 | const baseString = base.join(`-`); 87 | if (resolvedStreamfiles[baseUri] && resolvedStreamfiles[baseUri][baseString]) return resolvedStreamfiles[baseUri][baseString]; 88 | 89 | const workspace = await getWorkspaceFolder(baseUri); 90 | 91 | const paths = (workspace ? includePath[workspace.uri] : []) || []; 92 | 93 | try { 94 | const resolvedPath = await queue.add(() => {return connection.sendRequest("streamfileResolve", [base, paths])}) as string|undefined; 95 | // const resolvedPath = await connection.sendRequest("streamfileResolve", [base, paths]) as string|undefined; 96 | 97 | if (resolvedPath) { 98 | if (!resolvedStreamfiles[baseUri]) resolvedStreamfiles[baseUri] = {}; 99 | resolvedStreamfiles[baseUri][baseString] = resolvedPath; 100 | } 101 | 102 | return resolvedPath; 103 | } catch (e) { 104 | console.log(`Streamfile resolve failed.`); 105 | console.log(JSON.stringify({baseUri, base, paths})); 106 | console.log(e); 107 | } 108 | 109 | return undefined; 110 | } 111 | 112 | export function getWorkingDirectory(): Promise { 113 | return connection.sendRequest("getWorkingDirectory"); 114 | } 115 | 116 | export function getObject(objectPath: string): Promise { 117 | return connection.sendRequest("getObject", objectPath); 118 | } 119 | 120 | export interface PossibleInclude { 121 | uri: string; 122 | relative: string 123 | }; 124 | 125 | export async function getWorkspaceFolder(baseUri: string) { 126 | let workspaceFolder: WorkspaceFolder | undefined; 127 | 128 | const workspaceFolders = await connection.workspace.getWorkspaceFolders(); 129 | 130 | if (workspaceFolders) { 131 | workspaceFolder = workspaceFolders.find(folderUri => baseUri.startsWith(folderUri.uri)) 132 | } 133 | 134 | return workspaceFolder 135 | } 136 | 137 | export function handleClientRequests() { 138 | connection.onRequest(`clearTableCache`, () => { 139 | parser.clearTableCache(); 140 | }); 141 | 142 | connection.onRequest(`getCache`, (uri: string) => { 143 | return parser.getParsedCache(uri); 144 | }); 145 | } 146 | 147 | export interface BindingDirectory { 148 | lib?: string; 149 | name: string; 150 | } -------------------------------------------------------------------------------- /extension/server/src/data.ts: -------------------------------------------------------------------------------- 1 | import Declaration from '../../../language/models/declaration'; 2 | import { getPrettyType } from '../../../language/models/fixed'; 3 | 4 | export function isInMerlin(): boolean { 5 | const { MACHINE_EXEC_PORT } = process.env; 6 | return MACHINE_EXEC_PORT !== undefined; 7 | } 8 | 9 | export function parseMemberUri(path: string): {asp?: string, library?: string, file?: string, name: string} { 10 | const parts = path.split(`/`).map(s => s.split(`,`)).flat().filter(s => s.length >= 1); 11 | return { 12 | name: parts[parts.length - 1], 13 | file: parts[parts.length - 2], 14 | library: parts[parts.length - 3], 15 | asp: parts[parts.length - 4] 16 | } 17 | }; 18 | 19 | export function dspffdToRecordFormats(data: any, aliases = false): Declaration[] { 20 | let recordFormats: {[name: string]: Declaration} = {}; 21 | 22 | data.forEach((row: any) => { 23 | const { 24 | WHNAME: formatName, 25 | WHFLDT: type, 26 | WHFLDB: strLength, 27 | WHFLDD: digits, 28 | WHFLDP: decimals, 29 | WHFTXT: text, 30 | } = row; 31 | 32 | const aliasName: string|undefined = row.WHALIS ? row.WHALIS.trim() : undefined; 33 | const name = aliases ? aliasName || row.WHFLDE : row.WHFLDE; 34 | 35 | if (name.trim() === ``) return; 36 | if (name.startsWith(`*`)) return; 37 | 38 | let recordFormat; 39 | if (recordFormats[formatName]) { 40 | recordFormat = recordFormats[formatName]; 41 | } else { 42 | recordFormat = new Declaration(`struct`); 43 | recordFormat.name = formatName; 44 | recordFormats[formatName] = recordFormat; 45 | } 46 | 47 | const currentSubfield = new Declaration(`subitem`); 48 | currentSubfield.name = name; 49 | let keywords: {[key: string]: string|true} = {}; 50 | 51 | if (row.WHVARL === `Y`) keywords[`VARYING`] = true; 52 | 53 | currentSubfield.keyword = getPrettyType({ 54 | type, 55 | len: digits === 0 ? strLength : digits, 56 | decimals: decimals, 57 | keywords, 58 | field: ``, 59 | pos: `` 60 | }); 61 | 62 | currentSubfield.tags.push({tag: `description`, content: text.trim()}); 63 | 64 | recordFormat.subItems.push(currentSubfield); 65 | }); 66 | 67 | return Object.values(recordFormats); 68 | } -------------------------------------------------------------------------------- /extension/server/src/providers/definition.ts: -------------------------------------------------------------------------------- 1 | import { DefinitionParams, Location, Definition, Range } from 'vscode-languageserver'; 2 | import { documents, getWordRangeAtPosition, parser } from '.'; 3 | import Parser from '../../../../language/parser'; 4 | import Cache from '../../../../language/models/cache'; 5 | import Declaration from '../../../../language/models/declaration'; 6 | 7 | export default async function definitionProvider(handler: DefinitionParams): Promise { 8 | const currentUri = handler.textDocument.uri; 9 | const lineNumber = handler.position.line; 10 | const document = documents.get(currentUri); 11 | 12 | if (document) { 13 | const doc = await parser.getDocs(currentUri, document.getText()); 14 | if (doc) { 15 | const editingLine = document.getText(Range.create(lineNumber, 0, lineNumber, 200)); 16 | const possibleInclude = Parser.getIncludeFromDirective(editingLine); 17 | 18 | if (possibleInclude && parser.includeFileFetch) { 19 | const include = await parser.includeFileFetch(currentUri, possibleInclude); 20 | if (include.found && include.uri) { 21 | return Location.create(include.uri, Range.create(0, 0, 0, 0)); 22 | } 23 | 24 | } else { 25 | let def: Declaration|undefined; 26 | 27 | // First, we try and get the reference by offset 28 | def = Cache.referenceByOffset(currentUri, doc, document.offsetAt(handler.position)); 29 | 30 | if (def) { 31 | return Location.create( 32 | def.position.path, 33 | Range.create( 34 | def.position.range.line, 35 | 0, 36 | def.position.range.line, 37 | 0 38 | ) 39 | ); 40 | } 41 | 42 | // If we can't find the def by offset, we do a basic word lookup 43 | 44 | const word = getWordRangeAtPosition(document, handler.position); 45 | if (word) { 46 | def = doc.findDefinition(lineNumber, word); 47 | 48 | if (def) { 49 | return Location.create( 50 | def.position.path, 51 | Range.create( 52 | def.position.range.line, 53 | 0, 54 | def.position.range.line, 55 | 0 56 | ) 57 | ); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | return null; 65 | } -------------------------------------------------------------------------------- /extension/server/src/providers/documentSymbols.ts: -------------------------------------------------------------------------------- 1 | import { DocumentSymbol, DocumentSymbolParams, Range, SymbolKind } from 'vscode-languageserver'; 2 | import { documents, parser, prettyKeywords } from '.'; 3 | import Cache from '../../../../language/models/cache'; 4 | import Declaration from '../../../../language/models/declaration'; 5 | 6 | export default async function documentSymbolProvider(handler: DocumentSymbolParams): Promise { 7 | const currentPath = handler.textDocument.uri; 8 | const symbols: DocumentSymbol[] = []; 9 | const document = documents.get(currentPath); 10 | 11 | const validRange = (def: Declaration) => { 12 | return def.range.start !== null && def.range.start >= 0 && def.range.end !== null; 13 | } 14 | 15 | const expandStruct = (def: Declaration): DocumentSymbol => { 16 | let start = def.range.start || def.position.range.line; 17 | let end = def.range.end || def.position.range.line; 18 | let hasChildren = def.subItems && def.subItems.length > 0; 19 | 20 | const parent = DocumentSymbol.create( 21 | def.name, 22 | prettyKeywords(def.keyword), 23 | hasChildren ? SymbolKind.Struct : SymbolKind.Property, 24 | Range.create(start, 0, end, 0), 25 | Range.create(start, 0, start, 0), 26 | ); 27 | 28 | if (hasChildren) { 29 | parent.children = def.subItems 30 | .filter(subitem => subitem.position && subitem.position.path === currentPath) 31 | .map(subitem => expandStruct(subitem)); 32 | } 33 | 34 | return parent; 35 | } 36 | 37 | if (document) { 38 | const doc = await parser.getDocs(currentPath, document.getText()); 39 | 40 | /** 41 | * @param {Cache} scope 42 | * @returns {vscode.DocumentSymbol[]} 43 | */ 44 | const getScopeVars = (scope: Cache) => { 45 | const currentScopeDefs: DocumentSymbol[] = []; 46 | 47 | scope.procedures 48 | .filter(proc => proc.position && proc.position.path === currentPath && validRange(proc)) 49 | .forEach(proc => { 50 | const procDef = DocumentSymbol.create( 51 | proc.name, 52 | prettyKeywords(proc.keyword), 53 | SymbolKind.Function, 54 | Range.create(proc.range.start!, 0, proc.range.end!, 0), 55 | Range.create(proc.range.start!, 0, proc.range.start!, 0), 56 | ); 57 | 58 | if (proc.scope) { 59 | procDef.children = proc.subItems 60 | .filter(subitem => subitem.position && subitem.position.path === currentPath) 61 | .map(subitem => DocumentSymbol.create( 62 | subitem.name, 63 | prettyKeywords(subitem.keyword), 64 | SymbolKind.Property, 65 | Range.create(subitem.position.range.line, 0, subitem.position.range.line, 0), 66 | Range.create(subitem.position.range.line, 0, subitem.position.range.line, 0) 67 | )); 68 | 69 | procDef.children.push(...getScopeVars(proc.scope)); 70 | } 71 | 72 | currentScopeDefs.push(procDef); 73 | }); 74 | 75 | currentScopeDefs.push( 76 | ...scope.subroutines 77 | .filter(sub => sub.position && sub.position.path === currentPath && validRange(sub)) 78 | .map(def => DocumentSymbol.create( 79 | def.name, 80 | prettyKeywords(def.keyword), 81 | SymbolKind.Function, 82 | Range.create(def.range.start!, 0, def.range.end!, 0), 83 | Range.create(def.range.start!, 0, def.range.start!, 0), 84 | )), 85 | 86 | ...scope.variables 87 | .filter(variable => variable.position && variable.position.path === currentPath) 88 | .map(def => DocumentSymbol.create( 89 | def.name, 90 | prettyKeywords(def.keyword), 91 | SymbolKind.Variable, 92 | Range.create(def.position.range.line, 0, def.position.range.line, 0), 93 | Range.create(def.position.range.line, 0, def.position.range.line, 0) 94 | )) 95 | ); 96 | 97 | scope.constants 98 | .filter(constant => constant.position && constant.position.path === currentPath) 99 | .forEach(def => { 100 | const constantDef = DocumentSymbol.create( 101 | def.name, 102 | prettyKeywords(def.keyword), 103 | SymbolKind.Constant, 104 | Range.create(def.position.range.line, 0, def.position.range.line, 0), 105 | Range.create(def.position.range.line, 0, def.position.range.line, 0) 106 | ); 107 | 108 | if (def.subItems.length > 0) { 109 | constantDef.children = def.subItems 110 | .filter(subitem => subitem.position && subitem.position.path === currentPath) 111 | .map(subitem => DocumentSymbol.create( 112 | subitem.name, 113 | prettyKeywords(subitem.keyword), 114 | SymbolKind.Property, 115 | Range.create(subitem.position.range.line, 0, subitem.position.range.line, 0), 116 | Range.create(subitem.position.range.line, 0, subitem.position.range.line, 0) 117 | )); 118 | } 119 | 120 | currentScopeDefs.push(constantDef); 121 | }) 122 | 123 | scope.files 124 | .filter(struct => struct.position && struct.position.path === currentPath) 125 | .forEach(file => { 126 | const start = file.range.start || file.position.range.line; 127 | const end = file.range.end || file.position.range.line; 128 | const fileDef = DocumentSymbol.create( 129 | file.name, 130 | prettyKeywords(file.keyword), 131 | SymbolKind.File, 132 | Range.create(start, 0, end, 0), 133 | Range.create(start, 0, end, 0) 134 | ); 135 | 136 | fileDef.children = []; 137 | 138 | file.subItems 139 | .filter(recordFormat => recordFormat.position && recordFormat.position.path === currentPath) 140 | .forEach(recordFormat => { 141 | const recordFormatDef = DocumentSymbol.create( 142 | recordFormat.name, 143 | prettyKeywords(recordFormat.keyword), 144 | SymbolKind.Struct, 145 | Range.create(recordFormat.position.range.line, 0, recordFormat.position.range.line, 0), 146 | Range.create(recordFormat.position.range.line, 0, recordFormat.position.range.line, 0) 147 | ); 148 | 149 | recordFormatDef.children = recordFormat.subItems 150 | .filter(subitem => subitem.position && subitem.position.path === currentPath) 151 | .map(subitem => DocumentSymbol.create( 152 | subitem.name, 153 | prettyKeywords(subitem.keyword), 154 | SymbolKind.Property, 155 | Range.create(subitem.position.range.line, 0, subitem.position.range.line, 0), 156 | Range.create(subitem.position.range.line, 0, subitem.position.range.line, 0) 157 | )); 158 | 159 | if (fileDef.children) { 160 | fileDef.children.push(recordFormatDef); 161 | } 162 | }); 163 | 164 | currentScopeDefs.push(fileDef); 165 | }); 166 | 167 | scope.structs 168 | .filter(struct => struct.position && struct.position.path === currentPath && validRange(struct)) 169 | .forEach(struct => { 170 | currentScopeDefs.push(expandStruct(struct)); 171 | }); 172 | 173 | return currentScopeDefs; 174 | }; 175 | 176 | if (doc) { 177 | symbols.push( 178 | ...getScopeVars(doc), 179 | ); 180 | } 181 | } 182 | 183 | return symbols; 184 | } -------------------------------------------------------------------------------- /extension/server/src/providers/hover.ts: -------------------------------------------------------------------------------- 1 | import { Hover, HoverParams, MarkupKind, Range } from 'vscode-languageserver'; 2 | import { documents, getWordRangeAtPosition, parser, prettyKeywords } from '.'; 3 | import Parser from "../../../../language/parser"; 4 | import { URI } from 'vscode-uri'; 5 | import { Keywords } from '../../../../language/parserTypes'; 6 | 7 | export default async function hoverProvider(params: HoverParams): Promise { 8 | const currentPath = params.textDocument.uri; 9 | const currentLine = params.position.line; 10 | const document = documents.get(currentPath); 11 | 12 | if (document) { 13 | const doc = await parser.getDocs(currentPath, document.getText()); 14 | if (doc) { 15 | const word = getWordRangeAtPosition(document, params.position); 16 | if (!word) return; 17 | 18 | const procedure = doc.procedures.find(proc => proc.name.toUpperCase() === word.toUpperCase()); 19 | 20 | if (procedure) { 21 | let markdown = ``; 22 | let returnValue = `void` 23 | 24 | let returnKeywords: Keywords = { 25 | ...procedure.keyword, 26 | }; 27 | delete returnKeywords[`EXTPROC`]; 28 | 29 | if (Object.keys(returnKeywords).length > 0) returnValue = prettyKeywords(returnKeywords); 30 | 31 | const returnTag = procedure.tags.find(tag => tag.tag === `return`); 32 | const deprecatedTag = procedure.tags.find(tag => tag.tag === `deprecated`); 33 | 34 | // Deprecated notice 35 | if (deprecatedTag) { 36 | markdown += `**Deprecated:** ${deprecatedTag.content}\n\n`; 37 | } 38 | 39 | // Formatted code 40 | markdown += `\`\`\`vb\n${procedure.name}(`; 41 | 42 | if (procedure.subItems.length > 0) { 43 | markdown += `\n ${procedure.subItems.map(parm => `${parm.name}: ${prettyKeywords(parm.keyword)}`).join(`,\n `)}\n`; 44 | } 45 | 46 | markdown += `): ${returnValue}\n\`\`\` \n`; 47 | 48 | const titleTag = procedure.tags.find(tag => tag.tag === `title`); 49 | const descriptionTag = procedure.tags.find(tag => tag.tag === `description`); 50 | 51 | const header = [titleTag ? titleTag.content : undefined, descriptionTag ? descriptionTag.content : undefined].filter(x => x).join(` — `); 52 | 53 | // Header 54 | markdown += `${header}\n\n`; 55 | 56 | // Params 57 | markdown += procedure.subItems.map((parm) => `*@param* \`${parm.name.replace(new RegExp(`\\*`, `g`), `\\*`)}\` ${parm.tags.find(t => t.tag === `description`)?.content || ``}`).join(`\n\n`); 58 | 59 | // Return value 60 | if (returnTag) { 61 | markdown += `\n\n*@returns* ${returnTag.content}`; 62 | } 63 | 64 | if (procedure.position && currentPath !== procedure.position.path) { 65 | markdown += `\n\n*@file* \`${procedure.position.path}:${procedure.position.range.line+1}\``; 66 | } 67 | 68 | return { 69 | contents: { 70 | kind: MarkupKind.Markdown, 71 | value: markdown 72 | } 73 | }; 74 | } else { 75 | // If they're inside of a procedure, let's get the stuff from there too 76 | const currentProcedure = doc.procedures.find(proc => proc.range.start && proc.range.end && currentLine >= proc.range.start && currentLine <= proc.range.end); 77 | let theVariable; 78 | 79 | if (currentProcedure && currentProcedure.scope) { 80 | theVariable = currentProcedure.scope.find(word); 81 | } 82 | 83 | if (!theVariable) { 84 | theVariable = doc.find(word); 85 | } 86 | 87 | if (theVariable) { 88 | // Variable definition found 89 | const refs = theVariable.references.length; 90 | 91 | let markdown = `\`${theVariable.name} ${prettyKeywords(theVariable.keyword)}\` (${refs} reference${refs === 1 ? `` : `s`})`; 92 | 93 | if (theVariable.position && currentPath !== theVariable.position.path) { 94 | markdown += `\n\n*@file* \`${theVariable.position.path}:${theVariable.position.range.line+1}\``; 95 | } 96 | 97 | return { 98 | contents: { 99 | kind: MarkupKind.Markdown, 100 | value: markdown 101 | } 102 | }; 103 | 104 | } else { 105 | const lineContent = document.getText(Range.create(currentLine, 0, currentLine, 200)); 106 | 107 | const includeDirective = Parser.getIncludeFromDirective(lineContent); 108 | 109 | if (includeDirective && parser.includeFileFetch) { 110 | const include = await parser.includeFileFetch(currentPath, includeDirective); 111 | let displayName = includeDirective; 112 | 113 | if (include.found && include.uri) { 114 | const foundUri = URI.parse(include.uri); 115 | 116 | if (foundUri.scheme === `member`) { 117 | const lastIndex = foundUri.path.lastIndexOf(`.`); 118 | if (lastIndex >= 0) { 119 | displayName = foundUri.path.substring(0, lastIndex); 120 | } else { 121 | displayName = foundUri.path; 122 | } 123 | 124 | if (displayName.startsWith(`/`)) displayName = displayName.substring(1); 125 | 126 | } else { 127 | displayName = foundUri.path; 128 | } 129 | } 130 | 131 | return { 132 | contents: { 133 | kind: MarkupKind.Markdown, 134 | value: (include.found ? `\`${displayName}\`` : includeDirective) + ` (${include.found ? `found` : `not found`})` 135 | } 136 | }; 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | return; 144 | } -------------------------------------------------------------------------------- /extension/server/src/providers/implementation.ts: -------------------------------------------------------------------------------- 1 | import { Definition, ImplementationParams, Location, Range } from 'vscode-languageserver'; 2 | import { documents, parser, getWordRangeAtPosition } from '.'; 3 | 4 | import * as Project from './project'; 5 | 6 | export default async function implementationProvider(params: ImplementationParams): Promise { 7 | const currentPath = params.textDocument.uri; 8 | const document = documents.get(currentPath); 9 | 10 | // We only handle local implementations here. 11 | if (document) { 12 | const word = getWordRangeAtPosition(document, params.position); 13 | if (word) { 14 | const upperName = word.toUpperCase(); 15 | 16 | if (Project.isEnabled) { 17 | // If project mode is enabled, then we start by looking through the local cache 18 | const parsedFiles = Object.keys(parser.parsedCache); 19 | 20 | for (const uri of parsedFiles) { 21 | const cache = parser.getParsedCache(uri); 22 | for (const proc of cache.procedures) { 23 | const keyword = proc.keyword[`EXPORT`]; 24 | if (keyword) { 25 | if (proc.name.toUpperCase() === upperName) { 26 | return Location.create( 27 | proc.position.path, 28 | Range.create( 29 | proc.position.range.line, 30 | 0, 31 | proc.position.range.line, 32 | 0 33 | ) 34 | ); 35 | } 36 | } 37 | } 38 | }; 39 | } 40 | } 41 | } 42 | 43 | return; 44 | } -------------------------------------------------------------------------------- /extension/server/src/providers/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | TextDocuments, 4 | Position, 5 | Range, 6 | _Connection 7 | } from 'vscode-languageserver/node'; 8 | 9 | import { 10 | TextDocument 11 | } from 'vscode-languageserver-textdocument'; 12 | import Parser from '../../../../language/parser'; 13 | 14 | type Keywords = { [key: string]: string | boolean }; 15 | 16 | // Create a simple text document manager. 17 | export const documents: TextDocuments = new TextDocuments(TextDocument); 18 | 19 | export function findFile(fileString: string, scheme = ``) { 20 | return documents.keys().find(fileUri => fileUri.includes(fileString) && fileUri.startsWith(`${scheme}:`)); 21 | } 22 | 23 | export const parser = new Parser(); 24 | 25 | const wordMatch = /[\w\#\$@]/; 26 | 27 | export function getWordRangeAtPosition(document: TextDocument, position: Position): string | undefined { 28 | const lines = document.getText().split(`\n`); // Safe to assume \n because \r is then at end of lines 29 | const line = Math.min(lines.length - 1, Math.max(0, position.line)); 30 | const lineText = lines[line]; 31 | const character = Math.min(lineText.length - 1, Math.max(0, position.character)); 32 | 33 | let startChar = character; 34 | while (startChar > 0 && wordMatch.test(lineText.charAt(startChar - 1))) { 35 | startChar -= 1; 36 | } 37 | 38 | let endChar = character; 39 | while (endChar < lineText.length && wordMatch.test(lineText.charAt(endChar + 1))) { 40 | endChar += 1; 41 | } 42 | 43 | if (startChar === endChar) 44 | return undefined; 45 | else 46 | return document.getText(Range.create(line, Math.max(0, startChar), line, endChar + 1)).replace(/(\r\n|\n|\r)/gm, ""); 47 | } 48 | 49 | const filteredKeywords = ['QUALIFIED', 'EXPORT']; // TODO: Any other filtered keywords? 50 | 51 | export function prettyKeywords(keywords: Keywords, filter: boolean = false): string { 52 | return Object.keys(keywords).map(key => { 53 | if ((!filter || !filteredKeywords.includes(key)) && keywords[key]) { 54 | if (typeof keywords[key] === `boolean`) { 55 | return key.toLowerCase(); 56 | } 57 | 58 | return `${key.toLowerCase()}(${keywords[key]})`; 59 | } else { 60 | return undefined; 61 | } 62 | }).filter(k => k).join(` `); 63 | } -------------------------------------------------------------------------------- /extension/server/src/providers/language.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from 'vscode-languageserver-textdocument'; 2 | import { Range } from "vscode-languageserver"; 3 | import Cache from "../../../../language/models/cache"; 4 | 5 | export function caseInsensitiveReplaceAll(text: string, search: string, replace: string) { 6 | return text.replace(new RegExp(search, `gi`), replace); 7 | } 8 | 9 | export function createExtract(document: TextDocument, userRange: Range, docs: Cache) { 10 | const range = Range.create(userRange.start.line, 0, userRange.end.line, 1000); 11 | const references = docs.referencesInRange(document.uri, {start: document.offsetAt(range.start), end: document.offsetAt(range.end)}); 12 | const validRefs = references.filter(ref => [`struct`, `subitem`, `variable`].includes(ref.dec.type)); 13 | 14 | const nameDiffSize = 1; // Always once since we only add 'p' at the start 15 | const newParamNames = validRefs.map(ref => `p${ref.dec.name}`); 16 | let newBody = document.getText(range); 17 | 18 | const rangeStartOffset = document.offsetAt(range.start); 19 | 20 | // Fix the found offset lengths to be relative to the new procedure 21 | for (let i = validRefs.length - 1; i >= 0; i--) { 22 | for (let y = validRefs[i].refs.length - 1; y >= 0; y--) { 23 | validRefs[i].refs[y] = { 24 | start: validRefs[i].refs[y].start - rangeStartOffset, 25 | end: validRefs[i].refs[y].end - rangeStartOffset 26 | }; 27 | } 28 | } 29 | 30 | // Then let's fix the references to use the new names 31 | for (let i = validRefs.length - 1; i >= 0; i--) { 32 | for (let y = validRefs[i].refs.length - 1; y >= 0; y--) { 33 | const ref = validRefs[i].refs[y]; 34 | 35 | newBody = newBody.slice(0, ref.start) + newParamNames[i] + newBody.slice(ref.end); 36 | ref.end += nameDiffSize; 37 | 38 | // Then we need to update the offset of the next references 39 | for (let z = i - 1; z >= 0; z--) { 40 | for (let x = validRefs[z].refs.length - 1; x >= 0; x--) { 41 | if (validRefs[z].refs[x].start > ref.end) { 42 | validRefs[z].refs[x] = { 43 | start: validRefs[z].refs[x].start + nameDiffSize, 44 | end: validRefs[z].refs[x].end + nameDiffSize 45 | }; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | return { 53 | newBody, 54 | newParamNames, 55 | references: validRefs, 56 | range 57 | } 58 | } -------------------------------------------------------------------------------- /extension/server/src/providers/linter/codeActions.ts: -------------------------------------------------------------------------------- 1 | import { Range } from 'vscode-languageserver'; 2 | import { getActions, refreshLinterDiagnostics } from '.'; 3 | import { TextDocument } from 'vscode-languageserver-textdocument'; 4 | import Cache from '../../../../../language/models/cache'; 5 | 6 | /** 7 | * Get the CodeActions for a given document and range. 8 | */ 9 | export async function getLinterCodeActions(docs: Cache, document: TextDocument, range: Range) { 10 | const detail = await refreshLinterDiagnostics(document, docs, false); 11 | if (detail) { 12 | const fixErrors = detail.errors.filter(error => 13 | range.start.line >= document.positionAt(error.offset.start!).line && 14 | range.end.line <= document.positionAt(error.offset.end!).line 15 | ); 16 | 17 | if (fixErrors.length > 0) { 18 | return getActions(document, fixErrors); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /extension/server/src/providers/linter/documentFormatting.ts: -------------------------------------------------------------------------------- 1 | 2 | import { DocumentFormattingParams, ProgressToken, Range, TextEdit, WorkDoneProgress } from 'vscode-languageserver'; 3 | import { calculateOffset, getActions, getLintOptions } from '.'; 4 | import { documents, parser } from '..'; 5 | import Linter from '../../../../../language/linter'; 6 | 7 | export default async function documentFormattingProvider(params: DocumentFormattingParams): Promise { 8 | const uri = params.textDocument.uri; 9 | const document = documents.get(uri); 10 | 11 | if (document) { 12 | const isFree = (document.getText(Range.create(0, 0, 0, 6)).toUpperCase() === `**FREE`); 13 | if (isFree) { 14 | let options = (await getLintOptions(document.uri)); 15 | let docs = await parser.getDocs(document.uri); 16 | 17 | // If no lint config is provided, then set a default for indent 18 | if (Object.keys(options).length === 0) { 19 | options.indent = params.options.tabSize; 20 | } 21 | 22 | if (docs) { 23 | // Need to fetch the docs again incase comments were added 24 | // as part of RequiresProcedureDescription 25 | docs = await parser.getDocs(document.uri, document.getText(), { 26 | ignoreCache: true, 27 | withIncludes: true 28 | }); 29 | 30 | // Next up, let's fix all the other things! 31 | const { errors } = Linter.getErrors({ 32 | uri: document.uri, 33 | content: document.getText() 34 | }, options, docs); 35 | 36 | 37 | const actions = getActions( 38 | document, 39 | errors.filter(error => error.type !== `RequiresProcedureDescription`) 40 | ); 41 | 42 | let linesChanged: number[] = []; 43 | let skippedChanges: number = 0; 44 | 45 | let fixes: TextEdit[] = []; 46 | 47 | actions 48 | .filter(action => action.edit) 49 | .forEach(action => { 50 | if (action.edit && action.edit.changes) { 51 | const uris = action.edit.changes; 52 | const suggestedEdits = uris[document.uri]; 53 | const editedLineBefore = suggestedEdits[0] ? linesChanged.includes(suggestedEdits[0].range.start.line) : false; 54 | if (!editedLineBefore) { 55 | suggestedEdits.forEach(edit => { 56 | const changedLine = edit.range.start.line; 57 | fixes.push(edit); 58 | 59 | if (!linesChanged.includes(changedLine)) { 60 | linesChanged.push(changedLine); 61 | } 62 | }); 63 | } else { 64 | skippedChanges += 1; 65 | } 66 | } 67 | }); 68 | 69 | 70 | // First we do all the indentation fixes. 71 | const { indentErrors } = Linter.getErrors({ 72 | uri: document.uri, 73 | content: document.getText() 74 | }, options, docs); 75 | 76 | const indentFixes = indentErrors.map(error => { 77 | const range = Range.create(error.line, 0, error.line, error.currentIndent); 78 | return TextEdit.replace(range, ``.padEnd(error.expectedIndent, ` `)); 79 | }); 80 | 81 | return [...fixes, ...indentFixes]; 82 | } 83 | } 84 | } 85 | return []; 86 | } -------------------------------------------------------------------------------- /extension/server/src/providers/linter/skipRules.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, CompletionParams, InsertTextFormat, Position, Range } from 'vscode-languageserver'; 2 | 3 | const skipAll = CompletionItem.create(`@rpglint-skip`); 4 | skipAll.kind = CompletionItemKind.Snippet; 5 | skipAll.detail = `The next line will skip rules and indentation checking.`; 6 | 7 | const skipIndent = CompletionItem.create(`@rpglint-skip-indent`); 8 | skipIndent.kind = CompletionItemKind.Snippet; 9 | skipIndent.detail = `The next line will skip indentation checking.`; 10 | 11 | const skipRules = CompletionItem.create(`@rpglint-skip-rules`); 12 | skipRules.kind = CompletionItemKind.Snippet; 13 | skipRules.detail = `The next line will skip rules checking.`; 14 | 15 | export default [skipAll, skipIndent, skipRules]; -------------------------------------------------------------------------------- /extension/server/src/providers/project/exportInterfaces.ts: -------------------------------------------------------------------------------- 1 | import path = require('path'); 2 | import { APIInterface } from '../apis'; 3 | import { isEnabled } from '.'; 4 | import { parser, prettyKeywords } from '..'; 5 | 6 | export function getInterfaces(): APIInterface[] { 7 | let interfaces: APIInterface[] = []; 8 | 9 | if (isEnabled) { 10 | const parsedFiles = Object.keys(parser.parsedCache); 11 | 12 | parsedFiles.forEach(uri => { 13 | const uriDetail = path.parse(uri); 14 | const basename = uriDetail.base; 15 | const nameDetail = path.parse(basename); 16 | let objectName = path.basename(nameDetail.name).toUpperCase(); 17 | 18 | if (objectName.endsWith(`.PGM`)) objectName = objectName.substring(0, objectName.length-4); 19 | 20 | if (basename.toLowerCase().endsWith(`.rpgleinc`) === false) { 21 | const cache = parser.getParsedCache(uri); 22 | 23 | if (cache) { 24 | const entryPoint = cache.keyword[`MAIN`]; 25 | 26 | if (entryPoint) { 27 | // Okay, there MAIN keyword exists. It's a program for sure. 28 | if (typeof entryPoint === "string") { 29 | const entryFunction = cache.procedures.find(proc => proc.name.toUpperCase() === entryPoint.toUpperCase()); 30 | 31 | if (entryFunction) { 32 | 33 | // We assume the file name is the name of the object 34 | entryFunction.keyword[`EXTPGM`] = `'${objectName}'`; 35 | 36 | const prototype = [ 37 | `dcl-pr ${entryFunction.name} ${prettyKeywords(entryFunction.keyword)};`, 38 | ...entryFunction.subItems.map(subItem => 39 | ` ${subItem.name} ${prettyKeywords(subItem.keyword)};` 40 | ), 41 | `end-pr;` 42 | ]; 43 | 44 | interfaces.push({ 45 | name: objectName, 46 | insertText: `${objectName}(${entryFunction.subItems.map((parm, index) => `\${${index + 1}:${parm.name}}`).join(`:`)})`, 47 | detail: objectName, 48 | description: entryFunction.description, 49 | type: `function`, 50 | prototype 51 | }); 52 | } 53 | } 54 | 55 | } else 56 | if (cache.keyword[`NOMAIN`]) { 57 | // This might mean it is a module. Look for EXPORTs 58 | cache.procedures.forEach(proc => { 59 | if (proc.keyword[`EXPORT`]) { 60 | 61 | proc.keyword[`EXTPROC`] = `'${proc.name.toUpperCase()}'`; 62 | 63 | const prototype = [ 64 | `dcl-pr ${proc.name} ${prettyKeywords(proc.keyword)};`, 65 | ...proc.subItems.map(subItem => 66 | ` ${subItem.name} ${prettyKeywords(subItem.keyword)};` 67 | ), 68 | `end-pr;` 69 | ]; 70 | 71 | interfaces.push({ 72 | name: proc.name, 73 | insertText: `${proc.name}(${proc.subItems.map((parm, index) => `\${${index + 1}:${parm.name}}`).join(`:`)})`, 74 | detail: proc.name, 75 | description: proc.description, 76 | type: `function`, 77 | prototype 78 | }); 79 | } 80 | }); 81 | } 82 | } 83 | } 84 | }) 85 | } 86 | 87 | return interfaces; 88 | } -------------------------------------------------------------------------------- /extension/server/src/providers/project/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs/promises"; 2 | 3 | import { connection, getWorkspaceFolder, PossibleInclude, watchedFilesChangeEvent } from '../../connection'; 4 | import { documents, parser } from '..'; 5 | import Linter from '../../../../../language/linter'; 6 | import { DidChangeWatchedFilesParams, FileChangeType } from 'vscode-languageserver'; 7 | import { URI } from 'vscode-uri'; 8 | 9 | import { glob } from "glob"; 10 | import * as path from "path"; 11 | import { TextDocument } from 'vscode-languageserver-textdocument'; 12 | const projectFilesGlob = `**/*.{rpgle,sqlrpgle,rpgleinc}`; 13 | 14 | export let includePath: {[workspaceUri: string]: string[]} = {}; 15 | 16 | export let isEnabled = false; 17 | /** 18 | * Assumes client has workspace 19 | */ 20 | export async function initialise() { 21 | isEnabled = true; 22 | 23 | loadWorkspace(); 24 | 25 | watchedFilesChangeEvent.push((params: DidChangeWatchedFilesParams) => { 26 | params.changes.forEach(fileEvent => { 27 | const pathData = path.parse(fileEvent.uri); 28 | const ext = pathData.ext.toLowerCase(); 29 | 30 | switch (fileEvent.type) { 31 | case FileChangeType.Created: 32 | case FileChangeType.Changed: 33 | switch (ext) { 34 | case `.rpgleinc`: 35 | case `.rpgleh`: 36 | loadLocalFile(fileEvent.uri); 37 | 38 | currentIncludes = []; 39 | break; 40 | case `.json`: 41 | if (pathData.base === `iproj.json`) { 42 | updateIProj(fileEvent.uri); 43 | } 44 | break; 45 | } 46 | break; 47 | 48 | default: 49 | parser.clearParsedCache(fileEvent.uri); 50 | break; 51 | } 52 | }) 53 | }); 54 | } 55 | 56 | async function loadWorkspace() { 57 | const workspaces = await connection.workspace.getWorkspaceFolders(); 58 | 59 | if (workspaces) { 60 | let uris: string[] = []; 61 | 62 | workspaces.forEach((workspaceUri => { 63 | const folderPath = URI.parse(workspaceUri.uri).fsPath; 64 | 65 | console.log(`Starting search of: ${folderPath}`); 66 | const files = glob.sync(projectFilesGlob, { 67 | cwd: folderPath, 68 | absolute: true, 69 | nocase: true, 70 | }); 71 | 72 | console.log(`Found RPGLE files: ${files.length}`); 73 | 74 | uris.push(...files.map(file => URI.from({ 75 | scheme: `file`, 76 | path: file 77 | }).toString())); 78 | 79 | const iprojFiles = glob.sync(`**/iproj.json`, { 80 | cwd: folderPath, 81 | absolute: true, 82 | nocase: true, 83 | }); 84 | 85 | if (iprojFiles.length > 0) { 86 | const base = iprojFiles[0]; 87 | const iprojUri = URI.from({ 88 | scheme: `file`, 89 | path: base 90 | }).toString(); 91 | 92 | updateIProj(iprojUri); 93 | } 94 | })); 95 | 96 | if (uris.length < 1000) { 97 | for (const uri of uris) { 98 | await loadLocalFile(uri); 99 | } 100 | } else { 101 | console.log(`Disabling project mode for large project.`); 102 | isEnabled = false; 103 | } 104 | } 105 | } 106 | 107 | async function updateIProj(uri: string) { 108 | const workspace = await getWorkspaceFolder(uri); 109 | if (workspace) { 110 | const document = await getTextDoc(uri); 111 | const content = document?.getText(); 112 | 113 | if (content) { 114 | try { 115 | const asJson = JSON.parse(content); 116 | if (asJson.includePath && Array.isArray(asJson.includePath)) { 117 | const includeArray: any[] = asJson.includePath; 118 | 119 | const invalid = includeArray.some(v => typeof v !== `string`); 120 | 121 | if (!invalid) { 122 | includePath[workspace.uri] = asJson.includePath; 123 | } else { 124 | console.log(`${uri} -> 'includePath' is not a valid string array.`); 125 | } 126 | } 127 | 128 | } catch (e) { 129 | console.log(`Unable to parse JSON in ${uri}.`); 130 | } 131 | } 132 | } 133 | } 134 | 135 | async function loadLocalFile(uri: string) { 136 | const document = await getTextDoc(uri); 137 | 138 | if (document) { 139 | const content = document?.getText(); 140 | await parser.getDocs(uri, content); 141 | } 142 | } 143 | 144 | export async function getTextDoc(uri: string): Promise { 145 | let document = documents.get(uri); 146 | 147 | if (document) { 148 | return document; 149 | } 150 | 151 | try { 152 | const content = await fs.readFile(URI.parse(uri).fsPath, { encoding: `utf-8` }); 153 | return TextDocument.create(uri, `rpgle`, 1, content); 154 | } catch (e) {} 155 | 156 | return; 157 | } 158 | 159 | let currentIncludes: PossibleInclude[] = []; 160 | 161 | export async function getIncludes(baseUri: string) { 162 | const workspace = await getWorkspaceFolder(baseUri); 163 | if (workspace) { 164 | const workspacePath = URI.parse(workspace?.uri).path; 165 | 166 | if (!currentIncludes || currentIncludes && currentIncludes.length === 0) { 167 | currentIncludes = glob.sync(`**/*.{rpgleinc,rpgleh}`, { 168 | cwd: workspacePath, 169 | nocase: true, 170 | absolute: true 171 | }).map(truePath => ({ 172 | uri: URI.file(truePath).toString(), 173 | relative: path.relative(workspacePath, truePath) 174 | })) 175 | } 176 | } 177 | 178 | return currentIncludes; 179 | } 180 | -------------------------------------------------------------------------------- /extension/server/src/providers/project/references.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Location, Range } from 'vscode-languageserver'; 3 | import Declaration from '../../../../../language/models/declaration'; 4 | import { calculateOffset } from '../linter'; 5 | import { documents, parser } from '..'; 6 | import { getTextDoc, isEnabled } from '.'; 7 | 8 | export async function findAllProjectReferences(def: Declaration): Promise { 9 | let locations: Location[] = []; 10 | 11 | if (isEnabled) { 12 | const parsedFiles = Object.keys(parser.parsedCache); 13 | 14 | if (def.keyword[`EXPORT`]) { 15 | // If we are looking for references to an export function 16 | // scan entire project for `EXTPROC` definitions that point to this 17 | const upperName = def.name.toUpperCase(); 18 | 19 | for (const keyPath of parsedFiles) { 20 | const document = documents.get(keyPath); 21 | 22 | if (document) { 23 | const cache = parser.getParsedCache(keyPath); 24 | 25 | cache.procedures.forEach(proc => { 26 | let addReference = false; 27 | const keyword = proc.keyword[`EXTPROC`]; 28 | if (keyword) { 29 | if (keyword === true) { 30 | if (proc.name.toUpperCase() === upperName) { 31 | 32 | addReference = true; 33 | } 34 | } else 35 | if (trimQuotes(keyword).toUpperCase() === upperName) { 36 | addReference = true; 37 | } 38 | } else 39 | 40 | // Also turns out, any `DCL-PR` without any keywords is `EXTPROC` by default. 41 | if (!proc.keyword[`EXPORT`] && proc.name.toUpperCase() === upperName) { 42 | addReference = true; 43 | } 44 | 45 | if (addReference) { 46 | // Don't add duplicates 47 | if (!locations.some(loc => loc.uri === keyPath)) { 48 | locations.push( 49 | // Then we push the references. Empty for non-**free 50 | ...proc.references.map(ref => Location.create( 51 | keyPath, 52 | calculateOffset(document, ref) 53 | )) 54 | ); 55 | } 56 | } 57 | 58 | }) 59 | } 60 | } 61 | 62 | } 63 | } 64 | 65 | return locations; 66 | } 67 | 68 | function trimQuotes(input: string) { 69 | if (input[0] === `'`) input = input.substring(1); 70 | if (input[input.length - 1] === `'`) input = input.substring(0, input.length - 1); 71 | return input; 72 | } -------------------------------------------------------------------------------- /extension/server/src/providers/project/workspaceSymbol.ts: -------------------------------------------------------------------------------- 1 | import { Range, SymbolKind, WorkspaceSymbol, WorkspaceSymbolParams } from 'vscode-languageserver'; 2 | import { parser } from '..'; 3 | import * as Project from '.'; 4 | import path = require('path'); 5 | 6 | export default function workspaceSymbolProvider(params: WorkspaceSymbolParams): WorkspaceSymbol[] | undefined { 7 | console.log(params.query); 8 | if (Project.isEnabled) { 9 | const parsedFiles = Object.keys(parser.parsedCache); 10 | let symbols: WorkspaceSymbol[] = []; 11 | 12 | parsedFiles.forEach(uri => { 13 | const basename = path.basename(uri); 14 | const baseNameLower = basename.toLowerCase(); 15 | 16 | if (baseNameLower.endsWith(`.rpgleinc`)) { 17 | symbols.push( 18 | WorkspaceSymbol.create( 19 | basename, 20 | SymbolKind.File, 21 | uri, 22 | Range.create( 23 | 0, 24 | 0, 25 | 0, 26 | 0 27 | ) 28 | ) 29 | ) 30 | } else { 31 | const cache = parser.getParsedCache(uri); 32 | 33 | if (cache) { 34 | if (cache.keyword[`MAIN`]) { 35 | symbols.push( 36 | WorkspaceSymbol.create( 37 | basename, 38 | SymbolKind.Method, 39 | uri, 40 | Range.create( 41 | 0, 42 | 0, 43 | 0, 44 | 0 45 | ) 46 | ) 47 | ); 48 | } else 49 | if (cache.keyword[`NOMAIN`]) { 50 | cache.procedures.forEach(proc => { 51 | if (proc.keyword[`EXPORT`]) { 52 | symbols.push( 53 | WorkspaceSymbol.create( 54 | proc.name, 55 | SymbolKind.Function, 56 | uri, 57 | Range.create( 58 | proc.position.range.line, 59 | 0, 60 | proc.position.range.line, 61 | 0 62 | ) 63 | ) 64 | ); 65 | } 66 | }); 67 | } 68 | } 69 | } 70 | }); 71 | 72 | return symbols; 73 | } 74 | 75 | return; 76 | } -------------------------------------------------------------------------------- /extension/server/src/providers/reference.ts: -------------------------------------------------------------------------------- 1 | import { Location, Range, ReferenceParams } from 'vscode-languageserver'; 2 | import { documents, getWordRangeAtPosition, parser } from '.'; 3 | import Linter from '../../../../language/linter'; 4 | import { calculateOffset } from './linter'; 5 | 6 | import * as Project from "./project"; 7 | import { findAllProjectReferences as getAllProcedureReferences } from './project/references'; 8 | import Cache from '../../../../language/models/cache'; 9 | 10 | export async function referenceProvider(params: ReferenceParams): Promise { 11 | const uri = params.textDocument.uri; 12 | const position = params.position; 13 | const document = documents.get(uri); 14 | 15 | if (document) { 16 | const uri = params.textDocument.uri; 17 | const currentPos = params.position; 18 | const document = documents.get(uri); 19 | 20 | if (document) { 21 | const doc = await parser.getDocs(uri, document.getText()); 22 | 23 | if (doc) { 24 | const def = Cache.referenceByOffset(uri, doc, document.offsetAt(currentPos)); 25 | 26 | if (def) { 27 | let locations: Location[] = []; 28 | // TODO: Currently disabled due to reference bug in getAllProcedureReferences 29 | // if (Project.isEnabled) { 30 | // const procRefs = await getAllProcedureReferences(def); 31 | // locations.push(...procRefs); 32 | // } 33 | 34 | for (const ref of def.references) { 35 | let refDoc = documents.get(ref.uri); 36 | if (refDoc) { 37 | locations.push(Location.create( 38 | ref.uri, 39 | calculateOffset(refDoc, ref) 40 | )); 41 | } 42 | } 43 | 44 | return locations; 45 | } 46 | } 47 | } 48 | } 49 | 50 | return; 51 | } -------------------------------------------------------------------------------- /extension/server/src/providers/rename.ts: -------------------------------------------------------------------------------- 1 | 2 | import { documents, getWordRangeAtPosition, parser } from '.'; 3 | import { PrepareRenameParams, Range, RenameParams, TextEdit, WorkspaceEdit } from "vscode-languageserver"; 4 | import Linter from '../../../../language/linter'; 5 | import Cache from '../../../../language/models/cache'; 6 | import Declaration from '../../../../language/models/declaration'; 7 | 8 | export async function renamePrepareProvider(params: PrepareRenameParams): Promise { 9 | const uri = params.textDocument.uri; 10 | const currentPos = params.position; 11 | const document = documents.get(uri); 12 | 13 | if (document) { 14 | const doc = await parser.getDocs(uri, document.getText()); 15 | 16 | if (doc) { 17 | const def = Cache.referenceByOffset(uri, doc, document.offsetAt(currentPos)); 18 | 19 | if (def) { 20 | const uniqueUris = def.references.map(ref => ref.uri).filter((value, index, self) => self.indexOf(value) === index); 21 | 22 | if (uniqueUris.length > 1) { 23 | return; 24 | } 25 | 26 | const currentSelectedRef = def?.references.find(r => document.positionAt(r.offset.start).line === currentPos.line); 27 | 28 | if (currentSelectedRef) { 29 | return Range.create( 30 | document.positionAt(currentSelectedRef.offset.start), 31 | document.positionAt(currentSelectedRef.offset.end) 32 | ) 33 | } 34 | } 35 | } 36 | } 37 | 38 | return; 39 | } 40 | 41 | export async function renameRequestProvider(params: RenameParams): Promise { 42 | const uri = params.textDocument.uri; 43 | const currentPos = params.position; 44 | const document = documents.get(uri); 45 | 46 | if (document) { 47 | const isFree = (document.getText(Range.create(0, 0, 0, 6)).toUpperCase() === `**FREE`); 48 | 49 | const doc = await parser.getDocs(uri, document.getText()); 50 | 51 | if (doc) { 52 | const def = Cache.referenceByOffset(uri, doc, document.offsetAt(currentPos)); 53 | 54 | if (def) { 55 | let edits: {[uri: string]: TextEdit[]} = {}; 56 | 57 | const uniqueUris = def.references.map(ref => ref.uri).filter((value, index, self) => self.indexOf(value) === index); 58 | 59 | for (const uri of uniqueUris) { 60 | edits[uri] = def.references.filter(ref => ref.uri === uri).map(ref => ({ 61 | newText: params.newName, 62 | range: Range.create( 63 | document.positionAt(ref.offset.start), 64 | document.positionAt(ref.offset.end) 65 | ) 66 | })); 67 | } 68 | 69 | const workspaceEdit: WorkspaceEdit = { 70 | changes: edits 71 | } 72 | 73 | return workspaceEdit; 74 | } 75 | } 76 | } 77 | 78 | return; 79 | } -------------------------------------------------------------------------------- /extension/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es2020", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "out", 9 | "rootDir": "./../..", 10 | "lib": [ "es2020" ], 11 | "allowJs": true, 12 | "composite": true 13 | }, 14 | "include": [ 15 | "src", 16 | "../../language" 17 | ], 18 | "exclude": [ 19 | "node_modules", ".vscode-test", 20 | ] 21 | } -------------------------------------------------------------------------------- /extension/server/webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | // @ts-nocheck 7 | 8 | 'use strict'; 9 | 10 | const withDefaults = require(`../../shared.webpack.config`); 11 | const path = require(`path`); 12 | const webpack = require(`webpack`); 13 | 14 | module.exports = withDefaults({ 15 | context: path.join(__dirname), 16 | entry: { 17 | extension: `./src/server.ts`, 18 | }, 19 | output: { 20 | filename: `server.js`, 21 | path: path.join(__dirname, `..`, `..`, `out`) 22 | }, 23 | }); -------------------------------------------------------------------------------- /extension/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "noUnusedParameters": true, 5 | "strict": true, 6 | "allowJs": true 7 | } 8 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "checkJs": true, /* Typecheck .js files. */ 6 | "lib": [ 7 | "es6" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /language/document.ts: -------------------------------------------------------------------------------- 1 | // import Statement from "./statement"; 2 | import Statement from "./statement"; 3 | import { tokenise } from "./tokens"; 4 | import { IRange, Token } from "./types"; 5 | 6 | export enum StatementType { 7 | Normal = "Normal", 8 | Directive = "Directive", 9 | } 10 | 11 | export default class Document { 12 | statements: Statement[]; 13 | constructor(content: string) { 14 | this.statements = []; 15 | 16 | this.parseStatements(tokenise(content)); 17 | } 18 | 19 | private addStatement(indent: number, tokens: Token[]) { 20 | if (tokens.length > 0) { 21 | this.statements.push(new Statement( 22 | tokens, 23 | { 24 | line: tokens[0].range.line, 25 | start: tokens[0].range.start, 26 | end: tokens[tokens.length - 1].range.end, 27 | }, 28 | indent 29 | )); 30 | } 31 | } 32 | 33 | private parseStatements(tokens: Token[]) { 34 | let currentStatementType: StatementType = StatementType.Normal; 35 | let newLineToken: Token; 36 | 37 | let lastLine = { 38 | need: true, 39 | index: -1 40 | }; 41 | 42 | let statementStart = { 43 | index: 0, 44 | }; 45 | 46 | for (let i = 0; i < tokens.length; i++) { 47 | switch (tokens[i].type) { 48 | case `semicolon`: 49 | const statementTokens = Statement.trimTokens(tokens.slice(statementStart.index, i)); 50 | newLineToken = tokens[lastLine.index]; 51 | const indent = statementTokens[0] && newLineToken ? (statementTokens[0].range.start - newLineToken.range.end) : 0; 52 | lastLine.need = true; 53 | 54 | this.addStatement(indent, statementTokens); 55 | 56 | statementStart = { 57 | index: i+1 58 | }; 59 | break; 60 | 61 | case `format`: 62 | const formatTokens = Statement.trimTokens(tokens.slice(statementStart.index, i+1)); 63 | if (formatTokens.length === 1) { 64 | newLineToken = tokens[lastLine.index]; 65 | const indent = formatTokens[0] && newLineToken ? (formatTokens[0].range.start - newLineToken.range.end) : 0; 66 | lastLine.need = true; 67 | 68 | this.addStatement(indent, formatTokens); 69 | 70 | statementStart = { 71 | index: i+1 72 | }; 73 | } 74 | break; 75 | 76 | 77 | case `comment`: 78 | const commentToken = Statement.trimTokens(tokens.slice(statementStart.index, i+1)); 79 | if (commentToken.length === 1) { 80 | 81 | // Don't add the comment as a new statement if it proceeds another non-comment token 82 | if (tokens[i-1] && tokens[i-1].range.line !== commentToken[0].range.line) { 83 | newLineToken = tokens[lastLine.index]; 84 | const indent = commentToken[0] && newLineToken ? (commentToken[0].range.start - newLineToken.range.end) : 0; 85 | lastLine.need = true; 86 | 87 | this.addStatement(indent, commentToken); 88 | } 89 | 90 | statementStart = { 91 | index: i+1 92 | }; 93 | } 94 | break; 95 | 96 | case `directive`: 97 | const directiveTokens = Statement.trimTokens(tokens.slice(statementStart.index, i+1)); 98 | if (directiveTokens[0].type === `directive`) { 99 | currentStatementType = StatementType.Directive; 100 | } 101 | break; 102 | 103 | case `newline`: 104 | if (currentStatementType === StatementType.Directive) { 105 | const statementTokens = Statement.trimTokens(tokens.slice(statementStart.index, i)); 106 | newLineToken = tokens[lastLine.index]; 107 | const indent = statementTokens[0] && newLineToken ? (statementTokens[0].range.start - newLineToken.range.end) : 0; 108 | this.addStatement(indent, statementTokens); 109 | lastLine.need = true; 110 | 111 | statementStart = { 112 | index: i+1 113 | }; 114 | 115 | currentStatementType = StatementType.Normal; 116 | } 117 | 118 | if (lastLine.need || (tokens[i-1] && tokens[i-1].type === `newline`)) { 119 | lastLine.index = i; 120 | lastLine.need = false; 121 | } 122 | break; 123 | } 124 | } 125 | 126 | const lastStatementTokens = Statement.trimTokens(tokens.slice(statementStart.index, tokens.length)); 127 | 128 | if (lastStatementTokens.length > 0) { 129 | newLineToken = tokens[lastLine.index]; 130 | const indent = lastStatementTokens[0] ? (lastStatementTokens[0].range.start - newLineToken.range.end) : 0; 131 | this.addStatement(indent, lastStatementTokens); 132 | } 133 | } 134 | 135 | getStatementByLine(line: number) { 136 | return this.statements.find(stmt => stmt.range.line === line); 137 | } 138 | 139 | getStatementByOffset(offset: number) { 140 | return this.statements.find((statement, i) => { 141 | const end = (this.statements[i + 1] ? this.statements[i + 1].range.start : statement.range.end); 142 | return (offset >= statement.range.start && offset < end) || (i === (this.statements.length - 1) && offset >= end); 143 | }) 144 | } 145 | 146 | getTokenByOffset(offset: number): Token | undefined { 147 | const statement = this.getStatementByOffset(offset); 148 | 149 | if (statement) { 150 | return statement.getTokenByOffset(offset); 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /language/models/DataPoints.ts: -------------------------------------------------------------------------------- 1 | export class Position { 2 | line: number; 3 | character: number; 4 | constructor(line: number, character: number) { 5 | this.line = line; 6 | this.character = character; 7 | } 8 | } 9 | 10 | export class Range { 11 | constructor(public start: Position, public end: Position) {} 12 | 13 | static create(startLine: number, startChar: number, endLine: number, endChar: number) { 14 | return new this( 15 | new Position(startLine, startChar), 16 | new Position(endLine, endChar) 17 | ); 18 | } 19 | } -------------------------------------------------------------------------------- /language/models/declaration.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Keywords, Reference } from "../parserTypes"; 3 | import { IRangeWithLine } from "../types"; 4 | import Cache from "./cache"; 5 | 6 | type DeclarationType = "procedure"|"subroutine"|"file"|"struct"|"subitem"|"variable"|"constant"|"tag"; 7 | 8 | export default class Declaration { 9 | name: string = ``; 10 | keyword: Keywords = {}; 11 | tags: {tag: string, content: string}[] = []; 12 | position: {path: string, range: IRangeWithLine}; 13 | references: Reference[] = []; 14 | subItems: Declaration[] = []; 15 | readParms: boolean = false; 16 | range: {start: number|null, end: number|null} = {start: null, end: null}; 17 | scope: Cache|undefined; 18 | constructor(public type: DeclarationType) {} 19 | 20 | clone() { 21 | const clone = new Declaration(this.type); 22 | clone.name = this.name; 23 | clone.keyword = this.keyword; 24 | clone.tags = this.tags; 25 | 26 | if (this.position) { 27 | clone.position = {...this.position}; 28 | } 29 | 30 | clone.subItems = this.subItems.map(subItem => subItem.clone()); 31 | 32 | clone.range = { 33 | start: this.range.start, 34 | end: this.range.end 35 | }; 36 | 37 | //clone.references = this.references; 38 | //clone.readParms = this.readParms; 39 | //clone.scope = this.scope; 40 | return clone; 41 | } 42 | 43 | get description() { 44 | return this.tags.find(tag => tag.tag === `description`)?.content || ``; 45 | } 46 | } -------------------------------------------------------------------------------- /language/models/oneLineTriggers.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'DCL-DS': [`LIKEDS`, `LIKEREC`, `END-DS`], 3 | 'DCL-PI': [`END-PI`], 4 | 'DCL-PR': [`OVERLOAD`, `END-PR`] 5 | }; -------------------------------------------------------------------------------- /language/models/opcodes.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | `ACQ1`, 3 | `BEGSR`, 4 | `CALLP`, 5 | `CHAIN`, 6 | `CLEAR`, 7 | `CLOSE`, 8 | `COMMIT`, 9 | `DATA`, 10 | `DATA`, 11 | `DEALLOC`, 12 | `DELETE`, 13 | `DOU`, 14 | `DOW`, 15 | `DSPLY`, 16 | `DUMP1`, 17 | `ELSE`, 18 | `ELSEIF`, 19 | `ENDDO`, 20 | `ENDFOR`, 21 | `ENDIF`, 22 | `ENDMON`, 23 | `ENDSL`, 24 | `ENDSR`, 25 | `EVAL`, 26 | `EVALR`, 27 | `EVAL`, 28 | `EXCEPT`, 29 | `EXFMT`, 30 | `EXSR`, 31 | `FEOD`, 32 | `FOR`, 33 | `FORCE`, 34 | `IF`, 35 | `IN`, 36 | `ITER`, 37 | `LEAVE`, 38 | `LEAVESR`, 39 | `MONITOR`, 40 | `NEXT1`, 41 | `ON`, 42 | `OPEN`, 43 | `OTHER`, 44 | `OUT1`, 45 | `POST`, 46 | `READ`, 47 | `READC`, 48 | `READE`, 49 | `READP`, 50 | `READPE`, 51 | `REL`, 52 | `RESET`, 53 | `RETURN`, 54 | `ROLBK`, 55 | `SELECT`, 56 | `SETGT`, 57 | `SETLL`, 58 | `SORTA`, 59 | `TEST`, 60 | `UNLOCK`, 61 | `UPDATE`, 62 | `WHEN`, 63 | `WRITE`, 64 | `XML`, 65 | ]; -------------------------------------------------------------------------------- /language/models/tags.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'author': `Author of the source code`, 3 | 'date': `Date (any format)`, 4 | 'link': `A link for the documentation`, 5 | 'rev (revision)': `@rev date author , following lines are the description of the revision`, 6 | 'deprecated': `Description why a program or procedure shouldn't be used and stating any replacement.`, 7 | 'return': `Description of the return value`, 8 | 'param': `Description of the parameter`, 9 | 'project': `Name of the project (so that the module can be placed under the corresponding project in the UI)`, 10 | 'warning': `multi line`, 11 | 'info': `multi line`, 12 | 'throws': `ID and description of an escape message the user of the program/procedure can expect in certain cases`, 13 | 'version': `version of the module` 14 | }; -------------------------------------------------------------------------------- /language/parserTypes.ts: -------------------------------------------------------------------------------- 1 | import Declaration from './models/declaration'; 2 | import {Range} from "./models/DataPoints"; 3 | import { IRange } from './types'; 4 | 5 | export interface Keywords { 6 | [keyword: string]: string|true; 7 | } 8 | 9 | export interface IncludeStatement { 10 | /** vscode.Uri.path */ 11 | toPath: string; 12 | line: number; 13 | } 14 | 15 | export interface CacheProps { 16 | parameters?: Declaration[]; 17 | subroutines?: Declaration[]; 18 | procedures?: Declaration[]; 19 | files?: Declaration[]; 20 | variables?: Declaration[]; 21 | structs?: Declaration[]; 22 | constants?: Declaration[]; 23 | sqlReferences?: Declaration[]; 24 | indicators?: Declaration[]; 25 | includes?: IncludeStatement[]; 26 | tags?: Declaration[]; 27 | } 28 | 29 | export interface Rules { 30 | indent?: number; 31 | BlankStructNamesCheck?: boolean; 32 | QualifiedCheck?: boolean; 33 | PrototypeCheck?: boolean; 34 | ForceOptionalParens?: boolean; 35 | NoOCCURS?: boolean; 36 | NoSELECTAll?: boolean; 37 | UselessOperationCheck?: boolean; 38 | UppercaseConstants?: boolean; 39 | IncorrectVariableCase?: boolean; 40 | RequiresParameter?: boolean; 41 | RequiresProcedureDescription?: boolean; 42 | StringLiteralDupe?: boolean; 43 | literalMinimum?: number; 44 | RequireBlankSpecial?: boolean; 45 | CopybookDirective?: "copy"|"include"; 46 | DirectiveCase?: "lower"|"upper"; 47 | NoSQLJoins?: boolean; 48 | NoGlobalsInProcedures?: boolean; 49 | SpecificCasing?: {operation: string, expected: string}[]; 50 | NoCTDATA?: boolean; 51 | PrettyComments?: boolean; 52 | NoGlobalSubroutines?: boolean; 53 | NoLocalSubroutines?: boolean; 54 | NoUnreferenced?: boolean; 55 | NoExternalTo?: string[]; 56 | NoExecuteImmediate?: boolean; 57 | NoExtProgramVariable?: boolean; 58 | IncludeMustBeRelative?: boolean; 59 | SQLHostVarCheck?: boolean; 60 | RequireOtherBlock?: boolean; 61 | 62 | /** Not for user definition */ 63 | InvalidDeclareNumber?: void; 64 | UnexpectedEnd?: void; 65 | SQLRunner?: boolean; 66 | 67 | /** When true, will update Cache will references found in linter */ 68 | CollectReferences?: boolean; 69 | } 70 | 71 | export interface DefinitionPosition { 72 | path: string; 73 | line: number; 74 | } 75 | 76 | export interface Reference { 77 | uri: string; 78 | offset: IRange; 79 | } 80 | 81 | export interface IssueRange { 82 | offset: IRange; 83 | type?: keyof Rules; 84 | newValue?: string; 85 | } 86 | 87 | export interface SelectBlock { 88 | offset: IRange; 89 | otherBlockExists: boolean; 90 | } -------------------------------------------------------------------------------- /language/statement.ts: -------------------------------------------------------------------------------- 1 | import { createBlocks } from "./tokens"; 2 | import { IRange, IRangeWithLine, Token } from "./types"; 3 | 4 | export const NO_NAME = `*N`; 5 | 6 | export default class Statement { 7 | private beginBlock = false; 8 | 9 | constructor(public tokens: Token[], public range: IRangeWithLine, public indent: number = 0) {} 10 | 11 | getTokenByOffset(offset: number) { 12 | const blockSearch = (tokens: Token[]): Token|undefined => { 13 | const token = tokens.find(token => offset >= token.range.start && offset <= token.range.end); 14 | 15 | if (token?.type === `block` && token.block) { 16 | return blockSearch(token.block); 17 | } 18 | 19 | return token; 20 | } 21 | 22 | return blockSearch(this.tokens); 23 | } 24 | 25 | asBlocks() { 26 | return createBlocks(this.tokens); 27 | } 28 | 29 | static getParameters(tokens: Token[]) { 30 | let parameters: Token[] = []; 31 | let newBlock: Token[] = []; 32 | 33 | for (let i = 0; i < tokens.length; i++) { 34 | if (tokens[i].type === `seperator`) { 35 | parameters.push({ 36 | type: `block`, 37 | block: newBlock, 38 | range: { 39 | line: newBlock[0].range.line, 40 | start: newBlock[0].range.start, 41 | end: newBlock[newBlock.length-1].range.end, 42 | } 43 | }); 44 | 45 | newBlock = [tokens[i]]; 46 | } else { 47 | newBlock.push(tokens[i]); 48 | } 49 | } 50 | 51 | if (newBlock.length > 0) { 52 | parameters.push({ 53 | type: `block`, 54 | block: newBlock, 55 | range: { 56 | line: newBlock[0].range.line, 57 | start: newBlock[0].range.start, 58 | end: newBlock[newBlock.length-1].range.end, 59 | } 60 | }); 61 | } 62 | 63 | return parameters; 64 | } 65 | 66 | static trimTokens(tokens: Token[]) { 67 | if (tokens.length > 0) { 68 | let realFirstToken = tokens.findIndex(t => t.type !== `newline`); 69 | if (realFirstToken < 0) realFirstToken = 0; 70 | 71 | let realLastToken = 0; 72 | 73 | for (let i = tokens.length - 1; i >= 0; i--) { 74 | if (tokens[i].type !== `newline`) { 75 | realLastToken = i + 1; 76 | break; 77 | } 78 | } 79 | 80 | tokens = tokens.slice(realFirstToken, realLastToken); 81 | } 82 | 83 | return tokens; 84 | } 85 | } -------------------------------------------------------------------------------- /language/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IRange { 3 | start: number; 4 | end: number; 5 | } 6 | 7 | export interface IRangeWithLine extends IRange { 8 | line: number; 9 | } 10 | 11 | export interface Token { 12 | value?: string; 13 | block?: Token[]; 14 | type: string; 15 | range: IRangeWithLine; 16 | } -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/media/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-rpgle", 3 | "displayName": "RPGLE", 4 | "description": "RPGLE language tools and linter for VS Code", 5 | "author": { 6 | "name": "Halcyon Tech Ltd", 7 | "url": "https://github.com/halcyon-tech" 8 | }, 9 | "icon": "media/logo.png", 10 | "publisher": "halcyontechltd", 11 | "repository": { 12 | "url": "https://github.com/halcyon-tech/vscode-rpgle" 13 | }, 14 | "license": "MIT", 15 | "version": "0.29.2", 16 | "engines": { 17 | "vscode": "^1.70.0" 18 | }, 19 | "keywords": [ 20 | "rpg", 21 | "rpgle", 22 | "ibmi", 23 | "iseries", 24 | "as400" 25 | ], 26 | "categories": [ 27 | "Programming Languages", 28 | "Formatters" 29 | ], 30 | "activationEvents": [ 31 | "onLanguage:rpgle", 32 | "onCommand:workbench.action.showAllSymbols" 33 | ], 34 | "main": "./out/extension", 35 | "contributes": { 36 | "configuration": { 37 | "title": "RPGLE language tools", 38 | "properties": { 39 | "vscode-rpgle.rulerEnabledByDefault": { 40 | "type": "boolean", 41 | "default": true, 42 | "description": "Whether to show the fixed-format ruler by default." 43 | } 44 | } 45 | }, 46 | "snippets": [ 47 | { 48 | "path": "./schemas/rpgle.code-snippets", 49 | "language": "rpgle" 50 | } 51 | ], 52 | "jsonValidation": [ 53 | { 54 | "fileMatch": [ 55 | "rpglint.json", 56 | "RPGLINT.JSON" 57 | ], 58 | "url": "./schemas/rpglint.json" 59 | } 60 | ], 61 | "commands": [ 62 | { 63 | "command": "vscode-rpgle.openLintConfig", 64 | "title": "Open RPGLE lint configuration", 65 | "category": "RPGLE" 66 | }, 67 | { 68 | "command": "vscode-rpgle.assist.launchUI", 69 | "title": "Launch Column Assistant", 70 | "category": "RPGLE", 71 | "enablement": "editorLangId == rpgle" 72 | }, 73 | { 74 | "command": "vscode-rpgle.assist.toggleFixedRuler", 75 | "title": "Toggle Inline Editor Helper", 76 | "category": "RPGLE", 77 | "enablement": "editorLangId == rpgle" 78 | }, 79 | { 80 | "command": "vscode-rpgle.server.reloadCache", 81 | "title": "RPGLE: Reload Cache", 82 | "category": "RPGLE", 83 | "enablement": "code-for-ibmi:connected", 84 | "icon": "$(refresh)" 85 | }, 86 | { 87 | "command": "vscode-rpgle.assist.moveLeft", 88 | "title": "Move Left", 89 | "category": "RPGLE Fixed-Format", 90 | "icon": "$(arrow-left)", 91 | "when": "editorLangId == rpgle" 92 | }, 93 | { 94 | "command": "vscode-rpgle.assist.moveRight", 95 | "title": "Move Right", 96 | "category": "RPGLE Fixed-Format", 97 | "icon": "$(arrow-right)", 98 | "when": "editorLangId == rpgle" 99 | } 100 | ], 101 | "keybindings": [ 102 | { 103 | "command": "vscode-rpgle.assist.launchUI", 104 | "key": "ctrl+shift+f4", 105 | "mac": "cmd+shift+f4", 106 | "when": "editorLangId == rpgle" 107 | }, 108 | { 109 | "command": "vscode-rpgle.assist.toggleFixedRuler", 110 | "key": "shift+f4", 111 | "mac": "shift+f4", 112 | "when": "editorLangId == rpgle" 113 | }, 114 | { 115 | "command": "vscode-rpgle.assist.moveLeft", 116 | "key": "ctrl+[", 117 | "mac": "ctrl+[", 118 | "when": "editorLangId == rpgle" 119 | }, 120 | { 121 | "command": "vscode-rpgle.assist.moveRight", 122 | "key": "ctrl+]", 123 | "mac": "ctrl+]", 124 | "when": "editorLangId == rpgle" 125 | } 126 | ], 127 | "menus": { 128 | "view/item/context": [ 129 | { 130 | "command": "vscode-rpgle.openLintConfig", 131 | "when": "view == objectBrowser && viewItem =~ /^filter.*$/", 132 | "group": "1_LibActions@2" 133 | } 134 | ], 135 | "view/title": [ 136 | { 137 | "command": "vscode-rpgle.server.reloadCache", 138 | "group": "navigation", 139 | "when": "view == outline" 140 | } 141 | ] 142 | } 143 | }, 144 | "scripts": { 145 | "test": "vitest run", 146 | "test:watch": "vitest", 147 | "package": "vsce package", 148 | "vscode:prepublish": "npm run webpack", 149 | "webpack": "npm run clean && webpack --mode production --config ./extension/client/webpack.config.js && webpack --mode production --config ./extension/server/webpack.config.js", 150 | "webpack:dev": "npm run clean && webpack --mode none --config ./extension/client/webpack.config.js && webpack --mode none --config ./extension/server/webpack.config.js", 151 | "clean": "rimraf out && rimraf client/out && rimraf server/out", 152 | "compile": "tsc -b", 153 | "compile:tests": "tsc -b ./tests/tsconfig.json", 154 | "compile:client": "tsc -b ./extension/client/tsconfig.json", 155 | "compile:server": "tsc -b ./extension/server/tsconfig.json", 156 | "watch": "tsc -b -w", 157 | "lint": "eslint ./extension/client/src ./extension/server/src --ext .ts,.tsx", 158 | "postinstall": "cd extension/client && npm install && cd ../server && npm install && cd ../..", 159 | "cli:dev:rpglint": "cd cli/rpglint && npm run webpack:dev" 160 | }, 161 | "devDependencies": { 162 | "@halcyontech/vscode-ibmi-types": "^2.11.0", 163 | "@types/node": "^18.16.1", 164 | "@typescript-eslint/eslint-plugin": "^5.30.0", 165 | "@typescript-eslint/parser": "^5.30.0", 166 | "esbuild-loader": "^3.0.1", 167 | "eslint": "^8.13.0", 168 | "glob": "^7.2.0", 169 | "merge-options": "^3.0.4", 170 | "rimraf": "^3.0.2", 171 | "semver": "^7.3.5", 172 | "tsx": "^3.11.0", 173 | "typescript": "^4.8.4", 174 | "vitest": "^1.3.1", 175 | "webpack": "^5.76.0", 176 | "webpack-cli": "^4.5.0" 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /schemas/rpglint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://github.com/halcyon-tech/vscode-ibmi", 4 | "type": "object", 5 | "description": "Available lint configuration for RPGLE", 6 | "required": [], 7 | "properties": { 8 | "indent": { 9 | "$id": "#/properties/indent", 10 | "type": "number", 11 | "description": "Indent for RPGLE." 12 | }, 13 | "BlankStructNamesCheck": { 14 | "$id": "#/properties/BlankStructNamesCheck", 15 | "type": "boolean", 16 | "description": "Struct names cannot be blank (*N)." 17 | }, 18 | "QualifiedCheck": { 19 | "$id": "#/properties/QualifiedCheck", 20 | "type": "boolean", 21 | "description": "Struct names must be qualified (QUALIFIED)." 22 | }, 23 | "PrototypeCheck": { 24 | "$id": "#/properties/PrototypeCheck", 25 | "type": "boolean", 26 | "description": "Prototypes can only be defined with either EXTPGM or EXTPROC." 27 | }, 28 | "ForceOptionalParens": { 29 | "$id": "#/properties/ForceOptionalParens", 30 | "type": "boolean", 31 | "description": "Expressions must be surrounded by brackets." 32 | }, 33 | "NoOCCURS": { 34 | "$id": "#/properties/NoOCCURS", 35 | "type": "boolean", 36 | "description": "OCCURS is not allowed." 37 | }, 38 | "NoSELECTAll": { 39 | "$id": "#/properties/NoSELECTAll", 40 | "type": "boolean", 41 | "description": "'SELECT *' is not allowed in Embedded SQL." 42 | }, 43 | "UselessOperationCheck": { 44 | "$id": "#/properties/UselessOperationCheck", 45 | "type": "boolean", 46 | "description": "Redundant operation codes (EVAL, CALLP, DCL-PARM, DCL-SUBF) not allowed." 47 | }, 48 | "UppercaseConstants": { 49 | "$id": "#/properties/UppercaseConstants", 50 | "type": "boolean", 51 | "description": "Constants must be in uppercase." 52 | }, 53 | "IncorrectVariableCase": { 54 | "$id": "#/properties/IncorrectVariableCase", 55 | "type": "boolean", 56 | "description": "Variable names must match the case of the definition." 57 | }, 58 | "RequiresParameter": { 59 | "$id": "#/properties/RequiresParameter", 60 | "type": "boolean", 61 | "description": "Parentheses must be used on a procedure call, even if it has no parameters." 62 | }, 63 | "RequiresProcedureDescription": { 64 | "$id": "#/properties/RequiresProcedureDescription", 65 | "type": "boolean", 66 | "description": "Procedure titles and descriptions must be provided." 67 | }, 68 | "StringLiteralDupe": { 69 | "$id": "#/properties/StringLiteralDupe", 70 | "type": "boolean", 71 | "description": "Duplicate string literals suggestions are made. Enabled by default." 72 | 73 | }, 74 | "RequireBlankSpecial": { 75 | "$id": "#/properties/RequireBlankSpecial", 76 | "type": "boolean", 77 | "description": "*BLANK must be used over empty string literals." 78 | }, 79 | "CopybookDirective": { 80 | "$id": "#/properties/CopybookDirective", 81 | "type": "string", 82 | "enum": [ 83 | "copy", 84 | "include" 85 | ], 86 | "description": "Force which directive which must be used to include other source. (Copy or Include)" 87 | }, 88 | "DirectiveCase": { 89 | "$id": "#/properties/DirectiveCase", 90 | "type": "string", 91 | "enum": [ 92 | "lower", 93 | "upper" 94 | ], 95 | "description": "The expected casing of directives (lower or upper)." 96 | }, 97 | "UppercaseDirectives": { 98 | "$id": "#/properties/UppercaseDirectives", 99 | "type": "boolean", 100 | "description": "Directives must be in uppercase.", 101 | "deprecated": true 102 | }, 103 | "NoSQLJoins": { 104 | "$id": "#/properties/NoSQLJoins", 105 | "type": "boolean", 106 | "description": "JOINs in Embedded SQL are not allowed." 107 | }, 108 | "NoGlobalsInProcedures": { 109 | "$id": "#/properties/NoGlobalsInProcedures", 110 | "type": "boolean", 111 | "description": "Globals are not allowed in procedures." 112 | }, 113 | "SpecificCasing": { 114 | "$id": "#/properties/SpecificCasing", 115 | "type": "array", 116 | "items": { 117 | "type": "object", 118 | "properties": { 119 | "operation": { 120 | "type": "string", 121 | "description": "The operation code, declaration or built-in function to check. Can use the following special values: `*DECLARE`, `*BIF`" 122 | }, 123 | "expected": { 124 | "type": "string", 125 | "description": "The expected casing of the operation code, declaration or built-in function. Can use the following special values: `*LOWER`, `*UPPER`" 126 | } 127 | }, 128 | "additionalProperties": false 129 | }, 130 | "description": "Specific casing for op codes, declartions or built-in functions codes." 131 | }, 132 | "NoCTDATA": { 133 | "$id": "#/properties/NoCTDATA", 134 | "type": "boolean", 135 | "description": "CTDATA is not allowed." 136 | }, 137 | "PrettyComments": { 138 | "$id": "#/properties/PrettyComments", 139 | "type": "boolean", 140 | "description": "Comments cannot be blank, must start with a space and have correct indentation." 141 | }, 142 | "NoGlobalSubroutines": { 143 | "$id": "#/properties/NoGlobalSubroutines", 144 | "type": "boolean", 145 | "description": "Global subroutines are not allowed." 146 | }, 147 | "NoLocalSubroutines": { 148 | "$id": "#/properties/NoLocalSubroutines", 149 | "type": "boolean", 150 | "description": "Subroutines in procedures are not allowed." 151 | }, 152 | "NoUnreferenced": { 153 | "$id": "#/properties/NoUnreferenced", 154 | "type": "boolean", 155 | "description": "Unreferenced definitions are not allowed." 156 | }, 157 | "NoExternalTo": { 158 | "$id": "#/properties/NoExternalTo", 159 | "type": "array", 160 | "items": { 161 | "type": "string" 162 | }, 163 | "description": "Calls to certain APIs are not allowed. (EXTPROC / EXTPGM)" 164 | }, 165 | "NoExecuteImmediate": { 166 | "$id": "#/properties/NoExecuteImmediate", 167 | "type": "boolean", 168 | "description": "Embedded SQL statement with EXECUTE IMMEDIATE not allowed." 169 | }, 170 | "NoExtProgramVariable": { 171 | "$id": "#/properties/NoExtProgramVariable", 172 | "type": "boolean", 173 | "description": "Declaring a prototype with EXTPGM and EXTPROC using a procedure is now allowed." 174 | }, 175 | "IncludeMustBeRelative": { 176 | "$id": "#/properties/IncludeMustBeRelative", 177 | "type": "boolean", 178 | "description": "When using copy or include statements, path must be relative. For members, you must at least include the source file. For streamfiles, it is relative from the working directory." 179 | }, 180 | "SQLHostVarCheck": { 181 | "$id": "#/properties/SQLHostVarCheck", 182 | "type": "boolean", 183 | "description": "Warns when referencing variables in Embedded SQL that are also defined locally." 184 | }, 185 | "RequireOtherBlock": { 186 | "$id": "#/properties/RequireOtherBlock", 187 | "type": "boolean", 188 | "description": "Require SELECT blocks to have an OTHER block." 189 | } 190 | }, 191 | "additionalProperties": true 192 | } -------------------------------------------------------------------------------- /shared.webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | //@ts-check 7 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 8 | 9 | 'use strict'; 10 | 11 | const path = require(`path`); 12 | const merge = require(`merge-options`); 13 | 14 | module.exports = function withDefaults(/**@type WebpackConfig*/extConfig) { 15 | 16 | /** @type WebpackConfig */ 17 | let defaultConfig = { 18 | mode: `none`, // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 19 | target: `node`, // extensions run in a node context 20 | node: { 21 | __dirname: false // leave the __dirname-behaviour intact 22 | }, 23 | resolve: { 24 | mainFields: [`module`, `main`], 25 | extensions: [`.ts`, `.js`] // support ts-files and js-files 26 | }, 27 | module: { 28 | rules: [{ 29 | test: /\.ts$/, 30 | exclude: /node_modules/, 31 | use: [{ 32 | // configure TypeScript loader: 33 | // * enable sources maps for end-to-end source maps 34 | loader: `esbuild-loader` 35 | }] 36 | }] 37 | }, 38 | externals: { 39 | 'vscode': `commonjs vscode`, // ignored because it doesn't exist 40 | }, 41 | output: { 42 | // all output goes into `dist`. 43 | // packaging depends on that and this must always be like it 44 | filename: `[name].js`, 45 | // @ts-ignore 46 | path: path.join(extConfig.context, `out`), 47 | libraryTarget: `commonjs`, 48 | }, 49 | // yes, really source maps 50 | devtool: `source-map` 51 | }; 52 | 53 | return merge(defaultConfig, extConfig); 54 | }; -------------------------------------------------------------------------------- /tests/eof4.rpgle: -------------------------------------------------------------------------------- 1 | D UPPERCASE PR 4096 Varying 2 | D String 4096 Const Varying 3 | D Escaped n Const Options(*NoPass) 4 | /EoF 5 | Converts all of the letters in String to their 6 | UPPER CASE equivalents. Non-alphabetic characters 7 | remain unchanged. 8 | 9 | Escaped = *ON = converts characters that would crash iPDF and 10 | HTML to approximately equivalent characters. 11 | For example, translate " and ' to ` . 12 | (Default) 13 | *OFF= Do not convert any characters other than A-Z. -------------------------------------------------------------------------------- /tests/parserSetup.ts: -------------------------------------------------------------------------------- 1 | import Parser from '../language/parser'; 2 | 3 | import glob from "glob"; 4 | import path from 'path'; 5 | import fs from 'fs'; 6 | 7 | import { readFile } from 'fs/promises'; 8 | 9 | import tables from './tables'; 10 | import { dspffdToRecordFormats } from '../extension/server/src/data'; 11 | 12 | const TEST_INCLUDE_DIR = process.env.INCLUDE_DIR || path.join(__dirname, `..`, `tests`); 13 | 14 | export default function setupParser(projectRoot = TEST_INCLUDE_DIR): Parser { 15 | const parser = new Parser(); 16 | let ignore: string[] = []; 17 | 18 | if (projectRoot === TEST_INCLUDE_DIR) { 19 | ignore.push("sources/**") 20 | } 21 | 22 | let globSettings = { 23 | cwd: projectRoot, 24 | absolute: true, 25 | ignore, 26 | nocase: true, 27 | } 28 | 29 | const globCache = glob.sync(`**/*.*rpg*`, globSettings); 30 | 31 | parser.setIncludeFileFetch(async (baseFile: string, includeFile: string) => { 32 | if (includeFile.startsWith(`'`) && includeFile.endsWith(`'`)) { 33 | includeFile = includeFile.substring(1, includeFile.length - 1); 34 | } else if (includeFile.startsWith(`"`) && includeFile.endsWith(`"`)) { 35 | includeFile = includeFile.substring(1, includeFile.length - 1); 36 | } 37 | 38 | if (includeFile.includes(`,`) || !includeFile.includes(`.`)) { 39 | includeFile = includeFile.split(`,`).join(`/`) + `.*rpgl*`; 40 | } 41 | 42 | 43 | const globPath = path.join(`**`, includeFile); 44 | const files: string[] = glob.sync(globPath, {cache: globCache, ...globSettings}); 45 | 46 | if (files.length >= 1) { 47 | const file = files.find(f => f.toLowerCase().endsWith(`rpgleinc`)) || files[0]; 48 | 49 | const content = await readFile(file, { encoding: `utf-8` }); 50 | 51 | return { 52 | found: true, 53 | uri: file, 54 | content 55 | } 56 | } 57 | 58 | return { 59 | found: false 60 | }; 61 | }); 62 | 63 | parser.setTableFetch(async (table: string, aliases = false) => { 64 | const upperName = table.toUpperCase(); 65 | 66 | const data = tables[upperName] ? tables[upperName] : []; 67 | 68 | return dspffdToRecordFormats(data, aliases); 69 | }); 70 | 71 | return parser; 72 | } 73 | 74 | export function getTestProjectsDir(): string[] { 75 | // get a list of directories in the test directory using fs 76 | const sourcesDir = path.join(TEST_INCLUDE_DIR, `sources`); 77 | return fs.readdirSync(sourcesDir) 78 | .filter((f) => fs.statSync(path.join(TEST_INCLUDE_DIR, `sources`, f)).isDirectory()) 79 | .map((f) => path.join(sourcesDir, f)); 80 | } 81 | 82 | export function getSourcesList(fullDirPath: string): string[] { 83 | return glob.sync(`**/*.{rpgle,sqlrpgle}`, { 84 | cwd: fullDirPath, 85 | absolute: true, 86 | nocase: true, 87 | }); 88 | } 89 | 90 | export function getFileContent(fullPath: string) { 91 | return readFile(fullPath, { encoding: `utf-8` }); 92 | } -------------------------------------------------------------------------------- /tests/rpgle/CBKDTAARA.rpgle: -------------------------------------------------------------------------------- 1 | D wDTAARA UDS DTAARA(BBS400DTA) 2 | D wSoftVer 6A 3 | D wBBSNAM 40A 4 | D wLOCCRY 2A 5 | D wLOCCTY 40A 6 | D wTIMZON 3A 7 | D wCLOSED 1A 8 | D wNUSLVL 2A 9 | D wNUSSVY 4A 10 | D wMAINTM 1A 11 | D wSHWALD 1A 12 | D wUser 10A 13 | D wUserLvl 2A 14 | D wLvlDescr 15A 15 | D wUserLstLogin 8A 16 | D wLvlAuths 5A 17 | D wNewUsrNtfy 10A 18 | D wHIDESO 1A 19 | D wHLSOMS 1A 20 | D wUserLvlAuths DS 21 | D wAuthListUser 1A 22 | D wAuthSysInfor 1A 23 | D wAuthPostMsgs 1A 24 | D wAuthMsgUsers 1A 25 | D wAuthWhosOnli 1A 26 | -------------------------------------------------------------------------------- /tests/rpgle/CBKHEADER.rpgle: -------------------------------------------------------------------------------- 1 | * Get values from DTAARA and put them on screen 2 | C IN wDTAARA 3 | * Name of this BBS 4 | C EVAL SCRBBS = wBBSNAM 5 | * Nickname of logged in user 6 | C EVAL SCRNCK = wUser 7 | * Access Level of logged in user 8 | C MOVEL wUserLvl SCRLVL 9 | * Description of Access Level 10 | C IF wSHWALD = 'Y' 11 | C EVAL SCRLVD = wLvlDescr 12 | C ELSE 13 | C EVAL SCRLVD = *BLANK 14 | C ENDIF 15 | C UNLOCK wDTAARA 16 | -------------------------------------------------------------------------------- /tests/rpgle/CBKOPTIMIZ.rpgle: -------------------------------------------------------------------------------- 1 | * Level optimisation FULL 2 | * The most efficient code is generated. 3 | * Translation time is the longest. 4 | H OPTIMIZE(*FULL) 5 | -------------------------------------------------------------------------------- /tests/rpgle/CBKPCFGDCL.rpgle: -------------------------------------------------------------------------------- 1 | D wCfgBBSNAM S 30A 2 | D wCfgLOCCRY S 2A 3 | D wCfgLOCCTY S 40A 4 | D wCfgTIMZON S 3A 5 | D wCfgCLOSED S 1A 6 | D wCfgNUSLVL S 2P 0 7 | D wCfgNUSSVY S 4A 8 | D wCfgSHWALD S 1A 9 | D wCfgSHWWEL S 1A 10 | D wCfgNUSRNF S 10A 11 | D wCfgHIDESO S 1A 12 | D wCfgHLSOMS S 1A 13 | D wCfgSurvey DS 14 | D wCfgSvyRName 1A 15 | D wCfgSvyGendr 1A 16 | D wCfgSvyLocat 1A 17 | D wCfgSvyEmail 1A 18 | -------------------------------------------------------------------------------- /tests/rpgle/CBKPCFGREA.rpgle: -------------------------------------------------------------------------------- 1 | ********************************************************************** 2 | * Read configuration values from PCONFIG into variables 3 | * Uses *IN81 for CHAIN Not Found 4 | * This CopyBook needs: to be used with CBKPCFGDCL 5 | * FPCONFIG IF E K DISK 6 | ********************************************************************** 7 | C GetConfig BEGSR 8 | * Get BBS Name 9 | C EVAL wCfgKey = 'BBSNAM' 10 | C wCfgKey CHAIN PCONFIG 81 11 | C 81 GOTO ENDOFSR 12 | C EVAL wCfgBBSNAM = CNFVAL 13 | * Get BBS Location Country Code 14 | C EVAL wCfgKey = 'LOCCRY' 15 | C wCfgKey CHAIN PCONFIG 81 16 | C 81 GOTO ENDOFSR 17 | C EVAL wCfgLOCCRY = CNFVAL 18 | * Get BBS Location City 19 | C EVAL wCfgKey = 'LOCCTY' 20 | C wCfgKey CHAIN PCONFIG 81 21 | C 81 GOTO ENDOFSR 22 | C EVAL wCfgLOCCTY = CNFVAL 23 | * Get BBS Time Zone 24 | C EVAL wCfgKey = 'TIMZON' 25 | C wCfgKey CHAIN PCONFIG 81 26 | C 81 GOTO ENDOFSR 27 | C EVAL wCfgTIMZON = CNFVAL 28 | * Get BBS closed to new users? 29 | C EVAL wCfgKey = 'CLOSED' 30 | C wCfgKey CHAIN PCONFIG 81 31 | C 81 GOTO ENDOFSR 32 | C EVAL wCfgCLOSED = CNFVAL 33 | * Get BBS New User Level 34 | C EVAL wCfgKey = 'NUSLVL' 35 | C wCfgKey CHAIN PCONFIG 81 36 | C 81 GOTO ENDOFSR 37 | C MOVEL CNFVAL wCfgNUSLVL 38 | * Get BBS New User enabled survey questions 39 | C EVAL wCfgKey = 'NUSSVY' 40 | C wCfgKey CHAIN PCONFIG 81 41 | C 81 GOTO ENDOFSR 42 | C EVAL wCfgNUSSVY = CNFVAL 43 | * Get Show Access Level Description? 44 | C EVAL wCfgKey = 'SHWALD' 45 | C wCfgKey CHAIN PCONFIG 81 46 | C 81 GOTO ENDOFSR 47 | C EVAL wCfgSHWALD = CNFVAL 48 | C OUT wDTAARA 49 | C UNLOCK wDTAARA 50 | * Get Show Welcome screen 51 | C EVAL wCfgKey = 'SHWWEL' 52 | C wCfgKey CHAIN PCONFIG 81 53 | C 81 GOTO ENDOFSR 54 | C EVAL wCfgSHWWEL = CNFVAL 55 | * Get New User default Survey questions 56 | C EVAL wCfgKey = 'NUSSVY' 57 | C wCfgKey CHAIN PCONFIG 81 58 | C 81 GOTO ENDOFSR 59 | C EVAL wCfgSurvey = CNFVAL 60 | * Get New User Registration notify 61 | C EVAL wCfgKey = 'NUSRNF' 62 | C wCfgKey CHAIN PCONFIG 81 63 | C 81 GOTO ENDOFSR 64 | C EVAL wCfgNUSRNF = CNFVAL 65 | * Get Hide SysOp from Users List 66 | C EVAL wCfgKey = 'HIDESO' 67 | C wCfgKey CHAIN PCONFIG 81 68 | C 81 GOTO ENDOFSR 69 | C EVAL wCfgHIDESO = CNFVAL 70 | * Get Highlight SysOp's messages 71 | C EVAL wCfgKey = 'HLSOMS' 72 | C wCfgKey CHAIN PCONFIG 81 73 | C 81 GOTO ENDOFSR 74 | C EVAL wCfgHLSOMS = CNFVAL 75 | C ENDOFSR TAG 76 | C ENDSR 77 | -------------------------------------------------------------------------------- /tests/rpgle/apival01s.rpgleinc: -------------------------------------------------------------------------------- 1 | **free 2 | // Constants 3 | // --------- 4 | // field & array sizes 5 | dcl-c APIVAL01S_MAX_DIM 1000; // max array size 6 | dcl-c APIVAL01S_DATA_LEN 100; // max field data length 7 | dcl-c APIVAL01S_FIELD_LEN 50; // max field name length 8 | dcl-c APIVAL01S_VALID_LEN 500; // max validation length 9 | dcl-c APIVAL01S_JSON_LEN 1000; // JSON error response size 10 | // type constants 11 | dcl-c APIVAL01S_TYPE_STRING 'string'; 12 | dcl-c APIVAL01S_TYPE_NUMERIC 'numeric'; 13 | dcl-c APIVAL01S_TYPE_DATE 'date'; 14 | 15 | // DS templates 16 | // ------------ 17 | // list of validations to perform 18 | dcl-ds APIVAL01S_validationsDs qualified template; 19 | field varchar(APIVAL01S_FIELD_LEN); 20 | type varchar(50); 21 | validations varchar(APIVAL01S_VALID_LEN) dim(6); 22 | end-ds; 23 | 24 | // perform validation for the given list of validations (validationDs) 25 | // with the given input data (note: required char!) 26 | // returns the number of errors found (if any) 27 | // if errors returned = 0 then the validation succeeded 28 | // in case of validation failure o_errorJson will contain a list of errors in JSON format 29 | dcl-pr APIVAL01S_iws_validate int(10) extproc(*dclcase); 30 | i_validationsDs likeds(APIVAL01S_validationsDs) dim(APIVAL01S_MAX_DIM); 31 | i_data varchar(APIVAL01S_DATA_LEN) dim(APIVAL01S_MAX_DIM); 32 | o_errorJson varchar(APIVAL01S_JSON_LEN); 33 | end-pr; 34 | 35 | -------------------------------------------------------------------------------- /tests/rpgle/copy1.rpgle: -------------------------------------------------------------------------------- 1 | **FREE 2 | 3 | Dcl-Pr theExtProcedure ExtProc; 4 | theNewValue Char(20); 5 | End-Pr; -------------------------------------------------------------------------------- /tests/rpgle/copy2.rpgle: -------------------------------------------------------------------------------- 1 | **FREE 2 | 3 | Dcl-C LENGTH_t; 4 | 5 | Dcl-Ds TheStruct Qualified; 6 | SubItem Char(length_t); 7 | End-Ds; -------------------------------------------------------------------------------- /tests/rpgle/copy3.rpgle: -------------------------------------------------------------------------------- 1 | **FREE 2 | 3 | Dcl-S CustomerName_t varchar(40) template; 4 | -------------------------------------------------------------------------------- /tests/rpgle/copy4.rpgleinc: -------------------------------------------------------------------------------- 1 | **free 2 | 3 | /IF NOT DEFINED(QRPGLEH_RPMAR001) 4 | /DEFINE QRPGLEH_RPMAR001 5 | 6 | dcl-pr rpmar001_test extpgm('RPMAR001'); 7 | end-pr; 8 | 9 | /ENDIF -------------------------------------------------------------------------------- /tests/rpgle/copy5.rpgleinc: -------------------------------------------------------------------------------- 1 | **FREE 2 | 3 | Dcl-C LENGTH_t 20; 4 | 5 | Dcl-Ds TheStruct Qualified; 6 | SubItem Char(length_t); 7 | End-Ds; -------------------------------------------------------------------------------- /tests/rpgle/db00030s_h.rpgleinc: -------------------------------------------------------------------------------- 1 | **free 2 | Dcl-Pr DB00030_getDescriptionByCode Like(ds_Incoterms.Description) ExtProc('DB00030_getDescriptionByCode'); 3 | pIncotermCode Like(ds_Incoterms.IncotermCode) Const; 4 | End-Pr; 5 | 6 | Dcl-Pr DB00030_checkIncotermCode Ind ExtProc('DB00030_checkIncotermCode'); 7 | pIncotermCode Like(ds_Incoterms.IncotermCode) Const; 8 | End-Pr; 9 | 10 | Dcl-Pr DB00030_getError VarChar(256) ExtProc('DB00030_getError'); 11 | pErrId Int(10) Options(*NoPass:*Omit); 12 | End-Pr; 13 | 14 | Dcl-DS ds_Incoterms Ext ExtName('VW_INCTRMS') Alias Qualified; // view over functions table 15 | End-DS; -------------------------------------------------------------------------------- /tests/rpgle/db00040s_h.rpgleinc: -------------------------------------------------------------------------------- 1 | **free 2 | Dcl-Pr DB00040_getDescriptionByRecycCode Like(ds_recycling.description) ExtProc(*DclCase); 3 | pRecycCode Like(ds_recycling.RecycCode) Const; 4 | End-Pr; 5 | Dcl-Pr DB00040_getImageFileByRecycCode Like(ds_recycling.imageFile) ExtProc(*DclCase); 6 | pRecycCode Like(ds_recycling.RecycCode) Const; 7 | End-Pr; 8 | Dcl-Pr DB00040_checkRecordByRecycCode Ind ExtProc(*DclCase); 9 | pRecycCode Like(ds_recycling.RecycCode) Const; 10 | End-Pr; 11 | 12 | Dcl-Pr DB00040_getRecordByRecycCode LikeDs(ds_recycling) ExtProc(*DclCase); 13 | pRecycCode Like(ds_recycling.RecycCode) Const; 14 | End-Pr; 15 | 16 | Dcl-Pr DB00040_getRecycError VarChar(256) ExtProc(*DclCase); 17 | pErrId Int(10) Options(*NoPass:*Omit); 18 | End-Pr; 19 | 20 | Dcl-Ds ds_recycling Ext ExtName('VW_RECYC') Inz Alias Qualified; // view 21 | End-Ds; -------------------------------------------------------------------------------- /tests/rpgle/depth1.rpgleinc: -------------------------------------------------------------------------------- 1 | **FREE 2 | 3 | /include 'copy3.rpgle' 4 | 5 | Dcl-S Scooby varchar(40) template; -------------------------------------------------------------------------------- /tests/rpgle/eof4.rpgle: -------------------------------------------------------------------------------- 1 | D UPPERCASE PR 4096 Varying 2 | D String 4096 Const Varying 3 | D Escaped n Const Options(*NoPass) 4 | /EoF 5 | Converts all of the letters in String to their 6 | UPPER CASE equivalents. Non-alphabetic characters 7 | remain unchanged. 8 | 9 | Escaped = *ON = converts characters that would crash iPDF and 10 | HTML to approximately equivalent characters. 11 | For example, translate " and ' to ` . 12 | (Default) 13 | *OFF= Do not convert any characters other than A-Z. -------------------------------------------------------------------------------- /tests/rpgle/file1.rpgleinc: -------------------------------------------------------------------------------- 1 | **free 2 | // data structure for file SomeFile 3 | dcl-ds GlobalStruct extname('EMPLOYEE') qualified template; 4 | end-ds; -------------------------------------------------------------------------------- /tests/rpgle/fixed1.rpgleinc: -------------------------------------------------------------------------------- 1 | 2 | // CONSTANTS 3 | 4 | dcl-c a 'hello'; 5 | 6 | Dcl-ds t_onemod_strc0p Extname('STRC0P') Qualified Template; 7 | End-ds; 8 | 9 | // -------------------------------------------------------------------------- 10 | // Table Name: STRDLP 11 | // Purpose: Schedule Time Details 12 | // -------------------------------------------------------------------------- 13 | Dcl-ds t_onemod_strcasdasd0p Extname('DDDDD') Qualified Template; 14 | End-ds; 15 | dcl-pr invoice_approve_cash_refund 16 | likeds(t_invoice_process_cash_refund_rtn); 17 | p_system_key char(30) const; //system key (str/inv/cnt/dtl) 18 | p_salesman packed(5:0) const; //salesman 19 | p_note like(invoice_process_cash_refund_note) const; //note 20 | end-pr; 21 | 22 | Dcl-PR invoice_get_invoice; 23 | store Zoned(3:0) const; 24 | invoice Zoned(7:0) const; 25 | details LikeDS(invoice_get_invoice_sales_detail_ds) 26 | dim(invoice_max_details); 27 | count_details Zoned(3:0); 28 | error Like(TError); 29 | End-Pr; -------------------------------------------------------------------------------- /tests/rpgle/stat.rpgleinc: -------------------------------------------------------------------------------- 1 | D stat PR 10I 0 ExtProc('stat') 2 | D filename * value 3 | D buf * value -------------------------------------------------------------------------------- /tests/sources/random/hello.test.rpgle: -------------------------------------------------------------------------------- 1 | **free 2 | 3 | dcl-s text char(20); 4 | 5 | text = 'Hello, world!'; 6 | 7 | dsply text; 8 | 9 | return; -------------------------------------------------------------------------------- /tests/suite/docs.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import setupParser from "../parserSetup"; 3 | import Linter from "../../language/linter"; 4 | import { test, expect } from "vitest"; 5 | 6 | const parser = setupParser(); 7 | const uri = `source.rpgle`; 8 | 9 | test("issue_202", async () => { 10 | const lines = [ 11 | `**free`, 12 | `///`, 13 | `// Transform to lowercase`, 14 | `// This procedure will take a string and transform it to lowercase`, 15 | `//`, 16 | `// @param The string`, 17 | `// @return The lowercase value`, 18 | `///`, 19 | `Dcl-Proc ToLower Export;`, 20 | ` Dcl-Pi *N Char(20);`, 21 | ` stringIn Char(20);`, 22 | ` End-pi;`, 23 | ``, 24 | ` return STRLOWER(stringIn);`, 25 | `End-Proc;`, 26 | ].join(`\n`); 27 | 28 | const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true}); 29 | 30 | const toLower = cache.find(`ToLower`); 31 | 32 | const titleTag = toLower.tags.find(tag => tag.tag === `title`); 33 | expect(titleTag.content).toBe(`Transform to lowercase`); 34 | 35 | const descTag = toLower.tags.find(tag => tag.tag === `description`); 36 | expect(descTag.content).toBe(`This procedure will take a string and transform it to lowercase`); 37 | 38 | const tags = toLower.tags; 39 | expect(tags[2]).toEqual({ 40 | tag: `param`, 41 | content: `The string` 42 | }); 43 | 44 | expect(tags[3]).toEqual({ 45 | tag: `return`, 46 | content: `The lowercase value` 47 | }); 48 | 49 | const stringInParam = toLower.subItems[0]; 50 | const parmTag = stringInParam.tags.find(tag => tag.tag === `description`); 51 | expect(parmTag.content).toBe(`The string`); 52 | }); 53 | 54 | test("issue_231", async () => { 55 | const lines = [ 56 | `**FREE`, 57 | `Ctl-Opt Main(MainLine);`, 58 | `/// -------------------------------------`, 59 | `// Main`, 60 | `/// -------------------------------------`, 61 | `Dcl-Proc MainLine;`, 62 | ` Dcl-Pi MainLine Extpgm('MAINTLINE');`, 63 | ` Iof Char(1);`, 64 | ` End-Pi;`, 65 | ` Dcl-S myString Varchar(20);`, 66 | ``, 67 | ` myString = CvtToMixed(myString);`, 68 | `End-Proc;`, 69 | ``, 70 | `/// -------------------------------------`, 71 | `// CvtToMixed`, 72 | `// Convert the passed string to mixed case or `, 73 | `// what is normally called Title case.`, 74 | `// @param Source string`, 75 | `// @return Title cased string`, 76 | `/// -------------------------------------`, 77 | `Dcl-Proc CvtToMixed;`, 78 | ` Dcl-Pi CvtToMixed Extpgm('MAINTLINE');`, 79 | ` theString Varchar(100);`, 80 | ` End-Pi;`, 81 | ``, 82 | ` return theString;`, 83 | `End-Proc;`, 84 | ].join(`\n`); 85 | 86 | const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true}); 87 | 88 | const { indentErrors, errors } = Linter.getErrors({ uri, content: lines }, { 89 | indent: 2, 90 | PrettyComments: true, 91 | }, cache); 92 | 93 | expect(indentErrors.length).toBe(0); 94 | expect(errors.length).toBe(0); 95 | }); -------------------------------------------------------------------------------- /tests/suite/editing.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import setupParser from "../parserSetup"; 3 | import { test, expect } from "vitest"; 4 | 5 | const parser = setupParser(); 6 | const uri = `source.rpgle`; 7 | 8 | test("edit1", async () => { 9 | const lines = [ 10 | ` H ALTSEQ(*EXT) CURSYM('$') DATEDIT(*MDY) DATFMT(*MDY/) DEBUG(*YES)`, 11 | ` H DECEDIT('.') FORMSALIGN(*YES) FTRANS(*SRC) DFTNAME(name)`, 12 | ` H TIMFMT(*ISO)`, 13 | ` H COPYRIGHT('(C) Copyright ABC Programming - 1995')`, 14 | ``, 15 | ` d InType s 10`, 16 | ``, 17 | ` *`, 18 | ` * Date structure for retriving userspace info`, 19 | ` *`, 20 | ` d InputDs DS`, 21 | ` d UserSpace 1 20`, 22 | ` d SpaceName 1 10`, 23 | ` d SpaceLib 11 20`, 24 | ` d InpFileLib 29 48`, 25 | ` d InpFFilNam 29 38`, 26 | ` d InpFFilLib 39 48`, 27 | ` d InpRcdFmt 49 58`, 28 | ` d Worktype s 10 inz('*OUTQ')`, 29 | ``, 30 | ].join(`\n`); 31 | 32 | let currentDoc = ``; 33 | 34 | for (const char of lines) { 35 | currentDoc += char; 36 | 37 | await parser.getDocs(uri, currentDoc, { 38 | ignoreCache: true 39 | }); 40 | } 41 | }); 42 | 43 | test("edit2", async () => { 44 | const lines = [ 45 | `**free`, 46 | `Ctl-opt datfmt(*iso) timfmt(*iso) alwnull(*usrctl) debug;`, 47 | ``, 48 | `Dcl-F TESTFILE3 Keyed Usage(*Update :*Delete);`, 49 | ``, 50 | `Dcl-Pr TESTCHAIN1 ExtPgm('TESTCHAIN1');`, 51 | `Parm1 Char(1);`, 52 | `End-Pr TESTCHAIN1;`, 53 | ``, 54 | `Dcl-Pi TESTCHAIN1;`, 55 | `Parm1 Char(1);`, 56 | `End-Pi TESTCHAIN1;`, 57 | ``, 58 | `Dcl-DS AAA;`, 59 | `a Char(10);`, 60 | `Dcl-ds a;`, 61 | `End-ds a;`, 62 | `End-Ds AAA;`, 63 | ``, 64 | `If (Parm1 = 'N');`, 65 | `Chain ('CHIAVE' :1) TESTFILE3;`, 66 | `Else;`, 67 | `Chain ('CHIAVE' :1) TESTFILE3;`, 68 | `EndIf;`, 69 | ``, 70 | `job_name = 'TESTFILE1';`, 71 | ``, 72 | `Update TESTREC;`, 73 | ``, 74 | `Return;`, 75 | ``, 76 | `// ____________________________________________________________________________`, 77 | `Dcl-Proc aaa;`, 78 | ``, 79 | `Dcl-Pi aaa;`, 80 | `end-proc;`, 81 | `End-Pi aaa;`, 82 | `// ____________________________________________________________________________`, 83 | ``, 84 | `End-Proc aaa;`, 85 | ].join(`\n`); 86 | 87 | let currentDoc = ``; 88 | 89 | for (const char of lines) { 90 | currentDoc += char; 91 | 92 | await parser.getDocs(uri, currentDoc, { 93 | ignoreCache: true 94 | }); 95 | } 96 | }); 97 | 98 | test("edit3", async () => { 99 | const lines = [ 100 | ` * Field Definitions.`, 101 | ` * ~~~~~~~~~~~~~~~~~~~~~~~~`, 102 | ` D ObjNam s 10a`, 103 | ` D ObjLib s 10a`, 104 | ` D ObjTyp s 10a`, 105 | ``, 106 | ` P Obj_List B Export`, 107 | ` D Obj_List PI`, 108 | ` D pLibrary 10A Const`, 109 | ` D pObject 10A Const`, 110 | ` D pType 10A Const`, 111 | ` D Result s 5i 0`, 112 | ``, 113 | ` /Free`, 114 | ``, 115 | ` exsr $QUSCRTUS;`, 116 | ` ObjectLib = pObject + pLibrary;`, 117 | ` WorkType = pType;`, 118 | ``, 119 | ` Format = 'OBJL0200';`, 120 | ` $ListObjects( Userspace : Format : ObjectLib : WorkType);`, 121 | ` //`, 122 | ` // Retrive header entry and process the user space`, 123 | ` //`, 124 | ` StartPosit = 125;`, 125 | ` StartLen = 16;`, 126 | ` $UserSpace( Userspace : StartPosit : StartLen : GENDS);`, 127 | ``, 128 | ` StartPosit = OffsetHdr + 1;`, 129 | ` StartLen = %size(ObjectDS);`, 130 | ``, 131 | ` Return;`, 132 | ``, 133 | ` //--------------------------------------------------------`, 134 | ` // $QUSCRTUS - create userspace`, 135 | ` //--------------------------------------------------------`, 136 | ` begsr $QUSCRTUS;`, 137 | ``, 138 | ` system('DLTOBJ OBJ(QTEMP/LISTOUTQS) OBJTYPE(*USRSPC)');`, 139 | ``, 140 | ` BytesPrv = 116;`, 141 | ` Spacename = 'LISTOUTQS';`, 142 | ` SpaceLib = 'QTEMP';`, 143 | ``, 144 | ` // Create the user space`, 145 | ` $CreateSpace( Userspace : SpaceAttr : 4096 :`, 146 | ` SpaceVal : SpaceAuth : SpaceText : SpaceRepl:`, 147 | ` ErrorDs);`, 148 | ` endsr;`, 149 | ` /End-Free`, 150 | ` P E`, 151 | ``, 152 | ].join(`\n`); 153 | 154 | let currentDoc = ``; 155 | 156 | const parser = setupParser(); 157 | 158 | for (const char of lines) { 159 | currentDoc += char; 160 | 161 | await parser.getDocs(uri, currentDoc, { 162 | ignoreCache: true 163 | }); 164 | } 165 | }); -------------------------------------------------------------------------------- /tests/suite/files.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import path from "path"; 3 | import setupParser from "../parserSetup"; 4 | import { test, expect } from "vitest"; 5 | 6 | const parser = setupParser(); 7 | const uri = `source.rpgle`; 8 | 9 | test("simple_file", async () => { 10 | const lines = [ 11 | `**free`, 12 | ``, 13 | `dcl-f employee disk usage(*input);`, 14 | ``, 15 | `dsply employee.workdept;`, 16 | ``, 17 | `return;` 18 | ].join(`\n`); 19 | 20 | const cache = await parser.getDocs(uri, lines, {withIncludes: true, ignoreCache: true}); 21 | 22 | expect(cache.files.length).toBe(1); 23 | expect(cache.structs.length).toBe(0); 24 | 25 | const fileDef = cache.find(`employee`); 26 | expect(fileDef.name).toBe(`employee`); 27 | expect(fileDef.keyword[`DISK`]).toBe(true); 28 | expect(fileDef.keyword[`USAGE`]).toBe(`*input`); 29 | 30 | // file record formats should be expanded into the subitems 31 | expect(fileDef.subItems.length).toBe(1); 32 | 33 | const empRdcFmt = fileDef.subItems[0]; 34 | 35 | expect(empRdcFmt.name).toBe(`EMPLOYEE`); 36 | 37 | expect(empRdcFmt.subItems[1].keyword[`VARCHAR`]).toBe(`12`); 38 | // 14 fields inside of this record format 39 | expect(empRdcFmt.subItems.length).toBe(14); 40 | }); 41 | 42 | test("many_formats", async () => { 43 | const lines = [ 44 | `**free`, 45 | ``, 46 | `dcl-f emps workstn;`, 47 | ``, 48 | `write SFLDTA;`, 49 | ``, 50 | `return;` 51 | ].join(`\n`); 52 | 53 | const cache = await parser.getDocs(uri, lines, {withIncludes: true, ignoreCache: true}); 54 | 55 | expect(cache.files.length).toBe(1); 56 | 57 | const fileDef = cache.find(`emps`); 58 | expect(fileDef.name).toBe(`emps`); 59 | expect(fileDef.keyword[`WORKSTN`]).toBe(true); 60 | 61 | // file record formats should be expanded into the subitems 62 | expect(fileDef.subItems.length).toBe(2); 63 | 64 | const sfldta = fileDef.subItems[0]; 65 | expect(sfldta.name).toBe(`SFLDTA`); 66 | expect(sfldta.subItems.length).toBe(5); 67 | 68 | const sflctl = fileDef.subItems[1]; 69 | expect(sflctl.name).toBe(`SFLCTL`); 70 | expect(sflctl.subItems.length).toBe(1); 71 | }); 72 | 73 | test("ds_extname", async () => { 74 | const lines = [ 75 | `**free`, 76 | ``, 77 | `Dcl-Ds Employee ExtName('EMPLOYEE') Qualified;`, 78 | `end-ds;`, 79 | ``, 80 | `Dsply Employee.empno;`, 81 | ``, 82 | `return;` 83 | ].join(`\n`); 84 | 85 | const cache = await parser.getDocs(uri, lines, {withIncludes: true, ignoreCache: true}); 86 | 87 | expect(cache.files.length).toBe(0); 88 | expect(cache.structs.length).toBe(1); 89 | 90 | const structDef = cache.find(`employee`); 91 | expect(structDef.name).toBe(`Employee`); 92 | expect(structDef.subItems.length).toBe(14); 93 | }); 94 | 95 | test("ds_extname", async () => { 96 | const lines = [ 97 | `**free`, 98 | ``, 99 | `Dcl-Ds Employee ExtName('EMPLOYEE') Qualified;`, 100 | `end-ds;`, 101 | ``, 102 | `Dsply Employee.empno;`, 103 | ``, 104 | `return;` 105 | ].join(`\n`); 106 | 107 | const cache = await parser.getDocs(uri, lines, {withIncludes: true, ignoreCache: true}); 108 | 109 | expect(cache.files.length).toBe(0); 110 | expect(cache.structs.length).toBe(1); 111 | 112 | const structDef = cache.find(`employee`); 113 | expect(structDef.name).toBe(`Employee`); 114 | expect(structDef.subItems.length).toBe(14); 115 | }); 116 | 117 | test("ds_extname_template", async () => { 118 | const lines = [ 119 | `**free`, 120 | ``, 121 | `Dcl-Ds dept ExtName('department') Qualified template end-ds;`, 122 | ``, 123 | `Dcl-DS dsExample qualified inz;`, 124 | ` Field1 like(tmpDS.Field1) inz;`, 125 | ` Field2 like(tmpDS.Field2) inz;`, 126 | `END-DS`, 127 | ``, 128 | `return;` 129 | ].join(`\n`); 130 | 131 | const cache = await parser.getDocs(uri, lines, {withIncludes: true, ignoreCache: true}); 132 | 133 | expect(cache.structs.length).toBe(2); 134 | 135 | const dept = cache.find(`dsExample`); 136 | expect(dept.subItems.length).toBe(2); 137 | }); 138 | 139 | test("ds_extname_alias", async () => { 140 | const lines = [ 141 | `**free`, 142 | ``, 143 | `Dcl-Ds dept ExtName('department') alias Qualified;`, 144 | `end-ds;`, 145 | ``, 146 | `Dsply dept.deptname;`, 147 | ``, 148 | `return;` 149 | ].join(`\n`); 150 | 151 | const cache = await parser.getDocs(uri, lines, {withIncludes: true, ignoreCache: true}); 152 | 153 | expect(cache.files.length).toBe(0); 154 | expect(cache.structs.length).toBe(1); 155 | 156 | const dept = cache.find(`dept`); 157 | expect(dept.subItems.length).toBe(5); 158 | 159 | expect(dept.subItems[0].name).toBe(`DEPTNO`); 160 | expect(dept.subItems[1].name).toBe(`DEPTNAME`); 161 | }); 162 | 163 | test("file_prefix", async () => { 164 | const lines = [ 165 | `**free`, 166 | ``, 167 | `Dcl-f display workstn usropn prefix(d);`, 168 | ``, 169 | `Exfmt display;`, 170 | ``, 171 | `return;` 172 | ].join(`\n`); 173 | 174 | const cache = await parser.getDocs(uri, lines, {withIncludes: true, ignoreCache: true}); 175 | 176 | const disp = cache.find(`display`); 177 | expect(disp.subItems[0].subItems[0].name).toBe(`DE1_OPTION`); 178 | }); 179 | 180 | test('file DS in a copy book', async () => { 181 | const lines = [ 182 | `**free`, 183 | `ctl-opt main(Main);`, 184 | `/copy './rpgle/file1.rpgleinc'`, 185 | ``, 186 | `dcl-proc Main;`, 187 | `dcl-pi *n;`, 188 | `end-pi;`, 189 | ``, 190 | `dcl-ds SomeStruct likeds(GlobalStruct) inz;`, 191 | ``, 192 | `end-proc;`, 193 | ].join(`\n`); 194 | 195 | 196 | const cache = await parser.getDocs(uri, lines, {withIncludes: true, ignoreCache: true}); 197 | 198 | const globalStruct = cache.find(`GlobalStruct`); 199 | expect(globalStruct.subItems.length).toBeGreaterThan(0); 200 | 201 | const mainProc = cache.find(`Main`); 202 | 203 | expect(mainProc).toBeDefined(); 204 | 205 | const someStruct = mainProc.scope.find(`SomeStruct`); 206 | expect(someStruct).toBeDefined(); 207 | expect(someStruct.subItems.length).toBeGreaterThan(0); 208 | 209 | expect(someStruct.subItems.map(s => ({name: s.name, keyword: s.keyword}))).toMatchObject(globalStruct.subItems.map(s => ({name: s.name, keyword: s.keyword}))); 210 | }); 211 | 212 | -------------------------------------------------------------------------------- /tests/suite/keywords.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import setupParser from "../parserSetup"; 3 | import Linter from "../../language/linter"; 4 | import { test, expect } from "vitest"; 5 | 6 | const parser = setupParser(); 7 | const uri = `source.rpgle`; 8 | 9 | test("qualified1", async () => { 10 | const lines = [ 11 | `**FREE`, 12 | `Dcl-Ds Kx Likerec(TitXe :*Key);`, 13 | `Dcl-s MyVariable2 Char(20);`, 14 | ``, 15 | `Dsply MyVariable2;`, 16 | ``, 17 | `Return`, 18 | ].join(`\n`); 19 | 20 | const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true}); 21 | const { errors } = Linter.getErrors({uri, content: lines}, { 22 | QualifiedCheck: true, 23 | }, cache); 24 | 25 | expect(errors.length).toBe(0); 26 | }); 27 | 28 | test("ctdata1", async () => { 29 | const lines = [ 30 | `**free`, 31 | `dcl-s myarray char(100) dim(10) ctdata;`, 32 | `dcl-s xxField1 char(1);`, 33 | `dcl-ds master qualified inz;`, 34 | ``, 35 | ` dcl-ds a inz;`, 36 | ` fielda1 Like(xxFiel1);`, 37 | ` fielda2 packed(2);`, 38 | ` End-ds;`, 39 | ``, 40 | ` dcl-ds b inz;`, 41 | ` fieldb1 like(xxField1);`, 42 | ` fieldb2 packed(9);`, 43 | ` End-ds;`, 44 | `End-ds;`, 45 | ``, 46 | ``, 47 | `eval master.a.fielda1 = 'a1';`, 48 | `eval master.a.f;`, 49 | `eval master.b.fieldb1 = 'b1';`, 50 | `//eval myds2.p.field1 = 'p';`, 51 | `//eval myds2.o.`, 52 | ``, 53 | `*INLR = *ON;`, 54 | `**ctdata myarray`, 55 | `select RMSDESC ,RMACRONYM ,RMLPID ,RMCBAPLAN ,LTTYPE ,LTID ,LTATTREA`, 56 | `,digits( RHHRTYPE ) as RHHRTYPE ,varchar( PWDES ,30 )`, 57 | ` ,EOEMP as EMP ,min( RHEFFDT ) as EFFDATE`, 58 | ` ,dec( 0.0 ,7,2 ) as Hours`, 59 | ` ,dec( 0.0 ,10,5 ) as Earned`, 60 | ` ,dec( 0.0 ,7,2 ) as Taken`, 61 | ` ,dec( ifnull( PTHRS ,0 ) ,7,2 ) as Due`, 62 | ` ,dec( 0.0 ,7,2 ) as Prior`, 63 | ` ,'N' as SysGen`, 64 | `from PRPEMPV0 V0`, 65 | `cross join PRPLPMTB RM`, 66 | `inner join PRPLPTTB LO on LTLPID = RMLPID`, 67 | `inner join PRPLPHTB HT on RHLTID = LTID`, 68 | `inner join PRPPHRTP on PWHTP = RHHRTYPE`, 69 | `left join PRPHWLTB PT on EOEMP = PTEMP and PTLPID = LTLPID and PTTID = LTID`, 70 | ` and ( PTDTEOW between date( xEARNED_LEAVE_TO_x ) -7 days`, 71 | ` and date( xEARNED_LEAVE_TO_x ) -1 days )`, 72 | `where EOEFFDT = ( select EOEFFDT from PRPEOCPF where EOEMP = V0.EOEMP`, 73 | ` anD EOEFFDT <=xEARNED_LEAVE_TO_8x order by EOEFFDT desc fetch first row only )`, 74 | `and EHHDT = ( select EHHDT from PRPEHTPF where EHEMP = V0.EOEMP`, 75 | ` and EHHDT <=xEARNED_LEAVE_TO_8x order by EHHDT desc fetch first row only )`, 76 | `and ETEFFDT= ( select ETEFFDT from PRPETXPF where ETEMP = V0.EOEMP`, 77 | ` and ETEFFDT <=xEARNED_LEAVE_TO_8x order by ETEFFDT desc fetch first row only )`, 78 | `and RMACRONYM = 'CBA'`, 79 | `and EOEMP = xEMP_USEx`, 80 | `and LTEFFDT = ( select LTEFFDT from PRPLPTTB LI where LO.LTLPID = LI.LTLPID`, 81 | ` and LO.LTTYPE = LI.LTTYPE`, 82 | ` and LI.LTEFFDT <= xEARNED_LEAVE_TO_x`, 83 | ` order by LTEFFDT desc fetch first row only ) and LTSTS = 'A'`, 84 | `and RHEFFDT = ( select RHEFFDT from PRPLPHTB I where I.RHLTID = HT.RHLTID`, 85 | ` and I.RHEFFDT <= xEARNED_LEAVE_TO_x`, 86 | ` order by RHEFFDT desc fetch first row only ) and RHHTSTS = 'A'`, 87 | `group by RMSDESC ,RMACRONYM ,RMLPID ,RMCBAPLAN ,LTTYPE ,LTID ,LTATTREA`, 88 | ` ,RHHRTYPE ,PWDES ,EOEMP ,PTHRS`, 89 | `order by RMLPID ,LTID ,EFFDATE`, 90 | ].join(`\n`); 91 | 92 | const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true}); 93 | const { indentErrors } = Linter.getErrors({uri, content: lines}, { 94 | indent: 2 95 | }, cache); 96 | 97 | expect(indentErrors.length).toBe(0); 98 | }); 99 | 100 | test("ctdata2", async () => { 101 | const lines = [ 102 | `**FREE`, 103 | `ctl-opt debug option(*nodebugio: *srcstmt);`, 104 | `dcl-ds mything DIM(8) PERRCD(3) CTDATA;`, 105 | `end-ds;`, 106 | ``, 107 | `Dcl-s MyVariable2 Char(20);`, 108 | ``, 109 | `myVariable2 = *blank;`, 110 | ``, 111 | `If myVariable2 = *blank;`, 112 | `MyVariable2 = 'Hello world';`, 113 | `Endif;`, 114 | `Return;`, 115 | ``, 116 | `** ARC`, 117 | `Toronto 12:15:00Winnipeg 13:23:00Calgary 15:44:00`, 118 | `Sydney 17:24:30Edmonton 21:33:00Saskatoon 08:40:00`, 119 | `Regina 12:33:00Vancouver 13:20:00` 120 | ].join(`\n`); 121 | 122 | const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true}); 123 | 124 | expect(Object.keys(cache.keyword).length).toBe(2); 125 | expect(cache.keyword[`DEBUG`]).toBe(true); 126 | expect(cache.keyword[`OPTION`]).toBe(`*NODEBUGIO:*SRCSTMT`); 127 | 128 | expect(cache.variables.length).toBe(1); 129 | expect(cache.structs.length).toBe(1); 130 | }); 131 | 132 | test("ctdata3", async () => { 133 | const lines = [ 134 | ` DCL-F QSYSPRT PRINTER(132) USAGE(*OUTPUT) OFLIND(*INOF);`, 135 | ` `, 136 | ` DCL-S OVR_FILE CHAR(21);`, 137 | ``, 138 | ` DCL-S TP CHAR(1) DIM(6) CTDATA PERRCD(1); // Deduction types`, 139 | ` DCL-S TD CHAR(20) DIM(6) ALT(TP);`, 140 | ``, 141 | ` *INLR = *ON;`, 142 | ` Return;`, 143 | ``, 144 | `** TP and TD - Deduction types and descriptions`, 145 | `BBenefit Benefit`, 146 | `DDeferred CompDef Cmp`, 147 | `CChild supportCh Sup`, 148 | `GGarnishment Garnish`, 149 | `SStatutory Statut.`, 150 | `VVoluntary Volntry`, 151 | ].join(`\n`); 152 | 153 | const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true}); 154 | 155 | expect(cache.files.length).toBe(1); 156 | expect(cache.variables.length).toBe(3); 157 | }); 158 | 159 | test("likeds1", async () => { 160 | const lines = [ 161 | `**FREE`, 162 | `Dcl-s MyVariable2 CHAR(20);`, 163 | `Dcl-Ds astructure qualified;`, 164 | ` Subitem1 CHAR(20);`, 165 | ` Subitem2 CHAR(20);`, 166 | `End-ds;`, 167 | `Dcl-s MyVariable CHAR(20);`, 168 | `Dcl-Ds MyOtherStruct LikeDS(Astructure);`, 169 | `//Yes` 170 | ].join(`\n`); 171 | 172 | const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true}); 173 | 174 | expect(cache.variables.length).toBe(2); 175 | expect(cache.structs.length).toBe(2); 176 | 177 | const MyOtherStruct = cache.find(`MyOtherStruct`); 178 | expect(MyOtherStruct.name).toBe(`MyOtherStruct`); 179 | expect(MyOtherStruct.position.range.line).toBe(7); 180 | expect(MyOtherStruct.subItems.length).toBe(2); 181 | }); 182 | 183 | test("likeds2", async () => { 184 | const lines = [ 185 | `**FREE`, 186 | `Dcl-s MyVariable2 CHAR(20);`, 187 | `Dcl-Ds astructure qualified;`, 188 | ` Subitem1 CHAR(20);`, 189 | ` Subitem2 CHAR(20);`, 190 | `End-ds;`, 191 | `Dcl-s MyVariable CHAR(20);`, 192 | `Dsply MyVariable;`, 193 | `Return;`, 194 | `Dcl-Proc myprocedure;`, 195 | ` Dcl-Pi *N;`, 196 | ` inputDS Likeds(astructure);`, 197 | ` End-Pi;`, 198 | ` Dsply 'Inside';`, 199 | ` Return;`, 200 | `End-Proc;` 201 | ].join(`\n`); 202 | 203 | const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true}); 204 | 205 | expect(cache.variables.length).toBe(2); 206 | expect(cache.structs.length).toBe(1); 207 | expect(cache.procedures.length).toBe(1); 208 | 209 | const myProc = cache.find(`myprocedure`); 210 | expect(myProc.name).toBe(`myprocedure`); 211 | expect(myProc.position.range.line).toBe(9); 212 | expect(myProc.subItems.length).toBe(1); 213 | 214 | const parmInputDs = myProc.subItems[0]; 215 | expect(parmInputDs.name).toBe(`inputDS`); 216 | expect(parmInputDs.position.range.line).toBe(11); 217 | expect(parmInputDs.subItems.length).toBe(2); 218 | }); 219 | 220 | test("overload1", async () => { 221 | const lines = [ 222 | `**FREE`, 223 | ``, 224 | `Dcl-PR json_setBool pointer extproc(*CWIDEN : 'jx_SetBoolByName');`, 225 | ` node pointer value;`, 226 | ` nodePath pointer value options(*string);`, 227 | ` value ind value;`, 228 | `End-PR;`, 229 | ``, 230 | `Dcl-PR json_setNum pointer extproc(*CWIDEN : 'jx_SetDecByName');`, 231 | ` node pointer value;`, 232 | ` nodePath pointer value options(*string);`, 233 | ` value packed(30:15) value;`, 234 | `End-PR;`, 235 | ``, 236 | `Dcl-PR json_set pointer overload ( `, 237 | ` json_setBool: `, 238 | ` json_setNum `, 239 | `);`, 240 | ``, 241 | `Dcl-PR json_nodeType int(5) extproc(*CWIDEN : 'jx_GetNodeType');`, 242 | ` node pointer value;`, 243 | `End-PR;`, 244 | ].join(`\n`); 245 | 246 | const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true}); 247 | 248 | const { indentErrors } = Linter.getErrors({uri, content: lines}, { 249 | indent: 2, 250 | PrettyComments: true 251 | }, cache); 252 | 253 | expect(cache.procedures.length).toBe(4); 254 | expect(indentErrors.length).toBe(0); 255 | }); 256 | 257 | test(`extproc1`, async () => { 258 | const lines = [ 259 | `**free`, 260 | `dcl-pr APIVAL01S_iws_validate int(10) extproc(*dclcase);`, 261 | ` i_validationsDs likeds(APIVAL01S_validationsDs) dim(APIVAL01S_MAX_DIM);`, 262 | ` i_data varchar(APIVAL01S_DATA_LEN) dim(APIVAL01S_MAX_DIM);`, 263 | ` o_errorJson varchar(APIVAL01S_JSON_LEN);`, 264 | `end-pr;`, 265 | ].join(`\n`); 266 | 267 | const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true}); 268 | 269 | expect(cache.procedures.length).toBe(1); 270 | expect(cache.procedures[0].name).toBe(`APIVAL01S_iws_validate`); 271 | }) -------------------------------------------------------------------------------- /tests/suite/partial.test.ts: -------------------------------------------------------------------------------- 1 | import setupParser, { getFileContent, getSourcesList, getTestProjectsDir } from "../parserSetup"; 2 | import { test, expect, describe } from "vitest"; 3 | import path from "path"; 4 | 5 | const timeout = 1000 * 60 * 20; // 20 minutes 6 | const parser = setupParser(); 7 | 8 | // The purpose of this file is to test the parser against all the sources in the sources directory to ensure it doesn't crash. 9 | 10 | test("Parser partial tests", { timeout }, async () => { 11 | const projects = getTestProjectsDir(); 12 | 13 | const SPLIT_SIZE = 10; 14 | let totalFiles = 0; 15 | 16 | for (const projectPath of projects) { 17 | const parser = setupParser(projectPath); 18 | const list = await getSourcesList(projectPath); 19 | 20 | totalFiles += list.length; 21 | 22 | for (let i = 0; i < list.length; i++) { 23 | const relativePath = list[i]; 24 | const basename = path.basename(relativePath); 25 | 26 | const rs = performance.now(); 27 | const baseContent = await getFileContent(relativePath); 28 | const re = performance.now(); 29 | 30 | // These are typing tests. Can the parser accept half documents without crashing? 31 | 32 | let content = ``; 33 | 34 | let baseContentSplitUpIntoPieces = []; 35 | 36 | const pieceSize = Math.ceil(baseContent.length / SPLIT_SIZE); 37 | for (let i = 0; i < baseContent.length; i += pieceSize) { 38 | baseContentSplitUpIntoPieces.push(baseContent.substring(i, i + pieceSize)); 39 | } 40 | 41 | // console.log(`Testing ${basename} (${i}/${list.length})...`); 42 | 43 | let lengths: number[] = []; 44 | for (let i = 0; i < baseContentSplitUpIntoPieces.length; i++) { 45 | content += baseContentSplitUpIntoPieces[i]; 46 | 47 | const ps = performance.now(); 48 | const doc = await parser.getDocs(basename, content, { collectReferences: true, ignoreCache: true, withIncludes: false }); 49 | const pe = performance.now(); 50 | 51 | // console.log(`\tParsed ${i+1}/${baseContentSplitUpIntoPieces.length} (${content.length}) in ${pe - ps}ms. Got ${doc.getNames().length} names.`); 52 | 53 | lengths.push(pe - ps); 54 | } 55 | 56 | // const lengthsAverage = lengths.reduce((a, b) => a + b, 0) / lengths.length; 57 | // const total = lengths.reduce((a, b) => a + b, 0); 58 | // const last = lengths[lengths.length - 1]; 59 | // console.log(`\tAverage: ${lengthsAverage}ms, Full: ${last}ms, Total: ${total}`); 60 | // console.log(``); 61 | } 62 | } 63 | 64 | console.log(`Parsed ${totalFiles} files, ${SPLIT_SIZE} times each.`); 65 | }); -------------------------------------------------------------------------------- /tests/suite/sources.test.ts: -------------------------------------------------------------------------------- 1 | import setupParser, { getFileContent, getSourcesList, getTestProjectsDir } from "../parserSetup"; 2 | import { test } from "vitest"; 3 | import path from "path"; 4 | import { fail } from "assert"; 5 | import Declaration from "../../language/models/declaration"; 6 | import Cache from "../../language/models/cache"; 7 | import { Reference } from "../../language/parserTypes"; 8 | 9 | const timeout = 1000 * 60 * 10; // 10 minutes 10 | 11 | // The purpose of this file is to test the parser against all the sources in the sources directory to ensure it doesn't crash. 12 | 13 | test("Generic reference tests", { timeout }, async () => { 14 | const projects = getTestProjectsDir(); 15 | 16 | let totalFiles = 0; 17 | 18 | for (const projectPath of projects) { 19 | const parser = setupParser(projectPath); 20 | const list = await getSourcesList(projectPath); 21 | 22 | totalFiles += list.length; 23 | 24 | for (let i = 0; i < list.length; i++) { 25 | const relativePath = list[i]; 26 | const basename = path.basename(relativePath); 27 | 28 | const baseContent = await getFileContent(relativePath); 29 | 30 | const ps = performance.now(); 31 | const doc = await parser.getDocs(basename, baseContent, { collectReferences: true, ignoreCache: true, withIncludes: true }); 32 | const pe = performance.now(); 33 | 34 | let cachedFiles: {[uri: string]: string} = {}; 35 | let referencesCollected = 0; 36 | let errorCount = 0; 37 | 38 | const printReference = (def: Declaration, content: string, ref: Reference) => { 39 | console.log({ 40 | def: def.name, 41 | uri: ref.uri, 42 | offset: ref.offset, 43 | content: content.substring(ref.offset.start, ref.offset.end), 44 | about: content.substring(ref.offset.start - 10, ref.offset.end + 10) 45 | }) 46 | } 47 | 48 | const checkReferences = async (def: Declaration) => { 49 | const refs = def.references; 50 | const uniqueUris = refs.map(r => r.uri).filter((value, index, self) => self.indexOf(value) === index); 51 | 52 | for (const refUri of uniqueUris) { 53 | if (refUri === basename) { 54 | cachedFiles[refUri] = baseContent; 55 | } 56 | 57 | if (!cachedFiles[refUri]) { 58 | cachedFiles[refUri] = await getFileContent(refUri); 59 | } 60 | } 61 | 62 | for (const ref of refs) { 63 | const offsetContent = cachedFiles[ref.uri].substring(ref.offset.start, ref.offset.end); 64 | 65 | if (offsetContent.toUpperCase() === def.name.toUpperCase()) { 66 | referencesCollected++; 67 | } else { 68 | errorCount++; 69 | printReference(def, cachedFiles[ref.uri], ref); 70 | } 71 | } 72 | } 73 | 74 | const checkScope = async (scope: Cache) => { 75 | for (const def of [...scope.variables, ...scope.subroutines, ...scope.procedures, ...scope.constants, ...scope.structs, ...scope.files, ...scope.tags, ...scope.sqlReferences]) { 76 | await checkReferences(def); 77 | 78 | if (def.subItems && def.subItems.length > 0) { 79 | for (const sub of def.subItems) { 80 | await checkReferences(sub); 81 | } 82 | } 83 | 84 | if (def.scope) { 85 | await checkScope(def.scope); 86 | } 87 | } 88 | } 89 | 90 | const ss = performance.now(); 91 | await checkScope(doc); 92 | const se = performance.now(); 93 | 94 | if (errorCount > 0) { 95 | fail(`Found ${errorCount} errors in ${basename}`); 96 | } 97 | 98 | // console.log(`Parsed ${basename} in ${pe - ps}ms. Validated in ${se-ss} (${i+1}/${list.length}). Found ${referencesCollected} references.`); 99 | } 100 | } 101 | 102 | console.log(`Parsed ${totalFiles} files.`); 103 | }); -------------------------------------------------------------------------------- /tests/tables/display.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | "WHFILE": "DISPLAY", 4 | "WHLIB": "LIBRARY", 5 | "WHCRTD": "1220407", 6 | "WHFTYP": "D", 7 | "WHCNT": 6, 8 | "WHDTTM": "1221027133341", 9 | "WHNAME": "DISPLAYE1", 10 | "WHSEQ": "0B87A7CC21D82", 11 | "WHTEXT": "", 12 | "WHFLDN": 21, 13 | "WHRLEN": 131, 14 | "WHFLDI": "E1_OPTION", 15 | "WHFLDE": "E1_OPTION", 16 | "WHFOBO": 22, 17 | "WHIBO": 21, 18 | "WHFLDB": 1, 19 | "WHFLDD": 0, 20 | "WHFLDP": 0, 21 | "WHFTXT": "", 22 | "WHRCDE": 0, 23 | "WHRFIL": "", 24 | "WHRLIB": "", 25 | "WHRFMT": "", 26 | "WHRFLD": "", 27 | "WHCHD1": "", 28 | "WHCHD2": "", 29 | "WHCHD3": "", 30 | "WHFLDT": "A", 31 | "WHFIOB": "B", 32 | "WHECDE": "", 33 | "WHEWRD": "", 34 | "WHVCNE": 0, 35 | "WHNFLD": 20, 36 | "WHNIND": 1, 37 | "WHSHFT": "A", 38 | "WHALTY": "N", 39 | "WHALIS": "", 40 | "WHJREF": 0, 41 | "WHDFTL": 0, 42 | "WHDFT": "", 43 | "WHCHRI": "N", 44 | "WHCTNT": "N", 45 | "WHFONT": "", 46 | "WHCSWD": 0, 47 | "WHCSHI": 0, 48 | "WHBCNM": "", 49 | "WHBCHI": 0, 50 | "WHMAP": "N", 51 | "WHMAPS": 0, 52 | "WHMAPL": 0, 53 | "WHSYSN": "SYSTEM", 54 | "WHRES1": "", 55 | "WHSQLT": "", 56 | "WHHEX": "N", 57 | "WHPNTS": 0, 58 | "WHCSID": 0, 59 | "WHFMT": "", 60 | "WHSEP": "", 61 | "WHVARL": "N", 62 | "WHALLC": 0, 63 | "WHNULL": "N", 64 | "WHFCSN": "", 65 | "WHFCSL": "", 66 | "WHFCPN": "", 67 | "WHFCPL": "", 68 | "WHCDFN": "", 69 | "WHCDFL": "", 70 | "WHDCDF": "", 71 | "WHDCDL": "", 72 | "WHTXRT": "- 1", 73 | "WHFLDG": 0, 74 | "WHFDSL": 0, 75 | "WHFSPS": 0, 76 | "WHCFPS": 0, 77 | "WHIFPS": 0, 78 | "WHDBLL": 0, 79 | "WHDBUN": "", 80 | "WHDBUL": "", 81 | "WHDBFC": "", 82 | "WHDBFI": "", 83 | "WHDBRP": "", 84 | "WHDBWP": "", 85 | "WHDBRC": "", 86 | "WHDBOU": "", 87 | "WHPSUD": "", 88 | "WHBCUH": 0, 89 | "WHFPSW": 0, 90 | "WHFSPW": 0, 91 | "WHCFPW": 0, 92 | "WHIFPW": 0, 93 | "WHRWID": "N", 94 | "WHIDC": "N", 95 | "WHDROW": 7, 96 | "WHDCOL": 3, 97 | "WHALI2": "", 98 | "WHALCH": "", 99 | "WHNRML": "0", 100 | "WHJRF2": 0, 101 | "WHHDNCOL": "0", 102 | "WHRCTS": "", 103 | "WHFPPN": "", 104 | "WHFPLN": "" 105 | } 106 | ] -------------------------------------------------------------------------------- /tests/tables/index.ts: -------------------------------------------------------------------------------- 1 | import emps from './emps'; 2 | import employee from './employee'; 3 | import department from './department'; 4 | import display from './display'; 5 | 6 | export default { 7 | 'EMPS': emps, 8 | 'EMPLOYEE': employee, 9 | 'DEPARTMENT': department, 10 | 'DISPLAY': display 11 | }; --------------------------------------------------------------------------------