feat(linter): flat rule ids and static type checking - #815
Merged
Merged
Conversation
psteinroe
marked this pull request as ready for review
October 1, 2026 09:53
psteinroe
added this pull request to stack #820
October 1, 2026 10:41
psteinroe
force-pushed
the
feat/next-linter-version
branch
from
October 2, 2026 10:56
1acba80 to
6b28e2d
Compare
Adds the catalog overlay, the session state, and a conservative name resolver that reports a finding only when Postgres would certainly fail. The schema cache now records which tables are partitions or inheritance children.
Rules are identified by name (category lint/<rule>); groups are metadata. Rules are regrouped into correctness, safety, destructive, style, and typecheck, and migration-only rules are skipped outside the migrations directory. preferBigintOverInt and preferBigintOverSmallint are merged into preferBigInt, and concurrentRefreshMatviewLock is removed. New typecheck rules check names against the catalog: unknownRelation, unknownColumn, unknownSchema, ambiguousColumn, unknownFunction, insertColumnMismatch, unknownType, and missingFromClauseEntry. New correctness rule invalidDropTypeSignature.
linter.rules.<rule> and linter.groups.<group> replace group-nested rule configuration. linter.rules.safety.<rule> is still accepted and reported as deprecated.
Typecheck rules run when a schema is loaded and typecheck.enabled is set. EXPLAIN runs only for statements that reference unchanged database objects. linter.enabled now switches the lint rules.
The schema cache becomes the catalog's database snapshot (pgls_catalog::Snapshot), loaded with the db feature or from JSON. The JSON format, the generated SchemaCache TS type, and the invalidateSchemaCache command are unchanged. Removes the unused pgls_type_resolver crate.
Catalog::snapshot() turns the overlay back into a Snapshot. Completions and hover use it with the statements before the cursor applied, so they show objects the file created and hide the ones it dropped. The analyser now takes the snapshot from the catalog base.
Mirrors the layout of pgls_pretty_print: resolve/nodes/<node>.rs with resolve_<node>(r, n) functions and a single resolve_node_enum dispatcher. The names in scope move into the Resolver as a stack of query levels and CTEs.
Legacy specifiers keep working and report their flat replacement. `lint/safety` keeps its former meaning of every lint rule. The deprecated `linter.rules.safety` is marked deprecated in the JSON schema and keeps its former type name `Safety`.
Drop the empty `security` group and the stale `lint/performance` and `lint/safety` categories. The deprecated `linter.rules.safety` accepts only the rules it had. Document the two typecheck switches and that `migrationsDir` limits migration-only rules.
…ion by owner Rename view.rs to lookup.rs, move column naming into resolve/, make the search path expansion a Snapshot method, and split Session into settings (search path, role) and locks (lock, timeout, and constraint state). ROLLBACK now also forgets a SET LOCAL ROLE.
…erential test against Postgres
…node, regression corpus harness
…d RETURNING old/new Port resolveTargetListUnknowns for subqueries, CTEs, scalar subqueries and view outputs, make_const's int4/int8/numeric choice for T_Float integers (including hex, octal, binary and underscores), and Postgres 18's old/new in RETURNING lists.
…nknown The workspace dropped statements that don't parse before the analyser saw them, so a CREATE TABLE with newer syntax made every later use of the table report a missing relation. Such statements now taint the catalog unless they can't change it (queries, COPY, maintenance, cursors, ...).
…ry version Record the catalog of a fresh database per version next to the fixtures (just record-regress <major> --catalog-only) and replace the downloading, database-bound corpus test with an offline cargo test over Postgres 15-18 that snapshots the findings on rejected statements. Drop the CI job.
Name SQL/JSON constructors and query functions, MERGE_ACTION() and XML expressions like FigureColnameInternal does, and only add RETURNING's old/new when no item in this or an outer query has that name, like transformReturningClause.
Ports now follow the latest supported Postgres, pinned to REL_18_6, and link the function they port at that tag.
psteinroe
force-pushed
the
feat/next-linter-version
branch
from
October 4, 2026 17:05
6b28e2d to
93f3b66
Compare
psteinroe
removed this pull request from stack #820
October 4, 2026 17:05
This was referenced Oct 4, 2026
Postgres reads `a(p)` as field `a` of a composite column `p` (ParseFuncOrColumn). Only report an unknown function when the argument is known to be a scalar.
Removed rule names map to the rule that replaced them in suppressions and rule selectors, including concurrentRefreshMatviewLock, and the `safety` and `lint/safety` selectors keep selecting every lint rule.
Rewrite the type checking page, use flat rule ids in the linting docs, drop CLI flags that don't exist, move the known gaps into the module docs, and remove PLAN.md and TYPING.md.
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.
Restructures the linter around flat rule IDs and replaces the EXPLAIN-based typecheck with static type checking backed by an in-memory catalog. The linter infers the type of every expression and reports what Postgres would reject, without running the statement. EXPLAIN stays as a fallback. Existing configs and suppression comments keep working and print deprecation warnings. Completions and hover now see the tables and columns created earlier in the file.
This combines the former stack of #815, #819 and #823 into one PR.
Flat rule IDs and groups
Rules are identified by name:
lint/banDropColumn,linter.rules.banDropColumn,pgls-ignore banDropColumn. Groups are metadata, configured in bulk underlinter.groups, like oxlint's categories. Moving a rule to another group no longer breaks configs or suppressions. The rules were regrouped intocorrectness,safety,destructive,style,typecheck, andnursery.Precedence is rule > deprecated
linter.rules.safety.<rule>> group >recommended/allpreset.preferBigintOverIntandpreferBigintOverSmallintare merged intopreferBigIntwith the optionscheckIntandcheckSmallint.concurrentRefreshMatviewLockis removed;requireConcurrentRefreshMatviewcovers it, and the old name maps to it.Backward compatibility and warnings
Everything users write keeps working:
linter.rules.safety.<rule>,safety.recommended, andsafety.allstill apply, including the removed rule names, which map topreferBigInt. The CLI prints oneWarning:line per deprecated setting before it runs. The LSP shows a single warning message per session that lists them. The JSON schema markslinter.rules.safetyasdeprecated, so editors flag it in the config file.lint/safety/<rule>,lint/<group>, and the removed rule names still suppress; a removed rule stands for the rule that replaced it. Each such comment gets a warning diagnostic that names the flat form, likeUse `banDropColumn` instead.lint/safetykeeps its former meaning of every lint rule.safetyis today's group.onlyandskiplists of the workspace API accept the old IDs and the removed rule names.safetyandlint/safetystill select every lint rule.SafetyandSchemaCache. So do the JSON formats, theschema exportcommand, and theinvalidateSchemaCachecommand.What changes with an unchanged setup:
lint/safety/<rule>becomeslint/<rule>. Scripts that match on the LSP diagnostic code or the JSON, GitHub, GitLab, or JUnit reporter output need updating.linter.enabled: falsenow disables the linter. It used to be ignored.migrations.migrationsDir: when set, migration-only rules (most ofsafetyanddestructive) are skipped for files outside it.Catalog and name resolution
The new crate
pgls_catalogmergespgls_schema_cacheand adds:ROLLBACK/ savepoints;check_function_bodies;No DDL runs against the database. Anything the catalog can't be sure about stays silent, including statements that don't parse and may have changed the catalog. EXPLAIN remains as a fallback for statements that only reference unchanged database objects.
typecheck.enabledswitches both off. The unusedpgls_type_resolvercrate is deleted.Typecheck rules
All are recommended and report errors.
unknownRelation,unknownColumn,unknownSchema,unknownFunction,unknownTypeambiguousColumninsertColumnMismatchmissingFromClauseEntryt.colwithouttin FROMoperatorTypeMismatch1 + now(),'1' + '1'functionArgumentMismatchlength(1), ambiguous overloadsinvalidCastnow()::intassignmentTypeMismatchinsert into t (qty) values (now()), alsoUPDATEandON CONFLICTfunctionReturnTypeMismatchLANGUAGE sqlfunctions whose final statement returns the wrong number or types of columnsHow typing works
The snapshot now loads casts, operators, and typing metadata for types and functions.
pgls_catalog::typingports the algorithms of the latest supported Postgres (18, pinned toREL_18_6) for coercion, common types, polymorphic types, and function and operator overload selection. Each port links the Postgres function it follows at that tag. Postgres 15 to 17 resolve types the same way; where 18 accepts more (old/newinRETURNING), it is gated on the server version. The resolver types every clause Postgres types. Anything it doesn't model is unknown, and unknown never reports.AGENTS.mddescribes how to port and test the type rules.No false positives
Two tests compare the analysis with Postgres:
resolve/differential_tests.rschecks a corpus of statements against a live Postgres on every run. Statements Postgres accepts must give no findings, inferred column types must match what Postgres describes, and expected errors must be found.pgls_analyser/tests/postgres_regress.rsruns Postgres' own regression SQL for 15, 16, 17 and 18 (the fixtures from test: add Postgres regression fixtures for every supported version #824, about 170,000 statements) through the linter, against the catalog of a fresh database on each version. It is a normalcargo testwithout network or database and takes about 11 seconds. Any finding on a statement Postgres accepted fails it. The findings on statements Postgres rejected are snapshotted per version, so changes in detection show up in review.just record-regress <major> --catalog-onlyre-records a catalog.It found 644 false positives on Postgres 15 and 426 more on 16 to 18; all are fixed. One of them affected the editor beyond typing: statements that don't parse were dropped before linting, so a
CREATE TABLEwith newer syntax made every later use of the table report a missing relation. Such statements now make missing objects unknown for the rest of the file, unless they can't change the catalog (queries,COPY, maintenance, ...).Schema cache
The
SchemaCacheJSON gains casts, operators, and typing fields on types and functions. All of them are optional, so older exports still load.Review focus: false positives in
pgls_catalog/src/resolve,catalog/ddlandtyping. The known gaps are listed in the module docs ofcatalog/ddl,resolveandtyping. Statements with psql identifier parameters (:schema.table) are not typechecked.Fixes #624
Fixes #692
Fixes #369
Fixes #431