├── lsp ├── testdata │ ├── did_change_regression1.jq │ ├── syntax_error.jq │ ├── first_column_error_lc.jq │ ├── import_search │ │ ├── a │ │ │ └── b.jq │ │ ├── import.jq │ │ └── import.json │ ├── include_not_found.jq │ ├── include_syntax_error.jq │ ├── object_val_query.jq │ ├── include_valid.jq │ ├── global_bindings.jq │ ├── keywords.jq │ ├── valid.jq │ ├── dot-jq-lsp │ │ ├── test.jq │ │ ├── .jq-lsp.jq │ │ └── test.json │ ├── import_as_binding.jq │ ├── definition.jq │ ├── hover.jq │ ├── completion.jq │ ├── binop_bind.jq │ ├── at_func.jq │ ├── foreach-reduce-query.jq │ ├── not_found.jq │ ├── syntax.jq │ ├── did_save.json │ ├── syntax.json │ ├── did_open_valid.json │ ├── keywords.json │ ├── include_valid.json │ ├── import_as_binding.json │ ├── include_syntax_error.json │ ├── did_change_valid.json │ ├── global_bindings.json │ ├── completion_binding.json │ ├── completion_format.json │ ├── defintion_def.json │ ├── defintion_binding.json │ ├── at_func.json │ ├── object_val_query.json │ ├── did_open_syntax_error.json │ ├── first_column_error_lc.json │ ├── did_change_regression1.json │ ├── did_change_syntax_error.json │ ├── hover_def.json │ ├── hover_builtin.json │ ├── symbols.json │ ├── binop_bind.json │ ├── foreach-reudce-query.json │ ├── completion_def.json │ └── state.json ├── .jq-lsp.jq ├── gen_builtin_env.jq ├── gen_docs.jq ├── lsp_test.go ├── lsp.go └── builtin_env.jq ├── gojqparser ├── Makefile ├── README.md ├── LICENSE ├── operator.go ├── term_type.go ├── encoder.go ├── lexer.go ├── parser.go.y └── query.go ├── go.mod ├── go.sum ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── .vscode └── settings.json ├── main.go ├── LICENSE ├── profile └── profile.go ├── .goreleaser.yml ├── README.md └── internal └── difftest └── difftest.go /lsp/testdata/did_change_regression1.jq: -------------------------------------------------------------------------------- 1 | § -------------------------------------------------------------------------------- /lsp/testdata/syntax_error.jq: -------------------------------------------------------------------------------- 1 | def fn 123; -------------------------------------------------------------------------------- /lsp/testdata/first_column_error_lc.jq: -------------------------------------------------------------------------------- 1 | 2 | aaa -------------------------------------------------------------------------------- /lsp/testdata/import_search/a/b.jq: -------------------------------------------------------------------------------- 1 | def c: 123; 2 | -------------------------------------------------------------------------------- /lsp/testdata/include_not_found.jq: -------------------------------------------------------------------------------- 1 | include "not_found"; -------------------------------------------------------------------------------- /lsp/testdata/include_syntax_error.jq: -------------------------------------------------------------------------------- 1 | include "syntax_error"; -------------------------------------------------------------------------------- /lsp/testdata/object_val_query.jq: -------------------------------------------------------------------------------- 1 | def f: {a: 1+2+abc}; 2 | 3 | -------------------------------------------------------------------------------- /lsp/testdata/include_valid.jq: -------------------------------------------------------------------------------- 1 | include "valid"; 2 | 3 | fn | 4 | fn2 5 | -------------------------------------------------------------------------------- /lsp/testdata/global_bindings.jq: -------------------------------------------------------------------------------- 1 | $ENV | $JQ_BUILD_CONFIGURATION | $ARGS 2 | -------------------------------------------------------------------------------- /lsp/testdata/keywords.jq: -------------------------------------------------------------------------------- 1 | def null: 123; 2 | def true: 123; 3 | def false: 123; 4 | 5 | -------------------------------------------------------------------------------- /lsp/testdata/valid.jq: -------------------------------------------------------------------------------- 1 | def fn: 123; 2 | def fn2: 3 | def _fn2: null; 4 | 123; 5 | -------------------------------------------------------------------------------- /lsp/testdata/dot-jq-lsp/test.jq: -------------------------------------------------------------------------------- 1 | own_builtin 2 | | @own_format 3 | | $own_global_variable 4 | -------------------------------------------------------------------------------- /lsp/testdata/import_as_binding.jq: -------------------------------------------------------------------------------- 1 | import "test" as $name; 2 | import "test" as $name2; 3 | $name | 4 | $name2 5 | -------------------------------------------------------------------------------- /gojqparser/Makefile: -------------------------------------------------------------------------------- 1 | all: parser.go 2 | 3 | %.go: %.go.y 4 | go run golang.org/x/tools/cmd/goyacc@latest -o $@ $< 5 | -------------------------------------------------------------------------------- /lsp/testdata/definition.jq: -------------------------------------------------------------------------------- 1 | def fn: 123; 2 | def fn2: fn; 3 | 4 | def binding: 5 | ( 123 as $abc 6 | | $abc 7 | ); -------------------------------------------------------------------------------- /lsp/testdata/dot-jq-lsp/.jq-lsp.jq: -------------------------------------------------------------------------------- 1 | def own_builtin: empty; 2 | def @own_format: empty; 3 | def $own_global_variable: empty; 4 | -------------------------------------------------------------------------------- /lsp/testdata/hover.jq: -------------------------------------------------------------------------------- 1 | def fn: 123; 2 | def fn2(a): 123; 3 | 4 | def test: 5 | ( fromjson 6 | | @sh 7 | | fn 8 | | fn2(1) 9 | ); -------------------------------------------------------------------------------- /lsp/testdata/completion.jq: -------------------------------------------------------------------------------- 1 | def from: 123; 2 | def from2: from; 3 | 4 | def binding: 5 | ( 123 as $abc 6 | | 123 as $abc2 7 | | $a 8 | | @h 9 | ); -------------------------------------------------------------------------------- /lsp/testdata/binop_bind.jq: -------------------------------------------------------------------------------- 1 | def a1: 1 + 2 as $x | -$x; 2 | def a2: 1 + $x1 as $x | -$x2; 3 | def b1: true // 1 as $x | [$x]; 4 | def b2: true // $x1 as $x | [$x2]; 5 | -------------------------------------------------------------------------------- /lsp/.jq-lsp.jq: -------------------------------------------------------------------------------- 1 | def query_fromstring: empty; 2 | def query_tostring: empty; 3 | def readfile: empty; 4 | def stdin($fd): empty; 5 | def stdout: empty; 6 | def stdlog: empty; 7 | def eval($expr): empty; -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wader/jq-lsp 2 | 3 | go 1.24 4 | 5 | require github.com/itchyny/gojq v0.12.18-0.20251127003340-5d8a53c7bd00 6 | 7 | require github.com/itchyny/timefmt-go v0.1.6 // indirect 8 | -------------------------------------------------------------------------------- /lsp/testdata/at_func.jq: -------------------------------------------------------------------------------- 1 | def @own_format: 123; 2 | 3 | ( "abc" 4 | | @own_format 5 | , @sh 6 | , @text 7 | , @json 8 | , @html 9 | , @uri 10 | , @urid 11 | , @csv 12 | , @tsv 13 | , @sh 14 | , @base64 15 | , @base64d 16 | , @missing_format 17 | ) 18 | -------------------------------------------------------------------------------- /lsp/testdata/foreach-reduce-query.jq: -------------------------------------------------------------------------------- 1 | foreach ( 2 | $missins, 3 | $var, 4 | split("") 5 | ) as $_ ( 6 | $also; 7 | $not; 8 | $defined 9 | ), 10 | reduce ( 11 | $missins, 12 | $var 13 | ) as $_ ( 14 | $also; 15 | $not 16 | ) -------------------------------------------------------------------------------- /gojqparser/README.md: -------------------------------------------------------------------------------- 1 | This is a modified version of gojq's parser https://github.com/itchyny/gojq which adds 2 | - token position 3 | - query marshal/unmarshal 4 | - `def @name` support, used by jaq for custom @formats 5 | - `def $name` support, used by jq-lsp to define own globals in .jq-lsp.jq 6 | -------------------------------------------------------------------------------- /lsp/testdata/import_search/import.jq: -------------------------------------------------------------------------------- 1 | include "b" {search: "a"}; 2 | import "b" as ns1 {search: "a"}; 3 | import "b" as ns2 {search: ["a"]}; 4 | import "b" as ns3 {search: ["missing","a"]}; 5 | import "b" as $name1 {search: "a"}; 6 | import "b" as $name2 {search: ["a"]}; 7 | 8 | c, 9 | ns1::c, 10 | ns2::c, 11 | ns3::c, 12 | $name1, 13 | $name2 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/itchyny/gojq v0.12.18-0.20251127003340-5d8a53c7bd00 h1:LkupEh4zMxSbcBH4Kla2Qlr+2yP0FdVYZXnt7sgdTNE= 2 | github.com/itchyny/gojq v0.12.18-0.20251127003340-5d8a53c7bd00/go.mod h1:j5Phaaa27hiItmAoYPne5ogJyB/JOCXM4AKRL9nVWbo= 3 | github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= 4 | github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= 5 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | # enable manual trigger 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | name: go test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-go@v4 18 | with: 19 | go-version: stable 20 | - name: 21 | run: go test ./... -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-go@v4 17 | - uses: goreleaser/goreleaser-action@v5 18 | with: 19 | distribution: goreleaser 20 | version: latest 21 | args: release --clean 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 24 | -------------------------------------------------------------------------------- /lsp/testdata/not_found.jq: -------------------------------------------------------------------------------- 1 | def fn: a; 2 | def fn2: a(a; a); 3 | def binop: a + a; 4 | def unaryop: -a; 5 | def pipe: a | a; 6 | def comma: a, a; 7 | def _as: a as $_ | a; 8 | def paran: (a); 9 | def array: [a, a]; 10 | def object: {a: a}, {(a): a}, {$a}; 11 | def _if: if a then a end; 12 | def _if2: if a then a else a end; 13 | def _if3: if a then a elif a then a else a end; 14 | def _try: try a, try a catch a, a?; 15 | def _reduce: reduce a as $_ (a; a); 16 | def _foreach: foreach a as $_ (a; a); 17 | def _foreach2: foreach a as $_ (a; a; a); 18 | def string: "\(a)"; 19 | def _label: label $a | a | break $a; 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "argsenv", 4 | "bindenvs", 5 | "bytelength", 6 | "cond", 7 | "downcase", 8 | "elif", 9 | "fdenvs", 10 | "fromjson", 11 | "fromstring", 12 | "gojq", 13 | "gojqparser", 14 | "gsub", 15 | "interp", 16 | "labelenv", 17 | "println", 18 | "readline", 19 | "rtrimstr", 20 | "stdlog", 21 | "tonumber", 22 | "tostring" 23 | ], 24 | "go.lintTool": "golangci-lint", 25 | "files.trimTrailingWhitespace": true, 26 | "editor.formatOnSave": true 27 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime/debug" 7 | 8 | "github.com/wader/jq-lsp/lsp" 9 | "github.com/wader/jq-lsp/profile" 10 | ) 11 | 12 | var version string 13 | 14 | func main() { 15 | defer profile.MaybeProfile()() 16 | 17 | if version == "" { 18 | if bi, ok := debug.ReadBuildInfo(); ok { 19 | version = bi.Main.Version 20 | } 21 | } 22 | 23 | if err := lsp.Run(lsp.Env{ 24 | Version: version, 25 | ReadFile: os.ReadFile, 26 | Stdin: os.Stdin, 27 | Stdout: os.Stdout, 28 | Stderr: os.Stderr, 29 | Args: os.Args, 30 | Environ: os.Environ(), 31 | }); err != nil { 32 | fmt.Fprintf(os.Stderr, "%s\n", err) 33 | os.Exit(1) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Mattias Wadman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /gojqparser/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2021 itchyny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lsp/gen_builtin_env.jq: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env fq -d bytes -nf 2 | # ./lsp/gen_builtin_env.jq ../gojq/builtin.jq <(jq -rn 'builtins | tojson') <(gojq --yaml-input . ../jq/docs/content/manual/manual.yml) 3 | 4 | def gen_args: 5 | [range(.)] | map(.+97) | map([.] | implode); 6 | 7 | def _builtins_json: 8 | [ fromjson[] 9 | | split("/") as [$name,$args] 10 | | {"\($name)/\($args)": { 11 | str: $name, 12 | args: ($args | tonumber | gen_args), 13 | } 14 | } 15 | ] | add; 16 | 17 | def _builtins_jq: 18 | [ _query_fromstring 19 | | .func_defs 20 | | map(select(.name | startswith("_") | not)) 21 | | sort_by(.name) 22 | | .[] 23 | | (.args // []) as $args 24 | | {("\(.name)/\(.args | length)"): {str: .name, args: $args}} 25 | ] | add; 26 | 27 | def _builtins_doc: 28 | ( .. 29 | | select(.title | test("^`\\w+`"))? 30 | | .body as $doc 31 | | .title 32 | | split(", ")[] 33 | | .[1:-1] 34 | | {(.): ($doc | ltrimstr("\n") | rtrimstr("\n"))} 35 | ); 36 | 37 | ( ("def builtin_env:\n[{\n" | print) 38 | , ( ( ( input | tobytes | tostring | _builtins_json) 39 | + ( input | tobytes | tostring | _builtins_jq) 40 | | to_entries 41 | | sort_by(.key) 42 | | .[] 43 | | [(.key | tojson), ": ", (.value | tojson), ","] 44 | | join("") 45 | | println 46 | ) 47 | ) 48 | , ("}];\n" | print) 49 | ) -------------------------------------------------------------------------------- /lsp/testdata/syntax.jq: -------------------------------------------------------------------------------- 1 | 2 | def func: 123; 3 | def func($a): $a; 4 | def func($a; $b): $a + $b; 5 | 6 | def call1: func; 7 | def call2: func(1); 8 | def call3: func(1;2); 9 | 10 | def op1: func + func; 11 | 12 | def var: 123 as $var | $var; 13 | 14 | def pattern1: . as {$var} | $var; 15 | def pattern2: . as {var: $var} | $var; 16 | def pattern2: . as [$var] | $var; 17 | def pattern4: . as $var | . as {("\($var)"): $var2} | $var2; 18 | 19 | def object1: 1 as $var | {$var}; 20 | def object1: 1 as $var | {var: $var}; 21 | def object2: 1 as $var | {("\($var)"): $var}; 22 | 23 | def array1: 1 as $var | [$var]; 24 | 25 | def query1: (func); 26 | 27 | def comma1: func, func; 28 | 29 | def unary1: +func; 30 | 31 | def string1: "test \(func(1)) \(func)"; 32 | 33 | def try1: func?; 34 | def try2: try func catch func; 35 | 36 | def if1: 1 as $var | if $var then $var end; 37 | def if2: if func then func end; 38 | def if3: if func then func else func end; 39 | def if3: if func then func elif func then func else func end; 40 | 41 | def foreach1: foreach 1 as $var ($var;$var;$var); 42 | def foreach1($var): foreach 1 as {$var: $var2} ($var2;$var;$var); 43 | 44 | def reduce1: reduce 1 as $var ($var;$var); 45 | def reduce2($var): foreach 1 as {$var: $var2} ($var2;$var); 46 | 47 | def label1: label $out | break $out | 1; 48 | 49 | def object_val_query: {a: 1+2+3}; 50 | -------------------------------------------------------------------------------- /profile/profile.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "runtime" 7 | "runtime/pprof" 8 | ) 9 | 10 | func MaybeProfile() func() { 11 | return Profile(os.Getenv("CPUPROFILE"), os.Getenv("MEMPROFILE")) 12 | } 13 | 14 | func Profile(cpuProfilePath string, memProfilePath string) func() { 15 | var deferFns []func() 16 | 17 | if cpuProfilePath != "" { 18 | f, err := os.Create(cpuProfilePath) 19 | if err != nil { 20 | log.Fatal("could not create CPU profile: ", err) 21 | } 22 | if err := pprof.StartCPUProfile(f); err != nil { 23 | log.Fatal("could not start CPU profile: ", err) 24 | } else { 25 | deferFns = append(deferFns, func() { 26 | pprof.StopCPUProfile() 27 | if err := f.Close(); err != nil { 28 | log.Fatal("could not close CPU profile: ", err) 29 | } 30 | }) 31 | } 32 | } 33 | 34 | return func() { 35 | for _, fn := range deferFns { 36 | fn() 37 | } 38 | 39 | if memProfilePath != "" { 40 | f, err := os.Create(memProfilePath) 41 | if err != nil { 42 | log.Fatal("could not create memory profile: ", err) 43 | } 44 | if err := pprof.WriteHeapProfile(f); err != nil { 45 | log.Fatal("could not write memory profile: ", err) 46 | } else { 47 | runtime.GC() // get up-to-date statistics 48 | if err := f.Close(); err != nil { 49 | log.Fatal("could not close memory profile: ", err) 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lsp/testdata/did_save.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didSave", 37 | "params": { 38 | "textDocument": { 39 | "uri": "file:///valid.jq", 40 | "version": 2 41 | } 42 | } 43 | }, 44 | "response": {} 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # to test 2 | # docker run -ti -v "$PWD:$PWD" -w "$PWD" goreleaser/goreleaser:latest release --snapshot --rm-dist 3 | project_name: jq-lsp 4 | 5 | before: 6 | hooks: 7 | - go mod download 8 | 9 | release: 10 | draft: true 11 | 12 | builds: 13 | - env: 14 | - CGO_ENABLED=0 15 | goarch: 16 | - amd64 17 | - arm64 18 | goos: 19 | - linux 20 | - windows 21 | - darwin 22 | flags: 23 | - -trimpath 24 | ldflags: 25 | # omit symbol table and debug information 26 | - -s -w 27 | - -X main.version={{.Version}} 28 | - -X main.commit={{.Commit}} 29 | - -X main.date={{.CommitDate}} 30 | - -X main.builtBy=goreleaser 31 | checksum: 32 | name_template: "checksums.txt" 33 | 34 | archives: 35 | - files: 36 | # skip all other files 37 | - none* 38 | format_overrides: 39 | - goos: windows 40 | format: zip 41 | - goos: darwin 42 | format: zip 43 | # name_1.2.3_linux_amd64.tar.gz 44 | # name_1.2.3_macos_arm64.zip 45 | name_template: >- 46 | {{ .ProjectName }}_ 47 | {{- .Version }}_ 48 | {{- if eq .Os "darwin" }}macos_ 49 | {{- else }}{{ .Os }}_{{ end }} 50 | {{- .Arch }} 51 | 52 | changelog: 53 | sort: asc 54 | filters: 55 | exclude: 56 | - "^Merge" 57 | 58 | brews: 59 | - skip_upload: auto 60 | repository: 61 | owner: wader 62 | name: homebrew-tap 63 | folder: Formula 64 | homepage: https://github.com/wader/jq-lsp 65 | description: jq language server 66 | license: MIT 67 | test: | 68 | system "#{bin}/jq-lsp --version" 69 | -------------------------------------------------------------------------------- /lsp/testdata/syntax.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/hover", 37 | "params": { 38 | "context": { 39 | "triggerKind": 1 40 | }, 41 | "position": { 42 | "character": 5, 43 | "line": 4 44 | }, 45 | "textDocument": { 46 | "uri": "file:///syntax.jq" 47 | } 48 | } 49 | }, 50 | "response": { 51 | "id": 1, 52 | "jsonrpc": "2.0", 53 | "result": null 54 | } 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /lsp/testdata/did_open_valid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@valid.jq", 41 | "uri": "file:///valid.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [], 51 | "uri": "file:///valid.jq" 52 | } 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /lsp/testdata/dot-jq-lsp/test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@test.jq", 41 | "uri": "file:///test.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [], 51 | "uri": "file:///test.jq" 52 | } 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /lsp/testdata/keywords.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@keywords.jq", 41 | "uri": "file:///keywords.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [], 51 | "uri": "file:///keywords.jq" 52 | } 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /lsp/testdata/import_search/import.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@import.jq", 41 | "uri": "file:///import.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [], 51 | "uri": "file:///import.jq" 52 | } 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /lsp/testdata/include_valid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@include_valid.jq", 41 | "uri": "file:///include_valid.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [], 51 | "uri": "file:///include_valid.jq" 52 | } 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /lsp/testdata/import_as_binding.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@import_as_binding.jq", 41 | "uri": "file:///import_as_binding.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [], 51 | "uri": "file:///import_as_binding.jq" 52 | } 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /lsp/testdata/include_syntax_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@include_syntax_error.jq", 41 | "uri": "file:///include_syntax_error.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [], 51 | "uri": "file:///include_syntax_error.jq" 52 | } 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /lsp/testdata/did_change_valid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didChange", 37 | "params": { 38 | "contentChanges": [ 39 | { 40 | "text": "@valid.jq" 41 | } 42 | ], 43 | "textDocument": { 44 | "uri": "file:///valid.jq", 45 | "version": 2 46 | } 47 | } 48 | }, 49 | "response": { 50 | "jsonrpc": "2.0", 51 | "method": "textDocument/publishDiagnostics", 52 | "params": { 53 | "diagnostics": [], 54 | "uri": "file:///valid.jq" 55 | } 56 | } 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /lsp/testdata/global_bindings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didChange", 37 | "params": { 38 | "contentChanges": [ 39 | { 40 | "text": "@global_bindings.jq" 41 | } 42 | ], 43 | "textDocument": { 44 | "uri": "file:///global_bindings.jq", 45 | "version": 2 46 | } 47 | } 48 | }, 49 | "response": { 50 | "jsonrpc": "2.0", 51 | "method": "textDocument/publishDiagnostics", 52 | "params": { 53 | "diagnostics": [], 54 | "uri": "file:///global_bindings.jq" 55 | } 56 | } 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /lsp/testdata/completion_binding.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/completion", 37 | "params": { 38 | "position": { 39 | "character": 6, 40 | "line": 6 41 | }, 42 | "textDocument": { 43 | "uri": "file:///completion.jq" 44 | } 45 | } 46 | }, 47 | "response": { 48 | "id": 1, 49 | "jsonrpc": "2.0", 50 | "result": [ 51 | { 52 | "kind": 6, 53 | "label": "$abc2" 54 | }, 55 | { 56 | "kind": 6, 57 | "label": "$abc" 58 | } 59 | ] 60 | } 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /lsp/testdata/completion_format.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/completion", 37 | "params": { 38 | "position": { 39 | "character": 6, 40 | "line": 7 41 | }, 42 | "textDocument": { 43 | "uri": "file:///completion.jq" 44 | } 45 | } 46 | }, 47 | "response": { 48 | "id": 1, 49 | "jsonrpc": "2.0", 50 | "result": [ 51 | { 52 | "documentation": { 53 | "kind": "markdown", 54 | "value": "```jq\ndef @html:\n```\nApplies HTML/XML escaping, by mapping the characters \u003c\u003e\u0026'\" to their entity equivalents \u0026lt;, \u0026gt;, \u0026amp;, \u0026apos;, \u0026quot;." 55 | }, 56 | "kind": 3, 57 | "label": "@html" 58 | } 59 | ] 60 | } 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /lsp/testdata/defintion_def.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/definition", 37 | "params": { 38 | "context": { 39 | "triggerKind": 1 40 | }, 41 | "position": { 42 | "character": 10, 43 | "line": 1 44 | }, 45 | "textDocument": { 46 | "uri": "file:///definition.jq" 47 | } 48 | } 49 | }, 50 | "response": { 51 | "id": 1, 52 | "jsonrpc": "2.0", 53 | "result": { 54 | "range": { 55 | "end": { 56 | "character": 6, 57 | "line": 0 58 | }, 59 | "start": { 60 | "character": 4, 61 | "line": 0 62 | } 63 | }, 64 | "uri": "file:///definition.jq" 65 | } 66 | } 67 | } 68 | ] 69 | -------------------------------------------------------------------------------- /lsp/testdata/defintion_binding.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/definition", 37 | "params": { 38 | "context": { 39 | "triggerKind": 1 40 | }, 41 | "position": { 42 | "character": 7, 43 | "line": 5 44 | }, 45 | "textDocument": { 46 | "uri": "file:///definition.jq" 47 | } 48 | } 49 | }, 50 | "response": { 51 | "id": 1, 52 | "jsonrpc": "2.0", 53 | "result": { 54 | "range": { 55 | "end": { 56 | "character": 15, 57 | "line": 4 58 | }, 59 | "start": { 60 | "character": 11, 61 | "line": 4 62 | } 63 | }, 64 | "uri": "file:///definition.jq" 65 | } 66 | } 67 | } 68 | ] 69 | -------------------------------------------------------------------------------- /lsp/gen_docs.jq: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env fq -rf 2 | # ./lsp/gen_docs.jq <(gojq --yaml-input . ../jq/docs/content/manual/manual.yml) 3 | 4 | 5 | # TODO: skip ^$ 6 | def map_titles: 7 | { 8 | "Convert to/from JSON": "`fromjson`, `tojson`", 9 | "`$ENV`, `env`": "`env`", 10 | # missing ` 11 | "`sort, sort_by(path_expression)`": "`sort`, `sort_by(path_expression)`", 12 | "Dates": "`now`, `fromdateiso8601`, `todateiso8601`, `todate`, `fromdate`, `strptime(fmt)`, `strftime(fmt)`, `strflocaltime`, `mktime`, `gmtime`, `localtime`", 13 | "SQL-Style Operators": "`INDEX(stream; index_expression)`, `JOIN($idx; stream; idx_expr; join_expr)`, `JOIN($idx; stream; idx_expr)`, `JOIN($idx; idx_expr)`, `IN(s)`, `IN(source; s)`", 14 | # missing , 15 | "`sub(regex; tostring)` `sub(regex; string; flags)`": "`sub(regex; tostring)`, `sub(regex; string; flags)`", 16 | # missing , 17 | "`range(upto)`, `range(from;upto)` `range(from;upto;by)`": "`range(upto)`, `range(from;upto)`, `range(from;upto;by)`", 18 | "Math": "`acos`, `acosh`, `asin`, `asinh`, `atan`, `atanh`, `cbrt`, `ceil`, `cos`, `cosh`, `erf`, `erfc`, `exp`, `exp10`, `exp2`, `expm1`, `fabs`, `floor`, `gamma`, `j0`, `j1`, `lgamma`, `log`, `log10`, `log1p`, `log2`, `logb`, `nearbyint`, `pow10`, `rint`, `round`, `significand`, `sin`, `sinh`, `sqrt`, `tan`, `tanh`, `tgamma`, `trunc`, `y0`, `y1`, `atan2` `copysign` `drem`, `fdim`, `fmax`, `fmin`, `fmod`, `frexp`, `hypot`, `jn`, `ldexp`, `modf`, `nextafter`, `nexttoward`, `pow`, `remainder`, `scalb`, `scalbln`, `yn`, `fma`", 19 | # missing `, 20 | "`sort, sort_by(path_expression)`": "`sort`, `sort_by(path_expression)`", 21 | "`error(message)`": "`error`, `error(message)`", 22 | "'I/O'": "`stdout`" 23 | }; 24 | 25 | ( "# based on https://github.com/stedolan/jq/blob/master/docs/content/manual/manual.yml" 26 | , "def docs:" 27 | , ( [ .. 28 | | select(.title)? 29 | | .body as $doc 30 | | .title 31 | | if map_titles[.] then map_titles[.] else . end 32 | | select(test("^`\\w.*`$"))? 33 | | split(", ")[] | .[1:-1] | "\(.)" 34 | | select(. != "and" and . != "or" and . != "not") 35 | | {(split("(") | [.[0], "/", (.[1] | if . then split(";") | length else 0 end)] | join("")): 36 | ($doc | ltrimstr("\n") | rtrimstr("\n")) 37 | } 38 | ] 39 | | add 40 | ) 41 | , ";" 42 | ) 43 | -------------------------------------------------------------------------------- /lsp/testdata/at_func.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@at_func.jq", 41 | "uri": "file:///at_func.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [ 51 | { 52 | "message": "@missing_format not found", 53 | "range": { 54 | "end": { 55 | "character": 17, 56 | "line": 15 57 | }, 58 | "start": { 59 | "character": 2, 60 | "line": 15 61 | } 62 | } 63 | } 64 | ], 65 | "uri": "file:///at_func.jq" 66 | } 67 | } 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /lsp/testdata/object_val_query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@object_val_query.jq", 41 | "uri": "file:///object_val_query.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [ 51 | { 52 | "message": "abc not found", 53 | "range": { 54 | "end": { 55 | "character": 18, 56 | "line": 0 57 | }, 58 | "start": { 59 | "character": 15, 60 | "line": 0 61 | } 62 | } 63 | } 64 | ], 65 | "uri": "file:///object_val_query.jq" 66 | } 67 | } 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /lsp/testdata/did_open_syntax_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@syntax_error.jq", 41 | "uri": "file:///syntax_error.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [ 51 | { 52 | "message": "unexpected token \"123\"", 53 | "range": { 54 | "end": { 55 | "character": 10, 56 | "line": 0 57 | }, 58 | "start": { 59 | "character": 10, 60 | "line": 0 61 | } 62 | } 63 | } 64 | ], 65 | "uri": "file:///syntax_error.jq" 66 | } 67 | } 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /lsp/testdata/first_column_error_lc.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@first_column_error_lc.jq", 41 | "uri": "file:///first_column_error_lc.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [ 51 | { 52 | "message": "aaa not found", 53 | "range": { 54 | "end": { 55 | "character": 3, 56 | "line": 1 57 | }, 58 | "start": { 59 | "character": 0, 60 | "line": 1 61 | } 62 | } 63 | } 64 | ], 65 | "uri": "file:///first_column_error_lc.jq" 66 | } 67 | } 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /lsp/testdata/did_change_regression1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "jsonrpc": "2.0", 35 | "method": "textDocument/didChange", 36 | "params": { 37 | "contentChanges": [ 38 | { 39 | "text": "§" 40 | } 41 | ], 42 | "textDocument": { 43 | "uri": "file:///did_change_regression1.jq", 44 | "version": 2 45 | } 46 | } 47 | }, 48 | "response": { 49 | "jsonrpc": "2.0", 50 | "method": "textDocument/publishDiagnostics", 51 | "params": { 52 | "diagnostics": [ 53 | { 54 | "message": "unexpected token \"§\"", 55 | "range": { 56 | "end": { 57 | "character": 2, 58 | "line": 0 59 | }, 60 | "start": { 61 | "character": 2, 62 | "line": 0 63 | } 64 | } 65 | } 66 | ], 67 | "uri": "file:///did_change_regression1.jq" 68 | } 69 | } 70 | } 71 | ] 72 | -------------------------------------------------------------------------------- /lsp/testdata/did_change_syntax_error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didChange", 37 | "params": { 38 | "contentChanges": [ 39 | { 40 | "text": "@syntax_error.jq" 41 | } 42 | ], 43 | "textDocument": { 44 | "uri": "file:///syntax_error.jq", 45 | "version": 2 46 | } 47 | } 48 | }, 49 | "response": { 50 | "jsonrpc": "2.0", 51 | "method": "textDocument/publishDiagnostics", 52 | "params": { 53 | "diagnostics": [ 54 | { 55 | "message": "unexpected token \"123\"", 56 | "range": { 57 | "end": { 58 | "character": 10, 59 | "line": 0 60 | }, 61 | "start": { 62 | "character": 10, 63 | "line": 0 64 | } 65 | } 66 | } 67 | ], 68 | "uri": "file:///syntax_error.jq" 69 | } 70 | } 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /lsp/testdata/hover_def.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/hover", 37 | "params": { 38 | "context": { 39 | "triggerKind": 1 40 | }, 41 | "position": { 42 | "character": 5, 43 | "line": 6 44 | }, 45 | "textDocument": { 46 | "uri": "file:///hover.jq" 47 | } 48 | } 49 | }, 50 | "response": { 51 | "id": 1, 52 | "jsonrpc": "2.0", 53 | "result": { 54 | "contents": "```jq\ndef fn:\n```" 55 | } 56 | } 57 | }, 58 | { 59 | "request": { 60 | "id": 1, 61 | "jsonrpc": "2.0", 62 | "method": "textDocument/hover", 63 | "params": { 64 | "context": { 65 | "triggerKind": 1 66 | }, 67 | "position": { 68 | "character": 5, 69 | "line": 7 70 | }, 71 | "textDocument": { 72 | "uri": "file:///hover.jq" 73 | } 74 | } 75 | }, 76 | "response": { 77 | "id": 1, 78 | "jsonrpc": "2.0", 79 | "result": { 80 | "contents": "```jq\ndef fn2(a):\n```" 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /lsp/testdata/hover_builtin.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/hover", 37 | "params": { 38 | "context": { 39 | "triggerKind": 1 40 | }, 41 | "position": { 42 | "character": 5, 43 | "line": 4 44 | }, 45 | "textDocument": { 46 | "uri": "file:///hover.jq" 47 | } 48 | } 49 | }, 50 | "response": { 51 | "id": 1, 52 | "jsonrpc": "2.0", 53 | "result": { 54 | "contents": "```jq\ndef fromjson:\n```\nThe `tojson` and `fromjson` builtins dump values as JSON texts\nor parse JSON texts into values, respectively. The `tojson`\nbuiltin differs from `tostring` in that `tostring` returns strings\nunmodified, while `tojson` encodes strings as JSON strings." 55 | } 56 | } 57 | }, 58 | { 59 | "request": { 60 | "id": 1, 61 | "jsonrpc": "2.0", 62 | "method": "textDocument/hover", 63 | "params": { 64 | "context": { 65 | "triggerKind": 1 66 | }, 67 | "position": { 68 | "character": 5, 69 | "line": 5 70 | }, 71 | "textDocument": { 72 | "uri": "file:///hover.jq" 73 | } 74 | } 75 | }, 76 | "response": { 77 | "id": 1, 78 | "jsonrpc": "2.0", 79 | "result": { 80 | "contents": "```jq\ndef @sh:\n```\nThe input is escaped suitable for use in a command-line for a POSIX shell. If the input is an array, the output will be a series of space-separated strings." 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /gojqparser/operator.go: -------------------------------------------------------------------------------- 1 | package gojqparser 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Operator ... 9 | type Operator int 10 | 11 | // Operators ... 12 | const ( 13 | OpPipe Operator = iota + 1 14 | OpComma 15 | OpAdd 16 | OpSub 17 | OpMul 18 | OpDiv 19 | OpMod 20 | OpEq 21 | OpNe 22 | OpGt 23 | OpLt 24 | OpGe 25 | OpLe 26 | OpAnd 27 | OpOr 28 | OpAlt 29 | OpAssign 30 | OpModify 31 | OpUpdateAdd 32 | OpUpdateSub 33 | OpUpdateMul 34 | OpUpdateDiv 35 | OpUpdateMod 36 | OpUpdateAlt 37 | ) 38 | 39 | // String implements [fmt.Stringer]. 40 | func OperatorFromString(s string) Operator { 41 | switch s { 42 | case "|": 43 | return OpPipe 44 | case ",": 45 | return OpComma 46 | case "+": 47 | return OpAdd 48 | case "-": 49 | return OpSub 50 | case "*": 51 | return OpMul 52 | case "/": 53 | return OpDiv 54 | case "%": 55 | return OpMod 56 | case "==": 57 | return OpEq 58 | case "!=": 59 | return OpNe 60 | case ">": 61 | return OpGt 62 | case "<": 63 | return OpLt 64 | case ">=": 65 | return OpGe 66 | case "<=": 67 | return OpLe 68 | case "and": 69 | return OpAnd 70 | case "or": 71 | return OpOr 72 | case "//": 73 | return OpAlt 74 | case "=": 75 | return OpAssign 76 | case "|=": 77 | return OpModify 78 | case "+=": 79 | return OpUpdateAdd 80 | case "-=": 81 | return OpUpdateSub 82 | case "*=": 83 | return OpUpdateMul 84 | case "/=": 85 | return OpUpdateDiv 86 | case "%=": 87 | return OpUpdateMod 88 | case "//=": 89 | return OpUpdateAlt 90 | default: 91 | return 0 92 | } 93 | } 94 | 95 | // String implements [fmt.Stringer]. 96 | func (op Operator) String() string { 97 | switch op { 98 | case OpPipe: 99 | return "|" 100 | case OpComma: 101 | return "," 102 | case OpAdd: 103 | return "+" 104 | case OpSub: 105 | return "-" 106 | case OpMul: 107 | return "*" 108 | case OpDiv: 109 | return "/" 110 | case OpMod: 111 | return "%" 112 | case OpEq: 113 | return "==" 114 | case OpNe: 115 | return "!=" 116 | case OpGt: 117 | return ">" 118 | case OpLt: 119 | return "<" 120 | case OpGe: 121 | return ">=" 122 | case OpLe: 123 | return "<=" 124 | case OpAnd: 125 | return "and" 126 | case OpOr: 127 | return "or" 128 | case OpAlt: 129 | return "//" 130 | case OpAssign: 131 | return "=" 132 | case OpModify: 133 | return "|=" 134 | case OpUpdateAdd: 135 | return "+=" 136 | case OpUpdateSub: 137 | return "-=" 138 | case OpUpdateMul: 139 | return "*=" 140 | case OpUpdateDiv: 141 | return "/=" 142 | case OpUpdateMod: 143 | return "%=" 144 | case OpUpdateAlt: 145 | return "//=" 146 | default: 147 | return "" 148 | } 149 | } 150 | 151 | func (op Operator) MarshalJSON() ([]byte, error) { 152 | if op == 0 { 153 | return json.Marshal(nil) 154 | } 155 | return json.Marshal(op.String()) 156 | } 157 | 158 | func (op *Operator) UnmarshalJSON(text []byte) error { 159 | var s string 160 | err := json.Unmarshal(text, &s) 161 | if err != nil { 162 | return err 163 | } 164 | *op = OperatorFromString(s) 165 | if *op == 0 { 166 | return fmt.Errorf("unknown operator %v", s) 167 | } 168 | return nil 169 | } 170 | -------------------------------------------------------------------------------- /lsp/testdata/symbols.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/documentSymbol", 37 | "params": { 38 | "textDocument": { 39 | "uri": "file:///valid.jq", 40 | "version": 2 41 | } 42 | } 43 | }, 44 | "response": { 45 | "id": 1, 46 | "jsonrpc": "2.0", 47 | "result": [ 48 | { 49 | "kind": 12, 50 | "location": { 51 | "range": { 52 | "end": { 53 | "character": 12, 54 | "line": 2 55 | }, 56 | "start": { 57 | "character": 8, 58 | "line": 2 59 | } 60 | }, 61 | "uri": "file:///valid.jq" 62 | }, 63 | "name": "_fn2" 64 | }, 65 | { 66 | "kind": 12, 67 | "location": { 68 | "range": { 69 | "end": { 70 | "character": 6, 71 | "line": 0 72 | }, 73 | "start": { 74 | "character": 4, 75 | "line": 0 76 | } 77 | }, 78 | "uri": "file:///valid.jq" 79 | }, 80 | "name": "fn" 81 | }, 82 | { 83 | "kind": 12, 84 | "location": { 85 | "range": { 86 | "end": { 87 | "character": 7, 88 | "line": 1 89 | }, 90 | "start": { 91 | "character": 4, 92 | "line": 1 93 | } 94 | }, 95 | "uri": "file:///valid.jq" 96 | }, 97 | "name": "fn2" 98 | } 99 | ] 100 | } 101 | } 102 | ] 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jq-lsp 2 | 3 | jq language server. 4 | 5 | You probably want to use this via one of these: 6 | - [vscode-jq](https://github.com/wader/vscode-jq) 7 | - neovim using [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#jqls) and [mason.nvim](https://github.com/williamboman/mason.nvim) 8 | - [Emacs lsp-mode](https://github.com/emacs-lsp/lsp-mode) 9 | - [helix](https://github.com/helix-editor/helix) (since 25.01) 10 | - [IntelliJ with LSP4IJ](https://github.com/redhat-developer/lsp4ij/blob/main/docs/user-defined-ls/jq-lsp.md) 11 | 12 | It can currently do: 13 | - Syntax checking 14 | - Error on missing function and binding 15 | - Goto definition of function and binding 16 | - Auto complete function and binding 17 | - Include/Import support 18 | - Hover definition of function 19 | - Hover documentation for builtin 20 | - Function symbols per document 21 | - Additional builtins using `.jq-lsp.jq` 22 | 23 | ## Install 24 | 25 | ```sh 26 | # install latest release 27 | go install github.com/wader/jq-lsp@latest 28 | # install master 29 | go install github.com/wader/jq-lsp@master 30 | 31 | # copy binary to $PATH 32 | cp "$(go env GOPATH)/bin/jq-lsp" /usr/local/bin 33 | 34 | # build binary from cloned repo 35 | go build -o jq-lsp . 36 | ``` 37 | 38 | ## Additional builtins using `.jq-lsp.jq` 39 | 40 | To make jq-lsp aware of additional builtin function and variables you can use a `.jq-lsp.jq` file. The file is a normal jq file that is included automatically in each file. 41 | 42 | This `.jq-lsp.jq` file add a function `fetch` and the variable `$OPTIONS`: 43 | ```jq 44 | # function body is ignored but has to be valid jq 45 | def fetch: empty; # adds function fetch/0 46 | def $OPTIONS: empty; # adds variable $OPTIONS 47 | ``` 48 | 49 | ## Development 50 | 51 | ```sh 52 | # run all tests 53 | go test -v ./... 54 | # run tests and update responses 55 | go test -v ./... -update 56 | ``` 57 | 58 | `--eval` and `jsonrpc_call/2` can be used to generate test requests: 59 | ```sh 60 | # call initialize 61 | go run . --eval 'jsonrpc_call("initialize"; {})' | go run . 62 | # call textDocument/didOpen with file test.jq 63 | go run . --eval 'jsonrpc_call("textDocument/didOpen"; {textDocument: {"text": ("test.jq" | readfile), uri: "test.jq"}})' | go run . 64 | # like above but 65 | go run . --eval 'jsonrpc_call("textDocument/didOpen"; {textDocument: {"text": ("test.jq" | readfile), uri: "test.jq"}})' | go run . | go run . --eval 'jsonrpc_read.params | . as {$uri} | .diagnostics[] | "\($uri):\(.range.start.line+1): \(.message)\n" | stdout' 66 | ``` 67 | 68 | ## Thanks 69 | 70 | jq-lsp uses a modified version of 71 | [itchyny](https://github.com/itchyny)'s [gojq](https://github.com/itchyny/gojq) parser. 72 | 73 | builtins documentation is based on https://github.com/stedolan/jq/blob/master/docs/content/manual/manual.yml 74 | and is licensed under https://github.com/stedolan/jq/blob/master/COPYING 75 | 76 | ## TODO and ideas 77 | 78 | - Signature help 79 | - Semantic tokens 80 | - Own parser or modified gojq parser to be able to recover and give more useful errors 81 | - Server loop and https://github.com/itchyny/gojq/issues/86 82 | - Warn about unused functions and bindings 83 | - Better at handling broken syntax while typing 84 | - `$`/`@` auto complete, add temp name? 85 | - `a | n f`, add temp name/try fix syntax? 86 | - Transitive include behavior (jq/gojq behaves differently?) 87 | - Source formatting 88 | - Requires whitespace/comment support in lexer/parser 89 | - Goto definition for builtin functions somehow 90 | - Input completion. How to indicate input and to do safe eval? 91 | - REPL or some kind of eval support? 92 | - See https://github.com/ailisp/commonlisp-vscode 93 | -------------------------------------------------------------------------------- /gojqparser/term_type.go: -------------------------------------------------------------------------------- 1 | package gojqparser 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // TermType represents the type of [Term]. 9 | type TermType int 10 | 11 | // TermType list. 12 | const ( 13 | TermTypeIdentity TermType = iota + 1 14 | TermTypeRecurse 15 | TermTypeNull 16 | TermTypeTrue 17 | TermTypeFalse 18 | TermTypeIndex 19 | TermTypeFunc 20 | TermTypeObject 21 | TermTypeArray 22 | TermTypeNumber 23 | TermTypeUnary 24 | TermTypeFormat 25 | TermTypeString 26 | TermTypeIf 27 | TermTypeTry 28 | TermTypeReduce 29 | TermTypeForeach 30 | TermTypeLabel 31 | TermTypeBreak 32 | TermTypeQuery 33 | ) 34 | 35 | // GoString implements [fmt.GoStringer]. 36 | func TermTypeFromString(s string) TermType { 37 | switch s { 38 | case "TermTypeIdentity": 39 | return TermTypeIdentity 40 | case "TermTypeRecurse": 41 | return TermTypeRecurse 42 | case "TermTypeNull": 43 | return TermTypeNull 44 | case "TermTypeTrue": 45 | return TermTypeTrue 46 | case "TermTypeFalse": 47 | return TermTypeFalse 48 | case "TermTypeIndex": 49 | return TermTypeIndex 50 | case "TermTypeFunc": 51 | return TermTypeFunc 52 | case "TermTypeObject": 53 | return TermTypeObject 54 | case "TermTypeArray": 55 | return TermTypeArray 56 | case "TermTypeNumber": 57 | return TermTypeNumber 58 | case "TermTypeUnary": 59 | return TermTypeUnary 60 | case "TermTypeFormat": 61 | return TermTypeFormat 62 | case "TermTypeString": 63 | return TermTypeString 64 | case "TermTypeIf": 65 | return TermTypeIf 66 | case "TermTypeTry": 67 | return TermTypeTry 68 | case "TermTypeReduce": 69 | return TermTypeReduce 70 | case "TermTypeForeach": 71 | return TermTypeForeach 72 | case "TermTypeLabel": 73 | return TermTypeLabel 74 | case "TermTypeBreak": 75 | return TermTypeBreak 76 | case "TermTypeQuery": 77 | return TermTypeQuery 78 | default: 79 | return 0 80 | } 81 | } 82 | 83 | // GoString implements [fmt.GoStringer]. 84 | func (termType TermType) GoString() (str string) { 85 | defer func() { str = "gojq." + str }() 86 | switch termType { 87 | case TermTypeIdentity: 88 | return "TermTypeIdentity" 89 | case TermTypeRecurse: 90 | return "TermTypeRecurse" 91 | case TermTypeNull: 92 | return "TermTypeNull" 93 | case TermTypeTrue: 94 | return "TermTypeTrue" 95 | case TermTypeFalse: 96 | return "TermTypeFalse" 97 | case TermTypeIndex: 98 | return "TermTypeIndex" 99 | case TermTypeFunc: 100 | return "TermTypeFunc" 101 | case TermTypeObject: 102 | return "TermTypeObject" 103 | case TermTypeArray: 104 | return "TermTypeArray" 105 | case TermTypeNumber: 106 | return "TermTypeNumber" 107 | case TermTypeUnary: 108 | return "TermTypeUnary" 109 | case TermTypeFormat: 110 | return "TermTypeFormat" 111 | case TermTypeString: 112 | return "TermTypeString" 113 | case TermTypeIf: 114 | return "TermTypeIf" 115 | case TermTypeTry: 116 | return "TermTypeTry" 117 | case TermTypeReduce: 118 | return "TermTypeReduce" 119 | case TermTypeForeach: 120 | return "TermTypeForeach" 121 | case TermTypeLabel: 122 | return "TermTypeLabel" 123 | case TermTypeBreak: 124 | return "TermTypeBreak" 125 | case TermTypeQuery: 126 | return "TermTypeQuery" 127 | default: 128 | panic(termType) 129 | } 130 | } 131 | 132 | func (termType TermType) MarshalJSON() ([]byte, error) { 133 | if termType == 0 { 134 | return json.Marshal(nil) 135 | } 136 | // TODO: gojq. skips prefix 137 | return json.Marshal(termType.GoString()[5:]) 138 | } 139 | 140 | func (termType *TermType) UnmarshalJSON(text []byte) error { 141 | var s string 142 | err := json.Unmarshal(text, &s) 143 | if err != nil { 144 | return err 145 | } 146 | *termType = TermTypeFromString(s) 147 | if *termType == 0 { 148 | return fmt.Errorf("unknown term %v", s) 149 | } 150 | return nil 151 | } 152 | -------------------------------------------------------------------------------- /lsp/testdata/binop_bind.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@binop_bind.jq", 41 | "uri": "file:///binop_bind.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [ 51 | { 52 | "message": "$x1 not found", 53 | "range": { 54 | "end": { 55 | "character": 15, 56 | "line": 1 57 | }, 58 | "start": { 59 | "character": 12, 60 | "line": 1 61 | } 62 | } 63 | }, 64 | { 65 | "message": "$x2 not found", 66 | "range": { 67 | "end": { 68 | "character": 28, 69 | "line": 1 70 | }, 71 | "start": { 72 | "character": 25, 73 | "line": 1 74 | } 75 | } 76 | }, 77 | { 78 | "message": "$x1 not found", 79 | "range": { 80 | "end": { 81 | "character": 19, 82 | "line": 3 83 | }, 84 | "start": { 85 | "character": 16, 86 | "line": 3 87 | } 88 | } 89 | }, 90 | { 91 | "message": "$x2 not found", 92 | "range": { 93 | "end": { 94 | "character": 32, 95 | "line": 3 96 | }, 97 | "start": { 98 | "character": 29, 99 | "line": 3 100 | } 101 | } 102 | } 103 | ], 104 | "uri": "file:///binop_bind.jq" 105 | } 106 | } 107 | } 108 | ] 109 | -------------------------------------------------------------------------------- /lsp/lsp_test.go: -------------------------------------------------------------------------------- 1 | package lsp_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/wader/jq-lsp/internal/difftest" 15 | "github.com/wader/jq-lsp/lsp" 16 | ) 17 | 18 | var update = flag.Bool("update", false, "Update tests") 19 | 20 | // replace "@path" with content of file 21 | func replaceTextFile(t *testing.T, v any, readFile func(s string) ([]byte, error)) any { 22 | switch v := v.(type) { 23 | case []any: 24 | a := make([]any, len(v)) 25 | for i, ve := range v { 26 | a[i] = replaceTextFile(t, ve, readFile) 27 | } 28 | return a 29 | case map[string]any: 30 | a := make(map[string]any, len(v)) 31 | for k, ve := range v { 32 | a[k] = replaceTextFile(t, ve, readFile) 33 | } 34 | return a 35 | case string: 36 | if _, path, found := strings.Cut(v, "@"); found { 37 | if b, err := readFile(path); err == nil { 38 | return string(b) 39 | } else { 40 | t.Fatal(err) 41 | } 42 | } 43 | return v 44 | default: 45 | return v 46 | } 47 | } 48 | 49 | func TestLSP(t *testing.T) { 50 | difftest.TestWithOptions(t, difftest.Options{ 51 | Path: "testdata", 52 | Pattern: "*.json", 53 | ColorDiff: os.Getenv("TEST_COLOR") != "", 54 | WriteOutput: *update, 55 | Fn: func(t *testing.T, path string, input string) (string, string, error) { 56 | testBaseDir := filepath.Dir(path) 57 | 58 | readFile := func(s string) ([]byte, error) { 59 | return os.ReadFile(filepath.Join(testBaseDir, s)) 60 | } 61 | readFileOrEmpty := func(s string) []byte { 62 | b, err := readFile(s) 63 | if err != nil { 64 | return nil 65 | } 66 | return b 67 | } 68 | 69 | type request struct { 70 | Request any `json:"request"` 71 | Response any `json:"response"` 72 | } 73 | var requests []request 74 | err := json.Unmarshal([]byte(input), &requests) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | for i := range requests { 80 | requests[i].Response = map[string]any{} 81 | } 82 | 83 | stdinBuf := &bytes.Buffer{} 84 | for _, r := range requests { 85 | rr := replaceTextFile(t, r.Request, readFile) 86 | reqB, err := json.Marshal(&rr) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | fmt.Fprintf(stdinBuf, "Content-Length: %d\r\n\r\n", len(reqB)) 91 | stdinBuf.Write(reqB) 92 | } 93 | 94 | actualStdout := &bytes.Buffer{} 95 | actualStderr := &bytes.Buffer{} 96 | err = lsp.Run(lsp.Env{ 97 | Version: "test-version", 98 | ReadFile: readFile, 99 | Stdin: stdinBuf, 100 | Stdout: actualStdout, 101 | Stderr: actualStderr, 102 | Args: strings.Split(string(readFileOrEmpty("args")), " "), 103 | Environ: strings.Split(string(readFileOrEmpty("environ")), " "), 104 | }) 105 | if err != nil { 106 | return "", "", err 107 | } 108 | 109 | if actualStderr.Len() > 0 { 110 | t.Logf("stderr:\n%s\n", actualStderr) 111 | } 112 | 113 | s := actualStdout.String() 114 | i := 0 115 | for { 116 | h, rest, found := strings.Cut(s, "\r\n\r\n") 117 | if !found { 118 | break 119 | } 120 | kv := map[string]string{} 121 | for _, line := range strings.Split(h, "\r\n") { 122 | k, v, _ := strings.Cut(line, ": ") 123 | kv[k] = v 124 | } 125 | 126 | contentLength, _ := strconv.Atoi(kv["Content-Length"]) 127 | 128 | var resp any 129 | if err := json.Unmarshal([]byte(rest[0:contentLength]), &resp); err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | requests[i].Response = resp 134 | i++ 135 | 136 | s = rest[contentLength:] 137 | } 138 | 139 | actualRequests := &bytes.Buffer{} 140 | je := json.NewEncoder(actualRequests) 141 | je.SetIndent("", " ") 142 | if err := je.Encode(&requests); err != nil { 143 | t.Fatal(err) 144 | } 145 | 146 | return path, actualRequests.String(), nil 147 | }, 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /gojqparser/encoder.go: -------------------------------------------------------------------------------- 1 | package gojqparser 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "math" 8 | "math/big" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | "unicode/utf8" 13 | ) 14 | 15 | // Marshal returns the jq-flavored JSON encoding of v. 16 | // 17 | // This method accepts only limited types (nil, bool, int, float64, *big.Int, 18 | // string, []any, and map[string]any) because these are the possible types a 19 | // gojq iterator can emit. This method marshals NaN to null, truncates 20 | // infinities to (+|-) math.MaxFloat64, uses \b and \f in strings, and does not 21 | // escape '<', '>', '&', '\u2028', and '\u2029'. These behaviors are based on 22 | // the marshaler of jq command, and different from json.Marshal in the Go 23 | // standard library. Note that the result is not safe to embed in HTML. 24 | func Marshal(v any) ([]byte, error) { 25 | var b bytes.Buffer 26 | (&encoder{w: &b}).encode(v) 27 | return b.Bytes(), nil 28 | } 29 | 30 | func jsonMarshal(v any) string { 31 | var sb strings.Builder 32 | (&encoder{w: &sb}).encode(v) 33 | return sb.String() 34 | } 35 | 36 | func jsonEncodeString(sb *strings.Builder, v string) { 37 | (&encoder{w: sb}).encodeString(v) 38 | } 39 | 40 | type encoder struct { 41 | w interface { 42 | io.Writer 43 | io.ByteWriter 44 | io.StringWriter 45 | } 46 | buf [64]byte 47 | } 48 | 49 | func (e *encoder) encode(v any) { 50 | switch v := v.(type) { 51 | case nil: 52 | e.w.WriteString("null") 53 | case bool: 54 | if v { 55 | e.w.WriteString("true") 56 | } else { 57 | e.w.WriteString("false") 58 | } 59 | case int: 60 | e.w.Write(strconv.AppendInt(e.buf[:0], int64(v), 10)) 61 | case float64: 62 | e.encodeFloat64(v) 63 | case *big.Int: 64 | e.w.Write(v.Append(e.buf[:0], 10)) 65 | case string: 66 | e.encodeString(v) 67 | case []any: 68 | e.encodeArray(v) 69 | case map[string]any: 70 | e.encodeObject(v) 71 | default: 72 | panic(fmt.Sprintf("invalid type: %[1]T (%[1]v)", v)) 73 | } 74 | } 75 | 76 | // ref: floatEncoder in encoding/json 77 | func (e *encoder) encodeFloat64(f float64) { 78 | if math.IsNaN(f) { 79 | e.w.WriteString("null") 80 | return 81 | } 82 | f = min(max(f, -math.MaxFloat64), math.MaxFloat64) 83 | format := byte('f') 84 | if x := math.Abs(f); x != 0 && x < 1e-6 || x >= 1e21 { 85 | format = 'e' 86 | } 87 | buf := strconv.AppendFloat(e.buf[:0], f, format, -1, 64) 88 | if format == 'e' { 89 | // clean up e-09 to e-9 90 | if n := len(buf); n >= 4 && buf[n-4] == 'e' && buf[n-3] == '-' && buf[n-2] == '0' { 91 | buf[n-2] = buf[n-1] 92 | buf = buf[:n-1] 93 | } 94 | } 95 | e.w.Write(buf) 96 | } 97 | 98 | // ref: encodeState#string in encoding/json 99 | func (e *encoder) encodeString(s string) { 100 | e.w.WriteByte('"') 101 | start := 0 102 | for i := 0; i < len(s); { 103 | if b := s[i]; b < utf8.RuneSelf { 104 | if ' ' <= b && b <= '~' && b != '"' && b != '\\' { 105 | i++ 106 | continue 107 | } 108 | if start < i { 109 | e.w.WriteString(s[start:i]) 110 | } 111 | switch b { 112 | case '"': 113 | e.w.WriteString(`\"`) 114 | case '\\': 115 | e.w.WriteString(`\\`) 116 | case '\b': 117 | e.w.WriteString(`\b`) 118 | case '\f': 119 | e.w.WriteString(`\f`) 120 | case '\n': 121 | e.w.WriteString(`\n`) 122 | case '\r': 123 | e.w.WriteString(`\r`) 124 | case '\t': 125 | e.w.WriteString(`\t`) 126 | default: 127 | const hex = "0123456789abcdef" 128 | e.w.WriteString(`\u00`) 129 | e.w.WriteByte(hex[b>>4]) 130 | e.w.WriteByte(hex[b&0xF]) 131 | } 132 | i++ 133 | start = i 134 | continue 135 | } 136 | c, size := utf8.DecodeRuneInString(s[i:]) 137 | if c == utf8.RuneError && size == 1 { 138 | if start < i { 139 | e.w.WriteString(s[start:i]) 140 | } 141 | e.w.WriteString(`\ufffd`) 142 | i += size 143 | start = i 144 | continue 145 | } 146 | i += size 147 | } 148 | if start < len(s) { 149 | e.w.WriteString(s[start:]) 150 | } 151 | e.w.WriteByte('"') 152 | } 153 | 154 | func (e *encoder) encodeArray(vs []any) { 155 | e.w.WriteByte('[') 156 | for i, v := range vs { 157 | if i > 0 { 158 | e.w.WriteByte(',') 159 | } 160 | e.encode(v) 161 | } 162 | e.w.WriteByte(']') 163 | } 164 | 165 | func (e *encoder) encodeObject(vs map[string]any) { 166 | e.w.WriteByte('{') 167 | type keyVal struct { 168 | key string 169 | val any 170 | } 171 | kvs := make([]keyVal, len(vs)) 172 | var i int 173 | for k, v := range vs { 174 | kvs[i] = keyVal{k, v} 175 | i++ 176 | } 177 | sort.Slice(kvs, func(i, j int) bool { 178 | return kvs[i].key < kvs[j].key 179 | }) 180 | for i, kv := range kvs { 181 | if i > 0 { 182 | e.w.WriteByte(',') 183 | } 184 | e.encodeString(kv.key) 185 | e.w.WriteByte(':') 186 | e.encode(kv.val) 187 | } 188 | e.w.WriteByte('}') 189 | } 190 | -------------------------------------------------------------------------------- /lsp/testdata/foreach-reudce-query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didChange", 37 | "params": { 38 | "contentChanges": [ 39 | { 40 | "text": "@foreach-reduce-query.jq" 41 | } 42 | ], 43 | "textDocument": { 44 | "uri": "file:///foreach-reduce-query.jq", 45 | "version": 2 46 | } 47 | } 48 | }, 49 | "response": { 50 | "jsonrpc": "2.0", 51 | "method": "textDocument/publishDiagnostics", 52 | "params": { 53 | "diagnostics": [ 54 | { 55 | "message": "$missins not found", 56 | "range": { 57 | "end": { 58 | "character": 12, 59 | "line": 1 60 | }, 61 | "start": { 62 | "character": 4, 63 | "line": 1 64 | } 65 | } 66 | }, 67 | { 68 | "message": "$var not found", 69 | "range": { 70 | "end": { 71 | "character": 8, 72 | "line": 2 73 | }, 74 | "start": { 75 | "character": 4, 76 | "line": 2 77 | } 78 | } 79 | }, 80 | { 81 | "message": "$also not found", 82 | "range": { 83 | "end": { 84 | "character": 9, 85 | "line": 5 86 | }, 87 | "start": { 88 | "character": 4, 89 | "line": 5 90 | } 91 | } 92 | }, 93 | { 94 | "message": "$not not found", 95 | "range": { 96 | "end": { 97 | "character": 8, 98 | "line": 6 99 | }, 100 | "start": { 101 | "character": 4, 102 | "line": 6 103 | } 104 | } 105 | }, 106 | { 107 | "message": "$defined not found", 108 | "range": { 109 | "end": { 110 | "character": 12, 111 | "line": 7 112 | }, 113 | "start": { 114 | "character": 4, 115 | "line": 7 116 | } 117 | } 118 | }, 119 | { 120 | "message": "$missins not found", 121 | "range": { 122 | "end": { 123 | "character": 12, 124 | "line": 10 125 | }, 126 | "start": { 127 | "character": 4, 128 | "line": 10 129 | } 130 | } 131 | }, 132 | { 133 | "message": "$var not found", 134 | "range": { 135 | "end": { 136 | "character": 8, 137 | "line": 11 138 | }, 139 | "start": { 140 | "character": 4, 141 | "line": 11 142 | } 143 | } 144 | }, 145 | { 146 | "message": "$also not found", 147 | "range": { 148 | "end": { 149 | "character": 9, 150 | "line": 13 151 | }, 152 | "start": { 153 | "character": 4, 154 | "line": 13 155 | } 156 | } 157 | }, 158 | { 159 | "message": "$not not found", 160 | "range": { 161 | "end": { 162 | "character": 8, 163 | "line": 14 164 | }, 165 | "start": { 166 | "character": 4, 167 | "line": 14 168 | } 169 | } 170 | } 171 | ], 172 | "uri": "file:///foreach-reduce-query.jq" 173 | } 174 | } 175 | } 176 | ] 177 | -------------------------------------------------------------------------------- /lsp/lsp.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "embed" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "log" 11 | 12 | "github.com/itchyny/gojq" 13 | "github.com/wader/jq-lsp/gojqparser" 14 | ) 15 | 16 | //go:embed builtin_env.jq 17 | //go:embed docs.jq 18 | //go:embed lsp.jq 19 | var lspFS embed.FS 20 | 21 | type loadModule struct { 22 | init func() ([]*gojq.Query, error) 23 | load func(name string) (*gojq.Query, error) 24 | } 25 | 26 | func (l loadModule) LoadInitModules() ([]*gojq.Query, error) { return l.init() } 27 | func (l loadModule) LoadModule(name string) (*gojq.Query, error) { return l.load(name) } 28 | 29 | type Env struct { 30 | Version string 31 | ReadFile func(string) ([]byte, error) 32 | Stdin io.Reader 33 | Stdout io.Writer 34 | Stderr io.Writer 35 | Args []string 36 | Environ []string 37 | } 38 | 39 | type interp struct { 40 | env Env 41 | } 42 | 43 | type parseError struct { 44 | err error 45 | offset int 46 | } 47 | 48 | func (ce parseError) Value() any { 49 | return map[string]any{ 50 | "error": ce.err.Error(), 51 | "offset": ce.offset, 52 | } 53 | } 54 | 55 | func (ee parseError) Error() string { 56 | return fmt.Sprintf("%d: %s", ee.offset, ee.err.Error()) 57 | } 58 | 59 | func queryErrorPosition(v error) int { 60 | if pe, ok := v.(*gojqparser.ParseError); ok { //nolint:errorlint 61 | return pe.Offset 62 | } 63 | return 0 64 | } 65 | 66 | func mapFn[F any, T any](fs []F, fn func(F) T) []T { 67 | ts := make([]T, 0, len(fs)) 68 | for _, e := range fs { 69 | ts = append(ts, fn(e)) 70 | } 71 | return ts 72 | } 73 | 74 | func Run(env Env) error { 75 | i := &interp{ 76 | env: env, 77 | } 78 | 79 | var state any = map[string]any{ 80 | "args": mapFn(env.Args, func(s string) any { return s }), 81 | "env": mapFn(env.Environ, func(s string) any { return s }), 82 | "config": map[string]any{ 83 | "name": "jq-lsp", 84 | "version": env.Version, 85 | }, 86 | } 87 | 88 | gc, err := i.Compile("main") 89 | if err != nil { 90 | return err 91 | } 92 | 93 | iter := gc.RunWithContext(context.Background(), state) 94 | for { 95 | v, ok := iter.Next() 96 | if !ok { 97 | break 98 | } 99 | 100 | switch v := v.(type) { 101 | case error: 102 | if ve, ok := v.(gojq.ValueError); ok { 103 | if vev, ok := ve.Value().(string); ok && vev == "EOF" { 104 | // TODO: currently assume any EOF error means normal exit 105 | return nil 106 | } 107 | } 108 | return v 109 | case [2]any: 110 | fmt.Fprintln(env.Stderr, v[:]...) 111 | default: 112 | jd := json.NewEncoder(env.Stdout) 113 | jd.SetIndent("", " ") 114 | _ = jd.Encode(v) 115 | } 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func (i *interp) Compile(src string) (*gojq.Code, error) { 122 | gq, err := gojq.Parse(src) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | var compilerOpts []gojq.CompilerOption 128 | compilerOpts = append(compilerOpts, gojq.WithEnvironLoader(func() []string { return i.env.Environ })) 129 | compilerOpts = append(compilerOpts, gojq.WithModuleLoader(loadModule{ 130 | init: func() ([]*gojq.Query, error) { 131 | gq, err := gojq.Parse(`include "lsp";`) 132 | if err != nil { 133 | return nil, err 134 | } 135 | return []*gojq.Query{gq}, nil 136 | }, 137 | load: func(name string) (*gojq.Query, error) { 138 | f, err := lspFS.Open(name + ".jq") 139 | if err != nil { 140 | return nil, err 141 | } 142 | defer f.Close() 143 | b, err := io.ReadAll(f) 144 | if err != nil { 145 | return nil, err 146 | } 147 | gq, err := gojq.Parse(string(b)) 148 | if err != nil { 149 | return nil, err 150 | } 151 | return gq, nil 152 | }, 153 | })) 154 | 155 | compilerOpts = append(compilerOpts, gojq.WithFunction("readfile", 0, 0, i.readFile)) 156 | compilerOpts = append(compilerOpts, gojq.WithFunction("stdin", 0, 1, i.stdin)) 157 | compilerOpts = append(compilerOpts, gojq.WithIterFunction("stdout", 0, 0, i.stdout)) 158 | compilerOpts = append(compilerOpts, gojq.WithIterFunction("stderr", 0, 0, i.stderr)) 159 | compilerOpts = append(compilerOpts, gojq.WithIterFunction("stdlog", 0, 0, i.stdlog)) 160 | compilerOpts = append(compilerOpts, gojq.WithFunction("query_fromstring", 0, 0, i.queryFromString)) 161 | compilerOpts = append(compilerOpts, gojq.WithFunction("query_tostring", 0, 0, i.queryToString)) 162 | compilerOpts = append(compilerOpts, gojq.WithIterFunction("eval", 1, 1, i.eval)) 163 | 164 | gc, err := gojq.Compile(gq, compilerOpts...) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | return gc, nil 170 | } 171 | 172 | func (i *interp) readFile(c any, a []any) any { 173 | path, err := toString(c) 174 | if err != nil { 175 | return err 176 | } 177 | b, err := i.env.ReadFile(path) 178 | if err != nil { 179 | return err 180 | } 181 | return string(b) 182 | } 183 | 184 | func (i *interp) stdin(_ any, a []any) any { 185 | var n int 186 | if len(a) >= 1 { 187 | var err error 188 | n, err = toInt(a[0]) 189 | if err != nil { 190 | return err 191 | } 192 | } 193 | 194 | if n == 0 { 195 | b := &bytes.Buffer{} 196 | if _, err := io.Copy(b, i.env.Stdin); err != nil { 197 | return err 198 | } 199 | return b.String() 200 | } 201 | b := make([]byte, n) 202 | 203 | _, err := io.ReadFull(i.env.Stdin, b) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | return string(b) 209 | } 210 | 211 | func (i *interp) stdout(c any, a []any) gojq.Iter { 212 | if _, err := fmt.Fprint(i.env.Stdout, c); err != nil { 213 | return gojq.NewIter(err) 214 | } 215 | return gojq.NewIter[any]() 216 | } 217 | 218 | func (i *interp) stderr(c any, a []any) gojq.Iter { 219 | if _, err := fmt.Fprint(i.env.Stderr, c); err != nil { 220 | return gojq.NewIter(err) 221 | } 222 | return gojq.NewIter[any]() 223 | } 224 | 225 | func (i *interp) stdlog(c any, a []any) gojq.Iter { 226 | log.Println(c) 227 | return gojq.NewIter[any]() 228 | } 229 | 230 | func (i *interp) queryFromString(c any, a []any) any { 231 | s, err := toString(c) 232 | if err != nil { 233 | return err 234 | } 235 | q, err := gojqparser.Parse(s) 236 | if err != nil { 237 | offset := queryErrorPosition(err) 238 | return parseError{ 239 | err: err, 240 | offset: offset, 241 | } 242 | } 243 | 244 | b, err := json.Marshal(q) 245 | if err != nil { 246 | return err 247 | } 248 | 249 | var v any 250 | if err := json.Unmarshal(b, &v); err != nil { 251 | return err 252 | } 253 | 254 | return v 255 | } 256 | 257 | func (i *interp) queryToString(c any, a []any) any { 258 | b, err := json.Marshal(c) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | var q gojqparser.Query 264 | if err := json.Unmarshal(b, &q); err != nil { 265 | return err 266 | } 267 | 268 | return q.String() 269 | } 270 | 271 | func (i *interp) eval(c any, a []any) gojq.Iter { 272 | expr, ok := a[0].(string) 273 | if !ok { 274 | return gojq.NewIter(fmt.Errorf("expr can't be a string")) 275 | } 276 | 277 | gc, err := i.Compile(expr) 278 | if err != nil { 279 | return gojq.NewIter(fmt.Errorf("expr: %s", err)) 280 | } 281 | 282 | return gc.Run(nil) 283 | } 284 | 285 | func toString(v any) (string, error) { 286 | switch v := v.(type) { 287 | case string: 288 | return v, nil 289 | default: 290 | return "", fmt.Errorf("value can't be a string") 291 | } 292 | } 293 | 294 | func toInt(v any) (int, error) { 295 | // TODO: other types 296 | switch v := v.(type) { 297 | case int: 298 | return v, nil 299 | default: 300 | return 0, fmt.Errorf("value can't be a int") 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /lsp/testdata/completion_def.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/completion", 37 | "params": { 38 | "position": { 39 | "character": 15, 40 | "line": 1 41 | }, 42 | "textDocument": { 43 | "uri": "file:///completion.jq" 44 | } 45 | } 46 | }, 47 | "response": { 48 | "id": 1, 49 | "jsonrpc": "2.0", 50 | "result": [ 51 | { 52 | "documentation": { 53 | "kind": "markdown", 54 | "value": "```jq\ndef from2:\n```" 55 | }, 56 | "kind": 3, 57 | "label": "from2" 58 | }, 59 | { 60 | "documentation": { 61 | "kind": "markdown", 62 | "value": "```jq\ndef from:\n```" 63 | }, 64 | "kind": 3, 65 | "label": "from" 66 | }, 67 | { 68 | "documentation": { 69 | "kind": "markdown", 70 | "value": "```jq\ndef from_entries:\n```\nThese functions convert between an object and an array of\nkey-value pairs. If `to_entries` is passed an object, then\nfor each `k: v` entry in the input, the output array\nincludes `{\"key\": k, \"value\": v}`.\n\n`from_entries` does the opposite conversion, and `with_entries(f)`\nis a shorthand for `to_entries | map(f) | from_entries`, useful for\ndoing some operation to all keys and values of an object.\n`from_entries` accepts `\"key\"`, `\"Key\"`, `\"name\"`, `\"Name\"`,\n`\"value\"`, and `\"Value\"` as keys." 71 | }, 72 | "kind": 3, 73 | "label": "from_entries" 74 | }, 75 | { 76 | "documentation": { 77 | "kind": "markdown", 78 | "value": "```jq\ndef fromdate:\n```\njq provides some basic date handling functionality, with some\nhigh-level and low-level builtins. In all cases these\nbuiltins deal exclusively with time in UTC.\n\nThe `fromdateiso8601` builtin parses datetimes in the ISO 8601\nformat to a number of seconds since the Unix epoch\n(1970-01-01T00:00:00Z). The `todateiso8601` builtin does the\ninverse.\n\nThe `fromdate` builtin parses datetime strings. Currently\n`fromdate` only supports ISO 8601 datetime strings, but in the\nfuture it will attempt to parse datetime strings in more\nformats.\n\nThe `todate` builtin is an alias for `todateiso8601`.\n\nThe `now` builtin outputs the current time, in seconds since\nthe Unix epoch.\n\nLow-level jq interfaces to the C-library time functions are\nalso provided: `strptime`, `strftime`, `strflocaltime`,\n`mktime`, `gmtime`, and `localtime`. Refer to your host\noperating system's documentation for the format strings used\nby `strptime` and `strftime`. Note: these are not necessarily\nstable interfaces in jq, particularly as to their localization\nfunctionality.\n\nThe `gmtime` builtin consumes a number of seconds since the\nUnix epoch and outputs a \"broken down time\" representation of\nGreenwich Mean Time as an array of numbers representing\n(in this order): the year, the month (zero-based), the day of\nthe month (one-based), the hour of the day, the minute of the\nhour, the second of the minute, the day of the week, and the\nday of the year -- all one-based unless otherwise stated. The\nday of the week number may be wrong on some systems for dates\nbefore March 1st 1900, or after December 31 2099.\n\nThe `localtime` builtin works like the `gmtime` builtin, but\nusing the local timezone setting.\n\nThe `mktime` builtin consumes \"broken down time\"\nrepresentations of time output by `gmtime` and `strptime`.\n\nThe `strptime(fmt)` builtin parses input strings matching the\n`fmt` argument. The output is in the \"broken down time\"\nrepresentation consumed by `mktime` and output by `gmtime`.\n\nThe `strftime(fmt)` builtin formats a time (GMT) with the\ngiven format. The `strflocaltime` does the same, but using\nthe local timezone setting.\n\nThe format strings for `strptime` and `strftime` are described\nin typical C library documentation. The format string for ISO\n8601 datetime is `\"%Y-%m-%dT%H:%M:%SZ\"`.\n\njq may not support some or all of this date functionality on\nsome systems. In particular, the `%u` and `%j` specifiers for\n`strptime(fmt)` are not supported on macOS." 79 | }, 80 | "kind": 3, 81 | "label": "fromdate" 82 | }, 83 | { 84 | "documentation": { 85 | "kind": "markdown", 86 | "value": "```jq\ndef fromdateiso8601:\n```\njq provides some basic date handling functionality, with some\nhigh-level and low-level builtins. In all cases these\nbuiltins deal exclusively with time in UTC.\n\nThe `fromdateiso8601` builtin parses datetimes in the ISO 8601\nformat to a number of seconds since the Unix epoch\n(1970-01-01T00:00:00Z). The `todateiso8601` builtin does the\ninverse.\n\nThe `fromdate` builtin parses datetime strings. Currently\n`fromdate` only supports ISO 8601 datetime strings, but in the\nfuture it will attempt to parse datetime strings in more\nformats.\n\nThe `todate` builtin is an alias for `todateiso8601`.\n\nThe `now` builtin outputs the current time, in seconds since\nthe Unix epoch.\n\nLow-level jq interfaces to the C-library time functions are\nalso provided: `strptime`, `strftime`, `strflocaltime`,\n`mktime`, `gmtime`, and `localtime`. Refer to your host\noperating system's documentation for the format strings used\nby `strptime` and `strftime`. Note: these are not necessarily\nstable interfaces in jq, particularly as to their localization\nfunctionality.\n\nThe `gmtime` builtin consumes a number of seconds since the\nUnix epoch and outputs a \"broken down time\" representation of\nGreenwich Mean Time as an array of numbers representing\n(in this order): the year, the month (zero-based), the day of\nthe month (one-based), the hour of the day, the minute of the\nhour, the second of the minute, the day of the week, and the\nday of the year -- all one-based unless otherwise stated. The\nday of the week number may be wrong on some systems for dates\nbefore March 1st 1900, or after December 31 2099.\n\nThe `localtime` builtin works like the `gmtime` builtin, but\nusing the local timezone setting.\n\nThe `mktime` builtin consumes \"broken down time\"\nrepresentations of time output by `gmtime` and `strptime`.\n\nThe `strptime(fmt)` builtin parses input strings matching the\n`fmt` argument. The output is in the \"broken down time\"\nrepresentation consumed by `mktime` and output by `gmtime`.\n\nThe `strftime(fmt)` builtin formats a time (GMT) with the\ngiven format. The `strflocaltime` does the same, but using\nthe local timezone setting.\n\nThe format strings for `strptime` and `strftime` are described\nin typical C library documentation. The format string for ISO\n8601 datetime is `\"%Y-%m-%dT%H:%M:%SZ\"`.\n\njq may not support some or all of this date functionality on\nsome systems. In particular, the `%u` and `%j` specifiers for\n`strptime(fmt)` are not supported on macOS." 87 | }, 88 | "kind": 3, 89 | "label": "fromdateiso8601" 90 | }, 91 | { 92 | "documentation": { 93 | "kind": "markdown", 94 | "value": "```jq\ndef fromjson:\n```\nThe `tojson` and `fromjson` builtins dump values as JSON texts\nor parse JSON texts into values, respectively. The `tojson`\nbuiltin differs from `tostring` in that `tostring` returns strings\nunmodified, while `tojson` encodes strings as JSON strings." 95 | }, 96 | "kind": 3, 97 | "label": "fromjson" 98 | }, 99 | { 100 | "documentation": { 101 | "kind": "markdown", 102 | "value": "```jq\ndef fromstream(f):\n```\nOutputs values corresponding to the stream expression's\noutputs." 103 | }, 104 | "insertText": "fromstream($0)", 105 | "insertTextFormat": 2, 106 | "kind": 3, 107 | "label": "fromstream(f)" 108 | } 109 | ] 110 | } 111 | } 112 | ] 113 | -------------------------------------------------------------------------------- /lsp/builtin_env.jq: -------------------------------------------------------------------------------- 1 | def builtin_env: 2 | [{ 3 | 4 | "@text": {"args": [], "str": "@text"}, 5 | "@json": {"args": [], "str": "@json"}, 6 | "@html": {"args": [], "str": "@html"}, 7 | "@uri": {"args": [], "str": "@uri"}, 8 | "@urid": {"args": [], "str": "@urid"}, 9 | "@csv": {"args": [], "str": "@csv"}, 10 | "@tsv": {"args": [], "str": "@tsv"}, 11 | "@sh": {"args": [], "str": "@sh"}, 12 | "@base64": {"args": [], "str": "@base64"}, 13 | "@base64d": {"args": [], "str": "@base64d"}, 14 | 15 | "$ENV/0": {"args":[],"str":"$ENV"}, 16 | "$ARGS/0": {"args":[],"str":"$ARGS"}, 17 | "$JQ_BUILD_CONFIGURATION/0": {"args":[],"str":"$JQ_BUILD_CONFIGURATION"}, 18 | "$__loc__/0": {"args":[],"str":"$__loc__"}, 19 | 20 | "null/0": {"args":[],"str":"null"}, 21 | "true/0": {"args":[],"str":"true"}, 22 | "false/0": {"args":[],"str":"false"}, 23 | 24 | "IN/1": {"args":["s"],"str":"IN"}, 25 | "IN/2": {"args":["src","s"],"str":"IN"}, 26 | "INDEX/1": {"args":["idx_expr"],"str":"INDEX"}, 27 | "INDEX/2": {"args":["stream","idx_expr"],"str":"INDEX"}, 28 | "JOIN/2": {"args":["$idx","idx_expr"],"str":"JOIN"}, 29 | "JOIN/3": {"args":["$idx","stream","idx_expr"],"str":"JOIN"}, 30 | "JOIN/4": {"args":["$idx","stream","idx_expr","join_expr"],"str":"JOIN"}, 31 | "abs/0": {"args":[],"str":"abs"}, 32 | "acos/0": {"args":[],"str":"acos"}, 33 | "acosh/0": {"args":[],"str":"acosh"}, 34 | "add/0": {"args":[],"str":"add"}, 35 | "add/1": {"args":["f"],"str":"add"}, 36 | "all/0": {"args":[],"str":"all"}, 37 | "all/1": {"args":["y"],"str":"all"}, 38 | "all/2": {"args":["g","y"],"str":"all"}, 39 | "any/0": {"args":[],"str":"any"}, 40 | "any/1": {"args":["y"],"str":"any"}, 41 | "any/2": {"args":["g","y"],"str":"any"}, 42 | "arrays/0": {"args":[],"str":"arrays"}, 43 | "ascii_downcase/0": {"args":[],"str":"ascii_downcase"}, 44 | "ascii_upcase/0": {"args":[],"str":"ascii_upcase"}, 45 | "asin/0": {"args":[],"str":"asin"}, 46 | "asinh/0": {"args":[],"str":"asinh"}, 47 | "atan/0": {"args":[],"str":"atan"}, 48 | "atan2/2": {"args":["a","b"],"str":"atan2"}, 49 | "atanh/0": {"args":[],"str":"atanh"}, 50 | "booleans/0": {"args":[],"str":"booleans"}, 51 | "bsearch/1": {"args":["a"],"str":"bsearch"}, 52 | "builtins/0": {"args":[],"str":"builtins"}, 53 | "capture/1": {"args":["$re"],"str":"capture"}, 54 | "capture/2": {"args":["$re","$flags"],"str":"capture"}, 55 | "cbrt/0": {"args":[],"str":"cbrt"}, 56 | "ceil/0": {"args":[],"str":"ceil"}, 57 | "combinations/0": {"args":[],"str":"combinations"}, 58 | "combinations/1": {"args":["n"],"str":"combinations"}, 59 | "contains/1": {"args":["a"],"str":"contains"}, 60 | "copysign/2": {"args":["a","b"],"str":"copysign"}, 61 | "cos/0": {"args":[],"str":"cos"}, 62 | "cosh/0": {"args":[],"str":"cosh"}, 63 | "debug/0": {"args":[],"str":"debug"}, 64 | "debug/1": {"args":["a"],"str":"debug"}, 65 | "del/1": {"args":["f"],"str":"del"}, 66 | "delpaths/1": {"args":["a"],"str":"delpaths"}, 67 | "drem/2": {"args":["a","b"],"str":"drem"}, 68 | "empty/0": {"args":[],"str":"empty"}, 69 | "endswith/1": {"args":["a"],"str":"endswith"}, 70 | "env/0": {"args":[],"str":"env"}, 71 | "erf/0": {"args":[],"str":"erf"}, 72 | "erfc/0": {"args":[],"str":"erfc"}, 73 | "error/0": {"args":[],"str":"error"}, 74 | "error/1": {"args":["a"],"str":"error"}, 75 | "exp/0": {"args":[],"str":"exp"}, 76 | "exp10/0": {"args":[],"str":"exp10"}, 77 | "exp2/0": {"args":[],"str":"exp2"}, 78 | "explode/0": {"args":[],"str":"explode"}, 79 | "expm1/0": {"args":[],"str":"expm1"}, 80 | "fabs/0": {"args":[],"str":"fabs"}, 81 | "fdim/2": {"args":["a","b"],"str":"fdim"}, 82 | "finites/0": {"args":[],"str":"finites"}, 83 | "first/0": {"args":[],"str":"first"}, 84 | "first/1": {"args":["g"],"str":"first"}, 85 | "flatten/0": {"args":[],"str":"flatten"}, 86 | "flatten/1": {"args":["a"],"str":"flatten"}, 87 | "floor/0": {"args":[],"str":"floor"}, 88 | "fma/3": {"args":["a","b","c"],"str":"fma"}, 89 | "fmax/2": {"args":["a","b"],"str":"fmax"}, 90 | "fmin/2": {"args":["a","b"],"str":"fmin"}, 91 | "fmod/2": {"args":["a","b"],"str":"fmod"}, 92 | "format/1": {"args":["a"],"str":"format"}, 93 | "frexp/0": {"args":[],"str":"frexp"}, 94 | "from_entries/0": {"args":[],"str":"from_entries"}, 95 | "fromdate/0": {"args":[],"str":"fromdate"}, 96 | "fromdateiso8601/0": {"args":[],"str":"fromdateiso8601"}, 97 | "fromjson/0": {"args":[],"str":"fromjson"}, 98 | "fromstream/1": {"args":["f"],"str":"fromstream"}, 99 | "gamma/0": {"args":[],"str":"gamma"}, 100 | "get_jq_origin/0": {"args":[],"str":"get_jq_origin"}, 101 | "get_prog_origin/0": {"args":[],"str":"get_prog_origin"}, 102 | "get_search_list/0": {"args":[],"str":"get_search_list"}, 103 | "getpath/1": {"args":["a"],"str":"getpath"}, 104 | "gmtime/0": {"args":[],"str":"gmtime"}, 105 | "group_by/1": {"args":["f"],"str":"group_by"}, 106 | "gsub/2": {"args":["$re","str"],"str":"gsub"}, 107 | "gsub/3": {"args":["$re","str","$flags"],"str":"gsub"}, 108 | "halt/0": {"args":[],"str":"halt"}, 109 | "halt_error/0": {"args":[],"str":"halt_error"}, 110 | "halt_error/1": {"args":["a"],"str":"halt_error"}, 111 | "has/1": {"args":["a"],"str":"has"}, 112 | "have_decnum/0": {"args":[],"str":"have_decnum"}, 113 | "have_literal_numbers/0": {"args":[],"str":"have_literal_numbers"}, 114 | "hypot/2": {"args":["a","b"],"str":"hypot"}, 115 | "implode/0": {"args":[],"str":"implode"}, 116 | "in/1": {"args":["xs"],"str":"in"}, 117 | "index/1": {"args":["a"],"str":"index"}, 118 | "indices/1": {"args":["a"],"str":"indices"}, 119 | "infinite/0": {"args":[],"str":"infinite"}, 120 | "input/0": {"args":[],"str":"input"}, 121 | "input_filename/0": {"args":[],"str":"input_filename"}, 122 | "input_line_number/0": {"args":[],"str":"input_line_number"}, 123 | "inputs/0": {"args":[],"str":"inputs"}, 124 | "inside/1": {"args":["xs"],"str":"inside"}, 125 | "isempty/1": {"args":["g"],"str":"isempty"}, 126 | "isfinite/0": {"args":[],"str":"isfinite"}, 127 | "isinfinite/0": {"args":[],"str":"isinfinite"}, 128 | "isnan/0": {"args":[],"str":"isnan"}, 129 | "isnormal/0": {"args":[],"str":"isnormal"}, 130 | "iterables/0": {"args":[],"str":"iterables"}, 131 | "j0/0": {"args":[],"str":"j0"}, 132 | "j1/0": {"args":[],"str":"j1"}, 133 | "jn/2": {"args":["a","b"],"str":"jn"}, 134 | "join/1": {"args":["a"],"str":"join"}, 135 | "keys/0": {"args":[],"str":"keys"}, 136 | "keys_unsorted/0": {"args":[],"str":"keys_unsorted"}, 137 | "last/0": {"args":[],"str":"last"}, 138 | "last/1": {"args":["g"],"str":"last"}, 139 | "ldexp/2": {"args":["a","b"],"str":"ldexp"}, 140 | "length/0": {"args":[],"str":"length"}, 141 | "lgamma/0": {"args":[],"str":"lgamma"}, 142 | "lgamma_r/0": {"args":[],"str":"lgamma_r"}, 143 | "limit/2": {"args":["$n","g"],"str":"limit"}, 144 | "localtime/0": {"args":[],"str":"localtime"}, 145 | "log/0": {"args":[],"str":"log"}, 146 | "log10/0": {"args":[],"str":"log10"}, 147 | "log1p/0": {"args":[],"str":"log1p"}, 148 | "log2/0": {"args":[],"str":"log2"}, 149 | "logb/0": {"args":[],"str":"logb"}, 150 | "ltrim/0": {"args":[],"str":"ltrim"}, 151 | "ltrimstr/1": {"args":["a"],"str":"ltrimstr"}, 152 | "map/1": {"args":["f"],"str":"map"}, 153 | "map_values/1": {"args":["f"],"str":"map_values"}, 154 | "match/1": {"args":["$re"],"str":"match"}, 155 | "match/2": {"args":["$re","$flags"],"str":"match"}, 156 | "max/0": {"args":[],"str":"max"}, 157 | "max_by/1": {"args":["f"],"str":"max_by"}, 158 | "min/0": {"args":[],"str":"min"}, 159 | "min_by/1": {"args":["f"],"str":"min_by"}, 160 | "mktime/0": {"args":[],"str":"mktime"}, 161 | "modf/0": {"args":[],"str":"modf"}, 162 | "modulemeta/0": {"args":[],"str":"modulemeta"}, 163 | "nan/0": {"args":[],"str":"nan"}, 164 | "nearbyint/0": {"args":[],"str":"nearbyint"}, 165 | "nextafter/2": {"args":["a","b"],"str":"nextafter"}, 166 | "nexttoward/2": {"args":["a","b"],"str":"nexttoward"}, 167 | "normals/0": {"args":[],"str":"normals"}, 168 | "not/0": {"args":[],"str":"not"}, 169 | "now/0": {"args":[],"str":"now"}, 170 | "nth/1": {"args":["$n"],"str":"nth"}, 171 | "nth/2": {"args":["$n","g"],"str":"nth"}, 172 | "nulls/0": {"args":[],"str":"nulls"}, 173 | "numbers/0": {"args":[],"str":"numbers"}, 174 | "objects/0": {"args":[],"str":"objects"}, 175 | "path/1": {"args":["a"],"str":"path"}, 176 | "paths/0": {"args":[],"str":"paths"}, 177 | "paths/1": {"args":["f"],"str":"paths"}, 178 | "pick/1": {"args":["f"],"str":"pick"}, 179 | "pow/2": {"args":["a","b"],"str":"pow"}, 180 | "range/1": {"args":["$end"],"str":"range"}, 181 | "range/2": {"args":["$start","$end"],"str":"range"}, 182 | "range/3": {"args":["$start","$end","$step"],"str":"range"}, 183 | "recurse/0": {"args":[],"str":"recurse"}, 184 | "recurse/1": {"args":["f"],"str":"recurse"}, 185 | "recurse/2": {"args":["f","cond"],"str":"recurse"}, 186 | "remainder/2": {"args":["a","b"],"str":"remainder"}, 187 | "repeat/1": {"args":["f"],"str":"repeat"}, 188 | "reverse/0": {"args":[],"str":"reverse"}, 189 | "rindex/1": {"args":["a"],"str":"rindex"}, 190 | "rint/0": {"args":[],"str":"rint"}, 191 | "round/0": {"args":[],"str":"round"}, 192 | "rtrim/0": {"args":[],"str":"rtrim"}, 193 | "rtrimstr/1": {"args":["a"],"str":"rtrimstr"}, 194 | "scalars/0": {"args":[],"str":"scalars"}, 195 | "scalb/2": {"args":["a","b"],"str":"scalb"}, 196 | "scalbln/2": {"args":["a","b"],"str":"scalbln"}, 197 | "scan/1": {"args":["$re"],"str":"scan"}, 198 | "scan/2": {"args":["$re","$flags"],"str":"scan"}, 199 | "select/1": {"args":["f"],"str":"select"}, 200 | "setpath/2": {"args":["a","b"],"str":"setpath"}, 201 | "significand/0": {"args":[],"str":"significand"}, 202 | "sin/0": {"args":[],"str":"sin"}, 203 | "sinh/0": {"args":[],"str":"sinh"}, 204 | "skip/2": {"args":["$n","g"],"str":"skip"}, 205 | "sort/0": {"args":[],"str":"sort"}, 206 | "sort_by/1": {"args":["f"],"str":"sort_by"}, 207 | "split/1": {"args":["a"],"str":"split"}, 208 | "split/2": {"args":["a","b"],"str":"split"}, 209 | "splits/1": {"args":["$re"],"str":"splits"}, 210 | "splits/2": {"args":["$re","$flags"],"str":"splits"}, 211 | "sqrt/0": {"args":[],"str":"sqrt"}, 212 | "startswith/1": {"args":["a"],"str":"startswith"}, 213 | "stderr/0": {"args":[],"str":"stderr"}, 214 | "strflocaltime/1": {"args":["a"],"str":"strflocaltime"}, 215 | "strftime/1": {"args":["a"],"str":"strftime"}, 216 | "strings/0": {"args":[],"str":"strings"}, 217 | "strptime/1": {"args":["a"],"str":"strptime"}, 218 | "sub/2": {"args":["$re","str"],"str":"sub"}, 219 | "sub/3": {"args":["$re","str","$flags"],"str":"sub"}, 220 | "tan/0": {"args":[],"str":"tan"}, 221 | "tanh/0": {"args":[],"str":"tanh"}, 222 | "test/1": {"args":["$re"],"str":"test"}, 223 | "test/2": {"args":["$re","$flags"],"str":"test"}, 224 | "tgamma/0": {"args":[],"str":"tgamma"}, 225 | "to_entries/0": {"args":[],"str":"to_entries"}, 226 | "toboolean/0": {"args":[],"str":"toboolean"}, 227 | "todate/0": {"args":[],"str":"todate"}, 228 | "todateiso8601/0": {"args":[],"str":"todateiso8601"}, 229 | "tojson/0": {"args":[],"str":"tojson"}, 230 | "tonumber/0": {"args":[],"str":"tonumber"}, 231 | "tostream/0": {"args":[],"str":"tostream"}, 232 | "tostring/0": {"args":[],"str":"tostring"}, 233 | "transpose/0": {"args":[],"str":"transpose"}, 234 | "trim/0": {"args":[],"str":"trim"}, 235 | "trimstr/1": {"args":["a"],"str":"trimstr"}, 236 | "trunc/0": {"args":[],"str":"trunc"}, 237 | "truncate_stream/1": {"args":["f"],"str":"truncate_stream"}, 238 | "type/0": {"args":[],"str":"type"}, 239 | "unique/0": {"args":[],"str":"unique"}, 240 | "unique_by/1": {"args":["f"],"str":"unique_by"}, 241 | "until/2": {"args":["cond","next"],"str":"until"}, 242 | "utf8bytelength/0": {"args":[],"str":"utf8bytelength"}, 243 | "values/0": {"args":[],"str":"values"}, 244 | "walk/1": {"args":["f"],"str":"walk"}, 245 | "while/2": {"args":["cond","update"],"str":"while"}, 246 | "with_entries/1": {"args":["f"],"str":"with_entries"}, 247 | "y0/0": {"args":[],"str":"y0"}, 248 | "y1/0": {"args":[],"str":"y1"}, 249 | "yn/2": {"args":["a","b"],"str":"yn"}, 250 | }]; 251 | -------------------------------------------------------------------------------- /internal/difftest/difftest.go: -------------------------------------------------------------------------------- 1 | // Package difftest implement test based on diffing serialized string output 2 | // 3 | // User provides a function that get a input path and input string and returns a 4 | // output path and output string. Content of output path and output string is compared 5 | // and if there is a difference the test fails with a diff. 6 | // 7 | // Test inputs are read from files matching Pattern from Path. 8 | // 9 | // Note that output path can be the same as input which useful if the function 10 | // implements some kind of transcript that includes both input and output. 11 | package difftest 12 | 13 | import ( 14 | "bytes" 15 | "fmt" 16 | "os" 17 | "path/filepath" 18 | "sort" 19 | "strings" 20 | "testing" 21 | ) 22 | 23 | type tf interface { 24 | Helper() 25 | Errorf(format string, args ...any) 26 | Fatalf(format string, args ...any) 27 | } 28 | 29 | const green = "\x1b[32m" 30 | const red = "\x1b[31m" 31 | const reset = "\x1b[0m" 32 | 33 | func testDeepEqual(t tf, color bool, printfFn func(format string, args ...any), expected string, actual string) { 34 | t.Helper() 35 | 36 | uDiff := string(Diff("a", []byte(expected), "b", []byte(actual))) 37 | if uDiff == "" { 38 | return 39 | } 40 | 41 | if color { 42 | lines := strings.Split(uDiff, "\n") 43 | var coloredLines []string 44 | // diff looks like this: 45 | // --- expected 46 | // +++ actual 47 | // @@ -5,7 +5,7 @@ 48 | // - a 49 | // + b 50 | seenAt := false 51 | for _, l := range lines { 52 | if len(l) == 0 { 53 | continue 54 | } 55 | switch { 56 | case seenAt && l[0] == '+': 57 | coloredLines = append(coloredLines, green+l+reset) 58 | case seenAt && l[0] == '-': 59 | coloredLines = append(coloredLines, red+l+reset) 60 | default: 61 | if l[0] == '@' { 62 | seenAt = true 63 | } 64 | coloredLines = append(coloredLines, l) 65 | } 66 | } 67 | uDiff = strings.Join(coloredLines, "\n") 68 | } 69 | 70 | printfFn("%s", "\n"+uDiff) 71 | } 72 | 73 | func ErrorEx(t tf, color bool, expected string, actual string) { 74 | t.Helper() 75 | testDeepEqual(t, color, t.Errorf, expected, actual) 76 | } 77 | 78 | func Error(t tf, expected string, actual string) { 79 | t.Helper() 80 | testDeepEqual(t, false, t.Errorf, expected, actual) 81 | } 82 | 83 | func FatalEx(t tf, color bool, expected string, actual string) { 84 | t.Helper() 85 | testDeepEqual(t, color, t.Fatalf, expected, actual) 86 | } 87 | 88 | func Fatal(t tf, expected string, actual string) { 89 | t.Helper() 90 | testDeepEqual(t, false, t.Fatalf, expected, actual) 91 | } 92 | 93 | type Fn func(t *testing.T, path string, input string) (string, string, error) 94 | 95 | type Options struct { 96 | Path string 97 | Pattern string 98 | ColorDiff bool 99 | WriteOutput bool 100 | Fn Fn 101 | } 102 | 103 | func TestWithOptions(t *testing.T, opts Options) { 104 | t.Helper() 105 | 106 | t.Helper() 107 | 108 | // done in two steps as it seems hard to mark some functions inside filepath.Walk as test helpers 109 | var paths []string 110 | if err := filepath.Walk(opts.Path, func(path string, info os.FileInfo, err error) error { 111 | t.Helper() 112 | 113 | if err != nil { 114 | return err 115 | } 116 | match, err := filepath.Match(filepath.Join(filepath.Dir(path), opts.Pattern), path) 117 | if err != nil { 118 | return err 119 | } else if !match { 120 | return nil 121 | } 122 | 123 | paths = append(paths, path) 124 | return nil 125 | }); err != nil { 126 | t.Fatal(err) 127 | } 128 | 129 | for _, p := range paths { 130 | t.Run(p, func(t *testing.T) { 131 | t.Helper() 132 | input, err := os.ReadFile(p) 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | 137 | outputPath, output, err := opts.Fn(t, p, string(input)) 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | 142 | expectedOutput, expectedOutputErr := os.ReadFile(outputPath) 143 | if opts.WriteOutput { 144 | if expectedOutputErr == nil && string(expectedOutput) == output { 145 | return 146 | } 147 | 148 | if err := os.WriteFile(outputPath, []byte(output), 0644); err != nil { //nolint:gosec 149 | t.Fatal(err) 150 | } 151 | return 152 | } 153 | 154 | if expectedOutputErr != nil { 155 | t.Fatal(expectedOutputErr) 156 | } 157 | 158 | ErrorEx(t, opts.ColorDiff, string(expectedOutput), output) 159 | }) 160 | } 161 | } 162 | 163 | func Test(t *testing.T, pattern string, fn Fn) { 164 | t.Helper() 165 | 166 | TestWithOptions(t, Options{ 167 | Path: "testdata", 168 | Pattern: pattern, 169 | Fn: fn, 170 | }) 171 | } 172 | 173 | // Copyright 2022 The Go Authors. All rights reserved. 174 | // Use of this source code is governed by a BSD-style 175 | // license that can be found in the LICENSE file 176 | 177 | // A pair is a pair of values tracked for both the x and y side of a diff. 178 | // It is typically a pair of line indexes. 179 | type pair struct{ x, y int } 180 | 181 | // Diff returns an anchored diff of the two texts old and new 182 | // in the “unified diff” format. If old and new are identical, 183 | // Diff returns a nil slice (no output). 184 | // 185 | // Unix diff implementations typically look for a diff with 186 | // the smallest number of lines inserted and removed, 187 | // which can in the worst case take time quadratic in the 188 | // number of lines in the texts. As a result, many implementations 189 | // either can be made to run for a long time or cut off the search 190 | // after a predetermined amount of work. 191 | // 192 | // In contrast, this implementation looks for a diff with the 193 | // smallest number of “unique” lines inserted and removed, 194 | // where unique means a line that appears just once in both old and new. 195 | // We call this an “anchored diff” because the unique lines anchor 196 | // the chosen matching regions. An anchored diff is usually clearer 197 | // than a standard diff, because the algorithm does not try to 198 | // reuse unrelated blank lines or closing braces. 199 | // The algorithm also guarantees to run in O(n log n) time 200 | // instead of the standard O(n²) time. 201 | // 202 | // Some systems call this approach a “patience diff,” named for 203 | // the “patience sorting” algorithm, itself named for a solitaire card game. 204 | // We avoid that name for two reasons. First, the name has been used 205 | // for a few different variants of the algorithm, so it is imprecise. 206 | // Second, the name is frequently interpreted as meaning that you have 207 | // to wait longer (to be patient) for the diff, meaning that it is a slower algorithm, 208 | // when in fact the algorithm is faster than the standard one. 209 | func Diff(oldName string, old []byte, newName string, new_ []byte) []byte { 210 | if bytes.Equal(old, new_) { 211 | return nil 212 | } 213 | x := lines(old) 214 | y := lines(new_) 215 | 216 | // Print diff header. 217 | var out bytes.Buffer 218 | fmt.Fprintf(&out, "diff %s %s\n", oldName, newName) 219 | fmt.Fprintf(&out, "--- %s\n", oldName) 220 | fmt.Fprintf(&out, "+++ %s\n", newName) 221 | 222 | // Loop over matches to consider, 223 | // expanding each match to include surrounding lines, 224 | // and then printing diff chunks. 225 | // To avoid setup/teardown cases outside the loop, 226 | // tgs returns a leading {0,0} and trailing {len(x), len(y)} pair 227 | // in the sequence of matches. 228 | var ( 229 | done pair // printed up to x[:done.x] and y[:done.y] 230 | chunk pair // start lines of current chunk 231 | count pair // number of lines from each side in current chunk 232 | ctext []string // lines for current chunk 233 | ) 234 | for _, m := range tgs(x, y) { 235 | if m.x < done.x { 236 | // Already handled scanning forward from earlier match. 237 | continue 238 | } 239 | 240 | // Expand matching lines as far as possible, 241 | // establishing that x[start.x:end.x] == y[start.y:end.y]. 242 | // Note that on the first (or last) iteration we may (or definitely do) 243 | // have an empty match: start.x==end.x and start.y==end.y. 244 | start := m 245 | for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] { 246 | start.x-- 247 | start.y-- 248 | } 249 | end := m 250 | for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] { 251 | end.x++ 252 | end.y++ 253 | } 254 | 255 | // Emit the mismatched lines before start into this chunk. 256 | // (No effect on first sentinel iteration, when start = {0,0}.) 257 | for _, s := range x[done.x:start.x] { 258 | ctext = append(ctext, "-"+s) 259 | count.x++ 260 | } 261 | for _, s := range y[done.y:start.y] { 262 | ctext = append(ctext, "+"+s) 263 | count.y++ 264 | } 265 | 266 | // If we're not at EOF and have too few common lines, 267 | // the chunk includes all the common lines and continues. 268 | const C = 3 // number of context lines 269 | if (end.x < len(x) || end.y < len(y)) && 270 | (end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) { 271 | for _, s := range x[start.x:end.x] { 272 | ctext = append(ctext, " "+s) 273 | count.x++ 274 | count.y++ 275 | } 276 | done = end 277 | continue 278 | } 279 | 280 | // End chunk with common lines for context. 281 | if len(ctext) > 0 { 282 | n := end.x - start.x 283 | if n > C { 284 | n = C 285 | } 286 | for _, s := range x[start.x : start.x+n] { 287 | ctext = append(ctext, " "+s) 288 | count.x++ 289 | count.y++ 290 | } 291 | done = pair{start.x + n, start.y + n} 292 | 293 | // Format and emit chunk. 294 | // Convert line numbers to 1-indexed. 295 | // Special case: empty file shows up as 0,0 not 1,0. 296 | if count.x > 0 { 297 | chunk.x++ 298 | } 299 | if count.y > 0 { 300 | chunk.y++ 301 | } 302 | fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y) 303 | for _, s := range ctext { 304 | out.WriteString(s) 305 | } 306 | count.x = 0 307 | count.y = 0 308 | ctext = ctext[:0] 309 | } 310 | 311 | // If we reached EOF, we're done. 312 | if end.x >= len(x) && end.y >= len(y) { 313 | break 314 | } 315 | 316 | // Otherwise start a new chunk. 317 | chunk = pair{end.x - C, end.y - C} 318 | for _, s := range x[chunk.x:end.x] { 319 | ctext = append(ctext, " "+s) 320 | count.x++ 321 | count.y++ 322 | } 323 | done = end 324 | } 325 | 326 | return out.Bytes() 327 | } 328 | 329 | // lines returns the lines in the file x, including newlines. 330 | // If the file does not end in a newline, one is supplied 331 | // along with a warning about the missing newline. 332 | func lines(x []byte) []string { 333 | l := strings.SplitAfter(string(x), "\n") 334 | if l[len(l)-1] == "" { 335 | l = l[:len(l)-1] 336 | } else { 337 | // Treat last line as having a message about the missing newline attached, 338 | // using the same text as BSD/GNU diff (including the leading backslash). 339 | l[len(l)-1] += "\n\\ No newline at end of file\n" 340 | } 341 | return l 342 | } 343 | 344 | // tgs returns the pairs of indexes of the longest common subsequence 345 | // of unique lines in x and y, where a unique line is one that appears 346 | // once in x and once in y. 347 | // 348 | // The longest common subsequence algorithm is as described in 349 | // Thomas G. Szymanski, “A Special Case of the Maximal Common 350 | // Subsequence Problem,” Princeton TR #170 (January 1975), 351 | // available at https://research.swtch.com/tgs170.pdf. 352 | func tgs(x, y []string) []pair { 353 | // Count the number of times each string appears in a and b. 354 | // We only care about 0, 1, many, counted as 0, -1, -2 355 | // for the x side and 0, -4, -8 for the y side. 356 | // Using negative numbers now lets us distinguish positive line numbers later. 357 | m := make(map[string]int) 358 | for _, s := range x { 359 | if c := m[s]; c > -2 { 360 | m[s] = c - 1 361 | } 362 | } 363 | for _, s := range y { 364 | if c := m[s]; c > -8 { 365 | m[s] = c - 4 366 | } 367 | } 368 | 369 | // Now unique strings can be identified by m[s] = -1+-4. 370 | // 371 | // Gather the indexes of those strings in x and y, building: 372 | // xi[i] = increasing indexes of unique strings in x. 373 | // yi[i] = increasing indexes of unique strings in y. 374 | // inv[i] = index j such that x[xi[i]] = y[yi[j]]. 375 | var xi, yi, inv []int 376 | for i, s := range y { 377 | if m[s] == -1+-4 { 378 | m[s] = len(yi) 379 | yi = append(yi, i) 380 | } 381 | } 382 | for i, s := range x { 383 | if j, ok := m[s]; ok && j >= 0 { 384 | xi = append(xi, i) 385 | inv = append(inv, j) 386 | } 387 | } 388 | 389 | // Apply Algorithm A from Szymanski's paper. 390 | // In those terms, A = J = inv and B = [0, n). 391 | // We add sentinel pairs {0,0}, and {len(x),len(y)} 392 | // to the returned sequence, to help the processing loop. 393 | J := inv 394 | n := len(xi) 395 | T := make([]int, n) 396 | L := make([]int, n) 397 | for i := range T { 398 | T[i] = n + 1 399 | } 400 | for i := 0; i < n; i++ { 401 | k := sort.Search(n, func(k int) bool { 402 | return T[k] >= J[i] 403 | }) 404 | T[k] = J[i] 405 | L[i] = k + 1 406 | } 407 | k := 0 408 | for _, v := range L { 409 | if k < v { 410 | k = v 411 | } 412 | } 413 | seq := make([]pair, 2+k) 414 | seq[1+k] = pair{len(x), len(y)} // sentinel at end 415 | lastj := n 416 | for i := n - 1; i >= 0; i-- { 417 | if L[i] == k && J[i] < lastj { 418 | seq[k] = pair{xi[i], yi[J[i]]} 419 | k-- 420 | } 421 | } 422 | seq[0] = pair{0, 0} // sentinel at start 423 | return seq 424 | } 425 | -------------------------------------------------------------------------------- /gojqparser/lexer.go: -------------------------------------------------------------------------------- 1 | package gojqparser 2 | 3 | import ( 4 | "encoding/json" 5 | "unicode/utf8" 6 | ) 7 | 8 | type lexer struct { 9 | source string 10 | offset int 11 | result *Query 12 | token string 13 | tokenType int 14 | inString bool 15 | err error 16 | } 17 | 18 | type Token struct { 19 | Str string `json:"str"` 20 | Start int `json:"start"` 21 | Stop int `json:"stop"` 22 | } 23 | 24 | func newLexer(src string) *lexer { 25 | return &lexer{source: src} 26 | } 27 | 28 | const eof = -1 29 | 30 | var keywords = map[string]int{ 31 | "or": tokOrOp, 32 | "and": tokAndOp, 33 | "module": tokModule, 34 | "import": tokImport, 35 | "include": tokInclude, 36 | "def": tokDef, 37 | "as": tokAs, 38 | "label": tokLabel, 39 | "break": tokBreak, 40 | // jq-lsp: don't treat as keyword, jq, jaq etc treat as idents 41 | // "null": tokNull, 42 | // "true": tokTrue, 43 | // "false": tokFalse, 44 | "if": tokIf, 45 | "then": tokThen, 46 | "elif": tokElif, 47 | "else": tokElse, 48 | "end": tokEnd, 49 | "try": tokTry, 50 | "catch": tokCatch, 51 | "reduce": tokReduce, 52 | "foreach": tokForeach, 53 | } 54 | 55 | func (l *lexer) Lex(lval *yySymType) (tokenType int) { 56 | lval.token = &Token{} 57 | defer func() { 58 | l.tokenType = tokenType 59 | lval.token.Stop = l.offset 60 | }() 61 | if len(l.source) == l.offset { 62 | l.token = "" 63 | return eof 64 | } 65 | if l.inString { 66 | tok, str := l.scanString(l.offset) 67 | lval.token.Str = str 68 | return tok 69 | } 70 | ch, iseof := l.next() 71 | lval.token.Start = l.offset - 1 72 | if iseof { 73 | l.token = "" 74 | return eof 75 | } 76 | switch { 77 | case isIdent(ch, false): 78 | i := l.offset - 1 79 | j, isModule := l.scanIdentOrModule() 80 | l.token = l.source[i:j] 81 | lval.token.Str = l.token 82 | if isModule { 83 | return tokModuleIdent 84 | } 85 | if tok, ok := keywords[l.token]; ok { 86 | return tok 87 | } 88 | return tokIdent 89 | case isNumber(ch): 90 | if ch == '0' { 91 | base := l.peek() 92 | switch base { 93 | case 'b', 'o', 'x': 94 | i := l.offset - 1 95 | l.offset++ 96 | for isInteger(base, l.peek()) { 97 | l.offset++ 98 | } 99 | l.token = string(l.source[i:l.offset]) 100 | lval.token.Str = l.token 101 | return tokNumber 102 | } 103 | } 104 | 105 | i := l.offset - 1 106 | j := l.scanNumber(numberStateLead) 107 | if j < 0 { 108 | l.token = l.source[i:-j] 109 | return tokInvalid 110 | } 111 | l.token = l.source[i:j] 112 | lval.token.Str = l.token 113 | return tokNumber 114 | } 115 | switch ch { 116 | case '.': 117 | ch := l.peek() 118 | switch { 119 | case ch == '.': 120 | l.offset++ 121 | l.token = ".." 122 | return tokRecurse 123 | case isIdent(ch, false): 124 | l.token = l.source[l.offset-1 : l.scanIdent()] 125 | lval.token.Str = l.token[1:] 126 | return tokIndex 127 | case isNumber(ch): 128 | i := l.offset - 1 129 | j := l.scanNumber(numberStateFloat) 130 | if j < 0 { 131 | l.token = l.source[i:-j] 132 | return tokInvalid 133 | } 134 | l.token = l.source[i:j] 135 | lval.token.Str = l.token 136 | return tokNumber 137 | default: 138 | return '.' 139 | } 140 | case '$': 141 | if isIdent(l.peek(), false) { 142 | i := l.offset - 1 143 | j, isModule := l.scanIdentOrModule() 144 | l.token = l.source[i:j] 145 | lval.token.Str = l.token 146 | if isModule { 147 | return tokModuleVariable 148 | } 149 | return tokVariable 150 | } 151 | case '|': 152 | if l.peek() == '=' { 153 | l.offset++ 154 | l.token = "|=" 155 | lval.operator = OpModify 156 | return tokUpdateOp 157 | } 158 | case '?': 159 | if l.peek() == '/' { 160 | l.offset++ 161 | if l.peek() == '/' { 162 | l.offset++ 163 | l.token = "?//" 164 | return tokDestAltOp 165 | } 166 | l.offset-- 167 | } 168 | case '+': 169 | if l.peek() == '=' { 170 | l.offset++ 171 | l.token = "+=" 172 | lval.operator = OpUpdateAdd 173 | return tokUpdateOp 174 | } 175 | case '-': 176 | if l.peek() == '=' { 177 | l.offset++ 178 | l.token = "-=" 179 | lval.operator = OpUpdateSub 180 | return tokUpdateOp 181 | } 182 | case '*': 183 | if l.peek() == '=' { 184 | l.offset++ 185 | l.token = "*=" 186 | lval.operator = OpUpdateMul 187 | return tokUpdateOp 188 | } 189 | case '/': 190 | switch l.peek() { 191 | case '=': 192 | l.offset++ 193 | l.token = "/=" 194 | lval.operator = OpUpdateDiv 195 | return tokUpdateOp 196 | case '/': 197 | l.offset++ 198 | if l.peek() == '=' { 199 | l.offset++ 200 | l.token = "//=" 201 | lval.operator = OpUpdateAlt 202 | return tokUpdateOp 203 | } 204 | l.token = "//" 205 | lval.operator = OpAlt 206 | return tokAltOp 207 | } 208 | case '%': 209 | if l.peek() == '=' { 210 | l.offset++ 211 | l.token = "%=" 212 | lval.operator = OpUpdateMod 213 | return tokUpdateOp 214 | } 215 | case '=': 216 | if l.peek() == '=' { 217 | l.offset++ 218 | l.token = "==" 219 | lval.operator = OpEq 220 | return tokCompareOp 221 | } 222 | l.token = "=" 223 | lval.operator = OpAssign 224 | return tokUpdateOp 225 | case '!': 226 | if l.peek() == '=' { 227 | l.offset++ 228 | l.token = "!=" 229 | lval.operator = OpNe 230 | return tokCompareOp 231 | } 232 | case '>': 233 | if l.peek() == '=' { 234 | l.offset++ 235 | l.token = ">=" 236 | lval.operator = OpGe 237 | return tokCompareOp 238 | } 239 | l.token = ">" 240 | lval.operator = OpGt 241 | return tokCompareOp 242 | case '<': 243 | if l.peek() == '=' { 244 | l.offset++ 245 | l.token = "<=" 246 | lval.operator = OpLe 247 | return tokCompareOp 248 | } 249 | l.token = "<" 250 | lval.operator = OpLt 251 | return tokCompareOp 252 | case '@': 253 | if isIdent(l.peek(), true) { 254 | l.token = l.source[l.offset-1 : l.scanIdent()] 255 | lval.token.Str = l.token 256 | return tokFormat 257 | } 258 | case '"': 259 | tok, str := l.scanString(l.offset - 1) 260 | lval.token.Str = str 261 | return tok 262 | case '`': 263 | tok, str := l.scanRawString(l.offset - 1) 264 | lval.token.Str = str 265 | return tok 266 | default: 267 | if ch >= utf8.RuneSelf { 268 | r, size := utf8.DecodeRuneInString(l.source[l.offset-1:]) 269 | // -1 to adjust for first byte consumed by next() 270 | l.offset += size - 1 271 | l.token = string(r) 272 | } 273 | } 274 | return int(ch) 275 | } 276 | 277 | func (l *lexer) next() (byte, bool) { 278 | for { 279 | ch := l.source[l.offset] 280 | l.offset++ 281 | if ch == '#' { 282 | if l.skipComment() { 283 | return 0, true 284 | } 285 | } else if !isWhite(ch) { 286 | return ch, false 287 | } else if len(l.source) == l.offset { 288 | return 0, true 289 | } 290 | } 291 | } 292 | 293 | func (l *lexer) skipComment() bool { 294 | for { 295 | switch l.peek() { 296 | case 0: 297 | return true 298 | case '\\': 299 | switch l.offset++; l.peek() { 300 | case '\\', '\n': 301 | l.offset++ 302 | case '\r': 303 | if l.offset++; l.peek() == '\n' { 304 | l.offset++ 305 | } 306 | } 307 | case '\n', '\r': 308 | return false 309 | default: 310 | l.offset++ 311 | } 312 | } 313 | } 314 | 315 | func (l *lexer) peek() byte { 316 | if len(l.source) == l.offset { 317 | return 0 318 | } 319 | return l.source[l.offset] 320 | } 321 | 322 | func (l *lexer) scanIdent() int { 323 | for isIdent(l.peek(), true) { 324 | l.offset++ 325 | } 326 | return l.offset 327 | } 328 | 329 | func (l *lexer) scanIdentOrModule() (int, bool) { 330 | index := l.scanIdent() 331 | var isModule bool 332 | if l.peek() == ':' { 333 | l.offset++ 334 | if l.peek() == ':' { 335 | l.offset++ 336 | if isIdent(l.peek(), false) { 337 | l.offset++ 338 | index = l.scanIdent() 339 | isModule = true 340 | } else { 341 | l.offset -= 2 342 | } 343 | } else { 344 | l.offset-- 345 | } 346 | } 347 | return index, isModule 348 | } 349 | 350 | const ( 351 | numberStateLead = iota 352 | numberStateFloat 353 | numberStateExpLead 354 | numberStateExp 355 | ) 356 | 357 | func (l *lexer) scanNumber(state int) int { 358 | for { 359 | switch state { 360 | case numberStateLead, numberStateFloat: 361 | if ch := l.peek(); isNumber(ch) { 362 | l.offset++ 363 | } else { 364 | switch ch { 365 | case '.': 366 | if state != numberStateLead { 367 | l.offset++ 368 | return -l.offset 369 | } 370 | l.offset++ 371 | state = numberStateFloat 372 | case 'e', 'E': 373 | l.offset++ 374 | switch l.peek() { 375 | case '-', '+': 376 | l.offset++ 377 | } 378 | state = numberStateExpLead 379 | default: 380 | if isIdent(ch, false) { 381 | l.offset++ 382 | return -l.offset 383 | } 384 | return l.offset 385 | } 386 | } 387 | case numberStateExpLead, numberStateExp: 388 | if ch := l.peek(); !isNumber(ch) { 389 | if isIdent(ch, false) { 390 | l.offset++ 391 | return -l.offset 392 | } 393 | if state == numberStateExpLead { 394 | return -l.offset 395 | } 396 | return l.offset 397 | } 398 | l.offset++ 399 | state = numberStateExp 400 | default: 401 | panic(state) 402 | } 403 | } 404 | } 405 | 406 | func (l *lexer) scanString(start int) (int, string) { 407 | var decode bool 408 | var controls int 409 | unquote := func(src string, quote bool) (string, error) { 410 | if !decode { 411 | if quote { 412 | return src, nil 413 | } 414 | return src[1 : len(src)-1], nil 415 | } 416 | var buf []byte 417 | if !quote && controls == 0 { 418 | buf = []byte(src) 419 | } else { 420 | buf = quoteAndEscape(src, quote, controls) 421 | } 422 | if err := json.Unmarshal(buf, &src); err != nil { 423 | return "", err 424 | } 425 | return src, nil 426 | } 427 | for i := l.offset; i < len(l.source); i++ { 428 | ch := l.source[i] 429 | switch ch { 430 | case '\\': 431 | if i++; i >= len(l.source) { 432 | break 433 | } 434 | switch l.source[i] { 435 | case 'u': 436 | for j := 1; j <= 4; j++ { 437 | if i+j >= len(l.source) || !isHex(l.source[i+j]) { 438 | l.offset = i + j 439 | l.token = l.source[i-1 : l.offset] 440 | return tokInvalidEscapeSequence, "" 441 | } 442 | } 443 | i += 4 444 | fallthrough 445 | case '"', '/', '\\', 'b', 'f', 'n', 'r', 't': 446 | decode = true 447 | case '(': 448 | if !l.inString { 449 | l.inString = true 450 | return tokStringStart, "" 451 | } 452 | if i == l.offset+1 { 453 | l.offset += 2 454 | l.inString = false 455 | return tokStringQuery, "" 456 | } 457 | l.offset = i - 1 458 | l.token = l.source[start:l.offset] 459 | str, err := unquote(l.token, true) 460 | if err != nil { 461 | return tokInvalid, "" 462 | } 463 | return tokString, str 464 | default: 465 | l.offset = i + 1 466 | l.token = l.source[l.offset-2 : l.offset] 467 | return tokInvalidEscapeSequence, "" 468 | } 469 | case '"': 470 | if !l.inString { 471 | l.offset = i + 1 472 | l.token = l.source[start:l.offset] 473 | str, err := unquote(l.token, false) 474 | if err != nil { 475 | return tokInvalid, "" 476 | } 477 | return tokString, str 478 | } 479 | if i > l.offset { 480 | l.offset = i 481 | l.token = l.source[start:l.offset] 482 | str, err := unquote(l.token, true) 483 | if err != nil { 484 | return tokInvalid, "" 485 | } 486 | return tokString, str 487 | } 488 | l.inString = false 489 | l.offset = i + 1 490 | return tokStringEnd, "" 491 | default: 492 | if !decode { 493 | decode = ch > '~' 494 | } 495 | if ch < ' ' { // ref: unquoteBytes in encoding/json 496 | controls++ 497 | } 498 | } 499 | } 500 | l.offset = len(l.source) 501 | l.token = "" 502 | return tokUnterminatedString, "" 503 | } 504 | 505 | func (l *lexer) scanRawString(start int) (int, string) { 506 | for i := l.offset; i < len(l.source); i++ { 507 | ch := l.source[i] 508 | switch ch { 509 | case '`': 510 | l.offset = i + 1 511 | l.token = l.source[start:l.offset] 512 | return tokString, l.token[1 : len(l.token)-1] 513 | } 514 | } 515 | l.offset = len(l.source) 516 | l.token = "" 517 | return tokUnterminatedString, "" 518 | } 519 | 520 | func quoteAndEscape(src string, quote bool, controls int) []byte { 521 | size := len(src) + controls*5 522 | if quote { 523 | size += 2 524 | } 525 | buf := make([]byte, size) 526 | var j int 527 | if quote { 528 | buf[0] = '"' 529 | buf[len(buf)-1] = '"' 530 | j++ 531 | } 532 | for i := 0; i < len(src); i++ { 533 | if ch := src[i]; ch < ' ' { 534 | const hex = "0123456789abcdef" 535 | copy(buf[j:], `\u00`) 536 | buf[j+4] = hex[ch>>4] 537 | buf[j+5] = hex[ch&0xF] 538 | j += 6 539 | } else { 540 | buf[j] = ch 541 | j++ 542 | } 543 | } 544 | return buf 545 | } 546 | 547 | // ParseError represents a description of a query parsing error. 548 | type ParseError struct { 549 | Offset int // the error occurred after reading Offset bytes 550 | Token string // the Token that caused the error (may be empty) 551 | tokenType int 552 | } 553 | 554 | func (err *ParseError) Error() string { 555 | switch err.tokenType { 556 | case eof: 557 | return "unexpected EOF" 558 | case tokInvalid: 559 | return "invalid token " + jsonMarshal(err.Token) 560 | case tokInvalidEscapeSequence: 561 | return `invalid escape sequence "` + err.Token + `" in string literal` 562 | case tokUnterminatedString: 563 | return "unterminated string literal" 564 | default: 565 | return "unexpected token " + jsonMarshal(err.Token) 566 | } 567 | } 568 | 569 | func (l *lexer) Error(string) { 570 | offset, token := l.offset, l.token 571 | if l.tokenType != eof && l.tokenType < utf8.RuneSelf { 572 | token = string(rune(l.tokenType)) 573 | } 574 | l.err = &ParseError{offset, token, l.tokenType} 575 | } 576 | 577 | func isWhite(ch byte) bool { 578 | switch ch { 579 | case '\t', '\n', '\r', ' ': 580 | return true 581 | default: 582 | return false 583 | } 584 | } 585 | 586 | func isIdent(ch byte, tail bool) bool { 587 | return 'a' <= ch && ch <= 'z' || 588 | 'A' <= ch && ch <= 'Z' || ch == '_' || 589 | tail && isNumber(ch) 590 | } 591 | 592 | func isHex(ch byte) bool { 593 | return 'a' <= ch && ch <= 'f' || 594 | 'A' <= ch && ch <= 'F' || 595 | isNumber(ch) 596 | } 597 | 598 | func isBinary(ch byte) bool { 599 | return '0' == ch || ch == '1' 600 | } 601 | 602 | func isOctal(ch byte) bool { 603 | return '0' <= ch && ch <= '7' 604 | } 605 | 606 | func isNumber(ch byte) bool { 607 | return '0' <= ch && ch <= '9' 608 | } 609 | 610 | func isInteger(base byte, ch byte) bool { 611 | if ch == '_' { 612 | return true 613 | } 614 | switch base { 615 | case 'b': 616 | return isBinary(ch) 617 | case 'o': 618 | return isOctal(ch) 619 | case 'x': 620 | return isHex(ch) 621 | default: 622 | panic("unreachable") 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /gojqparser/parser.go.y: -------------------------------------------------------------------------------- 1 | %{ 2 | package gojqparser 3 | 4 | func reverseFuncDef(xs []*FuncDef) []*FuncDef { 5 | for i, j := 0, len(xs)-1; i < j; i, j = i+1, j-1 { 6 | xs[i], xs[j] = xs[j], xs[i] 7 | } 8 | return xs 9 | } 10 | 11 | func prependFuncDef(xs []*FuncDef, x *FuncDef) []*FuncDef { 12 | xs = append(xs, nil) 13 | copy(xs[1:], xs) 14 | xs[0] = x 15 | return xs 16 | } 17 | %} 18 | 19 | %union { 20 | value any 21 | token *Token 22 | operator Operator 23 | } 24 | 25 | %type program header imports import meta body funcdefs funcdef funcargs query 26 | %type bindpatterns pattern arraypatterns objectpatterns objectpattern 27 | %type expr term string stringparts suffix args ifelifs ifelse trycatch 28 | %type objectkeyvals objectkeyval objectval 29 | %type constterm constobject constobjectkeyvals constobjectkeyval constarray constarrayelems 30 | %type tokIdentVariable tokIdentModuleIdent tokVariableModuleVariable tokKeyword objectkey 31 | %type tokIdentVariableFormat 32 | %token tokAltOp tokUpdateOp tokDestAltOp tokCompareOp 33 | %token tokOrOp tokAndOp tokModule tokImport tokInclude tokDef tokAs tokLabel tokBreak 34 | %token tokNull tokTrue tokFalse 35 | %token tokIf tokThen tokElif tokElse tokEnd 36 | %token tokTry tokCatch tokReduce tokForeach 37 | %token tokIdent tokVariable tokModuleIdent tokModuleVariable 38 | %token tokRecurse tokIndex tokNumber tokFormat 39 | %token tokString tokStringStart tokStringQuery tokStringEnd 40 | %token tokInvalid tokInvalidEscapeSequence tokUnterminatedString 41 | 42 | %nonassoc tokFuncDefQuery tokExpr tokTerm 43 | %right '|' 44 | %left ',' 45 | %right tokAltOp 46 | %nonassoc tokUpdateOp 47 | %left tokOrOp 48 | %left tokAndOp 49 | %nonassoc tokCompareOp 50 | %left '+' '-' 51 | %left '*' '/' '%' 52 | %nonassoc tokAs tokIndex '.' '?' tokEmptyCatch 53 | %nonassoc '[' tokTry tokCatch 54 | 55 | %% 56 | 57 | program 58 | : header imports body 59 | { 60 | query := $3.(*Query) 61 | query.Meta = $1.(*ConstObject) 62 | query.Imports = $2.([]*Import) 63 | yylex.(*lexer).result = query 64 | } 65 | 66 | header 67 | : 68 | { 69 | $$ = (*ConstObject)(nil) 70 | } 71 | | tokModule constobject ';' 72 | { 73 | $$ = $2; 74 | } 75 | 76 | imports 77 | : 78 | { 79 | $$ = []*Import(nil) 80 | } 81 | | imports import 82 | { 83 | $$ = append($1.([]*Import), $2.(*Import)) 84 | } 85 | 86 | import 87 | : tokImport tokString tokAs tokIdentVariable meta ';' 88 | { 89 | $$ = &Import{ImportPath: $2, ImportAlias: $4, Meta: $5.(*ConstObject)} 90 | } 91 | | tokInclude tokString meta ';' 92 | { 93 | $$ = &Import{IncludePath: $2, Meta: $3.(*ConstObject)} 94 | } 95 | 96 | meta 97 | : 98 | { 99 | $$ = (*ConstObject)(nil) 100 | } 101 | | constobject 102 | 103 | body 104 | : funcdefs 105 | { 106 | $$ = &Query{FuncDefs: reverseFuncDef($1.([]*FuncDef))} 107 | } 108 | | query 109 | 110 | funcdefs 111 | : 112 | { 113 | $$ = []*FuncDef(nil) 114 | } 115 | | funcdef funcdefs 116 | { 117 | $$ = append($2.([]*FuncDef), $1.(*FuncDef)) 118 | } 119 | 120 | funcdef 121 | : tokDef tokIdentVariableFormat ':' query ';' 122 | { 123 | $$ = &FuncDef{Name: $2, Body: $4.(*Query)} 124 | } 125 | | tokDef tokIdentVariableFormat '(' funcargs ')' ':' query ';' 126 | { 127 | $$ = &FuncDef{$2, $4.([]*Token), $7.(*Query)} 128 | } 129 | 130 | funcargs 131 | : tokIdentVariable 132 | { 133 | $$ = []*Token{$1} 134 | } 135 | | funcargs ';' tokIdentVariable 136 | { 137 | $$ = append($1.([]*Token), $3) 138 | } 139 | 140 | tokIdentVariable 141 | : tokIdent 142 | | tokVariable 143 | 144 | tokIdentVariableFormat 145 | : tokIdent 146 | | tokVariable 147 | | tokFormat 148 | 149 | query 150 | : funcdef query %prec tokFuncDefQuery 151 | { 152 | query := $2.(*Query) 153 | query.FuncDefs = prependFuncDef(query.FuncDefs, $1.(*FuncDef)) 154 | $$ = query 155 | } 156 | | query '|' query 157 | { 158 | $$ = &Query{Left: $1.(*Query), Op: OpPipe, Right: $3.(*Query)} 159 | } 160 | | query tokAs bindpatterns '|' query 161 | { 162 | $$ = &Query{Left: $1.(*Query), Op: OpPipe, Right: $5.(*Query), Patterns: $3.([]*Pattern)} 163 | } 164 | | tokLabel tokVariable '|' query 165 | { 166 | $$ = &Query{Term: &Term{Type: TermTypeLabel, Label: &Label{$2, $4.(*Query)}}} 167 | } 168 | | query ',' query 169 | { 170 | $$ = &Query{Left: $1.(*Query), Op: OpComma, Right: $3.(*Query)} 171 | } 172 | | expr %prec tokExpr 173 | 174 | expr 175 | : expr tokAltOp expr 176 | { 177 | $$ = &Query{Left: $1.(*Query), Op: $2, Right: $3.(*Query)} 178 | } 179 | | expr tokUpdateOp expr 180 | { 181 | $$ = &Query{Left: $1.(*Query), Op: $2, Right: $3.(*Query)} 182 | } 183 | | expr tokOrOp expr 184 | { 185 | $$ = &Query{Left: $1.(*Query), Op: OpOr, Right: $3.(*Query)} 186 | } 187 | | expr tokAndOp expr 188 | { 189 | $$ = &Query{Left: $1.(*Query), Op: OpAnd, Right: $3.(*Query)} 190 | } 191 | | expr tokCompareOp expr 192 | { 193 | $$ = &Query{Left: $1.(*Query), Op: $2, Right: $3.(*Query)} 194 | } 195 | | expr '+' expr 196 | { 197 | $$ = &Query{Left: $1.(*Query), Op: OpAdd, Right: $3.(*Query)} 198 | } 199 | | expr '-' expr 200 | { 201 | $$ = &Query{Left: $1.(*Query), Op: OpSub, Right: $3.(*Query)} 202 | } 203 | | expr '*' expr 204 | { 205 | $$ = &Query{Left: $1.(*Query), Op: OpMul, Right: $3.(*Query)} 206 | } 207 | | expr '/' expr 208 | { 209 | $$ = &Query{Left: $1.(*Query), Op: OpDiv, Right: $3.(*Query)} 210 | } 211 | | expr '%' expr 212 | { 213 | $$ = &Query{Left: $1.(*Query), Op: OpMod, Right: $3.(*Query)} 214 | } 215 | | term %prec tokTerm 216 | { 217 | $$ = &Query{Term: $1.(*Term)} 218 | } 219 | 220 | bindpatterns 221 | : pattern 222 | { 223 | $$ = []*Pattern{$1.(*Pattern)} 224 | } 225 | | bindpatterns tokDestAltOp pattern 226 | { 227 | $$ = append($1.([]*Pattern), $3.(*Pattern)) 228 | } 229 | 230 | pattern 231 | : tokVariable 232 | { 233 | $$ = &Pattern{Name: $1} 234 | } 235 | | '[' arraypatterns ']' 236 | { 237 | $$ = &Pattern{Array: $2.([]*Pattern)} 238 | } 239 | | '{' objectpatterns '}' 240 | { 241 | $$ = &Pattern{Object: $2.([]*PatternObject)} 242 | } 243 | 244 | arraypatterns 245 | : pattern 246 | { 247 | $$ = []*Pattern{$1.(*Pattern)} 248 | } 249 | | arraypatterns ',' pattern 250 | { 251 | $$ = append($1.([]*Pattern), $3.(*Pattern)) 252 | } 253 | 254 | objectpatterns 255 | : objectpattern 256 | { 257 | $$ = []*PatternObject{$1.(*PatternObject)} 258 | } 259 | | objectpatterns ',' objectpattern 260 | { 261 | $$ = append($1.([]*PatternObject), $3.(*PatternObject)) 262 | } 263 | 264 | objectpattern 265 | : objectkey ':' pattern 266 | { 267 | $$ = &PatternObject{Key: $1, Val: $3.(*Pattern)} 268 | } 269 | | string ':' pattern 270 | { 271 | $$ = &PatternObject{KeyString: $1.(*String), Val: $3.(*Pattern)} 272 | } 273 | | '(' query ')' ':' pattern 274 | { 275 | $$ = &PatternObject{KeyQuery: $2.(*Query), Val: $5.(*Pattern)} 276 | } 277 | | tokVariable 278 | { 279 | $$ = &PatternObject{Key: $1} 280 | } 281 | 282 | term 283 | : '.' 284 | { 285 | $$ = &Term{Type: TermTypeIdentity} 286 | } 287 | | tokRecurse 288 | { 289 | $$ = &Term{Type: TermTypeRecurse} 290 | } 291 | | tokIndex 292 | { 293 | $$ = &Term{Type: TermTypeIndex, Index: &Index{Name: $1}} 294 | } 295 | | '.' suffix 296 | { 297 | suffix := $2.(*Suffix) 298 | if suffix.Iter { 299 | $$ = &Term{Type: TermTypeIdentity, SuffixList: []*Suffix{suffix}} 300 | } else { 301 | $$ = &Term{Type: TermTypeIndex, Index: suffix.Index} 302 | } 303 | } 304 | | '.' string 305 | { 306 | $$ = &Term{Type: TermTypeIndex, Index: &Index{Str: $2.(*String)}} 307 | } 308 | | tokNull 309 | { 310 | $$ = &Term{Type: TermTypeNull} 311 | } 312 | | tokTrue 313 | { 314 | $$ = &Term{Type: TermTypeTrue} 315 | } 316 | | tokFalse 317 | { 318 | $$ = &Term{Type: TermTypeFalse} 319 | } 320 | | tokIdentModuleIdent 321 | { 322 | $$ = &Term{Type: TermTypeFunc, Func: &Func{Name: $1}} 323 | } 324 | | tokIdentModuleIdent '(' args ')' 325 | { 326 | $$ = &Term{Type: TermTypeFunc, Func: &Func{Name: $1, Args: $3.([]*Query)}} 327 | } 328 | | tokVariableModuleVariable 329 | { 330 | $$ = &Term{Type: TermTypeFunc, Func: &Func{Name: $1}} 331 | } 332 | | '{' '}' 333 | { 334 | $$ = &Term{Type: TermTypeObject, Object: &Object{}} 335 | } 336 | | '{' objectkeyvals '}' 337 | { 338 | $$ = &Term{Type: TermTypeObject, Object: &Object{$2.([]*ObjectKeyVal)}} 339 | } 340 | | '{' objectkeyvals ',' '}' 341 | { 342 | $$ = &Term{Type: TermTypeObject, Object: &Object{$2.([]*ObjectKeyVal)}} 343 | } 344 | | '[' ']' 345 | { 346 | $$ = &Term{Type: TermTypeArray, Array: &Array{}} 347 | } 348 | | '[' query ']' 349 | { 350 | $$ = &Term{Type: TermTypeArray, Array: &Array{$2.(*Query)}} 351 | } 352 | | tokNumber 353 | { 354 | $$ = &Term{Type: TermTypeNumber, Number: $1} 355 | } 356 | | '+' term 357 | { 358 | $$ = &Term{Type: TermTypeUnary, Unary: &Unary{OpAdd, $2.(*Term)}} 359 | } 360 | | '-' term 361 | { 362 | $$ = &Term{Type: TermTypeUnary, Unary: &Unary{OpSub, $2.(*Term)}} 363 | } 364 | | tokFormat 365 | { 366 | $$ = &Term{Type: TermTypeFormat, Format: $1} 367 | } 368 | | tokFormat string 369 | { 370 | $$ = &Term{Type: TermTypeFormat, Format: $1, Str: $2.(*String)} 371 | } 372 | | string 373 | { 374 | $$ = &Term{Type: TermTypeString, Str: $1.(*String)} 375 | } 376 | | tokIf query tokThen query ifelifs ifelse tokEnd 377 | { 378 | $$ = &Term{Type: TermTypeIf, If: &If{$2.(*Query), $4.(*Query), $5.([]*IfElif), $6.(*Query)}} 379 | } 380 | | tokTry expr trycatch 381 | { 382 | $$ = &Term{Type: TermTypeTry, Try: &Try{$2.(*Query), $3.(*Query)}} 383 | } 384 | | tokReduce expr tokAs pattern '(' query ';' query ')' 385 | { 386 | $$ = &Term{Type: TermTypeReduce, Reduce: &Reduce{$2.(*Query), $4.(*Pattern), $6.(*Query), $8.(*Query)}} 387 | } 388 | | tokForeach expr tokAs pattern '(' query ';' query ')' 389 | { 390 | $$ = &Term{Type: TermTypeForeach, Foreach: &Foreach{$2.(*Query), $4.(*Pattern), $6.(*Query), $8.(*Query), nil}} 391 | } 392 | | tokForeach expr tokAs pattern '(' query ';' query ';' query ')' 393 | { 394 | $$ = &Term{Type: TermTypeForeach, Foreach: &Foreach{$2.(*Query), $4.(*Pattern), $6.(*Query), $8.(*Query), $10.(*Query)}} 395 | } 396 | | tokBreak tokVariable 397 | { 398 | $$ = &Term{Type: TermTypeBreak, Break: $2} 399 | } 400 | | '(' query ')' 401 | { 402 | $$ = &Term{Type: TermTypeQuery, Query: $2.(*Query)} 403 | } 404 | | term tokIndex 405 | { 406 | $1.(*Term).SuffixList = append($1.(*Term).SuffixList, &Suffix{Index: &Index{Name: $2}}) 407 | } 408 | | term suffix 409 | { 410 | $1.(*Term).SuffixList = append($1.(*Term).SuffixList, $2.(*Suffix)) 411 | } 412 | | term '?' 413 | { 414 | $1.(*Term).SuffixList = append($1.(*Term).SuffixList, &Suffix{Optional: true}) 415 | } 416 | | term '.' suffix 417 | { 418 | $1.(*Term).SuffixList = append($1.(*Term).SuffixList, $3.(*Suffix)) 419 | } 420 | | term '.' string 421 | { 422 | $1.(*Term).SuffixList = append($1.(*Term).SuffixList, &Suffix{Index: &Index{Str: $3.(*String)}}) 423 | } 424 | 425 | string 426 | : tokString 427 | { 428 | $$ = &String{Str: $1} 429 | } 430 | | tokStringStart stringparts tokStringEnd 431 | { 432 | $$ = &String{Queries: $2.([]*Query)} 433 | } 434 | 435 | stringparts 436 | : 437 | { 438 | $$ = []*Query{} 439 | } 440 | | stringparts tokString 441 | { 442 | $$ = append($1.([]*Query), &Query{Term: &Term{Type: TermTypeString, Str: &String{Str: $2}}}) 443 | } 444 | | stringparts tokStringQuery query ')' 445 | { 446 | yylex.(*lexer).inString = true 447 | $$ = append($1.([]*Query), &Query{Term: &Term{Type: TermTypeQuery, Query: $3.(*Query)}}) 448 | } 449 | 450 | tokIdentModuleIdent 451 | : tokIdent 452 | | tokModuleIdent 453 | 454 | tokVariableModuleVariable 455 | : tokVariable 456 | | tokModuleVariable 457 | 458 | suffix 459 | : '[' ']' 460 | { 461 | $$ = &Suffix{Iter: true} 462 | } 463 | | '[' query ']' 464 | { 465 | $$ = &Suffix{Index: &Index{Start: $2.(*Query)}} 466 | } 467 | | '[' query ':' ']' 468 | { 469 | $$ = &Suffix{Index: &Index{Start: $2.(*Query), IsSlice: true}} 470 | } 471 | | '[' ':' query ']' 472 | { 473 | $$ = &Suffix{Index: &Index{End: $3.(*Query), IsSlice: true}} 474 | } 475 | | '[' query ':' query ']' 476 | { 477 | $$ = &Suffix{Index: &Index{Start: $2.(*Query), End: $4.(*Query), IsSlice: true}} 478 | } 479 | 480 | args 481 | : query 482 | { 483 | $$ = []*Query{$1.(*Query)} 484 | } 485 | | args ';' query 486 | { 487 | $$ = append($1.([]*Query), $3.(*Query)) 488 | } 489 | 490 | ifelifs 491 | : 492 | { 493 | $$ = []*IfElif(nil) 494 | } 495 | | ifelifs tokElif query tokThen query 496 | { 497 | $$ = append($1.([]*IfElif), &IfElif{$3.(*Query), $5.(*Query)}) 498 | } 499 | 500 | ifelse 501 | : 502 | { 503 | $$ = (*Query)(nil) 504 | } 505 | | tokElse query 506 | { 507 | $$ = $2 508 | } 509 | 510 | trycatch 511 | : %prec tokEmptyCatch 512 | { 513 | $$ = (*Query)(nil) 514 | } 515 | | tokCatch expr 516 | { 517 | $$ = $2 518 | } 519 | 520 | objectkeyvals 521 | : objectkeyval 522 | { 523 | $$ = []*ObjectKeyVal{$1.(*ObjectKeyVal)} 524 | } 525 | | objectkeyvals ',' objectkeyval 526 | { 527 | $$ = append($1.([]*ObjectKeyVal), $3.(*ObjectKeyVal)) 528 | } 529 | 530 | objectkeyval 531 | : objectkey ':' objectval 532 | { 533 | $$ = &ObjectKeyVal{Key: $1, Val: $3.(*Query)} 534 | } 535 | | string ':' objectval 536 | { 537 | $$ = &ObjectKeyVal{KeyString: $1.(*String), Val: $3.(*Query)} 538 | } 539 | | '(' query ')' ':' objectval 540 | { 541 | $$ = &ObjectKeyVal{KeyQuery: $2.(*Query), Val: $5.(*Query)} 542 | } 543 | | objectkey 544 | { 545 | $$ = &ObjectKeyVal{Key: $1} 546 | } 547 | | string 548 | { 549 | $$ = &ObjectKeyVal{KeyString: $1.(*String)} 550 | } 551 | 552 | objectkey 553 | : tokIdent 554 | | tokVariable 555 | | tokKeyword 556 | 557 | objectval 558 | : objectval '|' objectval 559 | { 560 | $$ = &Query{Left: $1.(*Query), Op: OpPipe, Right: $3.(*Query)} 561 | } 562 | | expr 563 | 564 | constterm 565 | : constobject 566 | { 567 | $$ = &ConstTerm{Object: $1.(*ConstObject)} 568 | } 569 | | constarray 570 | { 571 | $$ = &ConstTerm{Array: $1.(*ConstArray)} 572 | } 573 | | tokNumber 574 | { 575 | $$ = &ConstTerm{Number: $1} 576 | } 577 | | tokString 578 | { 579 | $$ = &ConstTerm{Str: $1} 580 | } 581 | | tokNull 582 | { 583 | $$ = &ConstTerm{Null: true} 584 | } 585 | | tokTrue 586 | { 587 | $$ = &ConstTerm{True: true} 588 | } 589 | | tokFalse 590 | { 591 | $$ = &ConstTerm{False: true} 592 | } 593 | 594 | constobject 595 | : '{' '}' 596 | { 597 | $$ = &ConstObject{} 598 | } 599 | | '{' constobjectkeyvals '}' 600 | { 601 | $$ = &ConstObject{$2.([]*ConstObjectKeyVal)} 602 | } 603 | | '{' constobjectkeyvals ',' '}' 604 | { 605 | $$ = &ConstObject{$2.([]*ConstObjectKeyVal)} 606 | } 607 | 608 | constobjectkeyvals 609 | : constobjectkeyval 610 | { 611 | $$ = []*ConstObjectKeyVal{$1.(*ConstObjectKeyVal)} 612 | } 613 | | constobjectkeyvals ',' constobjectkeyval 614 | { 615 | $$ = append($1.([]*ConstObjectKeyVal), $3.(*ConstObjectKeyVal)) 616 | } 617 | 618 | constobjectkeyval 619 | : tokIdent ':' constterm 620 | { 621 | $$ = &ConstObjectKeyVal{Key: $1, Val: $3.(*ConstTerm)} 622 | } 623 | | tokKeyword ':' constterm 624 | { 625 | $$ = &ConstObjectKeyVal{Key: $1, Val: $3.(*ConstTerm)} 626 | } 627 | | tokString ':' constterm 628 | { 629 | $$ = &ConstObjectKeyVal{KeyString: $1, Val: $3.(*ConstTerm)} 630 | } 631 | 632 | constarray 633 | : '[' ']' 634 | { 635 | $$ = &ConstArray{} 636 | } 637 | | '[' constarrayelems ']' 638 | { 639 | $$ = &ConstArray{$2.([]*ConstTerm)} 640 | } 641 | 642 | constarrayelems 643 | : constterm 644 | { 645 | $$ = []*ConstTerm{$1.(*ConstTerm)} 646 | } 647 | | constarrayelems ',' constterm 648 | { 649 | $$ = append($1.([]*ConstTerm), $3.(*ConstTerm)) 650 | } 651 | 652 | tokKeyword 653 | : tokOrOp 654 | | tokAndOp 655 | | tokModule 656 | | tokImport 657 | | tokInclude 658 | | tokDef 659 | | tokAs 660 | | tokLabel 661 | | tokBreak 662 | | tokNull 663 | | tokTrue 664 | | tokFalse 665 | | tokIf 666 | | tokThen 667 | | tokElif 668 | | tokElse 669 | | tokEnd 670 | | tokTry 671 | | tokCatch 672 | | tokReduce 673 | | tokForeach 674 | 675 | %% 676 | -------------------------------------------------------------------------------- /gojqparser/query.go: -------------------------------------------------------------------------------- 1 | package gojqparser 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Parse a query string, and returns the query struct. 8 | // 9 | // If parsing failed, it returns an error of type [*ParseError], which has 10 | // the byte offset and the invalid token. The byte offset is the scanned bytes 11 | // when the error occurred. The token is empty if the error occurred after 12 | // scanning the entire query string. 13 | func Parse(src string) (*Query, error) { 14 | l := newLexer(src) 15 | if yyParse(l) > 0 { 16 | return nil, l.err 17 | } 18 | return l.result, nil 19 | } 20 | 21 | // Query represents the abstract syntax tree of a jq query. 22 | type Query struct { 23 | Meta *ConstObject `json:"meta,omitempty"` 24 | Imports []*Import `json:"imports,omitempty"` 25 | FuncDefs []*FuncDef `json:"func_defs,omitempty"` 26 | Term *Term `json:"term,omitempty"` 27 | Left *Query `json:"left,omitempty"` 28 | Right *Query `json:"right,omitempty"` 29 | Patterns []*Pattern `json:"patterns,omitempty"` 30 | Op Operator `json:"op,omitempty"` 31 | } 32 | 33 | func (e *Query) String() string { 34 | var s strings.Builder 35 | e.writeTo(&s) 36 | return s.String() 37 | } 38 | 39 | func (e *Query) writeTo(s *strings.Builder) { 40 | if e.Meta != nil { 41 | s.WriteString("module ") 42 | e.Meta.writeTo(s) 43 | s.WriteString(";\n") 44 | } 45 | for _, im := range e.Imports { 46 | im.writeTo(s) 47 | } 48 | for _, fd := range e.FuncDefs { 49 | fd.writeTo(s) 50 | s.WriteByte(' ') 51 | } 52 | if e.Term != nil { 53 | e.Term.writeTo(s) 54 | } else if e.Right != nil { 55 | e.Left.writeTo(s) 56 | if e.Op != OpComma { 57 | s.WriteByte(' ') 58 | } 59 | for i, p := range e.Patterns { 60 | if i == 0 { 61 | s.WriteString("as ") 62 | } else { 63 | s.WriteString("?// ") 64 | } 65 | p.writeTo(s) 66 | s.WriteByte(' ') 67 | } 68 | s.WriteString(e.Op.String()) 69 | s.WriteByte(' ') 70 | e.Right.writeTo(s) 71 | } 72 | } 73 | 74 | // Import ... 75 | type Import struct { 76 | ImportPath *Token `json:"import_path,omitempty"` 77 | ImportAlias *Token `json:"import_alias,omitempty"` 78 | IncludePath *Token `json:"include_path,omitempty"` 79 | Meta *ConstObject `json:"meta,omitempty"` 80 | } 81 | 82 | func (e *Import) String() string { 83 | var s strings.Builder 84 | e.writeTo(&s) 85 | return s.String() 86 | } 87 | 88 | func (e *Import) writeTo(s *strings.Builder) { 89 | if e.ImportPath.Str != "" { 90 | s.WriteString("import ") 91 | jsonEncodeString(s, e.ImportPath.Str) 92 | s.WriteString(" as ") 93 | s.WriteString(e.ImportAlias.Str) 94 | } else { 95 | s.WriteString("include ") 96 | jsonEncodeString(s, e.IncludePath.Str) 97 | } 98 | if e.Meta != nil { 99 | s.WriteByte(' ') 100 | e.Meta.writeTo(s) 101 | } 102 | s.WriteString(";\n") 103 | } 104 | 105 | // FuncDef ... 106 | type FuncDef struct { 107 | Name *Token `json:"name,omitempty"` 108 | Args []*Token `json:"args,omitempty"` 109 | Body *Query `json:"body,omitempty"` 110 | } 111 | 112 | func (e *FuncDef) String() string { 113 | var s strings.Builder 114 | e.writeTo(&s) 115 | return s.String() 116 | } 117 | 118 | func (e *FuncDef) writeTo(s *strings.Builder) { 119 | s.WriteString("def ") 120 | s.WriteString(e.Name.Str) 121 | if len(e.Args) > 0 { 122 | s.WriteByte('(') 123 | for i, e := range e.Args { 124 | if i > 0 { 125 | s.WriteString("; ") 126 | } 127 | s.WriteString(e.Str) 128 | } 129 | s.WriteByte(')') 130 | } 131 | s.WriteString(": ") 132 | e.Body.writeTo(s) 133 | s.WriteByte(';') 134 | } 135 | 136 | // Term ... 137 | type Term struct { 138 | Type TermType `json:"type,omitempty"` 139 | Index *Index `json:"index,omitempty"` 140 | Func *Func `json:"func,omitempty"` 141 | Object *Object `json:"object,omitempty"` 142 | Array *Array `json:"array,omitempty"` 143 | Number *Token `json:"number,omitempty"` 144 | Unary *Unary `json:"unary,omitempty"` 145 | Format *Token `json:"format,omitempty"` 146 | Str *String `json:"str,omitempty"` 147 | If *If `json:"if,omitempty"` 148 | Try *Try `json:"try,omitempty"` 149 | Reduce *Reduce `json:"reduce,omitempty"` 150 | Foreach *Foreach `json:"foreach,omitempty"` 151 | Label *Label `json:"label,omitempty"` 152 | Break *Token `json:"break,omitempty"` 153 | Query *Query `json:"query,omitempty"` 154 | SuffixList []*Suffix `json:"suffix_list,omitempty"` 155 | } 156 | 157 | func (e *Term) String() string { 158 | var s strings.Builder 159 | e.writeTo(&s) 160 | return s.String() 161 | } 162 | 163 | func (e *Term) writeTo(s *strings.Builder) { 164 | switch e.Type { 165 | case TermTypeIdentity: 166 | s.WriteByte('.') 167 | case TermTypeRecurse: 168 | s.WriteString("..") 169 | case TermTypeNull: 170 | s.WriteString("null") 171 | case TermTypeTrue: 172 | s.WriteString("true") 173 | case TermTypeFalse: 174 | s.WriteString("false") 175 | case TermTypeIndex: 176 | e.Index.writeTo(s) 177 | case TermTypeFunc: 178 | e.Func.writeTo(s) 179 | case TermTypeObject: 180 | e.Object.writeTo(s) 181 | case TermTypeArray: 182 | e.Array.writeTo(s) 183 | case TermTypeNumber: 184 | s.WriteString(e.Number.Str) 185 | case TermTypeUnary: 186 | e.Unary.writeTo(s) 187 | case TermTypeFormat: 188 | s.WriteString(e.Format.Str) 189 | if e.Str != nil { 190 | s.WriteByte(' ') 191 | e.Str.writeTo(s) 192 | } 193 | case TermTypeString: 194 | e.Str.writeTo(s) 195 | case TermTypeIf: 196 | e.If.writeTo(s) 197 | case TermTypeTry: 198 | e.Try.writeTo(s) 199 | case TermTypeReduce: 200 | e.Reduce.writeTo(s) 201 | case TermTypeForeach: 202 | e.Foreach.writeTo(s) 203 | case TermTypeLabel: 204 | e.Label.writeTo(s) 205 | case TermTypeBreak: 206 | s.WriteString("break ") 207 | s.WriteString(e.Break.Str) 208 | case TermTypeQuery: 209 | s.WriteByte('(') 210 | e.Query.writeTo(s) 211 | s.WriteByte(')') 212 | } 213 | for _, e := range e.SuffixList { 214 | e.writeTo(s) 215 | } 216 | } 217 | 218 | // Unary ... 219 | type Unary struct { 220 | Op Operator `json:"op,omitempty"` 221 | Term *Term `json:"term,omitempty"` 222 | } 223 | 224 | func (e *Unary) String() string { 225 | var s strings.Builder 226 | e.writeTo(&s) 227 | return s.String() 228 | } 229 | 230 | func (e *Unary) writeTo(s *strings.Builder) { 231 | s.WriteString(e.Op.String()) 232 | e.Term.writeTo(s) 233 | } 234 | 235 | // Pattern ... 236 | type Pattern struct { 237 | Name *Token `json:"name,omitempty"` 238 | Array []*Pattern `json:"array,omitempty"` 239 | Object []*PatternObject `json:"object,omitempty"` 240 | } 241 | 242 | func (e *Pattern) String() string { 243 | var s strings.Builder 244 | e.writeTo(&s) 245 | return s.String() 246 | } 247 | 248 | func (e *Pattern) writeTo(s *strings.Builder) { 249 | if e.Name.Str != "" { 250 | s.WriteString(e.Name.Str) 251 | } else if len(e.Array) > 0 { 252 | s.WriteByte('[') 253 | for i, e := range e.Array { 254 | if i > 0 { 255 | s.WriteString(", ") 256 | } 257 | e.writeTo(s) 258 | } 259 | s.WriteByte(']') 260 | } else if len(e.Object) > 0 { 261 | s.WriteByte('{') 262 | for i, e := range e.Object { 263 | if i > 0 { 264 | s.WriteString(", ") 265 | } 266 | e.writeTo(s) 267 | } 268 | s.WriteByte('}') 269 | } 270 | } 271 | 272 | // PatternObject ... 273 | type PatternObject struct { 274 | Key *Token `json:"key,omitempty"` 275 | KeyString *String `json:"key_string,omitempty"` 276 | KeyQuery *Query `json:"key_query,omitempty"` 277 | Val *Pattern `json:"val,omitempty"` 278 | } 279 | 280 | func (e *PatternObject) String() string { 281 | var s strings.Builder 282 | e.writeTo(&s) 283 | return s.String() 284 | } 285 | 286 | func (e *PatternObject) writeTo(s *strings.Builder) { 287 | if e.Key.Str != "" { 288 | s.WriteString(e.Key.Str) 289 | } else if e.KeyString != nil { 290 | e.KeyString.writeTo(s) 291 | } else if e.KeyQuery != nil { 292 | s.WriteByte('(') 293 | e.KeyQuery.writeTo(s) 294 | s.WriteByte(')') 295 | } 296 | if e.Val != nil { 297 | s.WriteString(": ") 298 | e.Val.writeTo(s) 299 | } 300 | } 301 | 302 | // Index ... 303 | type Index struct { 304 | Name *Token `json:"name,omitempty"` 305 | Str *String `json:"str,omitempty"` 306 | Start *Query `json:"start,omitempty"` 307 | End *Query `json:"end,omitempty"` 308 | IsSlice bool `json:"is_slice,omitempty"` 309 | } 310 | 311 | func (e *Index) String() string { 312 | var s strings.Builder 313 | e.writeTo(&s) 314 | return s.String() 315 | } 316 | 317 | func (e *Index) writeTo(s *strings.Builder) { 318 | if l := s.Len(); l > 0 { 319 | // ". .x" != "..x" and "0 .x" != "0.x" 320 | if c := s.String()[l-1]; c == '.' || '0' <= c && c <= '9' { 321 | s.WriteByte(' ') 322 | } 323 | } 324 | s.WriteByte('.') 325 | e.writeSuffixTo(s) 326 | } 327 | 328 | func (e *Index) writeSuffixTo(s *strings.Builder) { 329 | if e.Name.Str != "" { 330 | s.WriteString(e.Name.Str) 331 | } else if e.Str != nil { 332 | e.Str.writeTo(s) 333 | } else { 334 | s.WriteByte('[') 335 | if e.IsSlice { 336 | if e.Start != nil { 337 | e.Start.writeTo(s) 338 | } 339 | s.WriteByte(':') 340 | if e.End != nil { 341 | e.End.writeTo(s) 342 | } 343 | } else { 344 | e.Start.writeTo(s) 345 | } 346 | s.WriteByte(']') 347 | } 348 | } 349 | 350 | // Func ... 351 | type Func struct { 352 | Name *Token `json:"name,omitempty"` 353 | Args []*Query `json:"args,omitempty"` 354 | } 355 | 356 | func (e *Func) String() string { 357 | var s strings.Builder 358 | e.writeTo(&s) 359 | return s.String() 360 | } 361 | 362 | func (e *Func) writeTo(s *strings.Builder) { 363 | s.WriteString(e.Name.Str) 364 | if len(e.Args) > 0 { 365 | s.WriteByte('(') 366 | for i, e := range e.Args { 367 | if i > 0 { 368 | s.WriteString("; ") 369 | } 370 | e.writeTo(s) 371 | } 372 | s.WriteByte(')') 373 | } 374 | } 375 | 376 | // String ... 377 | type String struct { 378 | Str *Token `json:"str,omitempty"` 379 | Queries []*Query `json:"queries,omitempty"` 380 | } 381 | 382 | func (e *String) String() string { 383 | var s strings.Builder 384 | e.writeTo(&s) 385 | return s.String() 386 | } 387 | 388 | func (e *String) writeTo(s *strings.Builder) { 389 | if e.Queries == nil { 390 | jsonEncodeString(s, e.Str.Str) 391 | return 392 | } 393 | s.WriteByte('"') 394 | for _, e := range e.Queries { 395 | if e.Term.Str == nil { 396 | s.WriteString(`\`) 397 | e.writeTo(s) 398 | } else { 399 | es := e.String() 400 | s.WriteString(es[1 : len(es)-1]) 401 | } 402 | } 403 | s.WriteByte('"') 404 | } 405 | 406 | // Object ... 407 | type Object struct { 408 | KeyVals []*ObjectKeyVal `json:"key_vals,omitempty"` 409 | } 410 | 411 | func (e *Object) String() string { 412 | var s strings.Builder 413 | e.writeTo(&s) 414 | return s.String() 415 | } 416 | 417 | func (e *Object) writeTo(s *strings.Builder) { 418 | if len(e.KeyVals) == 0 { 419 | s.WriteString("{}") 420 | return 421 | } 422 | s.WriteString("{ ") 423 | for i, kv := range e.KeyVals { 424 | if i > 0 { 425 | s.WriteString(", ") 426 | } 427 | kv.writeTo(s) 428 | } 429 | s.WriteString(" }") 430 | } 431 | 432 | // ObjectKeyVal ... 433 | type ObjectKeyVal struct { 434 | Key *Token `json:"key,omitempty"` 435 | KeyString *String `json:"key_string,omitempty"` 436 | KeyQuery *Query `json:"key_query,omitempty"` 437 | Val *Query `json:"val,omitempty"` 438 | } 439 | 440 | func (e *ObjectKeyVal) String() string { 441 | var s strings.Builder 442 | e.writeTo(&s) 443 | return s.String() 444 | } 445 | 446 | func (e *ObjectKeyVal) writeTo(s *strings.Builder) { 447 | if e.Key.Str != "" { 448 | s.WriteString(e.Key.Str) 449 | } else if e.KeyString != nil { 450 | e.KeyString.writeTo(s) 451 | } else if e.KeyQuery != nil { 452 | s.WriteByte('(') 453 | e.KeyQuery.writeTo(s) 454 | s.WriteByte(')') 455 | } 456 | if e.Val != nil { 457 | s.WriteString(": ") 458 | e.Val.writeTo(s) 459 | } 460 | } 461 | 462 | // Array ... 463 | type Array struct { 464 | Query *Query `json:"query,omitempty"` 465 | } 466 | 467 | func (e *Array) String() string { 468 | var s strings.Builder 469 | e.writeTo(&s) 470 | return s.String() 471 | } 472 | 473 | func (e *Array) writeTo(s *strings.Builder) { 474 | s.WriteByte('[') 475 | if e.Query != nil { 476 | e.Query.writeTo(s) 477 | } 478 | s.WriteByte(']') 479 | } 480 | 481 | // Suffix ... 482 | type Suffix struct { 483 | Index *Index `json:"index,omitempty"` 484 | Iter bool `json:"iter,omitempty"` 485 | Optional bool `json:"optional,omitempty"` 486 | } 487 | 488 | func (e *Suffix) String() string { 489 | var s strings.Builder 490 | e.writeTo(&s) 491 | return s.String() 492 | } 493 | 494 | func (e *Suffix) writeTo(s *strings.Builder) { 495 | if e.Index != nil { 496 | if e.Index.Name.Str != "" || e.Index.Str != nil { 497 | e.Index.writeTo(s) 498 | } else { 499 | e.Index.writeSuffixTo(s) 500 | } 501 | } else if e.Iter { 502 | s.WriteString("[]") 503 | } else if e.Optional { 504 | s.WriteByte('?') 505 | } 506 | } 507 | 508 | // If ... 509 | type If struct { 510 | Cond *Query `json:"cond,omitempty"` 511 | Then *Query `json:"then,omitempty"` 512 | Elif []*IfElif `json:"elif,omitempty"` 513 | Else *Query `json:"else,omitempty"` 514 | } 515 | 516 | func (e *If) String() string { 517 | var s strings.Builder 518 | e.writeTo(&s) 519 | return s.String() 520 | } 521 | 522 | func (e *If) writeTo(s *strings.Builder) { 523 | s.WriteString("if ") 524 | e.Cond.writeTo(s) 525 | s.WriteString(" then ") 526 | e.Then.writeTo(s) 527 | for _, e := range e.Elif { 528 | s.WriteByte(' ') 529 | e.writeTo(s) 530 | } 531 | if e.Else != nil { 532 | s.WriteString(" else ") 533 | e.Else.writeTo(s) 534 | } 535 | s.WriteString(" end") 536 | } 537 | 538 | // IfElif ... 539 | type IfElif struct { 540 | Cond *Query `json:"cond,omitempty"` 541 | Then *Query `json:"then,omitempty"` 542 | } 543 | 544 | func (e *IfElif) String() string { 545 | var s strings.Builder 546 | e.writeTo(&s) 547 | return s.String() 548 | } 549 | 550 | func (e *IfElif) writeTo(s *strings.Builder) { 551 | s.WriteString("elif ") 552 | e.Cond.writeTo(s) 553 | s.WriteString(" then ") 554 | e.Then.writeTo(s) 555 | } 556 | 557 | // Try ... 558 | type Try struct { 559 | Body *Query `json:"body,omitempty"` 560 | Catch *Query `json:"catch,omitempty"` 561 | } 562 | 563 | func (e *Try) String() string { 564 | var s strings.Builder 565 | e.writeTo(&s) 566 | return s.String() 567 | } 568 | 569 | func (e *Try) writeTo(s *strings.Builder) { 570 | s.WriteString("try ") 571 | e.Body.writeTo(s) 572 | if e.Catch != nil { 573 | s.WriteString(" catch ") 574 | e.Catch.writeTo(s) 575 | } 576 | } 577 | 578 | // Reduce ... 579 | type Reduce struct { 580 | Query *Query `json:"query,omitempty"` 581 | Pattern *Pattern `json:"pattern,omitempty"` 582 | Start *Query `json:"start,omitempty"` 583 | Update *Query `json:"update,omitempty"` 584 | } 585 | 586 | func (e *Reduce) String() string { 587 | var s strings.Builder 588 | e.writeTo(&s) 589 | return s.String() 590 | } 591 | 592 | func (e *Reduce) writeTo(s *strings.Builder) { 593 | s.WriteString("reduce ") 594 | e.Query.writeTo(s) 595 | s.WriteString(" as ") 596 | e.Pattern.writeTo(s) 597 | s.WriteString(" (") 598 | e.Start.writeTo(s) 599 | s.WriteString("; ") 600 | e.Update.writeTo(s) 601 | s.WriteByte(')') 602 | } 603 | 604 | // Foreach ... 605 | type Foreach struct { 606 | Query *Query `json:"query,omitempty"` 607 | Pattern *Pattern `json:"pattern,omitempty"` 608 | Start *Query `json:"start,omitempty"` 609 | Update *Query `json:"update,omitempty"` 610 | Extract *Query `json:"extract,omitempty"` 611 | } 612 | 613 | func (e *Foreach) String() string { 614 | var s strings.Builder 615 | e.writeTo(&s) 616 | return s.String() 617 | } 618 | 619 | func (e *Foreach) writeTo(s *strings.Builder) { 620 | s.WriteString("foreach ") 621 | e.Query.writeTo(s) 622 | s.WriteString(" as ") 623 | e.Pattern.writeTo(s) 624 | s.WriteString(" (") 625 | e.Start.writeTo(s) 626 | s.WriteString("; ") 627 | e.Update.writeTo(s) 628 | if e.Extract != nil { 629 | s.WriteString("; ") 630 | e.Extract.writeTo(s) 631 | } 632 | s.WriteByte(')') 633 | } 634 | 635 | // Label ... 636 | type Label struct { 637 | Ident *Token `json:"ident,omitempty"` 638 | Body *Query `json:"body,omitempty"` 639 | } 640 | 641 | func (e *Label) String() string { 642 | var s strings.Builder 643 | e.writeTo(&s) 644 | return s.String() 645 | } 646 | 647 | func (e *Label) writeTo(s *strings.Builder) { 648 | s.WriteString("label ") 649 | s.WriteString(e.Ident.Str) 650 | s.WriteString(" | ") 651 | e.Body.writeTo(s) 652 | } 653 | 654 | // ConstTerm ... 655 | type ConstTerm struct { 656 | Object *ConstObject `json:"object,omitempty"` 657 | Array *ConstArray `json:"array,omitempty"` 658 | Number *Token `json:"number,omitempty"` 659 | Str *Token `json:"str,omitempty"` 660 | Null bool `json:"null,omitempty"` 661 | True bool `json:"true,omitempty"` 662 | False bool `json:"false,omitempty"` 663 | } 664 | 665 | func (e *ConstTerm) String() string { 666 | var s strings.Builder 667 | e.writeTo(&s) 668 | return s.String() 669 | } 670 | 671 | func (e *ConstTerm) writeTo(s *strings.Builder) { 672 | if e.Object != nil { 673 | e.Object.writeTo(s) 674 | } else if e.Array != nil { 675 | e.Array.writeTo(s) 676 | } else if e.Number.Str != "" { 677 | s.WriteString(e.Number.Str) 678 | } else if e.Null { 679 | s.WriteString("null") 680 | } else if e.True { 681 | s.WriteString("true") 682 | } else if e.False { 683 | s.WriteString("false") 684 | } else { 685 | jsonEncodeString(s, e.Str.Str) 686 | } 687 | } 688 | 689 | // ConstObject ... 690 | type ConstObject struct { 691 | KeyVals []*ConstObjectKeyVal `json:"keyvals,omitempty"` 692 | } 693 | 694 | func (e *ConstObject) String() string { 695 | var s strings.Builder 696 | e.writeTo(&s) 697 | return s.String() 698 | } 699 | 700 | func (e *ConstObject) writeTo(s *strings.Builder) { 701 | if len(e.KeyVals) == 0 { 702 | s.WriteString("{}") 703 | return 704 | } 705 | s.WriteString("{ ") 706 | for i, kv := range e.KeyVals { 707 | if i > 0 { 708 | s.WriteString(", ") 709 | } 710 | kv.writeTo(s) 711 | } 712 | s.WriteString(" }") 713 | } 714 | 715 | // ConstObjectKeyVal ... 716 | type ConstObjectKeyVal struct { 717 | Key *Token `json:"key,omitempty"` 718 | KeyString *Token `json:"key_string,omitempty"` 719 | Val *ConstTerm `json:"val,omitempty"` 720 | } 721 | 722 | func (e *ConstObjectKeyVal) String() string { 723 | var s strings.Builder 724 | e.writeTo(&s) 725 | return s.String() 726 | } 727 | 728 | func (e *ConstObjectKeyVal) writeTo(s *strings.Builder) { 729 | if e.Key.Str != "" { 730 | s.WriteString(e.Key.Str) 731 | } else { 732 | jsonEncodeString(s, e.KeyString.Str) 733 | } 734 | s.WriteString(": ") 735 | e.Val.writeTo(s) 736 | } 737 | 738 | // ConstArray ... 739 | type ConstArray struct { 740 | Elems []*ConstTerm `json:"elems,omitempty"` 741 | } 742 | 743 | func (e *ConstArray) String() string { 744 | var s strings.Builder 745 | e.writeTo(&s) 746 | return s.String() 747 | } 748 | 749 | func (e *ConstArray) writeTo(s *strings.Builder) { 750 | s.WriteByte('[') 751 | for i, e := range e.Elems { 752 | if i > 0 { 753 | s.WriteString(", ") 754 | } 755 | e.writeTo(s) 756 | } 757 | s.WriteByte(']') 758 | } 759 | -------------------------------------------------------------------------------- /lsp/testdata/state.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "id": 0, 5 | "jsonrpc": "2.0", 6 | "method": "initialize" 7 | }, 8 | "response": { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "result": { 12 | "capabilities": { 13 | "completionProvider": {}, 14 | "definitionProvider": true, 15 | "documentSymbolProvider": true, 16 | "hoverProvider": true, 17 | "publishDiagnostics": {}, 18 | "textDocumentSync": 1, 19 | "workspace": { 20 | "workspaceFolders": { 21 | "supported": true 22 | } 23 | } 24 | }, 25 | "serverInfo": { 26 | "name": "jq-lsp", 27 | "version": "test-version" 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "request": { 34 | "id": 1, 35 | "jsonrpc": "2.0", 36 | "method": "textDocument/didOpen", 37 | "params": { 38 | "textDocument": { 39 | "languageId": "plaintext", 40 | "text": "@valid.jq", 41 | "uri": "file:///valid.jq", 42 | "version": 1 43 | } 44 | } 45 | }, 46 | "response": { 47 | "jsonrpc": "2.0", 48 | "method": "textDocument/publishDiagnostics", 49 | "params": { 50 | "diagnostics": [], 51 | "uri": "file:///valid.jq" 52 | } 53 | } 54 | }, 55 | { 56 | "request": { 57 | "id": 1, 58 | "jsonrpc": "2.0", 59 | "method": "_internal/dump", 60 | "params": { 61 | "textDocument": { 62 | "languageId": "plaintext", 63 | "text": "@valid.jq", 64 | "uri": "file:///valid.jq", 65 | "version": 1 66 | } 67 | } 68 | }, 69 | "response": { 70 | "id": 1, 71 | "jsonrpc": "2.0", 72 | "result": { 73 | "args": [ 74 | "" 75 | ], 76 | "config": { 77 | "name": "jq-lsp", 78 | "version": "test-version" 79 | }, 80 | "env": [ 81 | "" 82 | ], 83 | "files": { 84 | "file:///valid.jq": { 85 | "query": { 86 | "func_defs": [ 87 | { 88 | "body": { 89 | "term": { 90 | "number": { 91 | "start": 8, 92 | "stop": 11, 93 | "str": "123" 94 | }, 95 | "type": "TermTypeNumber" 96 | } 97 | }, 98 | "name": { 99 | "start": 4, 100 | "stop": 6, 101 | "str": "fn" 102 | } 103 | }, 104 | { 105 | "body": { 106 | "func_defs": [ 107 | { 108 | "body": { 109 | "term": { 110 | "func": { 111 | "name": { 112 | "start": 36, 113 | "stop": 40, 114 | "str": "null" 115 | } 116 | }, 117 | "type": "TermTypeFunc" 118 | } 119 | }, 120 | "name": { 121 | "start": 30, 122 | "stop": 34, 123 | "str": "_fn2" 124 | } 125 | } 126 | ], 127 | "term": { 128 | "number": { 129 | "start": 46, 130 | "stop": 49, 131 | "str": "123" 132 | }, 133 | "type": "TermTypeNumber" 134 | } 135 | }, 136 | "name": { 137 | "start": 17, 138 | "stop": 20, 139 | "str": "fn2" 140 | } 141 | } 142 | ] 143 | }, 144 | "text": "def fn: 123;\ndef fn2:\n def _fn2: null;\n 123;\n" 145 | } 146 | } 147 | } 148 | } 149 | }, 150 | { 151 | "request": { 152 | "id": 1, 153 | "jsonrpc": "2.0", 154 | "method": "textDocument/didOpen", 155 | "params": { 156 | "textDocument": { 157 | "languageId": "plaintext", 158 | "text": "@valid.jq", 159 | "uri": "file:///valid.jq", 160 | "version": 1 161 | } 162 | } 163 | }, 164 | "response": { 165 | "jsonrpc": "2.0", 166 | "method": "textDocument/publishDiagnostics", 167 | "params": { 168 | "diagnostics": [], 169 | "uri": "file:///valid.jq" 170 | } 171 | } 172 | }, 173 | { 174 | "request": { 175 | "id": 1, 176 | "jsonrpc": "2.0", 177 | "method": "_internal/dump", 178 | "params": { 179 | "textDocument": { 180 | "languageId": "plaintext", 181 | "text": "@valid.jq", 182 | "uri": "file:///valid.jq", 183 | "version": 1 184 | } 185 | } 186 | }, 187 | "response": { 188 | "id": 1, 189 | "jsonrpc": "2.0", 190 | "result": { 191 | "args": [ 192 | "" 193 | ], 194 | "config": { 195 | "name": "jq-lsp", 196 | "version": "test-version" 197 | }, 198 | "env": [ 199 | "" 200 | ], 201 | "files": { 202 | "file:///valid.jq": { 203 | "query": { 204 | "func_defs": [ 205 | { 206 | "body": { 207 | "term": { 208 | "number": { 209 | "start": 8, 210 | "stop": 11, 211 | "str": "123" 212 | }, 213 | "type": "TermTypeNumber" 214 | } 215 | }, 216 | "name": { 217 | "start": 4, 218 | "stop": 6, 219 | "str": "fn" 220 | } 221 | }, 222 | { 223 | "body": { 224 | "func_defs": [ 225 | { 226 | "body": { 227 | "term": { 228 | "func": { 229 | "name": { 230 | "start": 36, 231 | "stop": 40, 232 | "str": "null" 233 | } 234 | }, 235 | "type": "TermTypeFunc" 236 | } 237 | }, 238 | "name": { 239 | "start": 30, 240 | "stop": 34, 241 | "str": "_fn2" 242 | } 243 | } 244 | ], 245 | "term": { 246 | "number": { 247 | "start": 46, 248 | "stop": 49, 249 | "str": "123" 250 | }, 251 | "type": "TermTypeNumber" 252 | } 253 | }, 254 | "name": { 255 | "start": 17, 256 | "stop": 20, 257 | "str": "fn2" 258 | } 259 | } 260 | ] 261 | }, 262 | "text": "def fn: 123;\ndef fn2:\n def _fn2: null;\n 123;\n" 263 | } 264 | } 265 | } 266 | } 267 | }, 268 | { 269 | "request": { 270 | "id": 1, 271 | "jsonrpc": "2.0", 272 | "method": "textDocument/didOpen", 273 | "params": { 274 | "textDocument": { 275 | "languageId": "plaintext", 276 | "text": "@keywords.jq", 277 | "uri": "file:///keywords.jq", 278 | "version": 1 279 | } 280 | } 281 | }, 282 | "response": { 283 | "jsonrpc": "2.0", 284 | "method": "textDocument/publishDiagnostics", 285 | "params": { 286 | "diagnostics": [], 287 | "uri": "file:///keywords.jq" 288 | } 289 | } 290 | }, 291 | { 292 | "request": { 293 | "id": 1, 294 | "jsonrpc": "2.0", 295 | "method": "_internal/dump", 296 | "params": { 297 | "textDocument": { 298 | "languageId": "plaintext", 299 | "text": "@valid.jq", 300 | "uri": "file:///valid.jq", 301 | "version": 1 302 | } 303 | } 304 | }, 305 | "response": { 306 | "id": 1, 307 | "jsonrpc": "2.0", 308 | "result": { 309 | "args": [ 310 | "" 311 | ], 312 | "config": { 313 | "name": "jq-lsp", 314 | "version": "test-version" 315 | }, 316 | "env": [ 317 | "" 318 | ], 319 | "files": { 320 | "file:///keywords.jq": { 321 | "query": { 322 | "func_defs": [ 323 | { 324 | "body": { 325 | "term": { 326 | "number": { 327 | "start": 10, 328 | "stop": 13, 329 | "str": "123" 330 | }, 331 | "type": "TermTypeNumber" 332 | } 333 | }, 334 | "name": { 335 | "start": 4, 336 | "stop": 8, 337 | "str": "null" 338 | } 339 | }, 340 | { 341 | "body": { 342 | "term": { 343 | "number": { 344 | "start": 25, 345 | "stop": 28, 346 | "str": "123" 347 | }, 348 | "type": "TermTypeNumber" 349 | } 350 | }, 351 | "name": { 352 | "start": 19, 353 | "stop": 23, 354 | "str": "true" 355 | } 356 | }, 357 | { 358 | "body": { 359 | "term": { 360 | "number": { 361 | "start": 41, 362 | "stop": 44, 363 | "str": "123" 364 | }, 365 | "type": "TermTypeNumber" 366 | } 367 | }, 368 | "name": { 369 | "start": 34, 370 | "stop": 39, 371 | "str": "false" 372 | } 373 | } 374 | ] 375 | }, 376 | "text": "def null: 123;\ndef true: 123;\ndef false: 123;\n\n" 377 | }, 378 | "file:///valid.jq": { 379 | "query": { 380 | "func_defs": [ 381 | { 382 | "body": { 383 | "term": { 384 | "number": { 385 | "start": 8, 386 | "stop": 11, 387 | "str": "123" 388 | }, 389 | "type": "TermTypeNumber" 390 | } 391 | }, 392 | "name": { 393 | "start": 4, 394 | "stop": 6, 395 | "str": "fn" 396 | } 397 | }, 398 | { 399 | "body": { 400 | "func_defs": [ 401 | { 402 | "body": { 403 | "term": { 404 | "func": { 405 | "name": { 406 | "start": 36, 407 | "stop": 40, 408 | "str": "null" 409 | } 410 | }, 411 | "type": "TermTypeFunc" 412 | } 413 | }, 414 | "name": { 415 | "start": 30, 416 | "stop": 34, 417 | "str": "_fn2" 418 | } 419 | } 420 | ], 421 | "term": { 422 | "number": { 423 | "start": 46, 424 | "stop": 49, 425 | "str": "123" 426 | }, 427 | "type": "TermTypeNumber" 428 | } 429 | }, 430 | "name": { 431 | "start": 17, 432 | "stop": 20, 433 | "str": "fn2" 434 | } 435 | } 436 | ] 437 | }, 438 | "text": "def fn: 123;\ndef fn2:\n def _fn2: null;\n 123;\n" 439 | } 440 | } 441 | } 442 | } 443 | } 444 | ] 445 | --------------------------------------------------------------------------------