From e5a4ec9902f6606bfc03e13fe050e7faed453979 Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:28:38 +0000 Subject: [PATCH 1/2] feat: add shellcheckExternalSources config to make --external-sources optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The --external-sources flag is currently hardcoded in every ShellCheck invocation. On projects with many shell scripts that cross-source each other (e.g. 100+ scripts with source-path=SCRIPTDIR in .shellcheckrc), this causes exponential AST expansion — individual shellcheck processes grow to 3-5 GB RSS and never terminate. Adds a shellcheckExternalSources boolean config option (default: true for backward compatibility) and SHELLCHECK_EXTERNAL_SOURCES env var. When set to false, --external-sources is omitted from the ShellCheck invocation, preventing the unbounded memory growth. Fixes #874 Fixes #1375 --- server/src/config.ts | 8 +++++++- server/src/server.ts | 5 ++++- server/src/shellcheck/index.ts | 7 +++++-- vscode-client/package.json | 7 ++++++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/server/src/config.ts b/server/src/config.ts index cf6fd99f9..408b4f667 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -24,7 +24,12 @@ export const ConfigSchema = z.object({ // If true, then all symbols from the workspace are included. includeAllWorkspaceSymbols: z.boolean().default(false), - // Additional ShellCheck arguments. Note that we already add the following arguments: --shell, --format, --external-sources." + // Controls whether ShellCheck is invoked with --external-sources. When enabled (default), + // ShellCheck follows source directives to lint referenced files. On projects with many + // cross-sourcing scripts this can cause unbounded memory growth. Set to false to disable. + shellcheckExternalSources: z.boolean().default(true), + + // Additional ShellCheck arguments. Note that we already add the following arguments: --shell, --format, and --external-sources (if shellcheckExternalSources is true). shellcheckArguments: z .preprocess((arg) => { let argsList: string[] = [] @@ -87,6 +92,7 @@ export function getConfigFromEnvironmentVariables(): { includeAllWorkspaceSymbols: toBoolean(process.env.INCLUDE_ALL_WORKSPACE_SYMBOLS), logLevel: process.env[LOG_LEVEL_ENV_VAR], shellcheckArguments: process.env.SHELLCHECK_ARGUMENTS, + shellcheckExternalSources: toBoolean(process.env.SHELLCHECK_EXTERNAL_SOURCES), shellcheckPath: process.env.SHELLCHECK_PATH, shfmt: { path: process.env.SHFMT_PATH, diff --git a/server/src/server.ts b/server/src/server.ts index 33e9ae81d..800b633dd 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -277,7 +277,10 @@ export default class BashServer { logger.info('ShellCheck linting is disabled as "shellcheckPath" was not set') this.linter = undefined } else { - this.linter = new Linter({ executablePath: shellcheckPath }) + this.linter = new Linter({ + executablePath: shellcheckPath, + externalSources: this.config.shellcheckExternalSources, + }) } const shfmtPath = this.config.shfmt?.path diff --git a/server/src/shellcheck/index.ts b/server/src/shellcheck/index.ts index 4aa7ee15b..c55bce636 100644 --- a/server/src/shellcheck/index.ts +++ b/server/src/shellcheck/index.ts @@ -33,6 +33,7 @@ function safeFileURLToPath(uri: string): string | null { type LinterOptions = { executablePath: string cwd?: string + externalSources?: boolean } export type LintingResult = { @@ -43,15 +44,17 @@ export type LintingResult = { export class Linter { private cwd: string public executablePath: string + private externalSources: boolean private uriToDebouncedExecuteLint: { [uri: string]: InstanceType['executeLint'] } private _canLint: boolean - constructor({ cwd, executablePath }: LinterOptions) { + constructor({ cwd, executablePath, externalSources = true }: LinterOptions) { this._canLint = true this.cwd = cwd || process.cwd() this.executablePath = executablePath + this.externalSources = externalSources this.uriToDebouncedExecuteLint = Object.create(null) } @@ -139,7 +142,7 @@ export class Linter { const args = [ '--format=json1', - '--external-sources', + ...(this.externalSources ? ['--external-sources'] : []), ...sourcePathsArgs, ...additionalArgs, ] diff --git a/vscode-client/package.json b/vscode-client/package.json index fd3a934f6..dd47393a9 100644 --- a/vscode-client/package.json +++ b/vscode-client/package.json @@ -73,10 +73,15 @@ "default": "shellcheck", "description": "Controls the executable used for ShellCheck linting information. An empty string will disable linting." }, + "bashIde.shellcheckExternalSources": { + "type": "boolean", + "default": true, + "description": "Controls whether ShellCheck is invoked with --external-sources. When enabled (default), ShellCheck follows source directives to lint referenced files. On projects with many cross-sourcing scripts this can cause unbounded memory growth. Set to false to disable." + }, "bashIde.shellcheckArguments": { "type": "string", "default": "", - "description": "Additional ShellCheck arguments. Note that we already add the following arguments: --shell, --format, --external-sources." + "description": "Additional ShellCheck arguments. Note that we already add the following arguments: --shell, --format, and --external-sources (if shellcheckExternalSources is true)." }, "bashIde.shfmt.path": { "type": "string", From e46e4a6961b0c47c059b80d45f70cf6683d8c869 Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:08:13 +0000 Subject: [PATCH 2/2] test: update inline snapshots to include shellcheckExternalSources --- server/src/__tests__/config.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/__tests__/config.test.ts b/server/src/__tests__/config.test.ts index edfeab43e..64f6d6b31 100644 --- a/server/src/__tests__/config.test.ts +++ b/server/src/__tests__/config.test.ts @@ -12,6 +12,7 @@ describe('ConfigSchema', () => { "includeAllWorkspaceSymbols": false, "logLevel": "info", "shellcheckArguments": [], + "shellcheckExternalSources": true, "shellcheckPath": "shellcheck", "shfmt": { "binaryNextLine": false, @@ -62,6 +63,7 @@ describe('ConfigSchema', () => { "-e", "SC2002", ], + "shellcheckExternalSources": true, "shellcheckPath": "", "shfmt": { "binaryNextLine": true, @@ -99,6 +101,7 @@ describe('getConfigFromEnvironmentVariables', () => { "includeAllWorkspaceSymbols": false, "logLevel": "info", "shellcheckArguments": [], + "shellcheckExternalSources": true, "shellcheckPath": "shellcheck", "shfmt": { "binaryNextLine": false, @@ -130,6 +133,7 @@ describe('getConfigFromEnvironmentVariables', () => { "includeAllWorkspaceSymbols": false, "logLevel": "info", "shellcheckArguments": [], + "shellcheckExternalSources": true, "shellcheckPath": "", "shfmt": { "binaryNextLine": false, @@ -170,6 +174,7 @@ describe('getConfigFromEnvironmentVariables', () => { "-e", "SC2001", ], + "shellcheckExternalSources": true, "shellcheckPath": "/path/to/shellcheck", "shfmt": { "binaryNextLine": false,