feat(lint): add @pgsql/lint — standalone source-level SQL/PL-pgSQL convention linter + CLI - #335
Merged
Conversation
…nvention linter + CLI
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…pters, and ESLint-style severity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New package
@pgsql/lint: a source-level SQL / PL/pgSQL convention linter, extracted from safegres'ssrc/lint/*so it can run without a database. It reasons about the text of aCREATE FUNCTIONdefinition from its AST and carries nopg/ catalog dependency, so the same engine runs over a migration on disk, an editor buffer, or a definition read from a live catalog viapg_get_functiondef. safegres will consume it in a follow-up PR (this is step one of "extract, then repoint").Rules are an ecosystem seam: they are injected as values, never discovered by a magic npm name — you
importa rule and pass it tocreateLinter. Severity is configuration (ESLint-styleoff/warn/error), so a downstream consumer (safegres) keeps full control without duplicating any engine logic.Runtime footprint is only the parser stack already in this repo:
pgsql-parser(SQL → AST),libpg-query(parsePlPgSQL),@pgsql/traverse(walk), pluschalk/minimistfor the CLI.Rules (stable ids +
Cxregistry codes)C1no-set-search-pathSET search_pathclause orset_config('search_path', …)C2no-variable-conflict#variable_conflictdirectiveC3require-qualified-refsFROM users); CTE names excludedC4no-dynamic-sqlEXECUTE,EXECUTE … USING,FOR … IN EXECUTEPlugin API — rules as values, severity as config
defineRule(rule)— identity helper that pins theLintRuleshape so a third-party rule type-checks without importing internals.severity—offdrops the rule (never runs);warnreports but does not fail;error(default when unmapped) fails the run. Attached to everyLintProblemasseverity.unit → problems; aSourceAdapterdecides where definitions come from. ShipsfilesAdapter/sqlTextAdapter;linter.lintSource(adapter)runs any adapter. safegres becomes "the catalog adapter" over the same engine.Public API
opts.keywordselects the suppression directive keyword; defaults to['pgsql-lint', 'safegres']so both brands work (lets safegres adopt without churning its waiver corpus).File runner / CLI — the "local" entry point
lintDefinitionexpects a single definition; the file runner slices out each top-levelCREATE FUNCTIONviastmt_location/stmt_len, lints each, and re-anchors findings to absolute file lines — so a mixed migration is never treated as one malformed definition.Exit code is
1when any error-severity (non-waived) finding remains,0otherwise;--warnfindings print but pass.Suppressions (ESLint/Prettier-style, in the body)
Forms:
disable-next-line,disable-line,disable…enable(range),disable-file.no-dynamic-sqlrequires a reason — a reasonless waiver does not silence it (finding stands, taggedinvalidSuppression: 'missing-reason'). Suppressed findings are reported as acknowledged, never dropped.Tests
packages/lint/__tests__— 37 tests, all DB-free: rule metadata; C1–C4 detection + negative cases; reason-required behavior; suppression scopes + keyword selection; severity (off/warnby id and code,errordefault);createLinterwith an injected custom rule; unparseable definitions; file-runner slicing / re-anchoring / attribution / directory scan;lintSourceadapters; CLI JSON, exit codes,--warn/--off,--rules,--help.Wiring
@pgsql/lintto the CI matrix in.github/workflows/run-tests.yaml.@pgsql/semantics; addsbin: { "pgsql-lint": "cli.js" }.makage) + a new.agents/skills/pgsql-lintskill (registered inAGENTS.md).Deferred (next PR)
Repointing safegres's
src/lint/*at@pgsql/lint(as the catalog adapter). No safegres changes here.Link to Devin session: https://app.devin.ai/sessions/af81a09043504701874ca63e67a9cd4b
Requested by: @pyramation