@codacy/tools-sqlint-0 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Codacy
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.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @codacy/tools-sqlint-0
2
+
3
+ ## Table of Contents
4
+
5
+ - [Overview](#overview)
6
+ - [Updating patterns](#updating-patterns)
7
+ - [Updating the SQLint version](#updating-the-sqlint-version)
8
+ - [Development](#development)
9
+ - [Notes for maintainers](#notes-for-maintainers)
10
+
11
+ ---
12
+
13
+ ## Overview
14
+
15
+ SQL syntax checker using the [SQLint](https://github.com/purcell/sqlint) Ruby gem. Uses the CLI execution strategy -- spawns `sqlint` via `spawnTool()` and parses its text output.
16
+
17
+ | Property | Value |
18
+ | ------------- | -------------------------------------- |
19
+ | Tool ID | `SQLint` |
20
+ | Codacy UUID | `25b6766b-06c6-4625-9df1-561d28386b5f` |
21
+ | Strategy | CLI |
22
+ | Languages | SQL, PLSQL |
23
+ | Binary | `sqlint` (installed via `gem install`) |
24
+ | File patterns | `**/*.sql` |
25
+
26
+ ## Updating patterns
27
+
28
+ ```bash
29
+ # Re-fetch pattern metadata from the Codacy API
30
+ pnpm prefetch
31
+
32
+ # Commit the result
33
+ git add src/patterns.json
34
+ ```
35
+
36
+ The `prefetch` script calls `packages/tools/scripts/fetch-patterns.mjs` with the tool UUID to download pattern metadata from the Codacy API.
37
+
38
+ SQLint has a single umbrella pattern: `SQLint_allIssues`. All syntax errors map to this one pattern.
39
+
40
+ ## Updating the SQLint version
41
+
42
+ 1. Update `SQLINT_VERSION` in `src/adapter.ts`
43
+ 2. Run `pnpm prefetch` to check for any changes
44
+ 3. Run `pnpm test` to verify compatibility
45
+ 4. If the major version changes, create a new adapter package (`sqlint-1/`)
46
+
47
+ ## Development
48
+
49
+ ```bash
50
+ pnpm build # Build with tsup
51
+ pnpm test # Run tests (requires ruby + sqlint in PATH for integration tests)
52
+ ```
53
+
54
+ To install sqlint locally for testing:
55
+
56
+ ```bash
57
+ gem install sqlint
58
+ ```
59
+
60
+ ## Notes for maintainers
61
+
62
+ - **PostgreSQL-only**: SQLint uses `pg_query` (libpg_query) under the hood, which means it only validates PostgreSQL dialect SQL. Non-PostgreSQL syntax may produce false positives.
63
+ - **Ruby gem dependency**: SQLint requires Ruby >= 2.7 as a runtime. The adapter uses `ensureRuby()` to download a portable Ruby if no system Ruby is found.
64
+ - **Native compilation**: The `pg_query` gem includes a native C extension that compiles libpg_query during installation. A C compiler (gcc/clang) and make are required on the system.
65
+ - Gems are installed in an isolated directory at `~/.codacy/runtimes/sqlint-0/gems/` via `GEM_HOME`/`GEM_PATH` environment variables.
66
+ - Output format is plain text, one finding per line: `file.sql:line:column:SEVERITY message`.
67
+ - Exit codes: 0 = no errors, 1 = errors found (both are success states for the adapter).
68
+ - SQLint has no configuration files -- it always checks all syntax.
69
+ - All findings map to the single umbrella pattern `SQLint_allIssues`.
@@ -0,0 +1,5 @@
1
+ import * as _codacy_tooling from '@codacy/tooling';
2
+
3
+ declare const _default: Readonly<_codacy_tooling.ToolAdapter>;
4
+
5
+ export { _default as default };
package/dist/index.js ADDED
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ default: () => index_default
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var import_tooling2 = require("@codacy/tooling");
37
+
38
+ // src/adapter.ts
39
+ var import_tooling = require("@codacy/tooling");
40
+ var import_path = __toESM(require("path"));
41
+ var import_promises = __toESM(require("fs/promises"));
42
+
43
+ // src/patterns.json
44
+ var patterns_default = [
45
+ {
46
+ id: "SQLint_allIssues",
47
+ category: "Compatibility",
48
+ severityLevel: "High",
49
+ title: "Enforce ANSI SQL Syntax Compliance",
50
+ enabled: true,
51
+ parameters: []
52
+ }
53
+ ];
54
+
55
+ // src/adapter.ts
56
+ var TOOL_ID = "SQLint";
57
+ var ADAPTER_ID = "sqlint-0";
58
+ var SQLINT_VERSION = "0.2.1";
59
+ var RUBY_VERSION_RANGE = ">=2.7.0";
60
+ var PATTERN_META = new Map(
61
+ patterns_default.map((p) => [
62
+ p.id,
63
+ {
64
+ category: p.category,
65
+ severity: p.severityLevel ?? "Info"
66
+ }
67
+ ])
68
+ );
69
+ var UMBRELLA_PATTERN = "SQLint_allIssues";
70
+ function ruleMetadata() {
71
+ return PATTERN_META.get(UMBRELLA_PATTERN) ?? { category: "Compatibility", severity: "High" };
72
+ }
73
+ var OUTPUT_LINE_REGEX = /^(.+?):(\d+):(\d+):(ERROR|WARNING)\s+(.+)$/;
74
+ function getLineContent(lines, lineNumber) {
75
+ return lines[lineNumber - 1] ?? "";
76
+ }
77
+ async function readFileLines(filePath) {
78
+ try {
79
+ const content = await import_promises.default.readFile(filePath, "utf-8");
80
+ return content.split("\n");
81
+ } catch {
82
+ return [];
83
+ }
84
+ }
85
+ function buildResult(startedAt, completedAt, issues, errors, filesAnalyzed) {
86
+ const durationMs = completedAt.getTime() - startedAt.getTime();
87
+ return {
88
+ metadata: {
89
+ startedAt: startedAt.toISOString(),
90
+ completedAt: completedAt.toISOString(),
91
+ durationMs
92
+ },
93
+ issues,
94
+ errors,
95
+ summary: {
96
+ toolId: TOOL_ID,
97
+ status: errors.some((e) => e.level === "error") ? "partial" : "success",
98
+ issueCount: issues.length,
99
+ errorCount: errors.length,
100
+ durationMs,
101
+ filesAnalyzed
102
+ }
103
+ };
104
+ }
105
+ async function resolveToolInvocation(ctx) {
106
+ const gemPaths = (0, import_tooling.getGemPaths)(ctx.globalDir, ADAPTER_ID);
107
+ const scriptPath = (0, import_tooling.resolveGemBinary)(gemPaths, "sqlint");
108
+ const portableRubyBin = (0, import_tooling.resolvePortableRubyBinary)(ctx.globalDir, "3.2.11");
109
+ try {
110
+ await import_promises.default.access(portableRubyBin);
111
+ await import_promises.default.access(scriptPath);
112
+ return { binary: portableRubyBin, prefixArgs: [scriptPath], rubyPath: portableRubyBin };
113
+ } catch {
114
+ }
115
+ const rubyInfo = await (0, import_tooling.findRuby)(RUBY_VERSION_RANGE).catch(() => null);
116
+ try {
117
+ await import_promises.default.access(scriptPath);
118
+ return {
119
+ binary: scriptPath,
120
+ prefixArgs: [],
121
+ rubyPath: rubyInfo?.path
122
+ };
123
+ } catch {
124
+ }
125
+ return { binary: "sqlint", prefixArgs: [], rubyPath: rubyInfo?.path };
126
+ }
127
+ var adapter = {
128
+ id: TOOL_ID,
129
+ displayName: "SQLint",
130
+ preferredVersion: SQLINT_VERSION,
131
+ compatibleVersions: ">=0.2.0",
132
+ website: "https://github.com/purcell/sqlint",
133
+ languages: ["SQL", "PLSQL"],
134
+ filePatterns: ["**/*.sql"],
135
+ supportedOS: ["linux", "darwin"],
136
+ supportedArch: ["x64", "arm64"],
137
+ supportedRunners: ["local", "container"],
138
+ prerequisites: [
139
+ {
140
+ name: "Ruby",
141
+ versionRange: RUBY_VERSION_RANGE,
142
+ preferredVersion: "3.2",
143
+ checkCommand: "ruby --version"
144
+ }
145
+ ],
146
+ /**
147
+ * Checks whether SQLint is available (Ruby runtime + sqlint gem).
148
+ *
149
+ * Checks:
150
+ * 1. Ruby runtime via findRuby
151
+ * 2. sqlint binary in the Codacy-managed gem directory or on PATH
152
+ */
153
+ async checkAvailability(ctx) {
154
+ ctx.logger.debug("Checking availability", { toolId: TOOL_ID });
155
+ const rubyInfo = await (0, import_tooling.findRuby)(RUBY_VERSION_RANGE, ctx.logger).catch(() => null);
156
+ let resolvedRubyPath = rubyInfo?.path;
157
+ const dependencies = rubyInfo ? [{ name: "Ruby", available: true, version: rubyInfo.version, installation: "global" }] : [{ name: "Ruby", available: false, reason: `Ruby (${RUBY_VERSION_RANGE}) not found` }];
158
+ if (!rubyInfo) {
159
+ const rubyBin = (0, import_tooling.resolvePortableRubyBinary)(ctx.globalDir, "3.2.11");
160
+ try {
161
+ await import_promises.default.access(rubyBin);
162
+ resolvedRubyPath = rubyBin;
163
+ dependencies[0] = {
164
+ name: "Ruby",
165
+ available: true,
166
+ version: "3.2.11",
167
+ installation: "codacy"
168
+ };
169
+ } catch {
170
+ return {
171
+ available: false,
172
+ reason: "Ruby runtime not found (required for SQLint)",
173
+ installHint: "Install Ruby 2.7+, or run with --install-dependencies for auto-download",
174
+ dependencies
175
+ };
176
+ }
177
+ }
178
+ const gemPaths = (0, import_tooling.getGemPaths)(ctx.globalDir, ADAPTER_ID);
179
+ const sqlintVersion = await (0, import_tooling.checkGemTool)(
180
+ gemPaths,
181
+ "sqlint",
182
+ "--version",
183
+ /(\d+\.\d+\.\d+)/,
184
+ resolvedRubyPath,
185
+ ctx.logger
186
+ );
187
+ if (sqlintVersion) {
188
+ return {
189
+ available: true,
190
+ version: sqlintVersion,
191
+ path: (0, import_tooling.resolveGemBinary)(gemPaths, "sqlint"),
192
+ installation: "codacy",
193
+ dependencies
194
+ };
195
+ }
196
+ try {
197
+ const { stdout, exitCode } = await (0, import_tooling.spawnTool)("sqlint", ["--version"], {
198
+ cwd: ctx.repositoryRoot,
199
+ logger: ctx.logger
200
+ });
201
+ if (exitCode === 0) {
202
+ const match = stdout.match(/(\d+\.\d+\.\d+)/);
203
+ if (match) {
204
+ return {
205
+ available: true,
206
+ version: match[1],
207
+ installation: "global",
208
+ dependencies
209
+ };
210
+ }
211
+ }
212
+ } catch {
213
+ }
214
+ return {
215
+ available: false,
216
+ reason: "SQLint not found in Codacy gem directory or on PATH",
217
+ installHint: "Run with --install-dependencies to auto-install, or run: gem install sqlint",
218
+ dependencies
219
+ };
220
+ },
221
+ /**
222
+ * Installs SQLint by ensuring a Ruby runtime is available and installing
223
+ * the sqlint gem into an isolated gem directory.
224
+ *
225
+ * Note: The sqlint gem depends on pg_query, which includes a native C
226
+ * extension. A C compiler is required on the system for installation.
227
+ */
228
+ async install(ctx) {
229
+ ctx.logger.info("Starting install", { toolId: TOOL_ID });
230
+ const ruby = await (0, import_tooling.ensureRuby)(ctx.globalDir, RUBY_VERSION_RANGE, ctx.logger);
231
+ if (!ruby) {
232
+ return {
233
+ success: false,
234
+ error: {
235
+ toolId: TOOL_ID,
236
+ phase: "requirementCheck",
237
+ kind: "MissingPrerequisite",
238
+ message: `Ruby (${RUBY_VERSION_RANGE}) could not be found or downloaded. SQLint requires a Ruby runtime.`,
239
+ level: "error"
240
+ }
241
+ };
242
+ }
243
+ try {
244
+ const gemPaths = (0, import_tooling.getGemPaths)(ctx.globalDir, ADAPTER_ID);
245
+ ctx.logger.info(`Installing SQLint ${SQLINT_VERSION} to ${gemPaths.gemHome}...`);
246
+ const prevConfigArgs = process.env.CONFIGURE_ARGS;
247
+ process.env.CONFIGURE_ARGS = `${prevConfigArgs ?? ""} --with-cflags=-DHAVE_STRCHRNUL`.trim();
248
+ try {
249
+ await (0, import_tooling.gemInstall)(gemPaths, ruby.path, ["google-protobuf:3.25.5"], ctx.logger);
250
+ await (0, import_tooling.gemInstall)(gemPaths, ruby.path, [`sqlint:${SQLINT_VERSION}`], ctx.logger);
251
+ } finally {
252
+ if (prevConfigArgs === void 0) {
253
+ delete process.env.CONFIGURE_ARGS;
254
+ } else {
255
+ process.env.CONFIGURE_ARGS = prevConfigArgs;
256
+ }
257
+ }
258
+ const installedPath = (0, import_tooling.resolveGemBinary)(gemPaths, "sqlint");
259
+ ctx.logger.info(`Installed SQLint to ${installedPath}`);
260
+ return { success: true, installedPath, installedVersion: SQLINT_VERSION };
261
+ } catch (err) {
262
+ return {
263
+ success: false,
264
+ error: {
265
+ toolId: TOOL_ID,
266
+ phase: "toolInstall",
267
+ kind: "InstallError",
268
+ message: `Failed to install SQLint: ${err instanceof Error ? err.message : String(err)}`,
269
+ level: "error"
270
+ }
271
+ };
272
+ }
273
+ },
274
+ /**
275
+ * SQLint has no configuration files -- it is a purely syntax-based checker.
276
+ * Always returns { found: false }.
277
+ */
278
+ async checkLocalConfigurationFile(_ctx) {
279
+ return { found: false };
280
+ },
281
+ /**
282
+ * Runs `sqlint` against the target SQL files and converts the text output
283
+ * into Codacy Issue objects.
284
+ *
285
+ * SQLint has no config generation and no rule selection -- it is a single
286
+ * umbrella pattern (`SQLint_allIssues`) that reports all syntax errors.
287
+ *
288
+ * Output format: one finding per line in the format:
289
+ * file.sql:line:column:SEVERITY message
290
+ *
291
+ * Exit codes:
292
+ * - 0: no errors found
293
+ * - 1: errors found (normal -- not treated as an error)
294
+ *
295
+ * Never throws -- all errors are captured in the returned `errors` array.
296
+ */
297
+ async analyze(ctx, codacyConfig) {
298
+ const startMs = Date.now();
299
+ ctx.logger.info("Starting analysis", { toolId: TOOL_ID, fileCount: ctx.targetFiles.length });
300
+ const startedAt = /* @__PURE__ */ new Date();
301
+ const issues = [];
302
+ const errors = [];
303
+ if (ctx.targetFiles.length === 0) {
304
+ return buildResult(startedAt, /* @__PURE__ */ new Date(), issues, errors, 0);
305
+ }
306
+ const enabledPatterns = new Set(codacyConfig.patterns.map((p) => p.patternId));
307
+ if (!codacyConfig.useLocalConfigurationFile && codacyConfig.patterns.length === 0) {
308
+ ctx.logger.info("Skipping analysis: no patterns enabled", { toolId: TOOL_ID });
309
+ return buildResult(startedAt, /* @__PURE__ */ new Date(), issues, errors, ctx.targetFiles.length);
310
+ }
311
+ const invocation = await resolveToolInvocation(ctx);
312
+ const absoluteFiles = ctx.targetFiles.map((f) => import_path.default.resolve(ctx.repositoryRoot, f));
313
+ const args = [...invocation.prefixArgs, ...absoluteFiles];
314
+ try {
315
+ const gemPaths = (0, import_tooling.getGemPaths)(ctx.globalDir, ADAPTER_ID);
316
+ const env = (0, import_tooling.buildGemEnv)(gemPaths, invocation.rubyPath);
317
+ const { stdout, stderr, exitCode } = await (0, import_tooling.spawnTool)(invocation.binary, args, {
318
+ cwd: ctx.repositoryRoot,
319
+ env,
320
+ logger: ctx.logger
321
+ });
322
+ if (exitCode !== 0 && exitCode !== 1) {
323
+ errors.push({
324
+ toolId: TOOL_ID,
325
+ phase: "toolInvoke",
326
+ kind: "NonZeroExit",
327
+ message: `sqlint exited with code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`,
328
+ level: "error"
329
+ });
330
+ return buildResult(startedAt, /* @__PURE__ */ new Date(), issues, errors, ctx.targetFiles.length);
331
+ }
332
+ const output = stdout + (stderr || "");
333
+ const outputLines = output.split("\n").filter((l) => l.trim());
334
+ const fileLineCache = /* @__PURE__ */ new Map();
335
+ const meta = ruleMetadata();
336
+ for (const outputLine of outputLines) {
337
+ const match = OUTPUT_LINE_REGEX.exec(outputLine);
338
+ if (!match) continue;
339
+ const [, rawFilePath, lineStr, colStr, , message] = match;
340
+ const line = parseInt(lineStr, 10);
341
+ const column = parseInt(colStr, 10);
342
+ let filePath = rawFilePath;
343
+ if (import_path.default.isAbsolute(filePath)) {
344
+ filePath = import_path.default.relative(ctx.repositoryRoot, filePath);
345
+ }
346
+ if (enabledPatterns.size > 0 && !enabledPatterns.has(UMBRELLA_PATTERN)) {
347
+ continue;
348
+ }
349
+ const absPath = import_path.default.resolve(ctx.repositoryRoot, filePath);
350
+ if (!fileLineCache.has(absPath)) {
351
+ fileLineCache.set(absPath, await readFileLines(absPath));
352
+ }
353
+ const fileLines = fileLineCache.get(absPath);
354
+ issues.push({
355
+ filePath,
356
+ patternId: UMBRELLA_PATTERN,
357
+ toolId: TOOL_ID,
358
+ line,
359
+ column,
360
+ message,
361
+ lineContent: getLineContent(fileLines, line),
362
+ multilineContent: (0, import_tooling.getMultilineContent)(fileLines, line, void 0),
363
+ category: meta.category,
364
+ severity: meta.severity
365
+ });
366
+ }
367
+ } catch (invokeErr) {
368
+ errors.push({
369
+ toolId: TOOL_ID,
370
+ phase: "toolInvoke",
371
+ kind: "InvocationError",
372
+ message: `Failed to run SQLint: ${invokeErr instanceof Error ? invokeErr.message : String(invokeErr)}`,
373
+ level: "error"
374
+ });
375
+ }
376
+ const durationMs = Date.now() - startMs;
377
+ ctx.logger.info("Analysis complete", {
378
+ toolId: TOOL_ID,
379
+ issueCount: issues.length,
380
+ errorCount: errors.length,
381
+ durationMs
382
+ });
383
+ return buildResult(startedAt, /* @__PURE__ */ new Date(), issues, errors, ctx.targetFiles.length);
384
+ }
385
+ };
386
+
387
+ // src/index.ts
388
+ var index_default = (0, import_tooling2.defineToolAdapter)(adapter);
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@codacy/tools-sqlint-0",
3
+ "version": "0.3.0",
4
+ "description": "SQLint adapter — CLI-mode SQL syntax checker",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "dependencies": {
11
+ "@codacy/tooling": "0.3.0"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Codacy <support@codacy.com> (https://www.codacy.com)",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/codacy/analysis-cli.git",
18
+ "directory": "packages/tools/sqlint-0"
19
+ },
20
+ "homepage": "https://github.com/codacy/analysis-cli",
21
+ "engines": {
22
+ "node": ">=20.0.0"
23
+ },
24
+ "scripts": {
25
+ "build": "tsup src/index.ts --format cjs --dts --clean",
26
+ "test": "vitest run",
27
+ "lint": "eslint src/",
28
+ "clean": "rm -rf dist",
29
+ "prefetch": "node ../scripts/fetch-patterns.mjs 25b6766b-06c6-4625-9df1-561d28386b5f src/patterns.json"
30
+ }
31
+ }