@pgsql/lint 18.1.1 → 18.2.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/README.md +38 -0
- package/changed.d.ts +41 -0
- package/changed.js +159 -0
- package/cli.d.ts +6 -4
- package/cli.js +158 -16
- package/config.d.ts +50 -0
- package/config.js +157 -0
- package/esm/changed.js +121 -0
- package/esm/cli.js +125 -16
- package/esm/config.js +118 -0
- package/esm/file-runner.js +6 -5
- package/esm/ignore.js +101 -0
- package/esm/index.js +3 -0
- package/esm/linter.js +2 -1
- package/file-runner.d.ts +16 -3
- package/file-runner.js +6 -5
- package/ignore.d.ts +22 -0
- package/ignore.js +138 -0
- package/index.d.ts +6 -1
- package/index.js +13 -1
- package/linter.d.ts +4 -0
- package/linter.js +2 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -46,11 +46,49 @@ pgsql-lint . --rules no-dynamic-sql # only some rules
|
|
|
46
46
|
pgsql-lint . --warn require-qualified-refs # downgrade to a warning (won't fail)
|
|
47
47
|
pgsql-lint . --off C2 # disable a rule (by id or code)
|
|
48
48
|
pgsql-lint . --json # machine-readable
|
|
49
|
+
pgsql-lint . --ignore 'sql/,**/generated/**' # exclude generated trees
|
|
50
|
+
pgsql-lint --changed # only .sql that this branch touched
|
|
51
|
+
pgsql-lint --changed origin/main # …against an explicit base
|
|
49
52
|
```
|
|
50
53
|
|
|
51
54
|
Exit code is `1` when any **error**-severity (and non-waived) finding remains,
|
|
52
55
|
`0` otherwise — drop it straight into CI. `--warn` findings print but don't fail.
|
|
53
56
|
|
|
57
|
+
### Changed files only
|
|
58
|
+
|
|
59
|
+
`--changed[=<base>]` lints just the `.sql` files a branch touched, so a CI gate
|
|
60
|
+
costs a second instead of scanning the whole tree. The base defaults to the pull
|
|
61
|
+
request's base branch (`$GITHUB_BASE_REF`) and otherwise to the repository's
|
|
62
|
+
default branch; the diff is taken against `git merge-base HEAD <base>`, so
|
|
63
|
+
commits landed on the base branch afterwards don't widen the set. Uncommitted and
|
|
64
|
+
untracked changes are included, deleted/renamed-away paths are dropped, and a
|
|
65
|
+
shallow clone or detached checkout (no resolvable merge base) falls back to the
|
|
66
|
+
working-tree diff against `HEAD`. Nothing changed is an exit-0 pass.
|
|
67
|
+
|
|
68
|
+
### Config file
|
|
69
|
+
|
|
70
|
+
`.pgsqllintrc.json`, discovered by walking up from the working directory (or
|
|
71
|
+
passed with `--config <file>`; `--no-config` skips discovery). The keys mirror the
|
|
72
|
+
flags, and any flag overrides the file:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"extends": "./ci/lint-base.json",
|
|
77
|
+
"paths": ["packages", "application/app"],
|
|
78
|
+
"ignore": ["sql/", "application/constructive/", "**/generated/**"],
|
|
79
|
+
"warn": ["require-qualified-refs"],
|
|
80
|
+
"off": ["C2"]
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`paths` supplies the default targets when none are given on the command line, so
|
|
85
|
+
`pgsql-lint` and `pgsql-lint --changed` need no arguments. `extends` names another
|
|
86
|
+
config *file* — a path relative to the file that declared it, or an npm module —
|
|
87
|
+
and the inheriting file wins key by key. Ignore patterns are gitignore-flavoured
|
|
88
|
+
globs relative to the config file's directory: `*` within a segment, `**` across
|
|
89
|
+
segments, a plain path excludes the whole subtree, and an unanchored pattern
|
|
90
|
+
matches at any segment boundary (`/` anchors it to the root).
|
|
91
|
+
|
|
54
92
|
## Suppressions
|
|
55
93
|
|
|
56
94
|
ESLint / Prettier-style comments, authored in the function body (they survive
|
package/changed.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Changed-file detection for `--changed`, so a CI gate (or a human before a
|
|
3
|
+
* commit) pays only for the SQL that a branch actually touched.
|
|
4
|
+
*
|
|
5
|
+
* Modelled on pgpm's bundle-drift check (`pgpm/core/src/packaging/check.ts` in
|
|
6
|
+
* constructive): resolve a base ref (explicit → `origin/$GITHUB_BASE_REF` in a
|
|
7
|
+
* PR → the repository's default branch), diff `HEAD` against the **merge base**
|
|
8
|
+
* so unrelated commits on the base branch don't widen the set, and union that
|
|
9
|
+
* with uncommitted/untracked working-tree changes. Deleted paths are dropped —
|
|
10
|
+
* there is nothing left on disk to lint.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the ref to diff against. An explicit `base` wins; otherwise the PR
|
|
14
|
+
* base branch (`origin/$GITHUB_BASE_REF`) when running in GitHub Actions;
|
|
15
|
+
* otherwise the repository's default branch. `undefined` means "no base" and
|
|
16
|
+
* the caller falls back to working-tree changes only.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveChangedBase(base?: string, cwd?: string): string | undefined;
|
|
19
|
+
export interface ChangedFilesResult {
|
|
20
|
+
/** Absolute paths of changed files that still exist on disk. */
|
|
21
|
+
files: string[];
|
|
22
|
+
/** The base ref used, if any. */
|
|
23
|
+
base?: string;
|
|
24
|
+
/** The merge base actually diffed against, if one was resolvable. */
|
|
25
|
+
mergeBase?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Collect the files that differ from `base` (via `git merge-base`) plus any
|
|
29
|
+
* uncommitted/untracked working-tree changes. Falls back to `git diff HEAD`
|
|
30
|
+
* when no base is resolvable or no merge base exists — a shallow clone or a
|
|
31
|
+
* detached CI checkout — rather than failing the run.
|
|
32
|
+
*/
|
|
33
|
+
export declare function changedFiles(options?: {
|
|
34
|
+
cwd?: string;
|
|
35
|
+
base?: string;
|
|
36
|
+
}): ChangedFilesResult;
|
|
37
|
+
/** {@link changedFiles}, narrowed to `.sql`. */
|
|
38
|
+
export declare function changedSqlFiles(options?: {
|
|
39
|
+
cwd?: string;
|
|
40
|
+
base?: string;
|
|
41
|
+
}): ChangedFilesResult;
|
package/changed.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Changed-file detection for `--changed`, so a CI gate (or a human before a
|
|
4
|
+
* commit) pays only for the SQL that a branch actually touched.
|
|
5
|
+
*
|
|
6
|
+
* Modelled on pgpm's bundle-drift check (`pgpm/core/src/packaging/check.ts` in
|
|
7
|
+
* constructive): resolve a base ref (explicit → `origin/$GITHUB_BASE_REF` in a
|
|
8
|
+
* PR → the repository's default branch), diff `HEAD` against the **merge base**
|
|
9
|
+
* so unrelated commits on the base branch don't widen the set, and union that
|
|
10
|
+
* with uncommitted/untracked working-tree changes. Deleted paths are dropped —
|
|
11
|
+
* there is nothing left on disk to lint.
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.resolveChangedBase = resolveChangedBase;
|
|
48
|
+
exports.changedFiles = changedFiles;
|
|
49
|
+
exports.changedSqlFiles = changedSqlFiles;
|
|
50
|
+
const child_process_1 = require("child_process");
|
|
51
|
+
const fs_1 = require("fs");
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
function git(args, cwd) {
|
|
54
|
+
return (0, child_process_1.execFileSync)('git', args, {
|
|
55
|
+
cwd,
|
|
56
|
+
encoding: 'utf-8',
|
|
57
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
58
|
+
maxBuffer: 64 * 1024 * 1024
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function tryGit(args, cwd) {
|
|
62
|
+
try {
|
|
63
|
+
return git(args, cwd);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** The repository's default branch as a remote-tracking ref, when discoverable. */
|
|
70
|
+
function defaultBranch(cwd) {
|
|
71
|
+
const head = tryGit(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], cwd);
|
|
72
|
+
if (head && head.trim())
|
|
73
|
+
return head.trim();
|
|
74
|
+
for (const candidate of ['origin/main', 'origin/master', 'main', 'master']) {
|
|
75
|
+
if (tryGit(['rev-parse', '--verify', '--quiet', candidate], cwd))
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the ref to diff against. An explicit `base` wins; otherwise the PR
|
|
82
|
+
* base branch (`origin/$GITHUB_BASE_REF`) when running in GitHub Actions;
|
|
83
|
+
* otherwise the repository's default branch. `undefined` means "no base" and
|
|
84
|
+
* the caller falls back to working-tree changes only.
|
|
85
|
+
*/
|
|
86
|
+
function resolveChangedBase(base, cwd = process.cwd()) {
|
|
87
|
+
if (base && base.trim())
|
|
88
|
+
return base.trim();
|
|
89
|
+
const prBase = process.env.GITHUB_BASE_REF;
|
|
90
|
+
if (prBase && prBase.trim()) {
|
|
91
|
+
const ref = `origin/${prBase.trim()}`;
|
|
92
|
+
if (tryGit(['rev-parse', '--verify', '--quiet', ref], cwd))
|
|
93
|
+
return ref;
|
|
94
|
+
return prBase.trim();
|
|
95
|
+
}
|
|
96
|
+
return defaultBranch(cwd);
|
|
97
|
+
}
|
|
98
|
+
/** Parse `git status --porcelain` into paths (rename target wins). */
|
|
99
|
+
function workingTreePaths(cwd) {
|
|
100
|
+
const out = [];
|
|
101
|
+
// `-uall` lists untracked *files*; the default collapses a new directory to
|
|
102
|
+
// the directory name, which would hide every file a new module adds.
|
|
103
|
+
const status = tryGit(['status', '--porcelain', '-uall'], cwd) ?? '';
|
|
104
|
+
for (const rawLine of status.split('\n')) {
|
|
105
|
+
const line = rawLine.trimEnd();
|
|
106
|
+
if (!line)
|
|
107
|
+
continue;
|
|
108
|
+
let p = line.slice(3);
|
|
109
|
+
const arrow = p.indexOf(' -> ');
|
|
110
|
+
if (arrow !== -1)
|
|
111
|
+
p = p.slice(arrow + 4);
|
|
112
|
+
p = p.replace(/^"|"$/g, '');
|
|
113
|
+
if (p)
|
|
114
|
+
out.push(p);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Collect the files that differ from `base` (via `git merge-base`) plus any
|
|
120
|
+
* uncommitted/untracked working-tree changes. Falls back to `git diff HEAD`
|
|
121
|
+
* when no base is resolvable or no merge base exists — a shallow clone or a
|
|
122
|
+
* detached CI checkout — rather than failing the run.
|
|
123
|
+
*/
|
|
124
|
+
function changedFiles(options = {}) {
|
|
125
|
+
const cwd = options.cwd ?? process.cwd();
|
|
126
|
+
if (!tryGit(['rev-parse', '--git-dir'], cwd)) {
|
|
127
|
+
throw new Error(`--changed needs a git repository; ${cwd} is not inside one`);
|
|
128
|
+
}
|
|
129
|
+
const files = new Set(workingTreePaths(cwd));
|
|
130
|
+
const base = resolveChangedBase(options.base, cwd);
|
|
131
|
+
let mergeBase;
|
|
132
|
+
if (base) {
|
|
133
|
+
const found = tryGit(['merge-base', 'HEAD', base], cwd);
|
|
134
|
+
mergeBase = found?.trim() || undefined;
|
|
135
|
+
}
|
|
136
|
+
// No base, or no common ancestor (shallow clone / detached checkout): the
|
|
137
|
+
// uncommitted diff against HEAD is all the history we can see.
|
|
138
|
+
const diffArgs = mergeBase
|
|
139
|
+
? ['diff', '--name-only', '--diff-filter=ACMR', mergeBase, 'HEAD']
|
|
140
|
+
: ['diff', '--name-only', '--diff-filter=ACMR', 'HEAD'];
|
|
141
|
+
for (const rawLine of (tryGit(diffArgs, cwd) ?? '').split('\n')) {
|
|
142
|
+
const p = rawLine.trim();
|
|
143
|
+
if (p)
|
|
144
|
+
files.add(p);
|
|
145
|
+
}
|
|
146
|
+
const abs = [];
|
|
147
|
+
for (const rel of files) {
|
|
148
|
+
const full = path.resolve(cwd, rel);
|
|
149
|
+
// Deleted or renamed-away paths have nothing left to lint.
|
|
150
|
+
if ((0, fs_1.existsSync)(full) && (0, fs_1.statSync)(full).isFile())
|
|
151
|
+
abs.push(full);
|
|
152
|
+
}
|
|
153
|
+
return { files: abs.sort(), base, mergeBase };
|
|
154
|
+
}
|
|
155
|
+
/** {@link changedFiles}, narrowed to `.sql`. */
|
|
156
|
+
function changedSqlFiles(options = {}) {
|
|
157
|
+
const result = changedFiles(options);
|
|
158
|
+
return { ...result, files: result.files.filter((f) => f.toLowerCase().endsWith('.sql')) };
|
|
159
|
+
}
|
package/cli.d.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `pgsql-lint` — lint SQL source files for the convention rules.
|
|
4
4
|
*
|
|
5
|
-
* pgsql-lint <path…> [--
|
|
5
|
+
* pgsql-lint <path…> [--changed[=<base>]] [--ignore <globs>] [--rules a,b]
|
|
6
|
+
* [--warn a,b] [--off a,b] [--config <file>] [--json] [--quiet]
|
|
6
7
|
*
|
|
7
8
|
* Paths may be files or directories (directories are scanned recursively for
|
|
8
|
-
* `.sql`). Exit code is 1 when any
|
|
9
|
-
* otherwise — so it drops straight into a
|
|
10
|
-
* downgraded with `--warn` print but never
|
|
9
|
+
* `.sql`), and default to `paths` from the config file. Exit code is 1 when any
|
|
10
|
+
* *error*-severity finding remains, 0 otherwise — so it drops straight into a
|
|
11
|
+
* pre-commit hook or CI step. Findings downgraded with `--warn` print but never
|
|
12
|
+
* fail the run.
|
|
11
13
|
*/
|
|
12
14
|
export {};
|
package/cli.js
CHANGED
|
@@ -3,19 +3,58 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* `pgsql-lint` — lint SQL source files for the convention rules.
|
|
5
5
|
*
|
|
6
|
-
* pgsql-lint <path…> [--
|
|
6
|
+
* pgsql-lint <path…> [--changed[=<base>]] [--ignore <globs>] [--rules a,b]
|
|
7
|
+
* [--warn a,b] [--off a,b] [--config <file>] [--json] [--quiet]
|
|
7
8
|
*
|
|
8
9
|
* Paths may be files or directories (directories are scanned recursively for
|
|
9
|
-
* `.sql`). Exit code is 1 when any
|
|
10
|
-
* otherwise — so it drops straight into a
|
|
11
|
-
* downgraded with `--warn` print but never
|
|
10
|
+
* `.sql`), and default to `paths` from the config file. Exit code is 1 when any
|
|
11
|
+
* *error*-severity finding remains, 0 otherwise — so it drops straight into a
|
|
12
|
+
* pre-commit hook or CI step. Findings downgraded with `--warn` print but never
|
|
13
|
+
* fail the run.
|
|
12
14
|
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
13
48
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
49
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
50
|
};
|
|
16
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
52
|
const chalk_1 = __importDefault(require("chalk"));
|
|
53
|
+
const fs_1 = require("fs");
|
|
18
54
|
const minimist_1 = __importDefault(require("minimist"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const changed_1 = require("./changed");
|
|
57
|
+
const config_1 = require("./config");
|
|
19
58
|
const file_runner_1 = require("./file-runner");
|
|
20
59
|
const rules_1 = require("./rules");
|
|
21
60
|
const HELP = `pgsql-lint — source-level SQL/PL/pgSQL convention linter
|
|
@@ -25,17 +64,29 @@ Usage:
|
|
|
25
64
|
|
|
26
65
|
Arguments:
|
|
27
66
|
path One or more .sql files or directories (scanned recursively).
|
|
67
|
+
Defaults to "paths" from the config file.
|
|
28
68
|
|
|
29
69
|
Options:
|
|
70
|
+
--changed[=<base>] Lint only .sql files that differ from <base> (default: the
|
|
71
|
+
PR base branch, else the repository default branch), using
|
|
72
|
+
the merge base, plus working-tree changes. Exits 0 when
|
|
73
|
+
nothing changed.
|
|
74
|
+
--ignore <globs> Exclude paths (comma-separated, repeatable).
|
|
30
75
|
--rules <ids> Comma-separated rule ids/codes to run (default: all).
|
|
31
76
|
--warn <ids> Report these rules as warnings (do not fail the run).
|
|
32
77
|
--off <ids> Disable these rules entirely.
|
|
33
78
|
--keyword <kw> Suppression directive keyword (default: pgsql-lint,safegres).
|
|
79
|
+
--config <file> Config file to use (default: nearest .pgsqllintrc.json).
|
|
80
|
+
--no-config Ignore any config file.
|
|
34
81
|
--json Emit findings as JSON.
|
|
35
82
|
--quiet Only print active findings (hide acknowledged waivers).
|
|
36
83
|
-h, --help Show this help.
|
|
37
84
|
-v, --version Show version.
|
|
38
85
|
|
|
86
|
+
Config file (.pgsqllintrc.json, discovered upward from cwd; flags override it):
|
|
87
|
+
{ "extends": "./base.json", "rules": [], "warn": [], "off": [],
|
|
88
|
+
"ignore": ["sql/", "**/generated/**"], "keyword": [], "paths": ["packages"] }
|
|
89
|
+
|
|
39
90
|
Rules:
|
|
40
91
|
${rules_1.LINT_RULES.map((r) => ` ${r.code} ${r.id} — ${r.title}`).join('\n')}
|
|
41
92
|
|
|
@@ -43,33 +94,114 @@ Suppress inline (in the function body):
|
|
|
43
94
|
-- pgsql-lint-disable-next-line no-dynamic-sql -- lookup-only: <why>
|
|
44
95
|
`;
|
|
45
96
|
function csv(v) {
|
|
46
|
-
if (
|
|
97
|
+
if (v === undefined)
|
|
47
98
|
return undefined;
|
|
48
|
-
const parts =
|
|
99
|
+
const parts = (Array.isArray(v) ? v : [v])
|
|
100
|
+
.flatMap((s) => s.split(','))
|
|
101
|
+
.map((s) => s.trim())
|
|
102
|
+
.filter(Boolean);
|
|
49
103
|
return parts.length > 0 ? parts : undefined;
|
|
50
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Own version. In the repo the compiled CLI sits in `dist/`, but the published
|
|
107
|
+
* package flattens `dist/` to its root, so `../package.json` only resolves in
|
|
108
|
+
* one of the two layouts — try both rather than crashing on `--version`.
|
|
109
|
+
*/
|
|
110
|
+
function packageVersion() {
|
|
111
|
+
for (const candidate of ['../package.json', './package.json']) {
|
|
112
|
+
try {
|
|
113
|
+
return require(candidate).version;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Wrong layout; try the next candidate.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return 'unknown';
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `--changed` takes an *optional* value, which minimist cannot express: with
|
|
123
|
+
* `string: ['changed']` it would swallow a following path. Bind the next token
|
|
124
|
+
* only when it is not a flag and not an existing path — i.e. when it reads as a
|
|
125
|
+
* git ref — and otherwise rewrite the flag to an empty value.
|
|
126
|
+
*/
|
|
127
|
+
function normalizeChanged(args) {
|
|
128
|
+
const out = [];
|
|
129
|
+
for (let i = 0; i < args.length; i++) {
|
|
130
|
+
if (args[i] !== '--changed') {
|
|
131
|
+
out.push(args[i]);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const next = args[i + 1];
|
|
135
|
+
if (next && !next.startsWith('-') && !(0, fs_1.existsSync)(next)) {
|
|
136
|
+
out.push(`--changed=${next}`);
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
out.push('--changed=');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
51
145
|
async function main() {
|
|
52
|
-
const
|
|
146
|
+
const raw = process.argv.slice(2);
|
|
147
|
+
// Handled here rather than as a minimist boolean: `config` also takes a value.
|
|
148
|
+
const noConfig = raw.includes('--no-config');
|
|
149
|
+
const argv = (0, minimist_1.default)(normalizeChanged(raw.filter((a) => a !== '--no-config')), {
|
|
53
150
|
boolean: ['json', 'quiet', 'help', 'version'],
|
|
54
|
-
string: ['rules', 'warn', 'off', 'keyword'],
|
|
151
|
+
string: ['rules', 'warn', 'off', 'keyword', 'changed', 'ignore', 'config'],
|
|
55
152
|
alias: { h: 'help', v: 'version' }
|
|
56
153
|
});
|
|
57
154
|
if (argv.version) {
|
|
58
|
-
console.log(
|
|
155
|
+
console.log(packageVersion());
|
|
59
156
|
return;
|
|
60
157
|
}
|
|
61
|
-
|
|
158
|
+
const cwd = process.cwd();
|
|
159
|
+
const loaded = noConfig
|
|
160
|
+
? { config: {} }
|
|
161
|
+
: (0, config_1.loadLintConfig)({ cwd, configFile: argv.config || undefined });
|
|
162
|
+
const fileConfig = loaded.config;
|
|
163
|
+
const baseDir = (0, config_1.configDir)(loaded, cwd);
|
|
164
|
+
const useChanged = argv.changed !== undefined;
|
|
165
|
+
const paths = argv._.length > 0
|
|
166
|
+
? argv._
|
|
167
|
+
: (fileConfig.paths ?? []).map((p) => path.resolve(baseDir, p));
|
|
168
|
+
if (argv.help || (!useChanged && paths.length === 0)) {
|
|
62
169
|
console.log(HELP);
|
|
63
|
-
process.exit(argv.
|
|
170
|
+
process.exit(argv.help ? 0 : 1);
|
|
64
171
|
}
|
|
65
|
-
const rules = csv(argv.rules);
|
|
66
|
-
const keyword = csv(argv.keyword);
|
|
172
|
+
const rules = csv(argv.rules) ?? fileConfig.rules;
|
|
173
|
+
const keyword = csv(argv.keyword) ?? (fileConfig.keyword ? csv(fileConfig.keyword) : undefined);
|
|
174
|
+
const ignore = csv(argv.ignore) ?? fileConfig.ignore;
|
|
67
175
|
const severity = {};
|
|
68
|
-
for (const id of csv(argv.warn) ?? [])
|
|
176
|
+
for (const id of csv(argv.warn) ?? fileConfig.warn ?? [])
|
|
69
177
|
severity[id] = 'warn';
|
|
70
|
-
for (const id of csv(argv.off) ?? [])
|
|
178
|
+
for (const id of csv(argv.off) ?? fileConfig.off ?? [])
|
|
71
179
|
severity[id] = 'off';
|
|
72
|
-
|
|
180
|
+
let targets;
|
|
181
|
+
if (useChanged) {
|
|
182
|
+
const changed = (0, changed_1.changedSqlFiles)({ cwd, base: argv.changed || undefined });
|
|
183
|
+
// Nothing to lint is a pass, not an error — the common case on a branch
|
|
184
|
+
// that touched no SQL at all.
|
|
185
|
+
if (changed.files.length === 0) {
|
|
186
|
+
if (argv.json)
|
|
187
|
+
console.log('[]');
|
|
188
|
+
else
|
|
189
|
+
console.log(chalk_1.default.green('0 errors'), chalk_1.default.gray('(no changed .sql files)'));
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
targets = paths.length > 0 ? withinPaths(changed.files, paths) : changed.files;
|
|
193
|
+
if (targets.length === 0) {
|
|
194
|
+
if (argv.json)
|
|
195
|
+
console.log('[]');
|
|
196
|
+
else
|
|
197
|
+
console.log(chalk_1.default.green('0 errors'), chalk_1.default.gray('(no changed .sql files under the given paths)'));
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
targets = paths;
|
|
203
|
+
}
|
|
204
|
+
const reports = await (0, file_runner_1.lintFiles)(targets, { rules, keyword, severity, ignore, cwd: baseDir });
|
|
73
205
|
if (argv.json) {
|
|
74
206
|
console.log(JSON.stringify(reports, null, 2));
|
|
75
207
|
}
|
|
@@ -109,6 +241,16 @@ async function main() {
|
|
|
109
241
|
}
|
|
110
242
|
process.exit(errors > 0 ? 1 : 0);
|
|
111
243
|
}
|
|
244
|
+
/** Keep only changed files that sit inside one of the requested paths. */
|
|
245
|
+
function withinPaths(files, paths) {
|
|
246
|
+
const roots = paths.map((p) => path.resolve(p));
|
|
247
|
+
return files.filter((file) => roots.some((root) => {
|
|
248
|
+
if (file === root)
|
|
249
|
+
return true;
|
|
250
|
+
const rel = path.relative(root, file);
|
|
251
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
112
254
|
function formatFinding(f) {
|
|
113
255
|
const loc = chalk_1.default.gray(`${String(f.line).padStart(4)}`);
|
|
114
256
|
const tag = f.acknowledged
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config-file support — so a repository states its lint policy once, in a file,
|
|
3
|
+
* instead of repeating flags in every workflow step and every shell history.
|
|
4
|
+
*
|
|
5
|
+
* The keys mirror the CLI flags exactly (`rules`, `warn`, `off`, `ignore`,
|
|
6
|
+
* `keyword`, `paths`), and a flag always overrides the file. Discovery walks up
|
|
7
|
+
* from `cwd` looking for `.pgsqllintrc.json` / `.pgsqllintrc`.
|
|
8
|
+
*
|
|
9
|
+
* `extends` names another config *file* (a relative path resolved against the
|
|
10
|
+
* file that declared it, or a resolvable npm module). This package has no
|
|
11
|
+
* built-in presets — its rule set is injected as values, not discovered by name
|
|
12
|
+
* — so unlike safegres's `extends: "safegres:constructive"` there is nothing to
|
|
13
|
+
* name but a file.
|
|
14
|
+
*/
|
|
15
|
+
/** The shape of `.pgsqllintrc.json`. Every key mirrors a CLI flag. */
|
|
16
|
+
export interface LintConfigFile {
|
|
17
|
+
/** Another config file to inherit from (path, or npm module). */
|
|
18
|
+
extends?: string | string[];
|
|
19
|
+
/** Rule ids/codes to run (default: all). */
|
|
20
|
+
rules?: string[];
|
|
21
|
+
/** Rules reported as warnings. */
|
|
22
|
+
warn?: string[];
|
|
23
|
+
/** Rules disabled entirely. */
|
|
24
|
+
off?: string[];
|
|
25
|
+
/** Glob patterns to exclude. */
|
|
26
|
+
ignore?: string[];
|
|
27
|
+
/** Suppression directive keyword(s). */
|
|
28
|
+
keyword?: string | string[];
|
|
29
|
+
/** Default paths to lint when none are given on the command line. */
|
|
30
|
+
paths?: string[];
|
|
31
|
+
}
|
|
32
|
+
export declare const CONFIG_FILENAMES: readonly [".pgsqllintrc.json", ".pgsqllintrc"];
|
|
33
|
+
/** Walk up from `cwd` for the first config file. */
|
|
34
|
+
export declare function findConfigFile(cwd?: string): string | undefined;
|
|
35
|
+
export interface LoadedLintConfig {
|
|
36
|
+
config: LintConfigFile;
|
|
37
|
+
/** The file the config was read from, when one was found. */
|
|
38
|
+
filepath?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Load the effective config: an explicit `configFile`, else the nearest
|
|
42
|
+
* discovered one, else empty. Paths in `ignore`/`paths` stay as written — the
|
|
43
|
+
* caller resolves them against the config file's directory (see `configDir`).
|
|
44
|
+
*/
|
|
45
|
+
export declare function loadLintConfig(params?: {
|
|
46
|
+
cwd?: string;
|
|
47
|
+
configFile?: string;
|
|
48
|
+
}): LoadedLintConfig;
|
|
49
|
+
/** The directory a discovered config's relative paths resolve against. */
|
|
50
|
+
export declare function configDir(loaded: LoadedLintConfig, cwd?: string): string;
|