From fe11b3b1bfed446b3faee7a560f7026d2ecaebe2 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Thu, 22 Oct 2026 15:45:45 +0530 Subject: [PATCH] fix: single-source core lint rules, derive CLI version, fix README dialect note - Add rules.CoreRules(CoreConfig) and rules.DefaultCoreConfig() in pkg/linter/rules as the one definition of the L001-L010 rule set. The gosqlx lint CLI, gosqlx action, MCP lint_sql, the C binding's gosqlx_lint and gosqlx.Lint's first ten rules now build from it. Each call site keeps its configuration and violation order (action keeps its lenient L003/L004/L005 settings and ID order). - cmd.Version now defaults to gosqlx.Version (still overridable with -ldflags -X); remove the dead Version literal in rootCmd; the version test runs `--version`. The release workflow fails when the tag does not match the Version const in pkg/gosqlx. - README: bigquery and redshift use the generic keyword set and only capability-gated grammar, not "keyword recognition and basic parsing". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- .github/workflows/release.yml | 19 +++++- README.md | 2 +- cmd/gosqlx/cmd/lint.go | 27 ++------ cmd/gosqlx/cmd/root.go | 7 +- cmd/gosqlx/cmd/version_test.go | 35 +++++++--- cmd/gosqlx/internal/actioncmd/action.go | 36 +++++----- pkg/cbinding/cbinding_ext.go | 31 ++------- pkg/gosqlx/lint.go | 30 +++------ pkg/linter/rules/core.go | 80 +++++++++++++++++++++++ pkg/linter/rules/core_test.go | 87 +++++++++++++++++++++++++ pkg/mcp/tools.go | 23 ++----- 11 files changed, 261 insertions(+), 116 deletions(-) create mode 100644 pkg/linter/rules/core.go create mode 100644 pkg/linter/rules/core_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31f4564f..212a066c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,24 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - + + # The CLI's unstamped --version and the LSP report gosqlx.Version, so a + # tag must match the Version const in pkg/gosqlx. Fail before building. + - name: Check tag matches gosqlx.Version + env: + TAG: ${{ github.ref_name }} + run: | + src="$(sed -n 's/^const Version = "\([^"]*\)".*/\1/p' pkg/gosqlx/gosqlx.go)" + if [ -z "$src" ]; then + echo "::error file=pkg/gosqlx/gosqlx.go::could not find 'const Version = \"...\"'" + exit 1 + fi + if [ "${TAG#v}" != "$src" ]; then + echo "::error file=pkg/gosqlx/gosqlx.go::tag ${TAG} does not match gosqlx.Version ${src}; bump the const before tagging" + exit 1 + fi + echo "tag ${TAG} matches gosqlx.Version ${src}" + - name: Set up Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: diff --git a/README.md b/README.md index 8666a9f0..cca5cd1c 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ Formatting turns one-line SQL into a readable query: - **[Analysis](https://gosqlx.dev/docs/reference/linting-rules/)** - SQL injection scanner, 30 lint rules (L001-L030), 20 optimizer rules and metadata extraction. - **[Tooling](https://gosqlx.dev/docs/cli-guide/)** - AST-based formatter, query transforms API, VS Code extension and GitHub Action. - **[Syntax highlighting](docs/HIGHLIGHTING.md)** - dialect-aware highlighting for terminals (ANSI), HTML/CSS, JSON spans, LSP semantic tokens and WASM; `gosqlx highlight query.sql` or `gosqlx.HighlightANSI(sql)`. -- **[Multi-dialect](https://gosqlx.dev/docs/reference/sql-compatibility/)** - 8 dialects with dedicated grammar support: PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite, Snowflake, ClickHouse. The `--dialect` flag and `WithDialect` accept 11 names: those 8 (`postgresql`, `mysql`, `mariadb`, `sqlserver`, `oracle`, `sqlite`, `snowflake`, `clickhouse`) plus `generic`, and `bigquery` and `redshift`, which have keyword recognition and basic parsing only. +- **[Multi-dialect](https://gosqlx.dev/docs/reference/sql-compatibility/)** - 8 dialects with dedicated grammar support: PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite, Snowflake, ClickHouse. The `--dialect` flag and `WithDialect` accept 11 names: those 8 (`postgresql`, `mysql`, `mariadb`, `sqlserver`, `oracle`, `sqlite`, `snowflake`, `clickhouse`) plus `generic`, `bigquery` and `redshift`. BigQuery and Redshift have no dialect-specific keyword set (they use the generic keywords) and only capability-gated grammar, such as BigQuery's `QUALIFY`, `UNNEST` in `FROM` and `(struct).field` access. - **[AI-ready](https://gosqlx.dev/docs/integrations/mcp/)** - MCP server with 7 tools, public remote endpoint, Streamable HTTP. - **[Tested](https://gosqlx.dev/docs/reference/sql-compatibility/)** - race-detector clean in CI, ~80-85% SQL-99 (estimated). diff --git a/cmd/gosqlx/cmd/lint.go b/cmd/gosqlx/cmd/lint.go index cc27d2c4..49c7ff3b 100644 --- a/cmd/gosqlx/cmd/lint.go +++ b/cmd/gosqlx/cmd/lint.go @@ -25,9 +25,7 @@ import ( "github.com/ajitpratap0/GoSQLX/pkg/gosqlx" "github.com/ajitpratap0/GoSQLX/pkg/linter" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace" + "github.com/ajitpratap0/GoSQLX/pkg/linter/rules" "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" sqlkeywords "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" sqlsecurity "github.com/ajitpratap0/GoSQLX/pkg/sql/security" @@ -411,25 +409,12 @@ func runSecurityScan(sql string, filename string, outWriter io.Writer) int { return result.TotalCount } -// createLinter creates a new linter instance with configured rules +// createLinter creates a new linter instance with the core rule set +// (L001-L010) from rules.CoreRules, using --max-length for L005. func createLinter() *linter.Linter { - l := linter.New( - // Whitespace rules (L001, L002, L003, L004, L005, L010) - whitespace.NewTrailingWhitespaceRule(), // L001 - whitespace.NewMixedIndentationRule(), // L002 - whitespace.NewConsecutiveBlankLinesRule(1), // L003 - whitespace.NewIndentationDepthRule(4, 4), // L004 - whitespace.NewLongLinesRule(lintMaxLength), // L005 - whitespace.NewRedundantWhitespaceRule(), // L010 - - // Style rules (L006, L008, L009) - style.NewColumnAlignmentRule(), // L006 - style.NewCommaPlacementRule(style.CommaTrailing), // L008 - style.NewAliasingConsistencyRule(true), // L009 - - // Keyword rules (L007) - keywords.NewKeywordCaseRule(keywords.CaseUpper), // L007 - ) + cfg := rules.DefaultCoreConfig() + cfg.MaxLineLength = lintMaxLength + l := linter.New(rules.CoreRules(cfg)...) if lintDialect != "" { l.SetDialect(sqlkeywords.SQLDialect(lintDialect)) } diff --git a/cmd/gosqlx/cmd/root.go b/cmd/gosqlx/cmd/root.go index 4161081b..38c14de0 100644 --- a/cmd/gosqlx/cmd/root.go +++ b/cmd/gosqlx/cmd/root.go @@ -21,6 +21,7 @@ import ( "github.com/ajitpratap0/GoSQLX/cmd/gosqlx/internal/cmdutil" "github.com/ajitpratap0/GoSQLX/cmd/gosqlx/internal/lspcmd" "github.com/ajitpratap0/GoSQLX/cmd/gosqlx/internal/optimizecmd" + "github.com/ajitpratap0/GoSQLX/pkg/gosqlx" ) // Version is the current version of gosqlx CLI. @@ -43,7 +44,7 @@ import ( // - ClickHouse SQL dialect support (PREWHERE, FINAL, GLOBAL IN) // - LSP semantic tokens + diagnostic debouncing // - Parser API consolidation (ParseFromModelTokens canonical) -var Version = "1.14.0" +var Version = gosqlx.Version var ( // verbose enables detailed output for debugging and troubleshooting. @@ -131,7 +132,6 @@ Key features: • CI/CD integration with proper exit codes Performance: 1.5M+ operations/second sustained, 1.97M peak. 100-1000x faster than competitors.`, - Version: "1.14.0", } // Execute adds all child commands to the root command and sets flags appropriately. @@ -177,7 +177,8 @@ func Execute() error { func init() { // Report the Version variable from --version so release builds can set it // with -ldflags "-X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version=". - // A plain `go build` keeps the hard-coded default. + // A plain `go build` reports the library's gosqlx.Version, so the CLI and + // the library (and the LSP, which reports gosqlx.Version) cannot drift. rootCmd.Version = Version // Global flags available to all subcommands diff --git a/cmd/gosqlx/cmd/version_test.go b/cmd/gosqlx/cmd/version_test.go index cb4b27f9..06a9340d 100644 --- a/cmd/gosqlx/cmd/version_test.go +++ b/cmd/gosqlx/cmd/version_test.go @@ -14,16 +14,33 @@ package cmd -import "testing" +import ( + "bytes" + "strings" + "testing" -// TestRootCmdVersionUsesVersionVar guards the release ldflags contract: -// --version must print the Version variable, which release builds override -// with -X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version=. -func TestRootCmdVersionUsesVersionVar(t *testing.T) { - if Version == "" { - t.Fatal("Version must have a non-empty default for dev builds") + "github.com/ajitpratap0/GoSQLX/pkg/gosqlx" +) + +// TestRootCmdVersionReportsLibraryVersion runs `gosqlx --version` and checks +// that an unstamped build reports the library's gosqlx.Version, so the CLI and +// library versions cannot drift. Release builds override cmd.Version with +// -ldflags "-X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version=". +func TestRootCmdVersionReportsLibraryVersion(t *testing.T) { + var out bytes.Buffer + rootCmd.SetOut(&out) + rootCmd.SetErr(&out) + rootCmd.SetArgs([]string{"--version"}) + t.Cleanup(func() { + rootCmd.SetOut(nil) + rootCmd.SetErr(nil) + rootCmd.SetArgs(nil) + }) + + if err := rootCmd.Execute(); err != nil { + t.Fatalf("gosqlx --version: %v", err) } - if rootCmd.Version != Version { - t.Errorf("rootCmd.Version = %q, want Version %q", rootCmd.Version, Version) + if got := out.String(); !strings.Contains(got, gosqlx.Version) { + t.Errorf("gosqlx --version output %q does not contain gosqlx.Version %q", got, gosqlx.Version) } } diff --git a/cmd/gosqlx/internal/actioncmd/action.go b/cmd/gosqlx/internal/actioncmd/action.go index 05957362..3e443237 100644 --- a/cmd/gosqlx/internal/actioncmd/action.go +++ b/cmd/gosqlx/internal/actioncmd/action.go @@ -20,6 +20,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "strconv" "strings" "time" @@ -27,27 +28,28 @@ import ( "github.com/spf13/cobra" "github.com/ajitpratap0/GoSQLX/pkg/linter" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace" + "github.com/ajitpratap0/GoSQLX/pkg/linter/rules" "github.com/ajitpratap0/GoSQLX/pkg/sql/parser" ) -// builtinRules returns all built-in lint rules. This is the single source of -// truth - both defaultLinter() and allRules() derive from it. +// actionCoreConfig is the action command's own configuration of the core +// rules, more lenient than `gosqlx lint`: L003 allows 2 blank lines, L004 +// depth 10, L005 120 columns. +var actionCoreConfig = rules.CoreConfig{ + MaxConsecutiveBlankLines: 2, + MaxIndentDepth: 10, + IndentSize: 4, + MaxLineLength: 120, +} + +// builtinRules returns all built-in lint rules (the core rules L001-L010 from +// rules.CoreRules), sorted by rule ID so violations keep the action's +// established L001..L010 output order. This is the single source of truth - +// both defaultLinter() and allRules() derive from it. func builtinRules() []linter.Rule { - return []linter.Rule{ - whitespace.NewTrailingWhitespaceRule(), // L001 - whitespace.NewMixedIndentationRule(), // L002 - whitespace.NewConsecutiveBlankLinesRule(2), // L003 - whitespace.NewIndentationDepthRule(10, 4), // L004 - whitespace.NewLongLinesRule(120), // L005 - style.NewColumnAlignmentRule(), // L006 - keywords.NewKeywordCaseRule(keywords.CaseUpper), // L007 - style.NewCommaPlacementRule(style.CommaTrailing), // L008 - style.NewAliasingConsistencyRule(true), // L009 - whitespace.NewRedundantWhitespaceRule(), // L010 - } + rs := rules.CoreRules(actionCoreConfig) + slices.SortFunc(rs, func(a, b linter.Rule) int { return strings.Compare(a.ID(), b.ID()) }) + return rs } // actionFlags holds the flag values for the action subcommand, diff --git a/pkg/cbinding/cbinding_ext.go b/pkg/cbinding/cbinding_ext.go index 1a913e6e..f1a59f73 100644 --- a/pkg/cbinding/cbinding_ext.go +++ b/pkg/cbinding/cbinding_ext.go @@ -33,9 +33,7 @@ import ( "github.com/ajitpratap0/GoSQLX/pkg/formatter" "github.com/ajitpratap0/GoSQLX/pkg/gosqlx" "github.com/ajitpratap0/GoSQLX/pkg/linter" - lintkeywords "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace" + lintrules "github.com/ajitpratap0/GoSQLX/pkg/linter/rules" "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" "github.com/ajitpratap0/GoSQLX/pkg/sql/parser" @@ -420,28 +418,13 @@ func parseLintOptions(raw string) (LintOptions, error) { return opts, nil } -// newLinter builds the rule set of the `gosqlx lint` CLI command -// (createLinter in cmd/gosqlx/cmd/lint.go, also mirrored by pkg/mcp's -// newFullLinter): L001-L010 with the CLI's rule configuration. Keep the three -// in sync when the CLI's default rule set changes. +// newLinter builds the rule set of the `gosqlx lint` CLI command: the core +// rules L001-L010 from rules.CoreRules with the CLI's configuration and the +// given L005 limit. func newLinter(maxLineLength int, dialect string) *linter.Linter { - l := linter.New( - // Whitespace rules (L001, L002, L003, L004, L005, L010) - whitespace.NewTrailingWhitespaceRule(), // L001 - whitespace.NewMixedIndentationRule(), // L002 - whitespace.NewConsecutiveBlankLinesRule(1), // L003 - whitespace.NewIndentationDepthRule(4, 4), // L004 - whitespace.NewLongLinesRule(maxLineLength), // L005 - whitespace.NewRedundantWhitespaceRule(), // L010 - - // Style rules (L006, L008, L009) - style.NewColumnAlignmentRule(), // L006 - style.NewCommaPlacementRule(style.CommaTrailing), // L008 - style.NewAliasingConsistencyRule(true), // L009 - - // Keyword rules (L007) - lintkeywords.NewKeywordCaseRule(lintkeywords.CaseUpper), // L007 - ) + cfg := lintrules.DefaultCoreConfig() + cfg.MaxLineLength = maxLineLength + l := linter.New(lintrules.CoreRules(cfg)...) if dialect != "" { l.SetDialect(keywords.SQLDialect(dialect)) } diff --git a/pkg/gosqlx/lint.go b/pkg/gosqlx/lint.go index 450efb97..2cb228d9 100644 --- a/pkg/gosqlx/lint.go +++ b/pkg/gosqlx/lint.go @@ -19,12 +19,10 @@ import ( "sync" "github.com/ajitpratap0/GoSQLX/pkg/linter" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords" + lintrules "github.com/ajitpratap0/GoSQLX/pkg/linter/rules" "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/naming" "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/performance" "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/safety" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace" sqlkeywords "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" ) @@ -116,25 +114,13 @@ func LintWithDialect(sql string, dialect sqlkeywords.SQLDialect) (*LintResult, e return &LintResult{Violations: fileResult.Violations}, nil } -// allRules returns all 30 built-in lint rules with sensible defaults. +// allRules returns all 30 built-in lint rules with sensible defaults: the +// core rules L001-L010 from rules.CoreRules (with a 120-column L005 limit) +// followed by L011-L030. func allRules() []linter.Rule { - return []linter.Rule{ - // Whitespace rules (L001-L005, L010) - whitespace.NewTrailingWhitespaceRule(), // L001 - whitespace.NewMixedIndentationRule(), // L002 - whitespace.NewConsecutiveBlankLinesRule(1), // L003 - whitespace.NewIndentationDepthRule(4, 4), // L004 - whitespace.NewLongLinesRule(120), // L005 - whitespace.NewRedundantWhitespaceRule(), // L010 - - // Style rules (L006, L008, L009) - style.NewColumnAlignmentRule(), // L006 - style.NewCommaPlacementRule(style.CommaTrailing), // L008 - style.NewAliasingConsistencyRule(true), // L009 - - // Keyword rules (L007) - keywords.NewKeywordCaseRule(keywords.CaseUpper), // L007 - + core := lintrules.DefaultCoreConfig() + core.MaxLineLength = 120 + return append(lintrules.CoreRules(core), // Safety rules (L011-L015) safety.NewDeleteWithoutWhereRule(), // L011 safety.NewUpdateWithoutWhereRule(), // L012 @@ -160,5 +146,5 @@ func allRules() []linter.Rule { naming.NewMissingOrderByLimitRule(), // L028 naming.NewSubqueryCanBeJoinRule(), // L029 naming.NewDistinctOnManyColumnsRule(), // L030 - } + ) } diff --git a/pkg/linter/rules/core.go b/pkg/linter/rules/core.go new file mode 100644 index 00000000..c95b6fbc --- /dev/null +++ b/pkg/linter/rules/core.go @@ -0,0 +1,80 @@ +// Copyright 2026 GoSQLX Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package rules assembles the built-in lint rules from its sub-packages +// (whitespace, style, keywords, safety, performance, naming) into ready-made +// rule sets. It lives above pkg/linter because every rule sub-package imports +// pkg/linter, so pkg/linter itself cannot import them. +package rules + +import ( + "github.com/ajitpratap0/GoSQLX/pkg/linter" + "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords" + "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style" + "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace" +) + +// CoreConfig holds the settings on which users of the core rule set +// (L001-L010) differ. The other core rules take no settings here: L006, +// L008 (trailing commas), L009 (explicit AS) and L007 (upper-case keywords) +// are configured identically everywhere. +type CoreConfig struct { + // MaxConsecutiveBlankLines is the L003 limit. + MaxConsecutiveBlankLines int + // MaxIndentDepth and IndentSize configure L004. + MaxIndentDepth int + IndentSize int + // MaxLineLength is the L005 limit. + MaxLineLength int +} + +// DefaultCoreConfig returns the configuration of the `gosqlx lint` CLI +// command (L003 1 blank line, L004 depth 4 with 4-space indents, L005 100 +// columns), which the MCP lint_sql tool and the C binding's gosqlx_lint share. +func DefaultCoreConfig() CoreConfig { + return CoreConfig{ + MaxConsecutiveBlankLines: 1, + MaxIndentDepth: 4, + IndentSize: 4, + MaxLineLength: 100, + } +} + +// CoreRules returns new instances of the ten core style rules L001-L010, +// configured by cfg. This is the single source of the rule set used by the +// `gosqlx lint` CLI, the `gosqlx action` command, the MCP lint_sql tool, the +// C binding and the first ten rules of gosqlx.Lint. +// +// The rules come grouped by sub-package, not by ID: whitespace (L001-L005, +// L010), then style (L006, L008, L009), then keywords (L007). The linter +// reports violations in rule order, so this order is part of the output. +func CoreRules(cfg CoreConfig) []linter.Rule { + return []linter.Rule{ + // Whitespace rules (L001-L005, L010) + whitespace.NewTrailingWhitespaceRule(), // L001 + whitespace.NewMixedIndentationRule(), // L002 + whitespace.NewConsecutiveBlankLinesRule(cfg.MaxConsecutiveBlankLines), // L003 + whitespace.NewIndentationDepthRule(cfg.MaxIndentDepth, cfg.IndentSize), // L004 + whitespace.NewLongLinesRule(cfg.MaxLineLength), // L005 + whitespace.NewRedundantWhitespaceRule(), // L010 + + // Style rules (L006, L008, L009) + style.NewColumnAlignmentRule(), // L006 + style.NewCommaPlacementRule(style.CommaTrailing), // L008 + style.NewAliasingConsistencyRule(true), // L009 + + // Keyword rules (L007) + keywords.NewKeywordCaseRule(keywords.CaseUpper), // L007 + } +} diff --git a/pkg/linter/rules/core_test.go b/pkg/linter/rules/core_test.go new file mode 100644 index 00000000..3611dc1c --- /dev/null +++ b/pkg/linter/rules/core_test.go @@ -0,0 +1,87 @@ +// Copyright 2026 GoSQLX Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules_test + +import ( + "slices" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/linter" + "github.com/ajitpratap0/GoSQLX/pkg/linter/rules" +) + +func ruleIDs(t *testing.T, cfg rules.CoreConfig) []string { + t.Helper() + rs := rules.CoreRules(cfg) + ids := make([]string, len(rs)) + for i, r := range rs { + ids[i] = r.ID() + } + return ids +} + +func TestCoreRules_ExactlyL001ToL010(t *testing.T) { + // The documented output order: whitespace, style, keywords. + want := []string{"L001", "L002", "L003", "L004", "L005", "L010", "L006", "L008", "L009", "L007"} + got := ruleIDs(t, rules.DefaultCoreConfig()) + if !slices.Equal(got, want) { + t.Fatalf("CoreRules IDs = %v, want %v", got, want) + } + + sorted := slices.Clone(got) + slices.Sort(sorted) + wantSet := []string{"L001", "L002", "L003", "L004", "L005", "L006", "L007", "L008", "L009", "L010"} + if !slices.Equal(sorted, wantSet) { + t.Fatalf("CoreRules must contain exactly L001..L010 once each, got %v", sorted) + } +} + +func TestCoreRules_ConfigApplied(t *testing.T) { + if def := rules.DefaultCoreConfig(); def != (rules.CoreConfig{ + MaxConsecutiveBlankLines: 1, MaxIndentDepth: 4, IndentSize: 4, MaxLineLength: 100, + }) { + t.Fatalf("DefaultCoreConfig changed: %+v", def) + } + + sql := "SELECT id, name, email FROM users WHERE id = 1" + countL005 := func(maxLen int) int { + cfg := rules.DefaultCoreConfig() + cfg.MaxLineLength = maxLen + res := linter.New(rules.CoreRules(cfg)...).LintString(sql, "") + n := 0 + for _, v := range res.Violations { + if v.Rule == "L005" { + n++ + } + } + return n + } + if n := countL005(100); n != 0 { + t.Errorf("MaxLineLength 100: got %d L005 violations, want 0", n) + } + if n := countL005(20); n != 1 { + t.Errorf("MaxLineLength 20: got %d L005 violations, want 1", n) + } +} + +func TestCoreRules_FreshInstances(t *testing.T) { + a := rules.CoreRules(rules.DefaultCoreConfig()) + b := rules.CoreRules(rules.DefaultCoreConfig()) + for i := range a { + if a[i] == b[i] { + t.Fatalf("rule %s shared between calls", a[i].ID()) + } + } +} diff --git a/pkg/mcp/tools.go b/pkg/mcp/tools.go index 43f99569..b0323302 100644 --- a/pkg/mcp/tools.go +++ b/pkg/mcp/tools.go @@ -27,9 +27,8 @@ import ( "github.com/ajitpratap0/GoSQLX/pkg/gosqlx" "github.com/ajitpratap0/GoSQLX/pkg/linter" + "github.com/ajitpratap0/GoSQLX/pkg/linter/rules" "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style" - "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace" sqlkeywords "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" "github.com/ajitpratap0/GoSQLX/pkg/sql/security" ) @@ -346,23 +345,11 @@ func lintSQLInternal(sql string) (map[string]any, error) { // Private helpers // --------------------------------------------------------------------------- -// newFullLinter mirrors createLinter() from cmd/gosqlx/cmd/lint.go exactly, -// using a fixed line-length of 100 for the MCP context. +// newFullLinter builds the rule set of the `gosqlx lint` CLI command (the +// core rules L001-L010 from rules.CoreRules with the CLI's configuration, +// L005 limit 100). func newFullLinter() *linter.Linter { - return linter.New( - whitespace.NewTrailingWhitespaceRule(), // L001 - whitespace.NewMixedIndentationRule(), // L002 - whitespace.NewConsecutiveBlankLinesRule(1), // L003 - whitespace.NewIndentationDepthRule(4, 4), // L004 - whitespace.NewLongLinesRule(100), // L005 - whitespace.NewRedundantWhitespaceRule(), // L010 - - style.NewColumnAlignmentRule(), // L006 - style.NewCommaPlacementRule(style.CommaTrailing), // L008 - style.NewAliasingConsistencyRule(true), // L009 - - keywords.NewKeywordCaseRule(keywords.CaseUpper), // L007 - ) + return linter.New(rules.CoreRules(rules.DefaultCoreConfig())...) } // toolResult marshals a map[string]any result into an MCP CallToolResult.