@pgsql/lint 18.1.2 → 18.2.1

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/esm/cli.js CHANGED
@@ -2,15 +2,21 @@
2
2
  /**
3
3
  * `pgsql-lint` — lint SQL source files for the convention rules.
4
4
  *
5
- * pgsql-lint <path…> [--rules a,b] [--warn a,b] [--off a,b] [--json] [--quiet]
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 *error*-severity finding remains, 0
9
- * otherwise — so it drops straight into a pre-commit hook or CI step. Findings
10
- * downgraded with `--warn` print but never fail the run.
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
  import chalk from 'chalk';
15
+ import { existsSync } from 'fs';
13
16
  import minimist from 'minimist';
17
+ import * as path from 'path';
18
+ import { changedSqlFiles } from './changed';
19
+ import { configDir, loadLintConfig } from './config';
14
20
  import { lintFiles } from './file-runner';
15
21
  import { LINT_RULES } from './rules';
16
22
  const HELP = `pgsql-lint — source-level SQL/PL/pgSQL convention linter
@@ -20,17 +26,29 @@ Usage:
20
26
 
21
27
  Arguments:
22
28
  path One or more .sql files or directories (scanned recursively).
29
+ Defaults to "paths" from the config file.
23
30
 
24
31
  Options:
32
+ --changed[=<base>] Lint only .sql files that differ from <base> (default: the
33
+ PR base branch, else the repository default branch), using
34
+ the merge base, plus working-tree changes. Exits 0 when
35
+ nothing changed.
36
+ --ignore <globs> Exclude paths (comma-separated, repeatable).
25
37
  --rules <ids> Comma-separated rule ids/codes to run (default: all).
26
38
  --warn <ids> Report these rules as warnings (do not fail the run).
27
39
  --off <ids> Disable these rules entirely.
28
40
  --keyword <kw> Suppression directive keyword (default: pgsql-lint,safegres).
41
+ --config <file> Config file to use (default: nearest .pgsqllintrc.json).
42
+ --no-config Ignore any config file.
29
43
  --json Emit findings as JSON.
30
44
  --quiet Only print active findings (hide acknowledged waivers).
31
45
  -h, --help Show this help.
32
46
  -v, --version Show version.
33
47
 
48
+ Config file (.pgsqllintrc.json, discovered upward from cwd; flags override it):
49
+ { "extends": "./base.json", "rules": [], "warn": [], "off": [],
50
+ "ignore": ["sql/", "**/generated/**"], "keyword": [], "paths": ["packages"] }
51
+
34
52
  Rules:
35
53
  ${LINT_RULES.map((r) => ` ${r.code} ${r.id} — ${r.title}`).join('\n')}
36
54
 
@@ -38,33 +56,114 @@ Suppress inline (in the function body):
38
56
  -- pgsql-lint-disable-next-line no-dynamic-sql -- lookup-only: <why>
39
57
  `;
40
58
  function csv(v) {
41
- if (!v)
59
+ if (v === undefined)
42
60
  return undefined;
43
- const parts = v.split(',').map((s) => s.trim()).filter(Boolean);
61
+ const parts = (Array.isArray(v) ? v : [v])
62
+ .flatMap((s) => s.split(','))
63
+ .map((s) => s.trim())
64
+ .filter(Boolean);
44
65
  return parts.length > 0 ? parts : undefined;
45
66
  }
67
+ /**
68
+ * Own version. In the repo the compiled CLI sits in `dist/`, but the published
69
+ * package flattens `dist/` to its root, so `../package.json` only resolves in
70
+ * one of the two layouts — try both rather than crashing on `--version`.
71
+ */
72
+ function packageVersion() {
73
+ for (const candidate of ['../package.json', './package.json']) {
74
+ try {
75
+ return require(candidate).version;
76
+ }
77
+ catch {
78
+ // Wrong layout; try the next candidate.
79
+ }
80
+ }
81
+ return 'unknown';
82
+ }
83
+ /**
84
+ * `--changed` takes an *optional* value, which minimist cannot express: with
85
+ * `string: ['changed']` it would swallow a following path. Bind the next token
86
+ * only when it is not a flag and not an existing path — i.e. when it reads as a
87
+ * git ref — and otherwise rewrite the flag to an empty value.
88
+ */
89
+ function normalizeChanged(args) {
90
+ const out = [];
91
+ for (let i = 0; i < args.length; i++) {
92
+ if (args[i] !== '--changed') {
93
+ out.push(args[i]);
94
+ continue;
95
+ }
96
+ const next = args[i + 1];
97
+ if (next && !next.startsWith('-') && !existsSync(next)) {
98
+ out.push(`--changed=${next}`);
99
+ i++;
100
+ }
101
+ else {
102
+ out.push('--changed=');
103
+ }
104
+ }
105
+ return out;
106
+ }
46
107
  async function main() {
47
- const argv = minimist(process.argv.slice(2), {
108
+ const raw = process.argv.slice(2);
109
+ // Handled here rather than as a minimist boolean: `config` also takes a value.
110
+ const noConfig = raw.includes('--no-config');
111
+ const argv = minimist(normalizeChanged(raw.filter((a) => a !== '--no-config')), {
48
112
  boolean: ['json', 'quiet', 'help', 'version'],
49
- string: ['rules', 'warn', 'off', 'keyword'],
113
+ string: ['rules', 'warn', 'off', 'keyword', 'changed', 'ignore', 'config'],
50
114
  alias: { h: 'help', v: 'version' }
51
115
  });
52
116
  if (argv.version) {
53
- console.log(require('../package.json').version);
117
+ console.log(packageVersion());
54
118
  return;
55
119
  }
56
- if (argv.help || argv._.length === 0) {
120
+ const cwd = process.cwd();
121
+ const loaded = noConfig
122
+ ? { config: {} }
123
+ : loadLintConfig({ cwd, configFile: argv.config || undefined });
124
+ const fileConfig = loaded.config;
125
+ const baseDir = configDir(loaded, cwd);
126
+ const useChanged = argv.changed !== undefined;
127
+ const paths = argv._.length > 0
128
+ ? argv._
129
+ : (fileConfig.paths ?? []).map((p) => path.resolve(baseDir, p));
130
+ if (argv.help || (!useChanged && paths.length === 0)) {
57
131
  console.log(HELP);
58
- process.exit(argv._.length === 0 && !argv.help ? 1 : 0);
132
+ process.exit(argv.help ? 0 : 1);
59
133
  }
60
- const rules = csv(argv.rules);
61
- const keyword = csv(argv.keyword);
134
+ const rules = csv(argv.rules) ?? fileConfig.rules;
135
+ const keyword = csv(argv.keyword) ?? (fileConfig.keyword ? csv(fileConfig.keyword) : undefined);
136
+ const ignore = csv(argv.ignore) ?? fileConfig.ignore;
62
137
  const severity = {};
63
- for (const id of csv(argv.warn) ?? [])
138
+ for (const id of csv(argv.warn) ?? fileConfig.warn ?? [])
64
139
  severity[id] = 'warn';
65
- for (const id of csv(argv.off) ?? [])
140
+ for (const id of csv(argv.off) ?? fileConfig.off ?? [])
66
141
  severity[id] = 'off';
67
- const reports = await lintFiles(argv._, { rules, keyword, severity });
142
+ let targets;
143
+ if (useChanged) {
144
+ const changed = changedSqlFiles({ cwd, base: argv.changed || undefined });
145
+ // Nothing to lint is a pass, not an error — the common case on a branch
146
+ // that touched no SQL at all.
147
+ if (changed.files.length === 0) {
148
+ if (argv.json)
149
+ console.log('[]');
150
+ else
151
+ console.log(chalk.green('0 errors'), chalk.gray('(no changed .sql files)'));
152
+ process.exit(0);
153
+ }
154
+ targets = paths.length > 0 ? withinPaths(changed.files, paths) : changed.files;
155
+ if (targets.length === 0) {
156
+ if (argv.json)
157
+ console.log('[]');
158
+ else
159
+ console.log(chalk.green('0 errors'), chalk.gray('(no changed .sql files under the given paths)'));
160
+ process.exit(0);
161
+ }
162
+ }
163
+ else {
164
+ targets = paths;
165
+ }
166
+ const reports = await lintFiles(targets, { rules, keyword, severity, ignore, cwd: baseDir });
68
167
  if (argv.json) {
69
168
  console.log(JSON.stringify(reports, null, 2));
70
169
  }
@@ -104,6 +203,16 @@ async function main() {
104
203
  }
105
204
  process.exit(errors > 0 ? 1 : 0);
106
205
  }
206
+ /** Keep only changed files that sit inside one of the requested paths. */
207
+ function withinPaths(files, paths) {
208
+ const roots = paths.map((p) => path.resolve(p));
209
+ return files.filter((file) => roots.some((root) => {
210
+ if (file === root)
211
+ return true;
212
+ const rel = path.relative(root, file);
213
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
214
+ }));
215
+ }
107
216
  function formatFinding(f) {
108
217
  const loc = chalk.gray(`${String(f.line).padStart(4)}`);
109
218
  const tag = f.acknowledged
package/esm/config.js ADDED
@@ -0,0 +1,118 @@
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
+ import { existsSync, readFileSync } from 'fs';
16
+ import * as path from 'path';
17
+ export const CONFIG_FILENAMES = ['.pgsqllintrc.json', '.pgsqllintrc'];
18
+ const KNOWN_KEYS = new Set([
19
+ '$schema',
20
+ 'extends',
21
+ 'rules',
22
+ 'warn',
23
+ 'off',
24
+ 'ignore',
25
+ 'keyword',
26
+ 'paths'
27
+ ]);
28
+ /** Walk up from `cwd` for the first config file. */
29
+ export function findConfigFile(cwd = process.cwd()) {
30
+ let dir = path.resolve(cwd);
31
+ for (;;) {
32
+ for (const name of CONFIG_FILENAMES) {
33
+ const candidate = path.join(dir, name);
34
+ if (existsSync(candidate))
35
+ return candidate;
36
+ }
37
+ const parent = path.dirname(dir);
38
+ if (parent === dir)
39
+ return undefined;
40
+ dir = parent;
41
+ }
42
+ }
43
+ function readConfigFile(file) {
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(readFileSync(file, 'utf8'));
47
+ }
48
+ catch (err) {
49
+ throw new Error(`could not parse ${file}: ${err.message}`);
50
+ }
51
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
52
+ throw new Error(`${file}: expected a JSON object`);
53
+ }
54
+ const config = parsed;
55
+ const unknown = Object.keys(config).filter((k) => !KNOWN_KEYS.has(k));
56
+ if (unknown.length > 0) {
57
+ throw new Error(`${file}: unknown key(s) ${unknown.join(', ')} — expected any of ` +
58
+ `${[...KNOWN_KEYS].filter((k) => k !== '$schema').join(', ')}`);
59
+ }
60
+ return config;
61
+ }
62
+ /** Resolve an `extends` target: a path relative to `from`, or an npm module. */
63
+ function resolveExtends(target, from) {
64
+ const asPath = path.resolve(path.dirname(from), target);
65
+ for (const candidate of [asPath, `${asPath}.json`]) {
66
+ if (existsSync(candidate))
67
+ return candidate;
68
+ }
69
+ try {
70
+ return require.resolve(target, { paths: [path.dirname(from)] });
71
+ }
72
+ catch {
73
+ throw new Error(`${from}: could not resolve "extends": ${target} — it is read as a path ` +
74
+ 'relative to the file that declared it, or as an npm module');
75
+ }
76
+ }
77
+ /** Later layers win; array keys replace rather than merge. */
78
+ function merge(base, over) {
79
+ const out = { ...base, ...over };
80
+ delete out.extends;
81
+ return out;
82
+ }
83
+ function expand(file, seen) {
84
+ if (seen.has(file))
85
+ throw new Error(`circular "extends" chain at ${file}`);
86
+ seen.add(file);
87
+ const config = readConfigFile(file);
88
+ const parents = config.extends
89
+ ? Array.isArray(config.extends)
90
+ ? config.extends
91
+ : [config.extends]
92
+ : [];
93
+ let acc = {};
94
+ for (const parent of parents) {
95
+ acc = merge(acc, expand(resolveExtends(parent, file), seen));
96
+ }
97
+ return merge(acc, config);
98
+ }
99
+ /**
100
+ * Load the effective config: an explicit `configFile`, else the nearest
101
+ * discovered one, else empty. Paths in `ignore`/`paths` stay as written — the
102
+ * caller resolves them against the config file's directory (see `configDir`).
103
+ */
104
+ export function loadLintConfig(params = {}) {
105
+ const cwd = params.cwd ?? process.cwd();
106
+ const filepath = params.configFile
107
+ ? path.resolve(cwd, params.configFile)
108
+ : findConfigFile(cwd);
109
+ if (!filepath)
110
+ return { config: {} };
111
+ if (!existsSync(filepath))
112
+ throw new Error(`config file not found: ${filepath}`);
113
+ return { config: expand(filepath, new Set()), filepath };
114
+ }
115
+ /** The directory a discovered config's relative paths resolve against. */
116
+ export function configDir(loaded, cwd = process.cwd()) {
117
+ return loaded.filepath ? path.dirname(loaded.filepath) : cwd;
118
+ }
@@ -13,6 +13,7 @@ import { promises as fs } from 'fs';
13
13
  import * as path from 'path';
14
14
  import { parse } from 'pgsql-parser';
15
15
  import { lintDefinition } from './engine';
16
+ import { applyIgnore } from './ignore';
16
17
  import { findAll } from './walk';
17
18
  /** Count newlines before `offset` — the 0-based line the offset sits on. */
18
19
  function lineAtOffset(text, offset) {
@@ -154,7 +155,7 @@ async function sqlFilesUnder(dir) {
154
155
  return out;
155
156
  }
156
157
  /** Resolve a mix of file and directory paths to a sorted list of `.sql` files. */
157
- export async function resolveSqlFiles(paths) {
158
+ export async function resolveSqlFiles(paths, options = {}) {
158
159
  const files = new Set();
159
160
  for (const p of paths) {
160
161
  const st = await fs.stat(p);
@@ -166,11 +167,11 @@ export async function resolveSqlFiles(paths) {
166
167
  files.add(p);
167
168
  }
168
169
  }
169
- return [...files].sort();
170
+ return applyIgnore([...files].sort(), options.ignore, options.cwd);
170
171
  }
171
172
  /** Lint every `.sql` file reachable from `paths` (files or directories). */
172
173
  export async function lintFiles(paths, options = {}) {
173
- const files = await resolveSqlFiles(paths);
174
+ const files = await resolveSqlFiles(paths, { ignore: options.ignore, cwd: options.cwd });
174
175
  const reports = [];
175
176
  for (const file of files) {
176
177
  const source = await fs.readFile(file, 'utf8');
@@ -184,11 +185,11 @@ export function sqlTextAdapter(source, file) {
184
185
  return { id: 'sql-text', definitions: () => sqlTextDefinitions(source, file) };
185
186
  }
186
187
  /** A {@link SourceAdapter} over `.sql` files/directories on disk. */
187
- export function filesAdapter(paths) {
188
+ export function filesAdapter(paths, options = {}) {
188
189
  return {
189
190
  id: 'files',
190
191
  definitions: async () => {
191
- const files = await resolveSqlFiles(paths);
192
+ const files = await resolveSqlFiles(paths, options);
192
193
  const out = [];
193
194
  for (const file of files) {
194
195
  out.push(...(await sqlTextDefinitions(await fs.readFile(file, 'utf8'), file)));
package/esm/ignore.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Path exclusion for the file runner — `--ignore` / `ignore` in a config file.
3
+ *
4
+ * Most repositories that lint SQL on disk also *generate* SQL on disk
5
+ * (packaged modules, introspected schemas, codegen output), and linting
6
+ * generated output is noise. Patterns are gitignore-flavoured globs matched
7
+ * against the path relative to `cwd`:
8
+ *
9
+ * - `*` matches within a segment, `**` across segments, `?` one character.
10
+ * - A pattern with no glob character matches that path *and everything under
11
+ * it*, so `sql/` and `sql` both exclude `sql/app--1.0.0.sql`.
12
+ * - An unanchored pattern (no leading `/`) also matches at any segment
13
+ * boundary, so `generated/`, `**` patterns, and `*.gen.sql` all work.
14
+ * - A leading `/` anchors the pattern to `cwd`.
15
+ */
16
+ import * as path from 'path';
17
+ /** Turn a glob into a `RegExp` source anchored at both ends. */
18
+ function globToRegExpSource(glob) {
19
+ let out = '';
20
+ for (let i = 0; i < glob.length; i++) {
21
+ const ch = glob[i];
22
+ if (ch === '*') {
23
+ if (glob[i + 1] === '*') {
24
+ // `**/` may match zero segments, so `**/gen/**` also matches `gen/x`.
25
+ if (glob[i + 2] === '/') {
26
+ out += '(?:.*/)?';
27
+ i += 2;
28
+ }
29
+ else {
30
+ out += '.*';
31
+ i += 1;
32
+ }
33
+ }
34
+ else {
35
+ out += '[^/]*';
36
+ }
37
+ continue;
38
+ }
39
+ if (ch === '?') {
40
+ out += '[^/]';
41
+ continue;
42
+ }
43
+ out += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
44
+ }
45
+ return out;
46
+ }
47
+ function compile(pattern) {
48
+ let p = pattern.trim().replace(/\\/g, '/');
49
+ if (!p || p.startsWith('#'))
50
+ return null;
51
+ const anchored = p.startsWith('/');
52
+ if (anchored)
53
+ p = p.slice(1);
54
+ p = p.replace(/^\.\//, '');
55
+ const dirOnly = p.endsWith('/');
56
+ if (dirOnly)
57
+ p = p.replace(/\/+$/, '');
58
+ if (!p)
59
+ return null;
60
+ // A plain path (or an explicit directory) excludes the subtree under it.
61
+ const source = /[*?]/.test(p) && !dirOnly
62
+ ? globToRegExpSource(p)
63
+ : `${globToRegExpSource(p)}(?:/.*)?`;
64
+ return { re: new RegExp(`^${source}$`), anchored };
65
+ }
66
+ /**
67
+ * Build a predicate that answers "is this file ignored?". Paths may be
68
+ * absolute or relative; both are compared as `cwd`-relative POSIX paths.
69
+ */
70
+ export function makeIgnoreFilter(patterns = [], cwd = process.cwd()) {
71
+ const compiled = patterns.map(compile).filter((c) => c !== null);
72
+ if (compiled.length === 0)
73
+ return () => false;
74
+ return (file) => {
75
+ const rel = path.relative(cwd, path.resolve(cwd, file)).replace(/\\/g, '/');
76
+ // Outside cwd entirely — no relative pattern can meaningfully describe it.
77
+ if (rel.startsWith('../'))
78
+ return false;
79
+ const segments = rel.split('/');
80
+ for (const { re, anchored } of compiled) {
81
+ if (re.test(rel))
82
+ return true;
83
+ if (anchored)
84
+ continue;
85
+ // Unanchored: also try every suffix that starts at a segment boundary,
86
+ // so `generated/` matches `packages/x/generated/y.sql`.
87
+ for (let i = 1; i < segments.length; i++) {
88
+ if (re.test(segments.slice(i).join('/')))
89
+ return true;
90
+ }
91
+ }
92
+ return false;
93
+ };
94
+ }
95
+ /** Filter a list of files through {@link makeIgnoreFilter}. */
96
+ export function applyIgnore(files, patterns, cwd) {
97
+ if (!patterns || patterns.length === 0)
98
+ return files;
99
+ const ignored = makeIgnoreFilter(patterns, cwd);
100
+ return files.filter((f) => !ignored(f));
101
+ }
package/esm/index.js CHANGED
@@ -10,8 +10,11 @@
10
10
  * Rules are injected as values, never discovered by npm name: build a linter
11
11
  * with `createLinter({ rules, severity })` and pass your own rules in.
12
12
  */
13
+ export { changedFiles, changedSqlFiles, resolveChangedBase } from './changed';
14
+ export { CONFIG_FILENAMES, configDir, findConfigFile, loadLintConfig } from './config';
13
15
  export { lintDefinition, severityOf } from './engine';
14
16
  export { filesAdapter, lintFiles, lintSource, lintSqlText, resolveSqlFiles, sqlTextAdapter, sqlTextDefinitions } from './file-runner';
17
+ export { applyIgnore, makeIgnoreFilter } from './ignore';
15
18
  export { createLinter } from './linter';
16
19
  export { parseUnit } from './parse-unit';
17
20
  export { LINT_RULES, LINT_RULES_BY_CODE, LINT_RULES_BY_ID, noDynamicSql, noSetSearchPath, noVariableConflict, requireQualifiedRefs } from './rules';
package/esm/linter.js CHANGED
@@ -17,6 +17,7 @@ export function createLinter(config = {}) {
17
17
  const rules = config.rules ?? LINT_RULES;
18
18
  const severity = config.severity ?? {};
19
19
  const keyword = config.keyword ?? DEFAULT_KEYWORDS;
20
+ const { ignore, cwd } = config;
20
21
  const bind = (options = {}) => ({
21
22
  ruleSet: rules,
22
23
  severity,
@@ -29,7 +30,7 @@ export function createLinter(config = {}) {
29
30
  keyword,
30
31
  lintDefinition: (text, language, name, options) => lintDefinition(text, language, name, bind(options)),
31
32
  lintSqlText: (source, options) => lintSqlText(source, bind(options)),
32
- lintFiles: (paths, options) => lintFiles(paths, bind(options)),
33
+ lintFiles: (paths, options) => lintFiles(paths, { ...bind(options), ignore, cwd }),
33
34
  lintSource: (adapter, options) => lintSource(adapter, bind(options))
34
35
  };
35
36
  }
package/file-runner.d.ts CHANGED
@@ -51,14 +51,27 @@ export declare function sqlTextDefinitions(source: string, file?: string): Promi
51
51
  * re-anchored to absolute lines within `source`.
52
52
  */
53
53
  export declare function lintSqlText(source: string, options?: LintOptions): Promise<FileReport>;
54
+ /** File-selection options layered on top of the lint options. */
55
+ export interface FileLintOptions extends LintOptions {
56
+ /** Glob patterns to exclude (see `makeIgnoreFilter`). */
57
+ ignore?: string[];
58
+ /** Directory the `ignore` patterns are relative to (default `process.cwd()`). */
59
+ cwd?: string;
60
+ }
54
61
  /** Resolve a mix of file and directory paths to a sorted list of `.sql` files. */
55
- export declare function resolveSqlFiles(paths: string[]): Promise<string[]>;
62
+ export declare function resolveSqlFiles(paths: string[], options?: {
63
+ ignore?: string[];
64
+ cwd?: string;
65
+ }): Promise<string[]>;
56
66
  /** Lint every `.sql` file reachable from `paths` (files or directories). */
57
- export declare function lintFiles(paths: string[], options?: LintOptions): Promise<FileReport[]>;
67
+ export declare function lintFiles(paths: string[], options?: FileLintOptions): Promise<FileReport[]>;
58
68
  /** A {@link SourceAdapter} over an in-memory SQL string. */
59
69
  export declare function sqlTextAdapter(source: string, file?: string): SourceAdapter;
60
70
  /** A {@link SourceAdapter} over `.sql` files/directories on disk. */
61
- export declare function filesAdapter(paths: string[]): SourceAdapter;
71
+ export declare function filesAdapter(paths: string[], options?: {
72
+ ignore?: string[];
73
+ cwd?: string;
74
+ }): SourceAdapter;
62
75
  /**
63
76
  * Lint every definition an adapter yields, grouped into one {@link FileReport}
64
77
  * per origin file. This is the generic entry point: `@pgsql/lint` ships file
package/file-runner.js CHANGED
@@ -55,6 +55,7 @@ const fs_1 = require("fs");
55
55
  const path = __importStar(require("path"));
56
56
  const pgsql_parser_1 = require("pgsql-parser");
57
57
  const engine_1 = require("./engine");
58
+ const ignore_1 = require("./ignore");
58
59
  const walk_1 = require("./walk");
59
60
  /** Count newlines before `offset` — the 0-based line the offset sits on. */
60
61
  function lineAtOffset(text, offset) {
@@ -196,7 +197,7 @@ async function sqlFilesUnder(dir) {
196
197
  return out;
197
198
  }
198
199
  /** Resolve a mix of file and directory paths to a sorted list of `.sql` files. */
199
- async function resolveSqlFiles(paths) {
200
+ async function resolveSqlFiles(paths, options = {}) {
200
201
  const files = new Set();
201
202
  for (const p of paths) {
202
203
  const st = await fs_1.promises.stat(p);
@@ -208,11 +209,11 @@ async function resolveSqlFiles(paths) {
208
209
  files.add(p);
209
210
  }
210
211
  }
211
- return [...files].sort();
212
+ return (0, ignore_1.applyIgnore)([...files].sort(), options.ignore, options.cwd);
212
213
  }
213
214
  /** Lint every `.sql` file reachable from `paths` (files or directories). */
214
215
  async function lintFiles(paths, options = {}) {
215
- const files = await resolveSqlFiles(paths);
216
+ const files = await resolveSqlFiles(paths, { ignore: options.ignore, cwd: options.cwd });
216
217
  const reports = [];
217
218
  for (const file of files) {
218
219
  const source = await fs_1.promises.readFile(file, 'utf8');
@@ -226,11 +227,11 @@ function sqlTextAdapter(source, file) {
226
227
  return { id: 'sql-text', definitions: () => sqlTextDefinitions(source, file) };
227
228
  }
228
229
  /** A {@link SourceAdapter} over `.sql` files/directories on disk. */
229
- function filesAdapter(paths) {
230
+ function filesAdapter(paths, options = {}) {
230
231
  return {
231
232
  id: 'files',
232
233
  definitions: async () => {
233
- const files = await resolveSqlFiles(paths);
234
+ const files = await resolveSqlFiles(paths, options);
234
235
  const out = [];
235
236
  for (const file of files) {
236
237
  out.push(...(await sqlTextDefinitions(await fs_1.promises.readFile(file, 'utf8'), file)));
package/ignore.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Path exclusion for the file runner — `--ignore` / `ignore` in a config file.
3
+ *
4
+ * Most repositories that lint SQL on disk also *generate* SQL on disk
5
+ * (packaged modules, introspected schemas, codegen output), and linting
6
+ * generated output is noise. Patterns are gitignore-flavoured globs matched
7
+ * against the path relative to `cwd`:
8
+ *
9
+ * - `*` matches within a segment, `**` across segments, `?` one character.
10
+ * - A pattern with no glob character matches that path *and everything under
11
+ * it*, so `sql/` and `sql` both exclude `sql/app--1.0.0.sql`.
12
+ * - An unanchored pattern (no leading `/`) also matches at any segment
13
+ * boundary, so `generated/`, `**` patterns, and `*.gen.sql` all work.
14
+ * - A leading `/` anchors the pattern to `cwd`.
15
+ */
16
+ /**
17
+ * Build a predicate that answers "is this file ignored?". Paths may be
18
+ * absolute or relative; both are compared as `cwd`-relative POSIX paths.
19
+ */
20
+ export declare function makeIgnoreFilter(patterns?: string[], cwd?: string): (file: string) => boolean;
21
+ /** Filter a list of files through {@link makeIgnoreFilter}. */
22
+ export declare function applyIgnore(files: string[], patterns?: string[], cwd?: string): string[];