diff --git a/extension/client/src/configuration.ts b/extension/client/src/configuration.ts index 81714f2f..f3144eb7 100644 --- a/extension/client/src/configuration.ts +++ b/extension/client/src/configuration.ts @@ -6,4 +6,6 @@ export function get(prop: string) { } export const RULER_ENABLED_BY_DEFAULT = `rulerEnabledByDefault`; +export const GLOBAL_LINT_CONFIG_PATH = `globalLintConfigPath`; +export const LINT_IGNORE_LIBRARIES = `lintIgnoreLibraries`; export const projectFilesGlob = `**/*.{rpgle,RPGLE,sqlrpgle,SQLRPGLE,rpgleinc,RPGLEINC}`; \ No newline at end of file diff --git a/extension/client/src/extension.ts b/extension/client/src/extension.ts index 6558266e..3f71215e 100644 --- a/extension/client/src/extension.ts +++ b/extension/client/src/extension.ts @@ -47,6 +47,20 @@ export function activate(context: ExtensionContext) { } }; + // The global configuration path for linter config file + const globalConfig = workspace.getConfiguration(`vscode-rpgle`); + const globalLintPath = globalConfig.get(`globalLintConfigPath`); + const lintIgnoreLibraries = globalConfig.get(`lintIgnoreLibraries`); + const env = { ...process.env } as NodeJS.ProcessEnv; + + if (globalLintPath) { + env.GLOBAL_LINT_CONFIG_PATH = globalLintPath; + } + + if (lintIgnoreLibraries) { + env.LINT_IGNORE_LIBRARIES = lintIgnoreLibraries; + } + loadBase(); // Options to control the language client diff --git a/extension/client/src/language/serverReferences.ts b/extension/client/src/language/serverReferences.ts index 147ccfe0..771e5a31 100644 --- a/extension/client/src/language/serverReferences.ts +++ b/extension/client/src/language/serverReferences.ts @@ -1,7 +1,7 @@ +import { ConnectionConfig, IBMiMember } from "@halcyontech/vscode-ibmi-types"; +import IBMi from "@halcyontech/vscode-ibmi-types/api/IBMi"; import { commands, Definition, DocumentSymbol, languages, Location, ProgressLocation, Range, SymbolInformation, SymbolKind, TextDocument, Uri, window, workspace } from "vscode"; import { getInstance } from "../base"; -import IBMi from "@halcyontech/vscode-ibmi-types/api/IBMi"; -import { ConnectionConfig, IBMiMember } from "@halcyontech/vscode-ibmi-types"; export function getServerSymbolProvider() { let latestFetch: ExportInfo[]|undefined; @@ -18,7 +18,7 @@ export function getServerSymbolProvider() { if (instance && instance.getConnection()) { const connection = instance.getConnection(); - const config = connection.config! //TODO in vscode-ibmi 3.0.0 - change to getConfig() + const config = connection.getConfig(); let member: IBMiMember|undefined; @@ -71,7 +71,7 @@ export function getServerImplementationProvider() { if (connection) { const word = document.getText(document.getWordRangeAtPosition(position)); - const config = connection.getConfig() //TODO in vscode-ibmi 3.0.0 - change to getConfig() + const config = connection.getConfig(); const uriPath = document.uri.path; const member = connection.parserMemberPath(uriPath); diff --git a/extension/client/src/linter.ts b/extension/client/src/linter.ts index de5a97ab..feba00b4 100644 --- a/extension/client/src/linter.ts +++ b/extension/client/src/linter.ts @@ -1,8 +1,10 @@ import path = require('path'); import { commands, ExtensionContext, Uri, ViewColumn, window, workspace } from 'vscode'; -import {getInstance} from './base'; +import { getInstance } from './base'; -import {DEFAULT_SCHEMA} from "./schemas/linter" +import { DEFAULT_SCHEMA } from "./schemas/linter" + +const GLOBAL_LINT_CONFIG_PATH = `/etc/vscode/rpglint.json`; export function initialise(context: ExtensionContext) { context.subscriptions.push( @@ -13,25 +15,19 @@ export function initialise(context: ExtensionContext) { let exists = false; if (editor && ![`member`, `streamfile`].includes(editor.document.uri.scheme)) { + // Local workspace file — existing behaviour unchanged const workspaces = workspace.workspaceFolders; if (workspaces && workspaces.length > 0) { const linter = await workspace.findFiles(`**/.vscode/rpglint.json`, `**/.git`, 1); let uri; if (linter && linter.length > 0) { uri = linter[0]; - - console.log(`Uri path: ${JSON.stringify(uri)}`); - } else { - console.log(`String path: ${path.join(workspaces[0].uri.fsPath, `.vscode`, `rpglint.json`)}`); - uri = Uri.from({ scheme: `file`, path: path.join(workspaces[0].uri.fsPath, `.vscode`, `rpglint.json`) }); - console.log(`Creating Uri path: ${JSON.stringify(uri)}`); - await workspace.fs.writeFile( uri, Buffer.from(JSON.stringify(DEFAULT_SCHEMA, null, 2), `utf8`) @@ -49,100 +45,109 @@ export function initialise(context: ExtensionContext) { const connection = instance.getConnection(); const content = instance.getContent(); - /** @type {"member"|"streamfile"} */ - let type = `member`; - let configPath: string | undefined; - - if (filter && filter.description) { - // Bad way to get the library for the filter .. - const library: string = (filter.description.split(`/`)[0]).toLocaleUpperCase(); - - if (library.includes(`*`)) { - window.showErrorMessage(`Cannot show lint config for a library filter.`); - return; - } - - configPath = `${library}/VSCODE/RPGLINT.JSON`; - - exists = (await connection.runCommand({ - command: `QSYS/CHKOBJ OBJ(${library}/VSCODE) OBJTYPE(*FILE) MBR(RPGLINT)`, - noLibList: true - })).code === 0; - - } else if (editor) { - //@ts-ignore - type = editor.document.uri.scheme; + // Check if global lint config is enabled + const useGlobal = workspace.getConfiguration(`vscode-rpgle`).get(`useGlobalLintConfig`, false); - console.log(`Uri remote path: ${JSON.stringify(editor.document.uri)}`); - - switch (type) { - case `member`: - const memberPath = parseMemberUri(editor.document.uri.path); - const cleanString = [ - memberPath.library, - `VSCODE`, - `RPGLINT.JSON` - ].join(`/`); - - const memberUri = Uri.from({ - scheme: `member`, - path: cleanString - }); + if (useGlobal) { + // Global IFS mode — use /etc/vscode/rpglint.json + const globalExists = await content.testStreamFile(GLOBAL_LINT_CONFIG_PATH, `r`); - configPath = memberUri.path; - - exists = (await connection.runCommand({ - command: `QSYS/CHKOBJ OBJ(${memberPath.library!.toLocaleUpperCase()}/VSCODE) OBJTYPE(*FILE) MBR(RPGLINT)`, - noLibList: true - })).code === 0; - break; + if (globalExists) { + await commands.executeCommand(`code-for-ibmi.openEditable`, GLOBAL_LINT_CONFIG_PATH); + } else { + const answer = await window.showInformationMessage( + `Global lint config does not exist at ${GLOBAL_LINT_CONFIG_PATH}. Would you like to create it?`, + `Yes`, `No` + ); - case `streamfile`: - const config = instance.getConfig(); - if (config.homeDirectory) { - configPath = path.posix.join(config.homeDirectory, `.vscode`, `rpglint.json`) - exists = await content.testStreamFile(configPath, `r`); + if (answer === `Yes`) { + const jsonString = JSON.stringify(DEFAULT_SCHEMA, null, 2); + try { + await content.writeStreamfile(GLOBAL_LINT_CONFIG_PATH, jsonString); + await commands.executeCommand(`code-for-ibmi.openEditable`, GLOBAL_LINT_CONFIG_PATH); + } catch (e) { + console.log(e); + window.showErrorMessage(`Failed to create global lint config at ${GLOBAL_LINT_CONFIG_PATH}. Ensure /etc/vscode/ directory exists.`); } - break; + } } } else { - window.showErrorMessage(`No active editor found.`); - } - - if (configPath) { - console.log(`Current path: ${configPath}`); - - if (exists) { - await commands.executeCommand(`code-for-ibmi.openEditable`, configPath); + // Per-library mode — existing behaviour + let type: `member` | `streamfile` = `member`; + let configPath: string | undefined; + + if (filter && filter.description) { + const library: string = (filter.description.split(`/`)[0]).toLocaleUpperCase(); + + if (library.includes(`*`)) { + window.showErrorMessage(`Cannot show lint config for a library filter.`); + return; + } + + configPath = `/${library}/VSCODE/RPGLINT.JSON`; + + exists = (await connection.runCommand({ + command: `QSYS/CHKOBJ OBJ(${library}/VSCODE) OBJTYPE(*FILE) MBR(RPGLINT)`, + noLibList: true + })).code === 0; + + } else if (editor) { + //@ts-ignore + type = editor.document.uri.scheme; + + switch (type) { + case `member`: + const memberPath = parseMemberUri(editor.document.uri.path); + const library = memberPath.library!.toLocaleUpperCase(); + + configPath = `/${library}/VSCODE/RPGLINT.JSON`; + + exists = (await connection.runCommand({ + command: `QSYS/CHKOBJ OBJ(${library}/VSCODE) OBJTYPE(*FILE) MBR(RPGLINT)`, + noLibList: true + })).code === 0; + break; + + case `streamfile`: + const config = instance.getConfig(); + if (config.homeDirectory) { + configPath = path.posix.join(config.homeDirectory, `.vscode`, `rpglint.json`) + exists = await content.testStreamFile(configPath, `r`); + } + break; + } } else { - window.showErrorMessage(`RPGLE linter config doesn't exist for this file. Would you like to create a default at ${configPath}?`, `Yes`, `No`).then - (async (value) => { + window.showErrorMessage(`No active editor found.`); + } + + if (configPath) { + if (exists) { + await commands.executeCommand(`code-for-ibmi.openEditable`, configPath); + } else { + window.showErrorMessage( + `RPGLE linter config doesn't exist. Would you like to create a default at ${configPath}?`, + `Yes`, `No` + ).then(async (value) => { if (value === `Yes`) { const jsonString = JSON.stringify(DEFAULT_SCHEMA, null, 2); switch (type) { case `member`: if (configPath) { - const memberPath = configPath.split(`/`); + const pathParts = configPath.split(`/`); + // pathParts = ['', 'LIBRARY', 'VSCODE', 'RPGLINT.JSON'] + const lib = pathParts[1]; - // Will not crash, even if it fails await connection.runCommand( - { - 'command': `QSYS/CRTSRCPF FILE(${memberPath[0]}/VSCODE) RCDLEN(112)` - } + { 'command': `QSYS/CRTSRCPF FILE(${lib}/VSCODE) RCDLEN(112)` } ); - // Will not crash, even if it fails await connection.runCommand( - { - command: `QSYS/ADDPFM FILE(${memberPath[0]}/VSCODE) MBR(RPGLINT) SRCTYPE(JSON)` - } + { command: `QSYS/ADDPFM FILE(${lib}/VSCODE) MBR(RPGLINT) SRCTYPE(JSON)` } ); try { - console.log(`Member path: ${[memberPath[0], `VSCODE`, `RPGLINT`].join(`/`)}`); - - await content.uploadMemberContent(undefined, memberPath[0], `VSCODE`, `RPGLINT`, jsonString); + await content.uploadMemberContent(undefined, lib, `VSCODE`, `RPGLINT`, jsonString); await commands.executeCommand(`code-for-ibmi.openEditable`, configPath); } catch (e) { console.log(e); @@ -152,8 +157,6 @@ export function initialise(context: ExtensionContext) { break; case `streamfile`: - console.log(`IFS path: ${configPath}`); - try { await content.writeStreamfile(configPath, jsonString); await commands.executeCommand(`code-for-ibmi.openEditable`, configPath); @@ -165,9 +168,10 @@ export function initialise(context: ExtensionContext) { } } }); + } + } else { + window.showErrorMessage(`No lint config path for this file. File must either be a member or a streamfile on the host IBM i.`); } - } else { - window.showErrorMessage(`No lint config path for this file. File must either be a member or a streamfile on the host IBM i.`); } } else { window.showErrorMessage(`Not connected to a system.`); @@ -176,7 +180,7 @@ export function initialise(context: ExtensionContext) { ) } -function parseMemberUri(fullPath: string): {asp?: string, library?: string, file?: string, name: string} { +function parseMemberUri(fullPath: string): { asp?: string, library?: string, file?: string, name: string } { const parts = fullPath.split(`/`).map(s => s.split(`,`)).flat().filter(s => s.length >= 1); return { name: path.parse(parts[parts.length - 1]).name, diff --git a/extension/server/src/providers/linter/index.ts b/extension/server/src/providers/linter/index.ts index ae09e502..c1eaac13 100644 --- a/extension/server/src/providers/linter/index.ts +++ b/extension/server/src/providers/linter/index.ts @@ -99,6 +99,18 @@ enum ResolvedState { }; let boundLintConfig: {[workingUri: string]: {resolved: ResolvedState, uri: string}} = {}; +const ignoredLibraries = (process.env.LINT_IGNORE_LIBRARIES || ``) + .split(`;`) + .map(lib => lib.trim().toUpperCase()) + .filter(Boolean); + +function shouldLintUri(uri: string) { + const parsed = URI.parse(uri); + if (parsed.scheme !== `member`) return true; + + const memberPath = parseMemberUri(parsed.path); + return !(memberPath.library && ignoredLibraries.includes(memberPath.library.toUpperCase())); +} export async function getLintConfigUri(workingUri: string) { const uri = URI.parse(workingUri); @@ -110,23 +122,57 @@ export async function getLintConfigUri(workingUri: string) { return cached.resolved === ResolvedState.Found ? cached.uri : undefined; } + if (uri.scheme === `member`) { + const globalPath = process.env.GLOBAL_LINT_CONFIG_PATH; + if (globalPath) { + cleanString = URI.from({ scheme: `member`, path: globalPath }).toString(); + cleanString = await validateUri(cleanString, `member`); + if (cleanString) { + boundLintConfig[workingUri] = { + resolved: ResolvedState.Found, + uri: cleanString + }; + return cleanString; + } + } + } + switch (uri.scheme) { case `member`: - const memberPath = parseMemberUri(uri.path); - cleanString = [ - ``, - memberPath.library, - `VSCODE`, - `RPGLINT.JSON` - ].join(`/`); - - cleanString = URI.from({ - scheme: `member`, - path: cleanString - }).toString(); - - if (jsonCache[cleanString]) return cleanString; - cleanString = await validateUri(cleanString); + // Check if global lint config is enabled + let useGlobal = false; + try { + const config = await connection.workspace.getConfiguration('vscode-rpgle'); + useGlobal = config?.useGlobalLintConfig === true; + } catch (e) { /* default to per-library */ } + + if (useGlobal) { + // Use /etc/vscode/rpglint.json via streamfile scheme + cleanString = URI.from({ + scheme: `streamfile`, + path: `/etc/vscode/rpglint.json` + }).toString(); + + if (jsonCache[cleanString]) return cleanString; + cleanString = await validateUri(cleanString, `streamfile`); + } else { + // Per-library config (existing behaviour) + const memberPath = parseMemberUri(uri.path); + cleanString = [ + ``, + memberPath.library, + `VSCODE`, + `RPGLINT.JSON` + ].join(`/`); + + cleanString = URI.from({ + scheme: `member`, + path: cleanString + }).toString(); + + if (jsonCache[cleanString]) return cleanString; + cleanString = await validateUri(cleanString); + } break; case `streamfile`: @@ -188,6 +234,26 @@ export async function getLintOptions(workingUri: string): Promise { const hintDiagnositcs: (keyof Rules)[] = [`SQLRunner`, `StringLiteralDupe`] export async function refreshLinterDiagnostics(document: TextDocument, docs: Cache, updateDiagnostics = true) { + // Check if this member's library is excluded from linting + const docUri = URI.parse(document.uri); + if (docUri.scheme === `member`) { + try { + const config = await connection.workspace.getConfiguration('vscode-rpgle'); + const excludeLibraries: string[] = config?.lintExcludeLibraries || []; + if (excludeLibraries.length > 0) { + const memberPath = parseMemberUri(docUri.path); + const memberLib = (memberPath.library || ``).toUpperCase(); + const excluded = excludeLibraries.map((l: string) => l.trim().toUpperCase()); + if (excluded.includes(memberLib)) { + if (updateDiagnostics) { + connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); + } + return; + } + } + } catch (e) { /* continue with linting if config read fails */ } + } + const isFree = (document.getText(Range.create(0, 0, 0, 6)).toUpperCase() === `**FREE`); if (isFree) { const text = document.getText(); diff --git a/package-lock.json b/package-lock.json index a0d63053..99182e26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "hasInstallScript": true, "license": "MIT", "devDependencies": { - "@halcyontech/vscode-ibmi-types": "^2.11.0", - "@types/node": "^20.19.41", + "@halcyontech/vscode-ibmi-types": "^3.0.10", + "@types/node": "^18.16.1", "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.0", "esbuild-loader": "^4.4.3", @@ -605,11 +605,10 @@ } }, "node_modules/@halcyontech/vscode-ibmi-types": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@halcyontech/vscode-ibmi-types/-/vscode-ibmi-types-2.18.0.tgz", - "integrity": "sha512-gHoLJW+wFG3uuGxnwXmCn7xRg5v4zqidAZmq6W/SS2m8v6CUl5ZXEb2S653gB1yY0VzQG8F2azD6CXw7EFT0dw==", - "dev": true, - "license": "ISC" + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@halcyontech/vscode-ibmi-types/-/vscode-ibmi-types-3.0.10.tgz", + "integrity": "sha512-8rby5IOfJYNOLf62P0pYqY3tXTbiR18jAD1Jtwm7BfN3+kfJ4XM2v3Fg/Lc5jLzBo8Ql1UMLemaEjOqcbCuB6g==", + "dev": true }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", diff --git a/package.json b/package.json index fd4104b3..43e38f76 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,19 @@ "default": true, "description": "Whether to show the fixed-format ruler by default." }, + "vscode-rpgle.useGlobalLintConfig": { + "type": "boolean", + "default": false, + "description": "When enabled, uses the global lint configuration at /etc/vscode/rpglint.json on the IBM i instead of per-library configs." + }, + "vscode-rpgle.lintExcludeLibraries": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of library names to exclude from linting. Members in these libraries will not be linted." + }, "vscode-rpgle.logLevel": { "type": "string", "default": "info", @@ -179,8 +192,8 @@ "cli:dev:rpglint": "cd cli/rpglint && npm run webpack:dev" }, "devDependencies": { - "@halcyontech/vscode-ibmi-types": "^2.11.0", - "@types/node": "^20.19.41", + "@halcyontech/vscode-ibmi-types": "^3.0.10", + "@types/node": "^18.16.1", "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.0", "esbuild-loader": "^4.4.3",