Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"

- name: Install Task
uses: arduino/setup-task@v2
with:
version: 3.x

- name: Install tools
run: task tools:install

- name: Run CI
run: task ci
32 changes: 32 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Binaries
bin/
*.exe
*.exe~
*.dll
*.so
*.dylib
scaf-lsp

# Test binary
*.test

# Output of go coverage
*.out

# Go workspace
go.work
go.work.sum

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Local config (contains credentials)
.scaf.yaml
62 changes: 62 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
version: "3"

tasks:
default:
desc: Run all CI checks
cmds:
- task: ci

ci:
desc: Run full CI pipeline (build, test, lint, format check)
cmds:
- task: build
- task: test
- task: fmt:check
- task: build:examples

build:
desc: Build all packages and binaries
cmds:
- go build ./...
- go build -o bin/scaf ./cmd/scaf
- go build -o bin/scaf-lsp ./cmd/scaf-lsp

build:examples:
desc: Build example modules
dir: example
cmds:
- go build ./...

test:
desc: Run all tests
cmds:
- go test ./...

fmt:
desc: Format code with gofumpt and goimports
cmds:
- gofumpt -w .
- goimports -w .

fmt:check:
desc: Check code formatting
cmds:
- test -z "$(gofumpt -l .)" || (echo "Run 'task fmt' to fix formatting" && gofumpt -d . && exit 1)
- test -z "$(goimports -l .)" || (echo "Run 'task fmt' to fix imports" && goimports -d . && exit 1)

lint:
desc: Run golangci-lint
cmds:
- golangci-lint run ./...

tools:install:
desc: Install required development tools
cmds:
- go install mvdan.cc/gofumpt@latest
- go install golang.org/x/tools/cmd/goimports@latest
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

clean:
desc: Clean build artifacts
cmds:
- rm -rf bin/
2 changes: 1 addition & 1 deletion adapters/neogo/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"strings"

"github.com/rlch/scaf"
"github.com/rlch/scaf/language/go"
golang "github.com/rlch/scaf/language/go"
)

// Binding implements golang.Binding for neogo.
Expand Down
2 changes: 1 addition & 1 deletion adapters/neogo/binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package neogo
import (
"testing"

"github.com/rlch/scaf/language/go"
golang "github.com/rlch/scaf/language/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down
2 changes: 1 addition & 1 deletion adapters/neogo/neogo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ type PointsTo struct {
type Owner struct {
neogo.Node `neo4j:"Owner"`

Name string `neo4j:"name"`
Name string `neo4j:"name"`
Pet neogo.One[Pet] `neo4j:"->"`
}

Expand Down
2 changes: 1 addition & 1 deletion analysis/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (a *Analyzer) Analyze(path string, content []byte) *AnalyzedFile {
// Convert parse errors to diagnostics
result.Diagnostics = append(result.Diagnostics, parseErrorsToDiagnostics(err)...)
// Don't return early - continue with partial AST for better LSP support

// Also do a recovery parse for completion context.
// This may give us better information about what the user was typing
// even though it might misparse valid syntax elsewhere.
Expand Down
4 changes: 2 additions & 2 deletions analysis/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ Q {
wantError: true,
},
{
name: "mid-query error still gets earlier queries",
input: "fn Valid1() `Q1`\nfn Valid2() `Q2`\nfn Broken() `incomplete",
name: "mid-query error still gets earlier queries",
input: "fn Valid1() `Q1`\nfn Valid2() `Q2`\nfn Broken() `incomplete",
wantQueries: []string{"Valid1", "Valid2"},
wantError: true,
},
Expand Down
32 changes: 16 additions & 16 deletions analysis/position_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ func TestPrevTokenAtPosition(t *testing.T) {
wantType: scaf.TokenRParen,
},
{
name: "after setup keyword with inline query",
input: "fn Q() `Q`\nQ {\n\tsetup `CREATE (n)`\n}\n",
name: "after setup keyword with inline query",
input: "fn Q() `Q`\nQ {\n\tsetup `CREATE (n)`\n}\n",
line: 3,
col: 8, // right after "setup " (at position of backtick)
wantValue: "setup",
wantType: scaf.TokenSetup,
},
{
name: "after dot in module reference",
input: "import fixtures \"./fixtures\"\nfn Q() `Q`\nQ {\n\tsetup fixtures.CreateUser()\n}\n",
name: "after dot in module reference",
input: "import fixtures \"./fixtures\"\nfn Q() `Q`\nQ {\n\tsetup fixtures.CreateUser()\n}\n",
line: 4,
col: 17, // right after "fixtures." (at position of 'C')
wantValue: ".",
Expand All @@ -61,48 +61,48 @@ func TestPrevTokenAtPosition(t *testing.T) {
wantType: scaf.TokenImport,
},
{
name: "after test keyword",
input: "fn Q() `Q`\nQ {\n\ttest \"my test\" {\n\t}\n}\n",
name: "after test keyword",
input: "fn Q() `Q`\nQ {\n\ttest \"my test\" {\n\t}\n}\n",
line: 3,
col: 7, // right after "test " (at position of quote)
wantValue: "test",
wantType: scaf.TokenTest,
},
{
name: "after group keyword",
input: "fn Q() `Q`\nQ {\n\tgroup \"my group\" {\n\t}\n}\n",
name: "after group keyword",
input: "fn Q() `Q`\nQ {\n\tgroup \"my group\" {\n\t}\n}\n",
line: 3,
col: 8, // right after "group " (at position of quote)
wantValue: "group",
wantType: scaf.TokenGroup,
},
{
name: "after assert keyword",
input: "fn Q() `Q`\nQ {\n\ttest \"t\" {\n\t\tassert { (true) }\n\t}\n}\n",
name: "after assert keyword",
input: "fn Q() `Q`\nQ {\n\ttest \"t\" {\n\t\tassert { (true) }\n\t}\n}\n",
line: 4,
col: 10, // right after "assert " (at position of '{')
wantValue: "assert",
wantType: scaf.TokenAssert,
},
{
name: "after open brace in test",
input: "fn Q() `Q`\nQ {\n\ttest \"t\" {\n\t\t$id: 1\n\t}\n}\n",
name: "after open brace in test",
input: "fn Q() `Q`\nQ {\n\ttest \"t\" {\n\t\t$id: 1\n\t}\n}\n",
line: 4,
col: 3, // at start of "$id" line
wantValue: "{",
wantType: scaf.TokenLBrace,
},
{
name: "after colon in statement",
input: "fn Q() `Q`\nQ {\n\ttest \"t\" {\n\t\t$id: 1\n\t}\n}\n",
name: "after colon in statement",
input: "fn Q() `Q`\nQ {\n\ttest \"t\" {\n\t\t$id: 1\n\t}\n}\n",
line: 4,
col: 8, // right after ": " (at position of '1')
wantValue: ":",
wantType: scaf.TokenColon,
},
{
name: "after parameter name",
input: "fn Q() `Q`\nQ {\n\ttest \"t\" {\n\t\t$id: 1\n\t}\n}\n",
name: "after parameter name",
input: "fn Q() `Q`\nQ {\n\ttest \"t\" {\n\t\t$id: 1\n\t}\n}\n",
line: 4,
col: 6, // right after "$id" (at position of ':')
wantValue: "$id",
Expand Down
18 changes: 9 additions & 9 deletions analysis/recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const (
// This is the main entry point for recovery-based completions.
//
// Strategy:
// 1. Look for nodes with RecoveredTokens in RecoverySuite (parser recovery)
// 1. Look for nodes with RecoveredTokens in RecoverySuite (parser recovery)
// 2. Look at parse error position and preceding tokens
// 3. Use token context at cursor position
func GetRecoveryCompletionContext(f *AnalyzedFile, pos lexer.Position) *RecoveryCompletionContext {
Expand Down Expand Up @@ -101,7 +101,7 @@ func GetRecoveryCompletionContext(f *AnalyzedFile, pos lexer.Position) *Recovery
} else {
ctx.PrevToken = PrevTokenAtPosition(f, errorPos)
}

// If cursor is at or near error position, use error context
if isNearPosition(pos, errorPos) {
analyzeErrorContext(ctx, f.Symbols)
Expand Down Expand Up @@ -210,13 +210,13 @@ func getErrorPosition(err error) lexer.Position {
return perr.Position()
}
}

// Check for single participle error
var perr participle.Error
if errors.As(err, &perr) {
return perr.Position()
}

return lexer.Position{}
}

Expand Down Expand Up @@ -245,7 +245,7 @@ func analyzeErrorContext(ctx *RecoveryCompletionContext, symbols *SymbolTable) {
if ctx.PrevToken == nil {
return
}

// Check what token precedes the error
switch ctx.PrevToken.Type {
case scaf.TokenSetup:
Expand Down Expand Up @@ -286,19 +286,19 @@ func analyzeTokenContext(ctx *RecoveryCompletionContext, pos lexer.Position, sui
if ctx.PrevToken == nil {
return
}

// Determine context from surrounding structure
for _, scope := range suite.Scopes {
if ContainsPosition(scope.Span(), pos) {
ctx.QueryScope = scope.FunctionName

// Check if we're in setup context (after "setup" keyword)
if ctx.PrevToken.Type == scaf.TokenSetup {
ctx.InSetup = true
ctx.Kind = RecoveryCompletionSetupAlias
return
}

// Check if previous token is a DOT - need to find module alias before it
if ctx.PrevToken.Type == scaf.TokenDot {
// Find the token before the dot
Expand All @@ -311,7 +311,7 @@ func analyzeTokenContext(ctx *RecoveryCompletionContext, pos lexer.Position, sui
}
}
}

// Check if previous token is an import alias (cursor might be ON the dot)
if ctx.PrevToken.Type == scaf.TokenIdent && symbols != nil {
if _, ok := symbols.Imports[ctx.PrevToken.Value]; ok {
Expand Down
22 changes: 11 additions & 11 deletions analysis/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ func DefaultRules() []*Rule {
duplicateQueryRule,
duplicateImportRule,
undefinedAssertQueryRule,
undefinedSetupQueryRule, // Cross-file validation
paramTypeMismatchRule, // Type checking for function parameters
returnTypeMismatchRule, // Type checking for return value assertions
undeclaredQueryParamRule, // Parameters used in query body but not declared
unknownParameterRule, // Using a parameter that doesn't exist in the query
duplicateTestRule, // Duplicate test names cause conflicts
duplicateGroupRule, // Duplicate group names cause conflicts
missingRequiredParamsRule, // Missing params will cause runtime failures
invalidExpressionRule, // Expression syntax/type errors (compile-time)
invalidTypeAnnotationRule, // Invalid type names in function signatures
undefinedSetupQueryRule, // Cross-file validation
paramTypeMismatchRule, // Type checking for function parameters
returnTypeMismatchRule, // Type checking for return value assertions
undeclaredQueryParamRule, // Parameters used in query body but not declared
unknownParameterRule, // Using a parameter that doesn't exist in the query
duplicateTestRule, // Duplicate test names cause conflicts
duplicateGroupRule, // Duplicate group names cause conflicts
missingRequiredParamsRule, // Missing params will cause runtime failures
invalidExpressionRule, // Expression syntax/type errors (compile-time)
invalidTypeAnnotationRule, // Invalid type names in function signatures

// Warning-level checks.
unusedImportRule,
Expand Down Expand Up @@ -2066,4 +2066,4 @@ func checkItemReturnTypes(f *AnalyzedFile, items []*scaf.TestOrGroup, returnType
checkItemReturnTypes(f, item.Group.Items, returnTypes, queryName)
}
}
}
}
2 changes: 1 addition & 1 deletion cmd/scaf-lsp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func run(ctx context.Context, startupLogger *zap.Logger, in io.Reader, out io.Wr
var stderrCore zapcore.Core
if logfile != "" {
// Log to file
file, err := os.OpenFile(logfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
file, err := os.OpenFile(logfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
startupLogger.Warn("Failed to open logfile, falling back to stderr", zap.Error(err))
stderrCore = createStderrCore(level)
Expand Down
6 changes: 3 additions & 3 deletions cmd/scaf/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/rlch/scaf"
"github.com/rlch/scaf/analysis"
"github.com/rlch/scaf/language"
"github.com/rlch/scaf/language/go"
golang "github.com/rlch/scaf/language/go"
"github.com/urfave/cli/v3"

// Register bindings and dialects.
Expand Down Expand Up @@ -67,8 +67,8 @@ func generateCommand() *cli.Command {
Aliases: []string{"s"},
Usage: "path to schema HCL file (e.g., .scaf-schema.hcl)",
},
},
Action: runGenerate,
},
Action: runGenerate,
}
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/scaf/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,4 @@ func collectTestFiles(args []string) ([]string, error) {
}

return files, nil
}
}
Loading