diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index 6ef0c76..0000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Go - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.19 - - - name: Test - run: go test -v ./... - - - name: Update coverage report - uses: ncruces/go-coverage-report@v0 - with: - report: true - chart: true - amend: true - reuse-go: true - continue-on-error: true - - - name: Build with xgo - uses: crazy-max/ghaction-xgo@v2 - with: - xgo_version: latest - go_version: ${{ matrix.go_version }} - dest: build - prefix: thriftls - targets: windows/386,windows/amd64,windows/arm64,linux/386,linux/amd64,linux/arm64,darwin/386,darwin/amd64,darwin/arm64 - v: true - x: false - race: false - ldflags: -s -w - buildmode: default - -# - uses: "marvinpinto/action-automatic-releases@latest" -# with: -# repo_token: "${{ secrets.GITHUB_TOKEN }}" -# automatic_release_tag: "latest" -# prerelease: true -# title: "Development Build" -# files: | -# build/thriftls* -# diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 6bcad3d..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: "tagged-release" - -on: - push: - tags: - - "v*" - -jobs: - tagged-release: - name: "Tagged Release" - runs-on: "ubuntu-latest" - - steps: - - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.19 - - - name: Test - run: go test -v ./... - - - name: Build with xgo - uses: crazy-max/ghaction-xgo@v2 - with: - xgo_version: latest - go_version: ${{ matrix.go_version }} - dest: build - prefix: thriftls - targets: windows/386,windows/amd64,windows/arm64,linux/386,linux/amd64,linux/arm64,darwin/386,darwin/amd64,darwin/arm64 - v: true - x: false - race: false - ldflags: -s -w - buildmode: default - - - uses: "marvinpinto/action-automatic-releases@latest" - with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" - prerelease: false - files: | - build/thriftls* - diff --git a/.gitignore b/.gitignore index f70f75c..b2be92b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -bin -coverage.out -.idea/ \ No newline at end of file +result diff --git a/LATEST_VERSION b/LATEST_VERSION deleted file mode 100644 index 994233d..0000000 --- a/LATEST_VERSION +++ /dev/null @@ -1 +0,0 @@ -v0.2.10 diff --git a/Makefile b/Makefile deleted file mode 100644 index 455222b..0000000 --- a/Makefile +++ /dev/null @@ -1,29 +0,0 @@ -# 获取系统架构 -UNAME_M := $(shell uname -m) - -# 根据系统架构设置 ARCH 变量 -ifeq ($(UNAME_M),x86_64) - ARCH := amd64 -else ifeq ($(UNAME_M),amd64) - ARCH := amd64 -else ifeq ($(UNAME_M),aarch64) - ARCH := arm64 -else ifeq ($(UNAME_M),arm64) - ARCH := arm64 -else - ARCH := unknown -endif - -.PHONY: build install install-for-mason test -build: - go build -o bin/thriftls main.go -install: - cp bin/thriftls /usr/local/bin/thriftls -install-for-mason: - cp bin/thriftls ~/.local/share/nvim/mason/packages/thriftls/thriftls-darwin-$(ARCH) -test: - @go test -gcflags=all=-l -gcflags=all=-d=checkptr=0 -race -coverpkg=./... -coverprofile=coverage.out $(shell go list ./...) - @go tool cover -func coverage.out | tail -n 1 | awk '{ print "Total coverage: " $$3 }' - -e2e-test: - @bash tests/e2e/run-e2e.sh diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..18507c0 --- /dev/null +++ b/NOTICE @@ -0,0 +1,6 @@ +thrift-ls + +Copyright (c) 2022-2023 Pengfei Jiang +Copyright (c) 2026 Karitham + +This project is a fork of https://github.com/joyme123/thrift-ls. diff --git a/README.md b/README.md index 43ffd11..e53cc7d 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,153 @@ -# Thrift language server +# thrift-ls -[![Go Coverage](https://github.com/joyme123/thrift-ls/wiki/coverage.svg)](https://raw.githack.com/wiki/joyme123/thrift-ls/coverage.html) -![Go](https://github.com/joyme123/thrift-ls/workflows/Go/badge.svg?branch=main) +A Thrift language server and formatter, written from scratch around a lossless +lexer and CST parser, a Prettier-style width-aware formatter, and a clean LSP +implementation. -thrift-ls implements language server protocol +- **Language server**: completion, go to definition, find references, hover, + diagnostics, rename, document symbols, and formatting — including + **range formatting** (Format Selection). +- **Formatter**: a full rewrite of the old template-based formatter. It + preserves comments and blank lines, understands width, and is deterministic + and idempotent. -## features +Fork of https://github.com/joyme123/thrift-ls, parser + lexer + formatter rewritten, lsp overhauled -- highlight -- code completion -- go to definition -- find references -- hover -- dignostic -- rename -- format -- document symbols +## Installation -## As Thrift Langugae Server - -### vim - -use thriftls as a lsp provider for thrift - -### neovim - -You can use [mason](https://github.com/williamboman/mason.nvim) to install thriftls. -And use [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) to configure thriftls +```bash +go install github.com/karitham/thrift-ls@latest +``` -`:LspInfo` to set lsp information. default log file location: `~/.local/state/nvim/lsp.log`. +This installs the `thriftls` binary. It speaks LSP over stdio and doubles as +a CLI formatter. -![neovim](./doc/image/neovim.png) +## Usage -### vscode +``` +thriftls [flags] run the language server (default) +thriftls lsp [flags] run the language server +thriftls format [flags] format a thrift file +``` -install thrift-language-server from extension market +Run `thriftls --help` or `thriftls format --help` for the full flag list. -![vscode](./doc/image/vscode.png) +### As a language server -## As Thrift Format Tool +```bash +thriftls +``` -**supported flags** +or, explicitly: -```plaintext -Usage of ./bin/thriftls: - -align string - Align enables align option for struct/enum/exception/union fields, Options: "field", "assign", "disable", Default is "field" if not set. (default "field") - -d Do not print reformatted sources to standard output. If a file's formatting is different than gofmt's, print diffs to standard output. - -f string - file path to format - -fieldLineComma string - FieldLineComma enables whether to add or remove comma at end of field line. Options: "add", "remove", "disable". If choose disable, user input will be retained without modification. Default is "disable" if not set (default "disable") - -format - use thrift-ls as a format tool - -indent string - Indent to use. Support: num*space, num*tab. example: 4spaces, 1tab, tab (default "4spaces") - -logLevel int - set log level (default -1) - -trailingNewline - Add trailing newline at end of file - -w Do not print reformatted sources to standard output. If a file's formatting is different from thriftls's, overwrite it with thrfitls's version. +```bash +thriftls lsp ``` -**how to use** +#### vim -```bash -# format single file -thriftls -format -w -indent 2spaces -f ./tests/galaxy-thrift-api/sds/Table.thrift +Use `thriftls` as the LSP provider for thrift files: -# batch format thrift files -find ./tests/galaxy-thrift-api -name "*.thrift" | xargs -n 1 thriftls -format -w -indent 8spaces -f +```vim +let g:lsp_settings = { 'thrift': { 'cmd': ['thriftls'] } } ``` -## Configurations +### As a formatter -config file location (in order of precedence): +```bash +# print the formatted file to stdout +thriftls format path/to/file.thrift -1. `THRIFTLS_CONFIG` env var (if set) -2. `~/.thriftls/config.yaml` on macos/linux -3. `C:\Users\${user}\.thriftls\config.yaml` on Windows +# overwrite the file in place +thriftls format -w path/to/file.thrift -### include_paths +# print a diff instead +thriftls format -d path/to/file.thrift -List of additional paths to search for included thrift files. When a thrift file uses `include "foo.thrift"`, thriftls first tries to resolve it relative to the current file's directory. If not found, it searches each path in `include_paths` in order. +# batch-format a tree +find . -name "*.thrift" | xargs -n 1 thriftls format -w +``` -This is similar to Apache Thrift's `-I` flag and is useful for monorepos with shared base definitions. +Formatting flags: + +| Flag | Meaning | +| ------------------- | -------------------------------------------------------------------------------------------------- | +| `-w` | Overwrite the file with the formatted result | +| `-d` | Print a diff instead of the formatted result | +| `--printWidth` | Target line width (default 80) | +| `--indent` | Indentation: a literal like `" "` or `"\t"`, a number like `8`, or a legacy spec like `"2spaces"` | +| `--align` | `field`, `assign`, or `disable` | +| `--fieldLineComma` | `add`, `remove`, or `disable` (keep as written) | +| `--config` | Path to a `thriftls.json` config file | +| `-I` | Additional include path, like the thrift compiler's `-I` (repeatable) | + +Flags override the config file. + +## Configuration + +Configuration lives in a `thriftls.json` file, discovered by walking up from +the file being formatted or the workspace root (like Biome). Set the +`THRIFTLS_CONFIG` env var to point at an explicit config file. + +```json +{ + "printWidth": 100, + "indent": " ", + "tabWidth": 4, + "align": "field", + "fieldLineComma": "disable", + "includePaths": ["/path/to/base"], + "logLevel": 3 +} +``` -### format +### printWidth -Controls formatting behavior. All format options can be set in the config file and overridden via CLI flags. +Target line width for breaking decisions. Groups (struct bodies, function +signatures, lists, annotations) stay on one line when they fit and break +otherwise, each deciding independently based on the remaining width at its +position. Default: `80`. -Example `~/.thriftls/config.yaml`: +### indent -```yaml -logLevel: 3 -include_paths: - - /path/to/base - - /path/to/shared-types -format: - indent: "2spaces" - alignByAssign: "field" - fieldLineComma: "disable" - trailingNewline: false -``` +Indentation can be written three ways: -#### indent +- a literal string: `" "` (two spaces) or `"\t"` (a tab) +- a number: `8` (eight spaces) +- a legacy spec: `"2spaces"`, `"1tab"` (kept as aliases) -String specifying indentation style. +Default: `" "` (four spaces). -Supported formats: -- `spaces`: e.g., `2spaces`, `4spaces` -- `tabs`: e.g., `1tab`, `2tabs` -- `tab` (equivalent to `1tab`) +### tabWidth -Default: `4spaces` +Display width of a tab when measuring line width. Default: `4`. -#### alignByAssign +### align -Controls alignment of struct/enum/exception/union fields. +Controls column alignment of struct/union/exception fields and enum values. -Options: -- `field`: Align field IDs, types, and names (default) -- `assign`: Align the `=`sign for default values +- `field`: Align field IDs, requiredness, and types (default) +- `assign`: Align the `=` sign for default values - `disable`: No alignment -#### fieldLineComma +### fieldLineComma -Controls trailing commas on field lines. +Controls trailing separators on field lines. -Options: -- `disable`: Preserve original (default) +- `disable`: Keep as written (default) - `add`: Always add trailing commas -- `remove`: Remove trailing commas - -#### trailingNewline - -Controls whether a trailing newline is added at end of file. +- `remove`: Remove trailing separators -Boolean: `true` or `false` +### includePaths -Default: `false` (no trailing newline added) +List of additional paths to search for included thrift files. When a thrift +file uses `include "foo.thrift"`, thrift-ls first tries to resolve it relative +to the current file's directory. If not found, it searches each path in +`includePaths` in order. This is similar to Apache Thrift's `-I` flag. ### logLevel -Controls logging verbosity: +Controls logging verbosity (the server logs to `$TMPDIR/thriftls.log`): - 1: fatal - 2: error @@ -152,6 +156,9 @@ Controls logging verbosity: - 5: debug - 6: trace -## TODO +## Development -[] optimize code completion +```bash +go test ./... # unit and fuzz regression tests +bash tests/e2e/run-e2e.sh # end-to-end formatter tests +``` diff --git a/utils/diff/diff.go b/diff.go similarity index 98% rename from utils/diff/diff.go rename to diff.go index 47b2856..730f2b2 100644 --- a/utils/diff/diff.go +++ b/diff.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package diff +package main import ( "bytes" @@ -116,10 +116,7 @@ func Diff(oldName string, old []byte, newName string, new []byte) []byte { // End chunk with common lines for context. if len(ctext) > 0 { - n := end.x - start.x - if n > C { - n = C - } + n := min(end.x-start.x, C) for _, s := range x[start.x : start.x+n] { ctext = append(ctext, " "+s) count.x++ @@ -234,7 +231,7 @@ func tgs(x, y []string) []pair { for i := range T { T[i] = n + 1 } - for i := 0; i < n; i++ { + for i := range n { k := sort.Search(n, func(k int) bool { return T[k] >= J[i] }) diff --git a/doc/codejump.md b/doc/codejump.md deleted file mode 100644 index 05ddb6c..0000000 --- a/doc/codejump.md +++ /dev/null @@ -1,15 +0,0 @@ -# codejump - -codejump 这里指代一切设计到代码跳转的特性。LSP 中支持的有: - -1. Goto Declaration: 在 thrift 中不需要支持 -2. Goto Definition: 在 thrift 中可以用来支持 在 FieldType 上实现 struct/union/enum/exception/typedef 的跳转。同时也可以实现在 ConstValue 上针对 enum 值的跳转 -3. Goto Type Definition: 在 thrift 中可以用来支持 field/const/function/typedef 中 identifier 的跳转。和 Goto Definition 的工作方式稍有不同, -当用户在标识符上使用时,需要推断该标识符的类型,并跳转到类型定义上。 从使用体验上考虑,如果用户在类型上使用时,可以直接使用 Goto Definition 进行跳转 -4. Find References: 查找引用。在 thrift 中主要用来做 struct/union/enum/exception/typedef 的引用查找,可以进行 references 查找的情况有: - 1. typename: 找到 type definition 以及引用的地方 - 2. identifier: struct/union/enum/enum value/exception/typedef/const - 3. constvalue: 查找 enum value/struct/union/exception/const/function arguments 的默认值。以及在 const/enum value 中的定义 - - - diff --git a/doc/completion.md b/doc/completion.md deleted file mode 100644 index ef0fd1a..0000000 --- a/doc/completion.md +++ /dev/null @@ -1,52 +0,0 @@ -# completion 实现 - -## 概述 - -completion 要求在用户输入时实时的提供补全的候选项。lsp client 会发送请求到 server,提供用户当前打开的文件以及输入的光标位置。server 在接收到请求后,根据用户输入的位置进行补全项的计算。 - -补全项的计算主要考虑以下几点: - -1. 用户当前输入位置的期望类型。比如是 Header 还是 Definition。如果是 Struct 类型的 Definition,那么是 Identifier 还是 Field。 -2. 根据类型进行输入的补全推荐。 -- 是 Identifier 的话,可以根据已存在的 Identifier 已经补全推荐。 -- 是 Field 中的 FieldType 的话,则提供 thrift 中常见类型的补全推荐。 - -在补全的过程中,会出现 thrift ast parse 失败的情况。因此需要保留该文件最新可以 parse 的 ast 版本,通过该版本的 ast 进行补全计算. - -## 交互细节设计 - -> 以 VIM 为例 - -1. vim 发送 CompletionRequest,参数有 - a. TriggerKind - b. Pos: 行号Line, 当前行的偏移量Character - c: URI: 打开文件的 URI - d: TriggerCharacter: 触发补全的字符。这个仅当 TriggerKind 是 TriggerCharacter 时有值 - -2. language server 收到请求后,开始计算 completion. - 1. 根据 URI 定位到文件,根据 Pos 计算得到 parser 的 Location. - 2. 再根据 AST 中的位置信息,找到 Location 对应的 AST node 以及路径。这里的路径指的是从 root node 到当前的 AST node 所有的 node。 - 3. 根据 AST node 和 path 信息,进行补全推荐。 - -基于语义的实现方式,对 parser 的要求较高,parser 需要在语法错误的情况下知道当前的输入位置的 node 属性。因此目前使用的是基于 token 的补全方案。 - -### 补全清单 - -1. Document -> BadHeader: 对输入进行 include snippet 补全 -2. Document -> Header -> Literal: 对 Path 进行补全 -3. Document -> BadDefinition: 对输入进行 struct/union/exception/servcie/Enum/Const/Typedef snippet 的补全 -4. Document -> Definition -> Struct/Union/Exception -> Bad Field: 对 required/optional 以及基础/自定义类型进行补全 -5. Document -> Definition -> Struct/Union/Exception -> Field -> Identifier: 根据已有的 Identifier 进行补全 -6. Document -> Definition -> Service -> - - -## fault tolerate parser - -1. https://eyalkalderon.com/blog/nom-error-recovery/ -2. https://github.com/ebkalderon/example-fault-tolerant-parser -3. https://arxiv.org/pdf/1806.11150.pdf -4. https://github.com/mna/pigeon -5. https://github.com/mna/pigeon/wiki -6. pigeon 实现的 thrift parser: https://github.com/samuel/go-thrift/blob/master/parser/grammar.peg -7. 写 parser 的教程: https://tiarkrompf.github.io/notes/?/just-write-the-parser/ -8. 生成好的语法错误信息提示:https://research.swtch.com/yyerror diff --git a/doc/design.md b/doc/design.md deleted file mode 100644 index 804be37..0000000 --- a/doc/design.md +++ /dev/null @@ -1,78 +0,0 @@ -## - -名词说明: - -- cache: 全局缓存,可以在同一个 server 下不同的 session 中共享 -- session: 会话。指一个 connection 接入的会话。一个 session 中会有多个 view -- view: 视图。一个 workspace 对应一个 view。 -- workspace: 指一个工程 -- snapshot: 指 view 的一个快照。保存了当前 view 中的状态,比如:包含的文件,目录等等 - -## 文件内容维护 - -当编辑器打开代码文件后,language server 就不能再从磁盘中读取文件了。此时编辑器会通过以下几个事件来通知 server: - -1. textDocument/didOpen: 整个文件的文本内容会通过该事件一起传到 server。 -2. textDocument/didChange: 编辑器通知 server 代码发生了变更。变更通知有三种方式: - - 不更新 - - 全量更新 - - 增量更新 -3. textDocument/didClose: 编辑器关闭了该文件。后续如果有需要该文件的地方,server 可以自己去磁盘中读取。 - -对于通过 include 引入的文件,server 需要自己去磁盘中读取该文件的内容进行解析。 - -对于未在编辑器中打开的文件,server 可以通过 fsnotify 进行文件内容变更检测,从而可以实时维护最新的文件内容。 - -## Parse 和 Analysis - -Parse: 解析代码文件生成 AST,这个过程中可以发现语法上的错误 - -Analysis: 根据已有的 AST 进行分析,主要用来找出一些非语法错误。 比如: - -1. Struct 在 field index 超出可用范围,field index 冲突 -2. 类型不存在 -3. 目标语言的关键字冲突。比如要根据 thrift 生成 go 语言,就需要在 thrift 代码里避免 go 语言的关键字。等等 - -Parse 和 Analysis 在以下情况下被触发: - -1. 在 textDocument/didOpen 时,会进行 Parse 和 Analysis 两个步骤。 如果发现已经进行过 Parse,则会跳过 Parse 和 Analysis。 -2. 在 textDocument/didChange 时,会仅针对当前文件进行 Parse,并进行全局的 Analysis。 -3. 在检测到磁盘上的文件发生变更时(比如 git 切换分支,丢弃变更等操作),会对变更的文件进行 Parse,以及全局的 Analysis。 - -## snapshot - -snapshot 是属于 view 的,在 view 的初始化时,会对 snapshot 进行初始化。为了防止 snapshot 被释放,view 需要自己持有一份 snapshot 的引用。当 view 不需要 -该 snapshot 时主动释放即可。 - -snapshot 是当前 view 的快照,保存了当前 view 的所有代码的 parse 和 analysis 结果。在发生 `open` `change` 或磁盘上的文件变更时,需要根据最新的文件内容进行 Parse 和 Analysis。 -因此这里可以参考上面的 `Parse 和 Analysis` 章节所说的,新的 snapshot 并不是完全重新 Parse 和 Analysis 所有的代码,而是继承之前的 Parse 和 Analysis 结果,尽量小的计算出结果。 -具体实现上,snapshot 对自己进行 clone,生成新的 snapshot 实例,然后再处理 changes。 - -## Promise - -promise 提供了一种缓存机制,防止对同一个方法进行重复的调用,并可以异步的获取结果。例如: - -1. 对 snapshot 中的某个文件进行 parse 时,该文件可能已经被其他事件触发,正在 parse 中。那么后面触发的这个 parse 就不需要重复进行,只需要等待之前的 parse 结果并获取即可。 - -## Mapper - -1. 在 LSP 定义中,Line 和 Column 是从 0 开始的。在 Parser 中, Line 和 Column 是从 1 开始的。 -2. 在 LSP 定义中,字符编码可以支持 utf8/utf16/utf32。默认支持 utf16。因此 language server 的实现中默认支持 utf16 即可。但是对于 parser 来说,是基于 rune 的。 -因此对于 Position 的计算使用的编码是不一样的。 - -以下面的字符为例: a 的 position 是(0, 2),在 parser 里的 position 是(1,2) -``` -😀a -``` - -因此需要 有一个 Mapper 负责:根据不同的 Position 和编码定义,在两边实现灵活的转换。 - -## thrift 解析范围 - -thrift 文件很多情况都作为一个项目下的子文件夹进行管理,因此在初始化项目文件时,很难确定 thrift 文件存放的具体位置范围,从而很难在不全量扫描的前提下解析所有的 thrift 文件。 - -但是解析所有 thrift 文件是一件必须要做的事情。像代码补全,代码跳转中的 find reference 功能,都需要有全量的视图来获取所有的 thrift 解析信息。因此需要在 LSP 的 Initialize 时,根据请求参数中的 `RootPath` 或 `RootURI` 或 `WorkspaceFolders` 去获取项目目录,并做全量扫描,获取所有的 .thrift 后缀文件并解析。 - -这个过程的耗时由项目文件数量 以及 thrift 文件的数量和大小决定。 - -在实际的测试中发现,以 neovim 为例,打开项目下的 thrift 文件后触发 Initialize 请求,但是上述的目录参数都不会有。 因此作为 fallback 的方案,会解析首次打开的文件所有目录以及以下的所有 thrift 文件 diff --git a/doc/diagnostic.md b/doc/diagnostic.md deleted file mode 100644 index 744fce5..0000000 --- a/doc/diagnostic.md +++ /dev/null @@ -1,33 +0,0 @@ -# Diagnostic - -诊断是基于 AST 的语义分析,用来发现明显的错误。在 thrift 中,一些可以通过诊断发现这些问题: - -1. field index 错误 - a. 超出边界 - b. 重复 -2. 循环依赖。因为大多数语言都不允许循环依赖,因此在 thrift 的代码中可以进行循环依赖的检测 -3. include 实际不存在 -4. 语法解析错误也可以通过诊断的接口提示给用户 -5. 语义分析 - 1. 标识符冲突。比如定义了两个相同的 struct 等等 - 2. 类型未定义 - -## 实现 - -### 触发时机 - -诊断在 LSP 的定义中,是由 Server 主动发送给 Client 的,因此诊断的触发时机由 Server 来决定。 -对开发者来说,当其打开 thrift 文件,编辑 thrift 文件后,都希望由 IDE 进行错误检查并提示。因此可以将诊断的触发 -时机放在: - -1. 用户第一次打开某个 thrift 文件。对这个文件以及相关联的文件进行诊断。 -2. 用户编辑了某个 thrift 文件。对这个文件,以及引用此文件的关联文件进行诊断。用户在编辑文本时,会不断的触发 didChange 事件, -为了性能考虑,可以设计一个冷却周期。对于某一个文件,一个诊断后会进入冷却。在此期间内的 didChange 事件不会重复触发诊断。 -并且 didChange 事件会重置冷却进度。直到用户停止输入一段时间(比如 2s),诊断会重新开始。 - -### 诊断队列 - -诊断是一个耗时的过程,并且单个文件修改会触发多个关联文件的诊断。因此要尽可能的避免无效的诊断触发。可以为每个文件创建一个诊断队列。同一个 -队列内,诊断是同步进行的。且新版本 snapshot 的文件诊断可以覆盖旧版本的文件诊断,从而尽可能的减少无效诊断。 - - diff --git a/doc/doc.go b/doc/doc.go new file mode 100644 index 0000000..5973bae --- /dev/null +++ b/doc/doc.go @@ -0,0 +1,170 @@ +// Package doc implements the Wadler/Prettier document IR and a width-aware +// printer. A document is a declarative description of formatted output: +// text, lines that break or stay flat depending on available width, groups +// that decide independently whether they fit, and conditional pieces. The +// single printer turns any document into a string given a print width. +// +// This is a faithful port of Prettier's document algebra +// (src/document/builders and src/document/printer). +// +// A document passed to Print is mutated by break propagation. Do not print +// the same document concurrently, and do not reuse a document after printing +// it. +package doc + +// Doc is a formatted document. The interface is sealed: only the concrete +// types in this package can implement it, so the printer can exhaustively +// switch over it. +type Doc interface { + isDoc() +} + +// Text is literal output. +type Text string + +func (Text) isDoc() {} + +// Concat is a sequence of documents printed in order. +type Concat []Doc + +func (Concat) isDoc() {} + +// Join returns a Concat of parts joined by sep. +func Join(sep Doc, parts []Doc) Doc { + if len(parts) == 0 { + return Concat(nil) + } + out := make(Concat, 0, len(parts)*2-1) + for i, part := range parts { + if i > 0 { + out = append(out, sep) + } + out = append(out, part) + } + return out +} + +// LineDoc is a line separator. In flat mode a plain line prints a space, a +// soft line prints nothing, and a hard line prints a newline regardless of +// mode. In break mode every line prints a newline followed by the current +// indentation. +type LineDoc struct { + Soft bool + Hard bool + Literal bool // like Hard, but the newline is followed by no indentation +} + +func (LineDoc) isDoc() {} + +// Lines, matching Prettier's builders: +// +// Line - a space in flat mode, a newline in break mode +// SoftLine - nothing in flat mode, a newline in break mode +// HardLine - always a newline; breaks enclosing groups +// LiteralLine - always a newline with no indentation; breaks enclosing groups +var ( + Line Doc = LineDoc{} + SoftLine Doc = LineDoc{Soft: true} + HardLine Doc = Concat{LineDoc{Hard: true}, BreakParent} + LiteralLine Doc = Concat{LineDoc{Hard: true, Literal: true}, BreakParent} + HardLineNoBreak Doc = LineDoc{Hard: true} + LiteralLineNoBreak Doc = LineDoc{Hard: true, Literal: true} +) + +// group is a piece of a document that fits on one line if possible and +// breaks otherwise. The printer decides per group based on the remaining +// width at its position, so nested groups break independently. +type group struct { + doc Doc + id int // non-zero ID lets IfBreak refer to this group + brk bool // forced break; mutated by break propagation + expanded []Doc // conditional group states, least expanded first +} + +func (*group) isDoc() {} + +// Group wraps d in a group that breaks only when it does not fit. +func Group(d Doc) Doc { return &group{doc: d} } + +// GroupBreak wraps d in a group that always breaks. +func GroupBreak(d Doc) Doc { return &group{doc: d, brk: true} } + +// GroupID wraps d in a group with an ID so IfBreak can query its mode. +func GroupID(id int, d Doc) Doc { return &group{doc: d, id: id} } + +// ConditionalGroup tries each state in order (least expanded first) and +// prints the first that fits; the last state breaks if none fit. +func ConditionalGroup(id int, states ...Doc) Doc { + return &group{doc: states[0], id: id, expanded: states} +} + +// ifBreak prints BreakDoc when the group it belongs to (or the group with +// GroupID) is broken, and FlatDoc when it is flat. +type ifBreak struct { + breakDoc Doc + flatDoc Doc + groupID int // 0 means the innermost enclosing group +} + +func (*ifBreak) isDoc() {} + +// IfBreak builds an IfBreak for the innermost enclosing group. +func IfBreak(broken, flat Doc) Doc { return &ifBreak{breakDoc: broken, flatDoc: flat} } + +// IfBreakFor builds an IfBreak that follows the group with the given ID. +func IfBreakFor(broken, flat Doc, groupID int) Doc { + return &ifBreak{breakDoc: broken, flatDoc: flat, groupID: groupID} +} + +type indent struct { + doc Doc +} + +func (*indent) isDoc() {} + +// Indent increases the indentation of its contents by one level. +func Indent(d Doc) Doc { return &indent{doc: d} } + +type align struct { + n int + doc Doc +} + +func (*align) isDoc() {} + +// Align indents its contents by n columns relative to the current +// indentation. With tabs enabled, n is rounded up to one tab. +func Align(n int, d Doc) Doc { return &align{n: n, doc: d} } + +type lineSuffix struct { + doc Doc +} + +func (*lineSuffix) isDoc() {} + +// LineSuffix prints its contents at the end of the current line, after the +// next line break (used for end-of-line comments). +func LineSuffix(d Doc) Doc { return &lineSuffix{doc: d} } + +type lineSuffixBoundary struct{} + +func (lineSuffixBoundary) isDoc() {} + +// LineSuffixBoundary ends the current line-suffix group: suffixes before the +// boundary print at the next line break, suffixes after it wait for the one +// after that. +var LineSuffixBoundary Doc = lineSuffixBoundary{} + +type trim struct{} + +func (trim) isDoc() {} + +// TrimDoc removes trailing whitespace from the output produced so far. +var TrimDoc Doc = trim{} + +type breakParent struct{} + +func (breakParent) isDoc() {} + +// BreakParent forces the nearest enclosing group to break. +var BreakParent Doc = breakParent{} diff --git a/doc/format.md b/doc/format.md deleted file mode 100644 index df511f1..0000000 --- a/doc/format.md +++ /dev/null @@ -1,43 +0,0 @@ -# format - -format 用来处理 document formatting request,对 thrift 文件进行格式化。 - -1. 对于没有语法错误的 thrift 代码。可以使用解析过的 AST,通过遍历 AST 的方式输出格式化后的代码。 -2. 对于有语法错误的 thrfit 代码。理论上可以在解析时生成 BadNode 时,保存出错的文本。在 AST 生成时原样输出即可。(暂不实现) - -实现的难点在于如何从 AST 到 source code。在第一版实现时发现,最难的地方在于如果正确的处理空格,换行,注释和缩进。 - - -``` -> from gofmt slide -Lessons learned: Application -Basic source code formatting is great initial goal. -True power lies in source code transformation tools. -Avoid formatting options. -Keep it simple. -Want: - -Go parser: source code => syntax tree -Make it easy to manipulate syntax tree in any way possible. -Go printer: syntax tree => source code - -Lessons learned: Implementation -Lots of trial and error in initial version. -Single biggest mistake: comments not attached to AST nodes. -=> Current design makes it extremely hard to manipulate AST -and maintain comments in right places. - -Cludge: ast.CommentMap -Want: - -Easy to manipulate syntax tree with comments attached. -``` - - - -## 参考资料 - -- [the design of a pretty-printing library](https://belle.sourceforge.net/doc/hughes95design.pdf) -- [a prettier printer](https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf) -- [elastic tabstops](https://nick-gravgaard.com/elastic-tabstops/) -- [gofmt slides](https://go.dev/talks/2015/gofmt-en.slide#10) diff --git a/doc/hover.md b/doc/hover.md deleted file mode 100644 index b93231d..0000000 --- a/doc/hover.md +++ /dev/null @@ -1,13 +0,0 @@ -# hover - -hover 用来展示代码的一些属性。比如某个 identifier 的类型,某个引用常量的值等等。lsp 中还有一个 signatureHelp 和 hover 类似, -不过在 thrift 中不存在函数的调用,因此这里只考虑 hover 的实现。 - -以下类型的 ast node 上可以使用 hover: - -1. FieldType:function type/struct field/union field/exception field/function argument/function throws field/typedef/const -2. ConstValue: field default value - -hover 的实现: - 1. 找到 definition: 复用 goto definition 的查找实现 - 2. 展示 definition: 复用 format 的实现。对指定的 definition ast node 进行 format 后展示即可 diff --git a/doc/image/neovim.png b/doc/image/neovim.png deleted file mode 100644 index 02b302c2744b19f9c0154b19fe7635710b809de1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 354515 zcmeAS@N?(olHy`uVBq!ia0y~yU|+|;z{-5-=82i5e z;TOfbM3@*Rm~5y>1BA}$~4nPo*{|jO0@sE=_@x#Br#j^2t0bJHD~ED?@$Zp z-tFIRsvId)T*uzv^Z4`ANQMk^XWP{d6Id&^$UDB#67^cb@It~tMnT|ntAM(Ljf#H$ zBChjoHyCf7R=CG#X#H6F+?x6i={z4k|BPggm>WKSg0l)&q+b22|BsIbORVQDz z;o^}`9TuVgHnf=+skyB(;CYBj*=^R%}*%_90O%?3Ss=gAk;K89^2mi|~ zSm#vdp>kxIK)Yx0;>9MvdVD-3_>*=-UC^9V6W5gXj%lKjN!;O`es+d>%jDBf?UXrt zM{3`m^XpITi<~+6#KjNy1-dK#{>kS$%6U>CnDM|0)=5h;I<_jB1w1+8zAD#XwUeRw zuKRB5dV-JUGaE*$@bZU!PR^Y9X@f=YULF67^Yz1YrhQmsy2B=Zj=;io{M&*Lyzjox z@hEEIl*u>FD+l#-zB=!!!NnQ+yl-pAlFnH*8>=_{d0=t&j$UNmnqzY6-9?8(tF$|O zMOZ)1+WDiSW{1VqKNVAS?9Ciy)#uGSD=)dak6oW3uz}l&fmP%Ib0Z|#{*^_5@Y0JKzbhlH{!Tj*ki=ow zB;i(a#P>&Mw|8mNl zkGaQo$UgaUq`9#E1mDa;wLGSn0{$26&FoI+=LMX;dSvaXxnJ`?$((pFdsg-vjddbZ zj5>r|3V+ls+ctUIbB69!i|$`@m(4!r5W16D`Tpko!#6x- z9IKgi-e`pN&ppn!`ojUKsw>;ue(gQ__2^{gN)`JbkF#r5X{{_+TlTi#?-EJopNlkq zuHrp5S7Wx;^pwuekupC-1vXX$9lW5ZV;*iE&Jc26vx^#W_V-Iv;F7V^;W;jepp#>A9#6{Y3Uw^wTDCbnQEk89FR@rIn!ub&>*>! zQU3vZlY@o?XOaVx2jk*@nh8w7J0tqqj2;?7tw$6!J*+0RdI+i- z$|2E@X5Aq3gnaOR9o=) zf=-E0RL41I@x@OwRCjT&b?smH{KD!C>0dH$k1;H3H8k`-!KX)X31%Oj!Hi zsSRFh*s>e`e@R`#`}&C81D83Cn+wEuv?+gZiD~dZl>Vsu!`2^sd&KgO=|BGdQST3T zqo0AQg>Vh0%^?XVu|%VVGXmUhxLG(@FqQ{fKh&M*ks|cA^In1ZGJ(yVmpi&UUUyb^ z^d1R&0+d2Ir)#Lp^Ba6{1Bfg=Uc8;Ub|ef)c2}qPt-k;_r#z`E7D6(Y5K(S zN&OS_C(fTxt+7iax=UZRO=*eGQk{i4OHBgw0&Z#AhS;vMy*huE!`Wr$7ETM!UZK9C ze8u{$Tehs(QnSS^+jC1=_SEdRSFP5T#=mCvo&U_`@uf={Ubm8Nl|?DPReNhX*Khk` z_KU$Ue||aoMeA4LFWX<$xozJ9a_4OesGe5VTDJC^>RWLu5!qRCN;jc+wK7A2;`SyzS;_o5zm*g*(U!{L#{__2< z|9AXzW;?batw56LSj@4oW1Mcg8805V*N{FTHy~@n?FB~{oDR5u;jqEUg095Yhlz*H z)Q)XQ{xoCf8PzkjXR^=qH=kiEWb0(pl?gF^acM!hTXSzykBghCTXoMmsalgX$#$vd z68gsLjQ3?X&9<1?V>UBq)4?Y#p3f7S57sraHhZ(Zm##T2Vp?j{Ydkk?*U>L~GPlp% z8M$NTnVjC6g`53~msL)v-1gJ!v+^@pF`jM)?#`oL-HXM}N3`kiX-^M5mz7o;IV;L7 zLQB^?dRxS|h?~)>QKFG=H}1;z-I^Ai9<_c;-llh(Hg1qiHNLy-*0X84N2ld#Z;Q>X zKJ{(x+`X}z?$+$K+P(an+c%rvPQSB$Tl1B)JG6W9P2~%h>FmonE_3{ApZMGWb$|7Z z!L65B)34k-9roo6~PZWH5OEBz~J#@P*5 z5>L0ymHKw{>SfdGrK?}RefaCJJD=Y6)FZDU_uS$%SE)*h=pxqB|}yKEFG@@4nyu<;ri|JKS<)KVGZ>N@Y@Oe>3e;8?!jE1gHDH>y_}+)zB&s#UvIm|y_xGY z*J{4kT;F--i`|oO?C9u;5G$OUeoXkos)|_Uz3%5EZpwIe_w>g||5P}o=_Pm7fJ@f2 z=t@^$`J&v#OD~ohDzu4jR(h7VruNN}8~5FX-H-RJllW_qS8?adj^K|yKR7>HKfbTV zqORk)WA2LyD|~XiE_r%+ukxyzE;DJ5&Cc928w`z)&G9z$oVeu5@<~CHgSIa%>*tl- zot0BPD?O_4*4DyrkC*jW8{bQ*dH3V6aEaOn^*8?NOVpRY_xXRM|6%y2-NqG?uZ?!G z#2m~zSe!VaVCe?uC;Cx(Rg+%M-)ZwXb6Wkit~Gpx3wHS2(R}E2%_}a=JthB4-E`r} zn{Ahyn_HIq%USB%zF!a$KTA1xUWQ%asUu-CH?N7fW_Ggt(kjo`PaZ$bSQkkwUvb{N zq5b=3FZQSYr+%|CvWl|4S6BDFXYZQdRdVX*O21%nzuF*)upg&QYGx%wT`oEOvUu73 z`3GlitW+~HPENb5^E&c#+Sc=Y^YY7;f1cmlZ7jZB z=VR2SM9spLB`>$8Hl8lixgH(2$`_I9xk?`&9R- z*Vk{1++1>Y`PN5U!?l~VzpXKj-Cl5a_t!&Tr<<|luDF@8ZDYyfM zTIaUYouCa*KVDn1c6nXm|DdgBQ=(sPJAZrW?b~HX-e#=(v~Ja{%w5HvrcrCxyt^@V z+v`W~CfxmMs%JX;yTSJ)-3GwG)5O`nLJ|^*?F9Z|`F6VOHJh@%GBC zbykJ3(Y0Q+uMbXSpDp!Aitmrzm+x=eb@)SN%xv~N;rM!DkMdr3?*20KQ&QIbsj}5K zOcuWU_R;Y8dXxUVJWIjKM_*2En0(y7&`;L(ZT+tclJ||-&u=<^ZSH5Q@alV?|1{d$ z{I|I9=xR{)!QW1QUFUc6u2qiekKx)=@}}s-)5+l#;pbv|_uZ<}{dlYQb>{Wkc^vsR zduLXEe!8|P+ID;TeZ@PPJ6Bh4e>3mp-c$Cn|L7Rf1F-hcPq^U8i-^T3V&)C3tUf{~cSJv_K6<$?m$us@uT>ZYa z_7STC@0*b0rFqH>)rp&D^X^PsZ?#IDh4ISW+69>|6JNet_H5BXY5s?``R}uh89aCy zYNmbv_HE7kw|{xwuejmMAf>+Y{VXlTL%Rr@5?hMd$0&op0O1}z2#1`Y-mMkxkX1_lO31_lN@Mrkegx$Von>i43TI2L|iWU^Gl)v>GThW&1ZB=I-DZi}*N zb9qg?SFP{K@p5nbmpIQK_h<9YKD<*^T`@uEncerDGw05od$;!d$(<*6Z{ECDHa4c( zbpMyHIx`fWUcQ*zFm-LZXJVRTUemsbQ&;ZXyKP;fZ)j#}<-&y%Q$tfXE?eg3bth1B zUEV#lM~@gSIJqQ?Kd)O-apBsvs9#4WPYb*D=@{?Vul8$y>KQA0?^_i+YkT9apSeLg=k?V@u3j>-xrzWoL7=J(bctNx#3QF-aX!oz`o_WRqP+dt|2SEoka2iHsO zj~!Pn_$&Q>&+3H>6~AO=r2P2wNz}l@*YnrM-|{-E@bj&#Q70-!@55>*?ZEckP@$09Bg~hwuyZbZcYX2Cz zt4}|6?AZ1A*mJpYQQOX(xWRJt$d4WKudjPrrB?FD;_H{M)AoO}y0c78{qGNF?s<#d z`lXEj_}Ars_VNb2AOQ|Wus8>!(*kZKhx*X6 zgWZ=z+m>A1=$N85_u%a9SFc_3^|uL*jXYv%8?j>b>i>Tx`@8i^%9>?tXlzuRt?BV$ z#flDJ>CnK;Rfddne?4&KZZ*?em-puTwoRLozFNQMSbb{>JFnc49KAXA>PCF~B@V9m zQ1|z+h3Gs1@z?jwFC4p;cQbpvsHoVi=!h4ur8}g!x{vqCrH1^teBLfTG_>;TtI#WX z5&x^JqS`L_bj1Y56}~yKFj3iU=dQ{b?q5l9YqTZE8E|PxP6L$2B|#Xaej{7?dACkR_}ICD6p50|37EW znrz?eXLNOYdwnNOj(Yjec)4E_S9o|h`}_G5r`?KPYrafPX6l|hosZY7Th-Ua^Ilq_ zr*Pk&SF3+kR<<2;3kx%Qv1Q4Uh!f4H`4pVQ;(bFjf!=+B><0l~q{ojPwcR4qLta@Oml&f)X( z>tPhPdM;&M&Aw2s@S1V@c`gpFi`Om+3f+->nR!P?$>!36%AMyM{_IPh`||A6^;@&gr1;0{?3^b zyK437bMq{tV?t`Ws{$Wh+uy%`_H5~hXI-B@&6x3Gw!i7(MeAp;ak{g2Yj5fMds&t@ zBRj;Ve@{!wk}*qYXk3_B8F}-hu)5$H-TY#2(-TvSDo!LQCqKS5>z4OK!N{2%>z7}D z>)eq0G9xQ%SJ~UBu(V(N_wV(Uay)r>(cIvO6UQ@8&Y-;eN0-e5Wt?Ea1#8#L*tgeciT^vi(>1>D*l&B8hA-><=jNQ< zv35gfrsvIjcdJFi?#ij2`2M&0`~7+QR*0u^BrIlLRZ>ziRkCDVTzGi@#|H(`p^+DN zo;xXxCG|j))LkjYMTd5OeY*IdFjI?zxa$LZ%MZOqD@rczZftYK72L55jFCr__9A?M zT09{08s&Nz0w3D`cy#Fs5z13Iwy1fmT)p@1@n5CaL_d6$z7o0a1V6;}2kQG8w)M@+ z;7MJ|dO>1#MAw%P6{J8!wv^GyL5WF3R8LUD2sxZlmB4u~)^TicYc0Pnd0_`9sBBZ- zPeF(*>F4Lge|qtGuHBN`&jNnk$-g~e z#p39$hJ6c{E}c7l+O)XZ*wDg6x5~n4uikAbW-77eX`JWWdVZGW z(_Zswk3aqS`TUsq{a;JkW0za!T5bQF)hA`@6CLf{QV%rg$k<|&vS`twP!o3kf8USAyRuzhXWD5Jl%|sPNnT9# z++TYc#nQWx94{ni{ATmNaM}DBpX}WxZSAhZ{%f8~OWAMtKjB=~Rp(P+bS=JECK?p3REKL7ft`+Uy5J#W=+#w~PyxoPrb z<({)=e3qE+lsvtY|DJW#l@Bc~ORmb-C?D-#y}Eni#EISAlSPt6&3t{Yw`|?E&FoN; z`l*G=&sKhY7dv6PjQGag!gnWW!zxZhnoH#PBBS-^q`G|S^|p!+as~bO&aGa#cyVxW zF!P0y(o)M3X|s0^ioT1g`AS`1A8#PN-$_m;bl$vqXB5=cw_i@TN~)0fzBBo_o!)6~ z?YP>B+Uw)?-nyXBec7h)kx96bmz=MK;$+m^7KH=P6F|lVe&cEOP-}Cy` z-O2^C*FXCd9{!N`!oHusnkOo^JFZy0`u#hJO&`C%e0`qfn2z?GDU&BNUshLNu4S@A z^6A9I>9v2^JUIr~zlFNp|6({Z$V|%FA_XPS0n* z_x$L8uZl;UkscP`9`5`6?)j->0#diWulu;T$G3-@i_84)W?M_u;FOhX*S<{9zrE}B zjT;>TQNh87UAg5A%*<|W&Hlgg@!#N^)rQR zqoQWnndC3qx9{FWW%UD7PD(pBiwFp-_if#@Y0bKI=g#i^^nRNyE2}D(?rm%3XDrDE zMpKSzO$!g{viZC)E%o`;tEN7C%WDb>98yEC&b!O5?|Srf|G|p~t8Q)a3=0eEc(k|1 z^4t6RvQxJmlv!0151Xx5TX*G>yL?eeNk?B_?C!kgg$vhBUCL^saJw|^n&?996O+~Y zxVTB8n$>9gs~;yR}Jtp5htPtEO8Q zv3x%e=IuRg+tM9Nwk%oF#2?gk;P$TFyN!Ooc_U+|a~r03Mb&&I8fR3! zS)-++b!%Vkec!6={P%a{xV>l1x}~9ZT<)pY#DvV(kA*FkZLv^)^(HNF>I#j!L02Yp zwV>CD7jy+)ynS)$)A21e^-ACw}tq}yXEoQbJDzb3aNiO8~;H>->IVZY4PUt`{zyU|7 z*XvYI7gyxr;I8?8cj~uqWlpInDOdim`Zu^d^P2ZrYS#U_X#4VaGI3we+ee4**)~gD zk%jeV-H(H}`S^^&Tz`1^`0RRquhOaah@@`y)06FTRTE5~9Na1R&^;pT#bkAV`#&c( z-+6ntocI5i%hN)8`R#sunVBwk{o1s@`6uK2?S9^AY+k-@dR4EaZN|?};&0zX{a>6G z8ejkSuXy~rr)ffgkG=`q+B3`Fe(Q^k$HhLYef-$Db?er~#@dA|*Q|LXZ`K!{d;i<+ z-MgjriykiSxBLH5opm>RQgW2qj?Bx~!o%+rWL89L+8vzZ*0TMNbKRR8ovQt(^8DiE zxBJenORvkye(lyjld0+V>-G2KO|vY5KXJDx^=+#8{r38dI2O&|RsWs{2WE)L$^AUg z>>W9EgYugd6(0^>yZidn_vK%#UFsOb^;*u){k|i^=1)}jmo**9X6`+E+E0GsYxQk0 z7J7Ai%F-BLi_Ghj9qU$bJ-_$t#=8RMT-lG`10ypT4GhgTzR5aav7>cY74Lx+eQ(1) zyj@(d#m-S!C-+X*r^}yD*XWx(hs=D2M!%ty< zorkU*qN1Yb|K#61`awF(amyBy`u|_G{r$`HDt~-1+{XFxwEq6+&zITOwJTgqKjJFc z{QuwY`TIT`x?JNbJWF)_-o3TQ6FJ^&%|8BK>r%1(-!I~%n#?XZpJHX z$5lqoYz(+xf22l5SxaBL!={vZl9P1K#Sfeku~BoROUot)=<9oLpOWtl>950@*Nu9u z4!i%YdK09){NSA+mMeMpb}vs3Xm}p*PfpC{>$BQzrq>3`ade?o6T>35sUJgKXLzl{p^U)(6hE5E(^LIm*A7VB{k>A zCGYcGl?7XD>mJ#B|L|ea&G@>Xoi_Y@%*@P@UnHwN-CtI5-}Jv49(<(vu-w8sJ?!ms zrcKi;zWB!}Wuudv%vAk7A9xn^M88@0-@)Kt>Bj2^o85Qlh^YKJ5_Mu-Y<1+}#KUb0 z8M04)oVTw(9(#8Fe+}+0Y@Vm4b_hKG^!dE~*(Zw@DLuTzWFDZ%kss2UwsB)&jJ(-` zum40oJuncsJO6^m9_JR}hq6bH+;QMBtleMdc*h~`LHOoa<4NBa`zU|RTD0?vdh?f^ z+R~ygcgr%lscjNH`0JOHO!LdDx-PgehgN3*u^ZfUj&MIZl6oszOgIxBk7(a4FlVPaxhuWR46 z|7Vxmvu@QY1>3EA6y(#?%*@v?YG-S#pa1pZ^B(r~n)=>53-n&zdAUh@r$O_dMY%#?uU$x#f`U1mWa4aIefu!!@s{r9)FhLkBf^d z{PSa_ar(K~-PgFSC)EA==zS!Kd+)m6FO82iA3t(L*0zl4TMxVVErZBi{%QX|oj%{h ze!M6lC&%XUnwqz4zkYmqSzK7iS&@<=NemXPrGdsWR-Wp3bS9A00_Sf%!P@k~jr0yhN-Pp*We^)9BZR!p*NT!8tx^MbZ z{L}C6ytDhZt?}BmbdUBWjPVYK?@kMpIfD!iA6vF;#gSyL?(QJdtRU4Jaq8ZOuW`}wr`yDW3; z?+4A_edkvB*VHhTGcB|=E=uGzHZq!Fka);L?LqPLbJ9VrKYmnv)Z(yk5xn@~$8ou; z;$maDS9Tc*1}jqXIOD!vT6%ictXUCJ4E_7l-z$b))tj=z{A#$cxVHrVzU1S12Je;K zcpjfkejL^4G)eM>p&85lw|A>KqVLMrmK?mYGI++vFE1y%n%}VfzU|mb&d2w67IR#x z-S_XtbmFMjaA;o9|kwe2$%{;+5t35)sR$`V=p?hb3(=4AKN7Pn267uH|h^kuc?$CC~3 zmYk3MdSk^lX~hM1Ys}JWW6w5RU6z!MjK{lu9=hT6JDrtfxS=-Hi3e6W%;-Q^db z`0pN>_H8>mKiccZ9+|6<-{~A+pzn0!@4@Od7LWJx+p_bpt=wC-Q77GFw!SR$VcYvP za{tP`_pII6yw`cbDp#FUzTbX3kDFSEY<;-<-`&rFugh2b{_yac+}itgvU1Be)h#S9 zD@)n>^((K|nJ23!DDoy=fBioza?6Izru=*7Mcla&QU6hy`@)kaCyo2*_4;+o z9reO;+3)vl&UyLx^o*hp3l!|_mb^FQ{Ky}fm#@N%TWw4@XWKb+wGGNJ)ghNGU-Twz^~2n6+PzD#VeR*_SVW+N)H@|+HR3iHc_08Ge1J~`p+O+z+LP=n3Xjt4F)ihA*V*QZtQSfUWRYv~2flbFpjU%`xrDYvbEEjh!HZe1Vj%-zcC{$)1X-|6+X z(z9ylA;_Rv+GNu#zd80kNx*eUhkU5ykU#a z`O8T%wpC1yXMHS_e=X79A9Xw>IazXPpZLG+GG;YQ$}1~(F|)T%?Bxw-piPT}K)T6a#ad}{w;*REd+534N}+jGzMyU(Oj<=dWj4>dbK z%bY8Ck5x`aYRO)=Io9_UOijIZc755|6KBr!Y;_V7RO6AfnZWr&B-O0Eu<%jn{Q7@i zx8MJJs@vn+*{7n~aZf(IUcY~H;$yzJnh%Yc;`(lzlXeF@k_(AHdp&H=0p003m9K7m z{H*cr#y|G>brw}mSj=zi|9|4U{hwo!H7};?*7pZL+;Qa2Bu>XhMjr=;1uMG5m;OF7 z_3rlf4|WtTz9`l$f9G)e{Mwz#>8G<8ryNpw(AdbaY4Sv0@9BDJ-#V6vNfoQFVm)wZ z!p~o;ug>9$|8>KgE85a}drZlizdX|GUj1_^?kxAniQ#(v?yPy-gODFTKkM%4DUi5d z``_AcS8LnGP+rSi6>ek*~A!>4T~pwrEJlNY9@*`@Dg!?%i9rPOTBS`FM43+^n-R*JS0@ z6e#+af4X>Xq4HyYxyLpZY%$ZO_Lj4|$(hxPMTJhE%2ipAVQ}u8)T&K^FJHdaTphIb zIG^IV=EFwn+h5#&%N{xTzwO;m3!d3nd(Qge(CQgzT5Kd9u%IWpwG3UAcR*!;Lyr~yqh}@O^^SpR8;Zg#m-r?qGDs` z{#$r(doP!{!M1DW21h+E37zjcHhH>W<=T}O&!$i4oOySFVK>*ONr?+qE;TMVb9mM) zs|m%%$&(hP#vi|=>lzy{Q9uE$JHAVqNp{JhV(y^5bDcwceq=qqTK4gQ-np_l?CE*U zO-y3CbG}}_wLjwHjf-zBuD$=L9w%7#>%`$=fl98kx3;d`v-ZM+b-RykRBAd{S(TA^ zM&ECF3fqaxXQh<_E!0;Q@`)xb3OxGq{l~}04ZKro=2?{r-CMI_`Snldw-tV#Fk{l( z8ji}}-(r25?_Ty1sGT-#nxXIie}Bu~|KfIe<)8gOA!Ox*^Hc4VRhB=Conv+NQ@oPB zq|oO_TYWBE&p(+fBrdN1(se5>vPtp z8;MUU({uEt($b%QQhb|#F6#82(6F%DKNq7HE<8BrcuLhbNx3@Dh?X}GFEWJ}6=~s?q;;lf#>`7 z)@ystyPVW?_v7Vsod@3pY;5jC8Y(LXuMj_F!+UPqws-zpCKa`F%>BGaNh?e|p+BMi z-m5!13-nHN=bl>;tKqq4)}{G8hZw!*?B2k6@~&*-q(f^?Ki|G7^2ricJ~{QpUUO}J z6>G(W-#>HDb=#9^1uI+q-cHd}&hOl}a%Qy0i`vz8eRp=-ehhe>6utb1{my^W{XQnY ze8ObEQrtS1cR{e*(px8I$=3&`9S|-5e#Z}ik`yT^xbMf3h&(5WeXw3;#d&kI%? z3%h*(_c=ik(Mb> z`;2XV-L+Y7g#Mp3vYk}z#E^UZ@R`z*Eyw%iPe&IvMlQ0*e5A21=Ku5iK4rzliB8vg z8%wjV|NHjyxvqjj*!PL9i5UuHWUi>#nWr?lu;_bm`KJ z19pXc>iYkFKDUn%<1Cin)1Mu-|9^dq80TB3&h4SKrW+#;Sw1>Dd;7<*U)K-+&B}^$ ze)Ih7+WNWtcT&(K9Vx$ms%NGh*59@1ckm-wr8lk5MLm6e#b3D^ z867S-d;fpV^G$m$JnLDqSmke)SB+7Jn|9&5MPVUt`Z!EXO*bW~yUIU%%znDf`A<)e zE^p77otMmi_vHNL$3{^AR|{*22vY!jYpwd>fi zW!DO3OsolPy?y3M@hvO1ZGClTmMz+2p!;yj1or2-OQW$SrR~lOltU^mZ_SqLYh1N_ z*W)Aot9V_u>fHPMbb5R;d)2r3CUY~dwI*dJ0HvV$-TY3PV(#9*XRFmq#xjlC;R}R-x>B&#ubY*QvbEIDKtz%(Kqa zOP7ph2rbz;f!%$I`2shgR|lKfH(d@EG(SGuIPpwI65)3?pcb`$m5 zK0o*n5ErLca^mUd=ZD?<9(m_@lrJl0_7+@qPq_w4M_1N?nxzWnx2Bzy zU-eus%k}8H4hQGj)1Q}YTb7#6Kif6d*Hu5+d+WVT8Ij+T1XuoY6H8PUH{kt|t@eZM ze(Bnknx_9s9P?x@d;DK>=Irl;KOKSowvWYxjw!7#?d@Uro1dGNy#MFc@Fl-r9-Q{_ zb)nQ3s-KN{wR2BdvvUI<{^$_&F(HSQM=#qFL6t> zeRqF1V<7kTUFGlY-}V1i?wy>>Y&54}sYk&Vi3g7^?z+?;<*eL&A+fmY(`DgBd@;Eh zv5wR8G+#Ag8JpWH@Pb1$?tZ}H#Y>kb%~KU^z3Iwjwt7{@ri6o+K0bE;_#pKOH{X5> znI{T6!rk`ES(SYG%b&9F(Vx}@nI{%lRBp?M9f)# zBKu%kQRU^d$UUnzb_!l+VcmY|cnoLbsG}^xR$UZ16bnd28ZVRau)DHTuj zjJYSld1T?0i`jqcCT13$XjRBusik|pnO3DQT(>SYO>_5jqhQg`51v`{ zY<-`>%X{ONti7W$v><^D`dF z*F~sb;Z9Ag>-_ik_uq|m+F62DRz)e?jtV?CQqq1|K`@$2Kr7j%-*isbLUU+^20MP-Ml%IGxPf6Bg_3~A9%Yx|Nh}reSdMg zs3n^0FY7-&x%qy-cz*tKvy2dkUU<*@d5* zW$7wmUw3}W#+ulLu^I;#PFWp!ZO($WumAoO_wWDs?0Co9zb_Vl|54#L&*pGSc(}OQ zgUO#CNa#M-5fYlTgHd?T-n}yd!j@T--M)IYRr0}R-}w>}{x$uNgSKvsj#yZJFYenT z@BBi0p9hgIckbJ=C1l1T_NqI*x3|eZU<+bNbg@S8rl08%{X}ztq zv~j-D>Fib}fBgkI6S+7L}GJ+I_tF?CO*sDG&G*J?8J;AbRqyNTc@6k2>F* zW=@SRta~lqvTmPd+La3d$DdCXsSb*bohz=Wkl#Faa?WqRG_N05SARYFbLF91eJ3TK zpIcY^=;88f`HscL=GPxdZ*S)%OZQk6&RKrmMQNgx*!AVU^Djk)RHUScTs!$AIOJ&024ha`Uw?eK(=015 zdHhtnRC{WBZg8{x(uBjtd$wk-Kh&U_wY%tP*9EqfhkriQaI&55J&WJvRC@*^$3c4d8da3WYlej~fJna}OuCVY0Pz_!1;|6O>PtlY+*nqK7e`Luq$ zL9?)WbZ}7Jr;n>Th4~uyU%qth)0YBwBjE{aFE8JI>8wp}sgIBM5xpPT_X5+xUhkin zeXXYPzv4u(XY=c~B|cj0#uu~gO=VhS`1Mmu9Fz;)_K0(w`0(K%`}&_he<`q9UOuh< z+Q2NY)9uN|t4{ne_jVM%o?~eo`LWl0TEC>Rk+t>hd-wQ0XXWHP3aQu~&}6O|UVMB1 z?)g=lQd5(zt@*voXXc6XnyHT;rWpQipRf7+{QmdP=l^%J_;YIN=^1SE)6aFB&wacx z=Ro4)Hd9g2sb_wCy&k^tw8~v|rvLx{d{)1Ay{Xk(KF%cnO+@6iA15cPuc^qLYH(-v zOhvD`@lqLKYk$o!f4?CiOKtJP8`-Cy#d~>q2~2G7yI1pi^6OXEAPLyzlx$P8KRdtN zfj^%$ibU#veKcO?5;d>i{%7CGH=ECHQNI&$s%OW>EXQA0r~FHKao|oU|D>$l+)ScN zEiKfdoMR8)&6iky$X7S!%a>lm)=P_W>?Yp%GA%S{-Np?clR_#A9jhumSF4&&P~wT+ z^ySjI+|;JS>P%|eYR;bg{GcFW*6ddQT47`1^yNW2b&Z0mL~dQV9+Lg?)mO>d>#e-? zhJT+-59i|DQImE#e)9ZU*XtKD4xL{AuIhj6x^hXA8`o|>zq0GvakbU2?i4=Cd3#*0 z`pB_cznZU}j;~r;%3J#D%EyC>jO*9_>F9S~^x@|JzXrLpc%J9)i~4#)c$lv~qUj-xplc;CxlNJ8j+1%w{L)mtNY|{u0kqT(q;B z;?HbuuV1-(Z9v8ZnPLs?r#7pn=B>OSs);elzMxw1f}qx)m;P;kwok74wbFIhu3Zv` z%kOvouN33WIXkQLyv=8e;68bCw}@^T5gWeRM@Kg9S+!~vSK)&1XJ?!;Rp8qHqexf( z-?!Vpb{0S1prqkrdoQGLNe-y!k0itc_Q}&~Rq(!-%d2 z+FUgivI2|rV&!aBv`&52S#{>jnH{Uk?#>r_KjHWL`D$m>&i|Gwsa1(N-1_pedST`3 z{kwPS=x?!9{;77#P-w&&C%rIuLpD_3XUx~2F-YVI>tRjoI_-n4yfem7nBrrzJo+ECAr zSASWZn0+tu=Elc{!v6EW$eUifcKP?o-v46h=`*b@Ud`TSm)^p>V^8hu`yGaPihr4p z`@4LaHm9XxiTn0~5|eG)eC_JpGEP1@C#!Cep>wmXjjdnap6y6U(4;rBrE1o#Ti5sM zuf`|lvq!hd(R+l zpLJ!!p+k=rEZA2webR;fCG!u32|X>{pA)mAK(1%wjrfR&BVjk=YhIQHHD>@?>(qlB(3ct1tA6jC$j9}NbMor& z?QLflJ-oU3dHS909xoo={r>O$_xq{;c~9-|wak-y1c+1t+ z{r~5D{cLo-oMq9Ttru2LcbRg*<=54kl#j)?x4ljIe?YnWg!%nH4W-=IG|LuzzBiGR zedg0jL8b>sU&i@d);Pg>K6hU2JJUBl&t26_OqL5KY$};QZ{E8#C%4~Umw3EdOk~O& zo61?5t+Rz@u+8sS^Ze6l?R(EYo#mKcyYJb7#=}bdoSmI9m)C)M%ZH{5SLiD-ewpBL zSzl#FHiO9Yr>$n`D)(Jxy1`1@sl5GP?(#L>`Ep{5%FQ_8f~FJF=PC+RY&varZ{6p2 zaP!_`^%>K3e(!ZFD!Be2V11*r1pmC*68~@iPZ8YlY5lB6lVs=bjgQv6{hR5Ql#j{1 zUrnV(Kh?BMZr`iiYogE2CeaXl-@(FWf9~b0(&ok=pGn94iGN~uvuk=^?JYKTn*!lDAn&0Iue!gD6Y5zWfDGgsUb=Vr% z?{7$)oY6a9lG|R~?hlXkCTSBZvt-4ayEcUF%6Wf@{TOTI$xE}ZgiW6|^~qF`v@JJX zcBRf3!@B2^#>*5Io|<=;ed4+&yQcn7 zH){U=@%z)P7g6+3o0<-ZM8T7PuT&C4<}va7VTg@q;;-db+Nzbf$6jjJiO ztgJuR{eP^k_J7Ma@j$bm7cT4*eka#FW$TVdyT9K#(4#G#HTzfeL*cpe?6=3q$ICNK z3zDmUG}Wx#k!@~M!<5xGH>x`?=XuXpzG$uTKRYdLT?VydYSUsRC8M7!)G}{gw#>~S zPnC7;>*DQ+XX?uD=e~TjEIA`2Fhrs_;r-ckyUgn|F79@o-|%YFrjEDK+Qpx5-*~s%~yAT&AKsRKf9uEh zxhDkc|JQ!E`Tb_{iG8Pbad64$&2Qd)_TK+uW)-pj-)8q4tk1LU{x5SZ+MJ=v=#EuR zX!W~gAv<1q2I?w0aCo>l9JSYNKBjl=!UZkOGy7`d%FJ{yR^z5}toX4yC{p;+eNK)o z2Ay{^R&7qxJ&?iO9r85j#fK8Tn2r|((OEYbS(e0~J#b*(3CE8&lDj{Om410XUwXGP z|GCUCv$_9&@6X@$SjXl1qUB;QPhJRKuXDCrK!IhKIMeiVIWMp7F3%0k{4QnII}Ni~ z=3u(p&-MO}i~OR~8&b2?-%J(zxnvO+%aV9sb#?XDx(T*_&Th~1c)#w#nr>}=c{?>P zuKP#XYnJo&&*Le-CA$2l5L1i8b(aOJoTh1a#-Rtkq60?>PvblH*7BTrtD;j=PoC4& z(a~A8V!@uts}}w`pS9e#I^o-!OpD?(H*Ro97HMf|J$@{_@AtpyckaCV6<=6Y!e9SDYcRRpYQ)Kx#`NrB9<2?7syDjQbe0PfGw+FbXqW5(II5Vo2Fjr`grr*dFA&C zE?+7R+y6h*f7bWA!f9WZy!>)v=Y`1&-Q&V$iQrl$d}k)din}u}FN+jL>#me@Fx|3h zD?j0Ld(N_T`(|$3`DjaZ?zXpjY`@y3Z7*4|t4giJ{ccdpNlS#ucdS@Cxb~Ocn3N7% zU;5yiVnSkS+xNGx`>T{#I2vDb?D*X)V^X$pYx4cqU4B0--@H@*@Q%g(tll9vJC^)c zucFLfZn*zp=RxOZ2Rhr9t=qGD!b1L%M>A&DZ`j3`w{V}M@NeGPujQ-O*DU8UjjKJL zs8XhPa8Xpjj)jMxv3!}YbZNETs~1wqS2RaPAu~>f&y3vZ z7#Sab`$mL*{B6D7dGiV{Z_Pe8uX)ZL=SxE8)70h*n}rAORy?sR|3=B5y5nwdL~e1U za!2QLXPa+%b$9pAwaeD6Tex!Fx|Iu8E?u}#_fuP2)vsd}c~`E6am>}2qMH-5aQ!L` zp{v(IriX=2dm%lesrlUbKEbP6+PAE}ItuT9@G;XfZKh}Xld~qKTNfUA@pSc+%r$Rn zd3nz6`Q3NvnrVvN#xo2RQ*Rdk-FP=wg=b4CUsGXluyX#PmP6Vr!^-BBovU>X_f1*0 zfx|8;c*Bpx44rN(bE9o$T(dN`?7#LpD&~mZLbukQ-;x|ZT7CL^g^IEtn-`P30=84>uN83Xe|6J9nv(f{xW58CfvFy*$Ym+w= zludivZDMTv)^qZrozTgVKkCr++6?Z8tIJEc*R9duv)?!`TR}l2Fycv3V%#dORUNax z+9;MBvDpyMT6wZiaCh~+HyLg%4J9mZVwz=Ro7cSAb51U5^`fcm|2+cQAM}L=Kh%qB z{>U-u`{Jcb7thku0WF!@>bOtwy23ZfqR2wNh*A!{d5KxCq+W6^_tA}c6Pxm7b)NC$ z+v+8&&a5#xe&o!zS6A6ioQg|0pqrawviDGUax$~ks`YDEt<}@l-yJk#cEwi5OFzG* zg}r-l`St7U8<&`z`b$fAE5swVH1P1{w($AvSYH(T_;YEs$aBl;km2X4c+c5 z+S-=wNqzq;^<4J#SuVVxBE^^VlA7ZF9ex-b>uam~Fs0O$Q;ea-x8P(z&aP`ETNkGC zE?&1SC}Tmuw$drCvz|Xc(*5Z`%-*PWxvg7De0AomkKL`76CE12Kq~rK@C_ED=zIaq z$1bT)k3N~<99AaxEbJWDbn`F0YjR|3FIs8-nouOa)>PH?=8m}qryWD1=5+frD;LLP zucq4FWra1$Q{F{9yet=<*zBZ!(XODeBg*gU)G< zms;~T@N6}m+L3kTqM?A1{;J)q@r!RcB;_X7ZFD)kE}?DiSDr0PUMR;V+L*98J+OCS za6DM%_wUxNsy;bp5jNT6oD&xtC&Y1hPUl}Gw`x`t`~N-p#w%_vn>p)@U$2Iy&+f9d z7cb|}m>lg<8_O#-E9%DQK*O1}X-nM-_U;LoARHat>#KNumej$S*Ol%_S9%E4^G1FC zv{UztoN##w|I~BMi>FR*@7Z+TgD+i9AaYGXR*!JHS;Zt{$D8S4r+(g=#dZI{Mn1hp z(Nxyfo#q0o67F5x5UBcI|GlKQRQT)cKTYosU2=`|%**+~H<4H9(2Pinl%+09)aMC> zYHBw*vNdeFbua7GrvqPj=jvq1$(MR~_53+HS=yDi)ph@b&bN0ZCQhs1yUoqxoAdh9 zk%|8Tl71?xzFV?^ThwTGV%wds7W*O>oy%)lTEAa2ZJJ+YWMM>Uz{Lrx5S7dWKj#I_ z_1gC{4KCI|L!N`Nm7{{SCwWSq)0e=+70Z^+)tTe_Z|by?ILWK3`FEO4b+WcDF^DeJ zNaA;ESa$8Ppu<1*mz_TksE71O7# zb&vGCx77H5K`J}Tl8Q@LO%FB6R}`3>KfPng%9W2F-MoL~jGkGI`ivwE=l1ii2^K~b z3Vt$|9kr&Od9r!c?^POS6WJQLZywmVmrZf+9CmY~Vh*0kPuZ_uxzxE+Bdtrc%59yN zvvT*bW2<+*e5kPFZmqk^yqhth^Z1kRKj;*@wl2I#NLcT!>qYepr?My)*OfX=5vRDn zl%Ls}UDo$cNlqqrBSV*e!1ka&9JyWVRF1l4Mck~ODzb#@tL(J9HEoM@pGAbmooHIT zbjPZdT2qtKQnHpG@_AU*yvO#M5-*f)hv0Z!i)co$-d-0;@9a#i2BByZmG^&_WDaMT>((z4Afyh5hrX=ZsGwPtDb+wWZPL19tD#EVXn4Q*;= z|Ng3lg^Fx`5t5qbWc%aks#Qj{waMx09X%|S)-?5;QCU#FK5lJ#W}oX;t*!@62b&Ko zX#U#3Wvm%0SErm17Tc=uZkl+;yIHS{D&r)s-fK+$plQ2AT+_Fv`Ixe$#f~~VcNyod zKZ+lMC(M=+*EH>&w0nQfoRyqLTym1@Ce|;08Kqp)q{Pn8r~3HdLz$hC6AT|;`6s_% z$`7qm-Up)Z%}Aa5?(QC2k9}cVmnoR@6X^~XR>TU5UGuOp$ z1yY4m&cPJh$~C>+`bz$qof~~!zw$bAEni&bvP@>0dDy~>*RI{!@lf60|K@(i3fm6{ z);PCM`SC7rVGvcVD-!_`g*%Sgq*s?=+v<$GuPE zYkt1H9{>N$Q~|rcPuA{!pZAsX?5C&tc{exZ-u(2vB~1m|o-46tiFg*nW9@3WwEDJM zYNDz#(=OLpBI*7AB(#KtuZL**zq;h4^)*l0;pMd4-%8!{IvmpPD{kxicgyP6)~%tH zk`uW;W@Y6ZNaFW6S;pG7{Mg(Vm-83(t(&;VzgSzcUAne#dd<`PFr@C4k z2|8-oxU_ZV%8$pVRyup^=&ba1G2nZ6XUnFK2bN55V`kSX;{Vi?T|CG1vhr(_?t1Z=iW#y} z&cfLf;}Qd%gRfuQE5Cl#>b0jlvrj&4TIHB5HdQ1o)^7h)X%F!|zj~66)mS(61~YuR zvEsvoSp{wndqm6}J5%lGp;k44|vyw&`5|AAf%@!7nE) z932rMA|hgtHAUxu%fGJ&+1DG&*{G|VD=MD6b?emi>$h*;E`HorTJAqXb<_Ws$Ir9z zN-?B*dTQDz-@9}5>d}J-?KC!&9+%IW|Mz~SZ2 zA+=~DpYnp-l!$P>b0)VE^DjwD%{<}v!f8|f2k{%z@=~(yt}cH+Yt^PRQ@WoXjpX|K z;DmLywf>xUtiK+6`R&n|7LzzxZkfr}lGGaatUVSh%n}atthw^x(I1hYn#wywE9C-C zU*hoCKBe4smNjEzbX9u;JA2!yih__v1-CCINr883+EwH16a)+I-1g92biv}&7q?b- zZY`5%UeV7Tl5M6`6?QH>1v?AiYghM;W`8mLGuf_8mrA=$4kB)iOI(8lPbf4gO5v>Y+g4{>~h23EUSQT#t>|?hURjGxXO_t>P3>G-P=PkXe)Vtxvx?tx>=Q~$DjM}V);}<_vH&2jcsQOc<_($e_*C!_x*NK~s zYS}(C>1IsdF7u=I$HonX$K$x3bgf*c!*=4(!bwQGip!0d*4A(P<{{$7!~gm7L#5dzi(`u>@8P2FVyzahdIyZ*GoP5I_Kv1 zHUB@y=h=V$=h?%4|Ht=dQ?>0M{Mwk@_Ty*eXQh@-;Z#A5ix)S3zpMZIcHiC$*BdrW zc&+-&6VhtFP{@+=`^(P%YrlW(N&UUxvcBt@`fr_mzq9JSk1cs{We4M8@lO(N8w_qt zoI6!)r2$)SvFd{6gF%+N*xcs)dfsQvgTcR3d-yK_Kc5IpO1QAuUKW!-- z3o2w{ns0n%R93sY>F@L@QD0bC@?Od|afBs=goiJd(mZ1Ho2AE%FXL)R#rz8^4>7Ah ztu68j4{5deJ^g&Xpk}M<6XuT}rY`OE>#;mH&w#74Y<oW?Y$kr{nOR&# zfvL$ArEAi^?wrYpwH3~lWmXo}PMbEf#&P+~y{sp*^PV!L*7+$dI;8l(#wTs@qt9m! z&acpo$b4_79LSO@TUy5L_U?@(hq|`dlch&o-#kh?#iC>Q>Xp>BtD(*Q$7dY;taL^8 zX84-7srB=nn~k+vb0?ma>CgMJqKE4^Ly>WEgmB`TjTbmm_L(&tmb>j^7Z)F{dsO|s z)BOu~E-LqkuGa3F7&j-p@ZgOz6Kz#yH-Ah`uh=ABk)j#$sr0j1-KME+W<5(nnKi`E zH|DH%%nbCqvh$it#B%;MjwSmyyG!g-`sn@SrM-^1vX$w-7Ovww!rapOHy)fzJS}=` z*{1sMTBo^R-tZB<5GV@koENYwT{yh*1H0S~d2!K)0(02iKX&Wyi{1Ll_u|pIFBgCM zeE!3&U2%1m{B`?JCzz6x7q1Ln&av`CyId86+0mm%o#bR@{bl!iaCWxyg=^Q2rAJoo z4CpM;Rx(c1g)QV261;HX>dfya!?Uh07M#o6Bv!XS@9iYkJ(UlxtbF-U-21Zej%CHH zWnL1=+YkCYeXgo>Qu7A~FRw1k->G7$+{%)IQ^Uf;Lqp?2;-gs)ow#zzlbNx->KG&Q zvRIpc$ItSwiC*Gn2w8%eQaOvsuJFhWCB3U>(*^t zo8JDsq`R|N=$!^AT_rtNxxG0e>c+8P)<28G=L>ce9^>4lwd2I<@Ye^9b*nA3m%`7}~&V4zx==#J9chocH^z}K`*1lb8JUcW+d(}FPmeog2yG!(+37Yx% zgR@_&@w32b-G?voPn)UvP)lg#>HcY%0t!8I<#l>&m47BxJM{H0+x(P;OwWlT$KeiR-teSGKQsZUBQvp4G~)gE5TTx#M3p?bEHDK1{At}?&Ztl_GvdHvAax0P|V(<7;U8yT`iPi{Q^d2_Uw z^eo<#{d}pelGD44KJdQ0TF&#VFF#b;{eST9{o!x#oKzNJ`)SEFY3Js)>F3{Pr!NaafdkGjS@tJ1I)AsXuH%Oj$qc;rkT?9R8pv#0X*JKIhBPrQWg-M*m7ap&(H z?FR?5wN72vsXUSXUGCd=mhX0of3N+d_}Tv9N%09ApDaF^yzbhSGOzwqb2x%` zKLf4a+%7TAPwsDP$d8k?_d92?^X-w35n7S>^7emMxm~dr&!$Tp>zU!(>V7`<#$v!g=hkKeVA08+ke#T)ujU#&|kJTl6muYS? z`(rjfw0^?dBDLYk1A&PrqF0EV+;?!Y&YIX6m#(ekzc=CI?7ELP@|C=QDzUw}m@Z;{ zPgrs9$BESwZ_1=u?K;M$X68}TnCfa#{_#zG+${DCEGU&d(Nv{^({T> zf3K*l92}>4NvHnnT-)9MFJ8GKV^^bbZ~m#Iliu5MZ*TlndvvFQ>si>i&H^UJ`-#ed zyB}Y^8v2t}-0aWN>>Q(#Z_aq$GcZTK0}-W}Mk&se&HTMkRgQw&|TuU-Y3XJL~knO}hGLr@z>?#DF*O30JIQ zdAs*##~pj7eO@Rfbu4D34~OJ0hUkw;Yz?dLU$}Xy$MOE@^_pEhhJMGN)p!|-m%oyn zVO`C)X7yU1PTNKKIjzTwE{8_m65DfQ5?jl!U8`n4Joxb7Ex&2COP3fdUbv3!WuA}D z9B=N+3tz4Yk1XWtpBVnoSoPhE8r{CdFOCRnPFebT;`{JcHvZz)omB;|qi*dokIfd^ zdi4FRr^Ty^7!9jeKU*zO?-`m^XX|WSEgspV{7vgzes$#2UAlh}$6hqfYj)sYerWy4 zx0a!GFOTdFo|*do-l=Gh6(`T?a&XA(tGOB6%+6m@T-WxHc+;{(*?q>*kMjFA7nOaMGAa48XlaG<(|7vzVYmJ*)824*-cNy# zNu{QudwX;v1D1FBIUL*&Ww2lCj7nH!#LR=6w3mApewXeOR5}$Jd5rCfRTWP`=DjE9 z(tC@~JGGzHJmv4-Vb!4ExNdsh$=MSM+!YJ7-w8;wOTIkmmy{#yb7x)XRMiKSbN8|> zJ0h2RS&oe_Z(#!SMXrf!d$`>BvJKXDbe4USI$|_=%S-mmR)M1Ghc~LqI27O9{p%(% z^PWSBa;NZ@8&eN8EuQJ0qS0lsb)oNnC1zdS*o+COuHN44o&6jB9N=lY^EG1jd54Hk ztgc5sE46dZz5nahE2Af7jxGh2o@Te*9*Y~PK6_+#t@V%kq#5iyJH&r& zx}?4EuzbSKsT0y=q`u8K%GFr2diIqKqWjiN>1I0?w&2jjC%cMQewZyOr?+?C!d5Q+ zP|>3&4ckK`HfSBm&NhqBxH>K9-{(V_>)h1LKGog0xOnfnSm${%Kcg}>s4JMqmT{~) zbp5CDeyN!^xy7dzMy+XEwypku^;}!0$fBv*GcRWCy&Cy>Ro9|Jdstf6c|co>pxVGz z8kE7qLqcZEnk6BO$b~}o$Y)Nb+_Ev^62Tx{I#DXl@`Z*wEA><)|AJ` zXVz%jWv}w`Ig|6$c6;_|%UR7#lgck0HsRKW&-TGov$WZ6`QPJ39GUN{rhc zEj}M@-E(J8ce$>MUee9Fo}Ejm{ekE9H;EFLN>r2zy59-hU(hMRu;bR9FC3l92RSEO z?7np9m@}7X-AmP&o|H*h=j3RQ;NZ!xctkU}|H*dlU#;EMBCR<= z&`F|FSH_0FPfY8!s`_ymM(&l6cE0(`SEo;zD#gbZ zQ|7XK-W&bOZM|;6(Z}{^SO0AHOZ&5XW2b4(hUF^iwp~jXrEy#dnW~k2+t4;F=7PY= zga#Xl0v)G>M#f|}wP}5pZ|;OWyOw8h(5cn`$^6X8T*uqy?Kp9_po!&U#I6H>&-FR&n#JjBnz)Yh>OVPnkwt1x zq4LKkza#3V3B~K*H_b#01TIilyl{AB!ID>HMMWj$<)U$$GcSuAk#aeHgil5yEnz`^ z#tLm8@pq4vt-Eb(|F*?wczT|clY7_9e)`|H-0QE8ob3L7*Z%*DiE}fazI!%fPEqsi zvYkrFkj^P%D@RV`&NBzD9Wzug`1oRC^mM7#(ppvPc~3t&Jc^Y$bnN)z)%jdI7cT6} z7U6PR_RXjNgGno!t>?>!_LF?wC8vkShx5wG>`Z;6{7vDMTsyB(^^?eBx9{dpnLBxY z)aSy2OS;uU4JP6*Iqq~>AGvYoiO0eF_l~9YJ@C<m}MAUVJ`7C*g#7$ikP>X11pPTxxTda?R!okG0tun0R}_!SC8vxTlJl#qD!^ z?A2Yo=v-c!wWveubg?Ck!p9%E2PZT8pGkUq;rKn(1E=o2{?aJlD(6xZbtrT1GiSTj zs#@maeN&p&pDS_Q`J?hsNuskJ_vWj`s^TKc$_{R0exmf;3~4XP0of)8^Tmf&Klx`K zP&>Eof9>HFfyyQ(CiAL_9*W1WOFiaqe|1xK+>LL$-`CYuT&&5k`~78ayL{!eP~FHs z4GR}4KGBWd#@So{?`Qw^`*mlJ9C@<0e7bIZspxtsLz^}E@%ur0_Ds*)?oN4pZ0*d* z@EvK_d~R;@-P6IIZgD{!+^w10=a8Bgmy;K_eD~Dyh`?FJTR&>`Eie8qeQaexFt_-& z-SX~xKdO1;1%yYpPCmQCE&To({}(D@XFMlMrJS05nmg9gl*=tU^?B#{IXxLaCmQn} zu&MqbE+?jC{Ml#_T8W-EnVdtOxqvvIrj0TymENgH)qR-vV$F0Wto+^eRFM7 zznkq*eA&KpYR-9=KTi*HeGYppakAs`>}EE9F&GPhUlD>Q2?s z<-8QR<#hj^^xRly?(Fcob;+fnlGhYx%seD({7G)XM(_YD^TJ)LT<$5DKAhA3K4Hm! zGlMNVYU)<+X$+VwR4$Ug&aq|F>hl6AYrOW!2^b}Y0FkvyENtB-d22SN~g!G z+7}O=9XXPst*xD(fAjy_moFvj*X)0&|9=1fo0f@(EF%17mz{rcQF&4FqghhYu`zRG zW#075T5Ep!bHSNkCvJ~{{eFgzFSl;p>LK{<Lf9a}$Zs_-?g)XMf%*-zhkUj8f)P|Lq!U1$f}jVGVo>kgi*pNp?@u>%n~&?d*=eWzPOQwl^=8WZb`8A~E?qTy1Qq(Xwp@6~pSILZA!wg3 z{~3mOJ+A%npt&ZYB_4akgnla-LrZ<927wpXI7&pd{#K(IHYD--B9l5S#HDi z>DAR=FS>XCSUF<`H(!17&A`;m=&jM}(s3(8 ztZ9ffRtwyfE_9Z2a1^!*B=Yi9$^6lXU){vco_xBZ;KG9G=W~PSCQ8mrEckW6pOHCT z;o$b}^#6~VOhnH;byvFb<=t8}oiYcv71r6J!u!IfMJ<`hb^WsW)>6KtUuNgexH#p` zf552|X?ePSQH})1wPVvJE;wks@u6DIl|$U__3n1Ut;g;$KGw~#t;oxeap=0Dx*+4A z?_oR3XEP3kb>z0kl{SeKXGhlX#C7QJEBPgFtE^r2_P_)2SkIKuH^RT!9Ns++?0+mD zDI}V>Hh?YuOxIM=qZW?^E2FAbpIpbiqSED*^MRH9KdxN6nC_a^!(y>&^AQ8%hw6*Y zwYt9CB)b5WqfG!d5^0h7JmH)(lpm-Z@6On zbEl___LFIjTvF=a1rBtS=cV}sec^3y(vabS%{zgc7!x`i&M!a2cx1~zTdx_`O;F!SPQl+Bc5f#AE|r+hhJ=I z9@Y%s!b&Q{m$lV@nye?kX~X9>UZbSPPxfwl6eB9QK-S^#A+NG?#o|7bA3YGewK_b% zM_2P#+YQgF#VmJ=U!JjFl^e0P#9Q;;iDl0fLG8#3-B!EWbS`sP&)l$Si>cx~5edf( zu{h7Yw*uO~$OgShWL{~`o(kgsR6!$bK zsi@{-dTne>*R_RTWt|QToibmnxR8C){2#2n&VtsBB5}KSbnN9wKK`-aH9RQP=J~|$ zY<>>Q4tLIe&rzLzbCP15mDMkuWFBoLZN7Ux$J9^!{c_~;9u3zA)l-*9aOVoV?oGP% zwAtxt!^LhX>1gE_8=o<5l4JY1(cl(^`LtIVQK%A zS~jJ8q;nS=^Lh`+I<`LMnrkK7w|$CttB+=U{ol1XaQCH%#mgbo-kM1ugG%qiC$HKBf&}Fk_#&ln?@(Rzy zsA_dNX}!Kh6COFv;EV3E-EL^Km3jM#?vCwGi>g^u)}1(OlqzDDA00CxFgEf`m(E+x z^^?RO9V&19?bl;}zd6KMeKy~;cXtJ^ z$;N^spBmQf>pamWKJjbxYD-DJ>mDK+u{&HAoaljsWrM&APmV8TC55S!;LrjQe}o)HYo?@7o(*ShCQdQ+Ruz zT1MvU+m`HMVIrc>j-9?OArUP->&7&(G=n5@HnDB@B6I`hsyaV$zkh8Sqm2bd+nLXbnmN|>ulwn+xs>n7O;MYpW7*@mQ*`>{4bjcW&DJlgqPU*+Ko}_Imk)v%8sAx2|Yfb|iX5;E5UByiZb( z#FQ(%?%pFmaoOd{f*b;fIU~vLp01Pf>QF%Yk3o2;+vER>P!hooeQI#NgjnUl2yxh21_yJehN9N;#oeyXs<3s8Q6W*b>sPJZSzG6q^k?bbpf^u*&#bjI-BRN6t8G_SaGj>;1%kPke-M7Xb9G|5J>$UO2}%Gbw_`DPYcnoDlb4k*Qov^xw_p`Mfd~4-PasuU{AOSH8(F7CLbG#fIez zudeRfJzKVXaR?9tFXGdjy|~0XRnFsn9LpbPwJobXQ(ZSscrx{dnet1S6$RSX=lOyk z-eX*R_3F|3*`_8}!a}FKcqO&#Cy#%pwUkB1vWXj)wzZXgX{}$%bLP~&+b4CG_@3_i zbf{x;{npH5yaIpn5;C5N8(BVkRJKlpOYYy|(yu*U-7UM`sbs&F$jW$S61R5!TFuvf zkC$zP)@TCo)te1PEJyz5X>NbRX_}X}?$|N6b?fwO6OByRAyM48u92ZA=?9xX=w#Bv zg(ci!Czg6`f3nWwg$v6muk{>zC1*!0)(FbV$apC|gX`>+lV*O6pQiQph96A*&ElJ} zV!fd0ne(0NHO`7^x`NiOSnX!}RmxLBjyt$tcQBECIc-s!Pb|Y1 zWpd*_JLM#wUTx{usO$DEY17Q|4k=B3ouwTb{IKs`qTf43&yB~6FDqX>aw0@5flu+{ zC&nDp6Mo%HqJ7b;_pDL)bcO9j?A~^Z9c)Ki-fQlfbYxe+kIpUXtBmJISpFz);Bt@8 z?0jyJ;4FN5myMw)U#hcntk)gSR_9m0ysEF)y_s&M`OD+Fyl(FVjI67oRFEF{kpFCb zkdhuEzk4_y&`yw9d`4(df85EQN4^JE6wTzkr0dGN^2Sub9h2r3yuD?5RoP@~$(3ti zqSMdLTD|h*rkT}0_)HD9obj-_Z2l%rWb-AZNWX$5ZiZ&t-Tj2po;@;^u(9A-XLe=d zGcCK=z3sEK`hSOoUk|w$7Q)(hZ!5d8^zHhqtF|7#rkA_b zZ_WriyB_PLal_WaWhYcB-e z-Pm?I?(TV^`v>FW?+dt{KW}3kW4J(GrGaGvgOCH02Lq=9qe=tImd1u{OAbAabop_9 z`;(hyTTKjR>n)Sp6Ial+GSJno@{xXYbo4DFL&KMiQ~NJZ_hI~YHD*Tv3#Z<^ zxl=oQZFbfnQ%4g~5s0Y{UI#APHhHi%FFg~o{ffjo(fdrl{N@;G?lnzc=Ce@iJkM#y z?vU8`h%hE@8$5>4GM zyuhK$KVeOceydc(96t5UCy!~Kuud?W!lvZfxWKC}G%_u<(CBhnPpYZCx8A&o0tO<9!C~kYRc~YntFyIWP8fDSvf@oU*4v+dVXzU z`q_E-RL1-XtrxV`Ximuw4ZM7J|0K?pIg0~UE;{_s^sz*A1?$m?XKh|*do9%CIv#P5 zZQj&?eoFZ?Vpbg_pe07$$gJQBR{D&$AO!+Sse zL?5|&|9Aa|PoF|wOgsJb&ZaME8CKQb{w#hyPuZ-0&Cc&TpPTL2VR66u{MzcepV5i# zAxfZxG~s}R&<1XvJ)htDy4e_aEVNLqJa6pZlV;Giw>o*-cF#qR!zS<1>N)ptMpT5Q zW9RcjYgnu#uIj$jo9w%xl<(op+n>&eI|oiVv*hrZmy`c0cDg+dv6!;XuKY{A&-N7C zA3K%f4X3WkpY2)Ys8?}niepx5ab?&UMUhhnFP49J5a#uM;_Qh5p-yk-SY8MWEIE6( zTky~1#%t%7_8!L1LS;uwzRq%+ zV}2#nAbf6o&;4_oOUgd>G`&2)wj_>mvzGTI?#<^7clU3xnR0&8Y=54X>q=rL@0^u- zIpd-|Bq4w^5;z+TEs=1ybBx z;Gox4TyQ*b{hd9b(^dcOKci|<@^$;}GP_>_vT-Lir5^RXF4g6oarOh(ntKZumKpD8 zmHoyNx-&pS(#*K!muTNQMc&eJyN{cfEabc%b0=c)1-FRsRhM^*F0aw{+}+)B_3x|%r@fK2iMg5fwvP`ry?-Yw z;_j7MURL%bU5(?EvBcYu`oLIEuUJkSt%tHE=O?{qvKQRu$0*)cf7(DgGVt;FNu1}~ zL_+mX?X}sYxTqYxAUf8>u<-sQu5uaS2kkr`T9B`#nvef zovLrDCz`!yNo}{^b;{f<@8B+8Gr2hKdA0v;JnQ$^e$+p&rAt*_f9`e7%+O~UCvp$n z+O%c$@6Op>i(X$AU&DIyrtu`d*2~H(@;u9D+BlU)O3m?;o4776@Y*&xgThJE*R0h$ zeY3j8WB~$1pLcxtk{=dO*a=Q`b(6QmTGNL-yZtq z%gun3Rxj_!wVJs^h82kVyepqRxhobp0RX~ z!Yb`%8;cMZOHles;USl!4*R{*X4(nxni)`BZO~5lc zy1UEuM!RiJNe3h#z-b5+h6g%?4utFwS-*0nrblnl(zMsFR>)57>Fss@lI!psqG|yn zhF5=Uv*Av+D*9i8+$jM~oP1x}H zlD{BhoMJM!^t+j#*7*A9W-fg-tz^2V_LOJwhAFdeSO#ui@vvdiWmik_wt4LBxyH@Q z?>g2P#ive5aratw(7~wNb?G72)R)YXJg)kAVsYL-A{VwVSRuMf-?e?|&1+?G8@#!$ z-%)$|XnsKboNtAmsMu8U=urP+$eWwLXU)4>s z`ICOQBQ{p{6!%tsL#;P`=w6oqr-pqVS2%*%aF(-d)Mdso-ZE+a^B)uY&W zXR`Xk6t)k~v-S0=f7jlZ4DsUbyS#h5xQdEErAnM?(!xVucW&SaPqVnHn`-4Z&)Zwt zU&Cqb_2|+ii&WNbbYz&VXLcn#Y-(y1>zTGmmjfL3tocxtxb0T{ij}R(2`#+>lh1p1 zPY_tOr0?v{-NHqCcTWA>`|3ux*Jt*V&CdG!_VU|&Gzjq6@MT{9^OYM!+vK)} z>|mR(Dt7w7p;@ci!lNu!{j6$T696qJASu6rWx@dtK~Vn4sd9U^B{NV>?O2Rnd!*i- z>(`}qy1Lo#*k`relPKwi1mi5h8{dDORx8`)cTjO>x?g`xrif5UMK+)WiIJ&H6t?%&)DKKC|H4T-mi}{LeXjI}@bNeAD1-$N#O*`TVq378ANy9xui2Cyl;YA^Z7~X4mzA}l`Fppa!K5URLzJ6pMe$2u}H0$JO4SqB&K{h zYkq%yTDd?#Adc-T!S~c5aY|g8gHa zHw=~ms}JdIxpd_YU&_P{P1ks0H*8ya&F=d3X*_qQ_Oz#Il*Z3`_(U!&YC_1A;<#m3 zPa2=PYAmERbH(oUi{oFM4cm4_w{g;+BaiNMU+bRNFz5f(%exP!+cL;sf3PAo@_PTt z_n)+ktzx=fc|AyZ<$G4v=5JclGKJqtI%~`}ZzwOCKdb7czNa_y||X3@8|JnXXd%isjdi!XL5FG;{T(3x9io1hl``P zy<<{c!}9<6{QCTzPoKTKynL?B$MC#7yZPt8e_XgvTvUAepFQdJ92GEwCnz#>rzrBD zNSl3TN2cwSF9|PSUcRc%Wpv)4Hx$$?4B7O_XZxox)df5Dvf3(4UU$Upu3E6MU5Tc? zPssMgTHn5!9-Y9|9vl&_Ho3gb#-d_{xX8*!?KdoTwC?`x!z`J1J4NN6_Nw>a7eqh0 zDgXCS-nLDJ+&uduBci8Gu8LXv^_gDWVbks<8;ihff)*5v379#(_Akdq z&%J+^cZc_EJ|5z}J-|X6)LQy2`ek~dDcAJox@j^o%!)h?CR_&}2*5)GnxA?U8$0K7 z)h9d^UiG>5+fMU)6`z}@YQNvKb!%o$j!ngbjI^-$x8Ya+E`W9zeB_t{VkG>mrw1ep zb9*dw`c}5UNqALsmc#ij|F@rQ?www8K{!8ah0o3}?6oUrGDt?VUsre$y=X<1>E-kz zZ>Gv92S;62k=bydPjPnkd!JN4?#-#Ir(d{hDsUnxYi3_to7<<^=}|X#7)||Zs`|3C z>-3I~Z}%wh>rFIyx@@k}>eI2cX(gqluU<K5dmhr34xEl5>bHme>JOXF+rq)lB zxvRX)=OW|z%9qPcKlkqiw}6qtNJ5`QtxhZEZaI_I(weVVzrXtCR1K+#XFOri>Yf~` zvS8QJF20L73QQYRmw80+R-U_cDyMSxtW!0%%w`wOxmNSsR8uG~FE_gqDw6V5JMZwD z>?2#7yd!4uy6&C%OYzjBV^Pk0q7A#+cdX1?pyz5S?o(lQ=KSo*(HgmT0&cGl;(B4c zPBx{u;#}NC=BEb_w&fmCt}IB5t@QkN_1G_?TZ+K~=2w@*{JJ|M`|T^+vu93qo;7C9?p#HPYk(b|Ai%5bluqlmDA1{sqWco8I_i^?cBkD>vATKmi?M_N+)e{ zm(lSPKGT=-&&(5j+bMbe5a&CY19Le;tM>jrsAf?8cBy!%q1-OsC$E>kc(J8RI)o?x zh4LbHn2jKpH3ljeoT?A)JJeIqk*L_P`cONovXvvWhi`Crhdw+u;pa16 zt>qb*Sa-@tMVI%~EcT4aH&+)ktlk*7diFK>*&2l*$OePDb#2WJ?+Pp&3 z6rNta+U3ZV1#2W3oz24i=l9x}E^goJHoswMt7iZ4w=Z63=n5OSKmUBzx2HEMI$B%n z^^9MJ6Fn!KYM(a0si-N{s3UbTH z7LB*t>h0D!T}q&1#^#`K_vOp9?6rSxV7t4KTSy|6>HXjDQ(wF|F>jt`;=|7~ob!J^ zs?E<|-^Lc0bBAkLozD~f{T27`-w*$JAvQMl;Y4%I?H6x_B`2GmY~KIzSoQD!o!4(! z7#g6scA@ZrkRKN$VTo|Y9u^*+-ot5kcwdG)T)2AmqE=?w!A&XKIAE){!)>y_y%^Fk}j9j%kimGu99IWkl991CZUY2&S32g8?zhklK|YIjX> zwfVmJ>)Bs6R2F{4Om z?cW!TI5NY(8|rtSDgW@Da~5}z=e^XQVOLgoGl)HQ7EjErOmBC&yDy`kWx@dm7Bic# zR+}@g_xU?JH!oE3O8KI+tnSaLW5?Vcn^*M6K*q=*snS80>DDB!`@23e$$r1Nq;~(2 z{6+fht_8+hW94k_w;j*V&DRwV`#Y;B;>x{67e4#G3*5YS(XQ&MzI4?&nmgBBXwwM& zzVZmCnIbqB9oV6kkoS_|<8<-mMxP%nSh@0qOi3KyI>m&`*RCn$NwtMOZ00lH$Ft^b zO|A1Y(<|Xi;{JJ0J@If~)4qL*5zo@+1s;{lJ!ai_VCnW7zkhuF@($6Sw2HCXxkT{K^)S5UCg6#cVd zg>KBP?NunL&eIRnI6JdFG3w05{>w((jK)9ZYymj8}^nx0x%#WS1*e_ zzJ7T1X-(q6q@&BTCi)|dSgt7AQQ_7Uxp;+}(!4jVNg)SK*X1V3#n`4_RsJG!Xu3{z z^{=J(@71h)bIUY}^UyEZkd6P8t4>Bq+NSH-RAG{{Z(ykp6&lFKV4)$@?+!s?@bq!vJ4*0Y!s+`Eql_PuT3w;X{+9@ zqoz7p|E1RleDaW+eXqXq;h~>r1mo&n-u(NUf8WPzY0HG3Y|VaG_o&l7^J#4P+KjBM zs#hJZ)tSN0N(vsP%Ne?MI`{-2Le_Up~BF@bk;%wfFyjP|y7JJpYyLue{^$O!?O<&9csm|8|Jm z-}c)Nx1TzDUKuN`3#q?)HT3KEo>z7kg;4T~HK)gZiHzuiIKPDl1wyialRXS{^^aJUI5S*~3FS zj>vs_&~)fem&~iYY4^`quCl&(=2PeK@JTimnUOzN2B-Vav6(#Ie{E%X*)+Y!3+^_l zo@_QZGgDQG*-`S6>&=;yPm9WHwzFP~zno@t=v(f5XE_<8XO)$#Q>TVrc=aMdKiT-j z(}jnRA5c?M^E&qFqC9I>Fre%+BKD9)>)5ZCmp7*(>up( zx=;=|1`?VCFDwdu7<>2b%AM(=pF~ZsYhPKtXwGZ*r&6C-kCwlw>e}|a_)Jnv(@SrG zyl-8F&x7mP|5rTh_doZ~VOF30zQdPVqEgh0SG)*bCLRarNgmiB3?1Z6nV44S^=V%E zJc)0LQ!Gs0+?C$E*H=h(`>YTi_GG@}R}P(8x#mSxVp!9peKpnk8LgpbymHh+C$BB$ zIDcna`aX%SMJZ~5t4m%UKAYaQI9g+;$hCl-_A?KkRcABNO$tn|o#`VS=cmFX!~J>Y z_kh#w-0Na*{{C}0C96O^=kUGy1v5@w5|8^`ntWfr`pc31d&*}fyJer`Pv4^Ld($B*C5Yh8C`+bCC+JU#U2=oP2xLhtS( z;~ZB=1qUsy7+AJsgjU35ZtAXeSaDwFXR+UpA`LaZA6oAt9Tj#_J-43;QkV9yFls#8UGzC@5(F*Z1S9sQ+NeV2VvhW<|lY@!dkGQ^z&6|R>bvAvAyy_UA>+>W)|`=i!fF-_Zi^Mb3&tMJP)I(fFW z&$upMy)1oglD8E<-}HoUXSa(i&Agm)J^OO4{~Qb7=La1*cfQ=uKW9O7s7S(s9-dcq z2V}S3cPnpX**;T}UvK}6e^E&w!%ZP_QdY{ zS#`Pmww+z!pYr^CeS;r=j5D9b-jgszX&(!>F?^Mc^X@w}%SnFvO0TAuZ(g0z*}LM1 zLt=1YZsdl^^(%@GY}B^437en`LEabkHyW!)9s1I*GA2+LaITrQj$uUhUE6B*V z?6-?E`C7N8W`-|)EwA0R&OhE^>7qHd%A$ISZEEi2ul`%__#YLnt9$Ff7qiT#uQr)j z%(YR9-MeT_u5APJ$-?Vl;!l^zyneOlo#6@jcl$OwaPIEE7W(wW_C;w*f(2jrRW3*V z7Y;3z>XTM|x-{GR!M7P__^Sgyg(i8%S>L|>m51j>j9&1{X+A=)-wI{17N*bsVtn(e zV{S}lM$)2b+NHgpKXb2HqqFea3Gw;+_txLv|507^f2fR*QmjhdgZ#{Uk;}z}D_b3T zdA>aTzJ=xQxA%1+<|?hklv$ zNVx_l=LVi@ZT$2ghp#`~Z|NDYPwOwn2FzH&(o_9?b<53}H*ZaxoSy7Hwsw#fBIsNR9 zCzJoH#dq6F{{Ow%fBi(s<9Fsn)$_J5Ud(JMEA!^zmhH>GnKc}4^S5ZQC0E^Zx(NA(?uNXI0C^M&|Y>?5w_FWpcs#FV?0VzLH_1yi8c~=b@!brpH&l zo?C9;Vq`R7YkRMAeD&A=DLEfdS_z z$)q)Xt+m6hvv>2&4E4?h?pZPUrB~_AF8|y-!*$t99RB#tySQ%N-%~LKzH61z?nbL? zob&8`|7OAohL0bXCzO?zI@F5JRNFIW1xv#0Q+NAMKC3_eGG+I!6;pmWu+Pm>XJcwK zFsizEEdBbiBQCpl?>-omUtIh-boIODn(v#X?@ue7G;P|)1FH;Azm>BUtADL)ety3E z?_<%k-|tE`?0mafzw8vt%4<(Qobyl-ay});^yBe)<=y4AaxyX%zq@YV`}=-+R%^>K zNKpaq^*S&;?mG}5ew62W%k8K8?wqO+GrKzNdgi|epQDfeFF(EQ_+h)4Z;#%;H{BAz z*_|o(@QmV-+@u{QZ|^ent_)Y=e--Q*pW_6sfEpJHpRld`)E^$7@ayO2t>T-{1>WQ> znsaaOUWG)K`IXZxiTx74heZq_F?+#_TKZC{uTfJng2CTHiu7H zlDc#bg8=W>sR|{frA{vkG!))FyBuyO5f!P!%$=Ps;KgmoZ>MQzmK4Z*W^%5LQmIGD z*I&8PBd5lSO&Ko87-M{HETGp6_z9qf_$ulr5|7m27$N>G1A& zm2ZkQtN1i6yRuI^Du^D=f3fV2yi)8jr)&`}=AR)MJ+ML{km+|^edTH9?eG78$kh<6 zd-z*X*Gb0Y+^bJHNy!Jx)1``g&OY3PPs`rdng=d6}6bx6rkd!AYy-92xf z-IZxOi+&yd!}HRs-XSn}V&%UZ=Ax=0Cq-hz_A`mhW8Byhnt4xE>A~wGY5~(GvI%b~ zD3IyQjIy%4m}e>WZ1H3X(Gczzjx(85CO!~1YTS#I~kw8Ut&e*&VNsDwExxlRVx`GxT&FJr{0O>zyB5X zYQ?WGc1?f$CUi^D*E72_W@@fL>XAv9GM$|oI@7(cC#JKYO39a3bi-2Pn5rM=7fpQ@ zw$ANX$l7~tR5kd$%dC72zf2;dHqU*;q#}JcdzTe3EdH4Xa87b z%OUN_JooQ$>uGykn&~w~-JtRE?fq(-cfVeIK6RPojxW<9`@K#poiyRC^89vyrzhGY zTCetFX_Q>x!w(aMkNht_Z{x&oVs3Wo+&MA5$&-HBzWeY*c=emNzur0&{O+3Vxqi{3 zeJStK;{&ueJ^WGi`nSu1`25|eauy5q-9v7CKK=fIy-v@ndX2nH%i3qB91b5`RaO-j zeE83wzqJQHU0Jz#%id{cI{Mh56O)j!p2m;f3+^vokZjS*J3GDS!V7JsX?H|RwMASt z*Jt#uU<}WZEtT(t*-uEEuXV=ch8p|1~pqT$Qlet)$PC} zJR7tK%Du0salwt-*Y2L_U{|;G+P5I%?D_QxvE-=z`+W}|JkL+d&QD6@RJYjk>-_wM=DHV+L-wdVIyU3*=l84H z=QTaOJ%MT4rcEuAEn4UND3<=OZ0ryg=I7xdFh8GH{qB@`^LAF9HU6S1xbN35_rl`S zZM?5}d3ZdEX1>_HYnNW^wF#5X2&{^kXE)d3)r|a`yPP}!o||jGCT_2hRn)HdyyD`| zkNs`*lkFE7pE{?haQN(OYu$fO3f`sZ7<2D&0oNDMvO%Jl$;xkCo(hL;>|(?Iv?okE z^eYrqcI?cVyesYD!XD$yt*y+xruTJ=rlqHxpEB?9Ois?!mCQ36<4Wgy%}Ef36vZb5 z3{p=2t5?Zd!6-d1GX9(UvTn1OTRU5%W=E8impgt7i!grh-$+?}mFWDa<5ugkm#pAB ze$n>OS!<~``bXZskjZ(Td-d+3rDg&Z-qgofzkK=Zl=LX&6}CU#f4pRL^y)Fs@*7)?jEs1+`mbNPHqT<>gt@^s6K+qu@c!pZ zZF~Fqj~-3BJ#Bs13;Ul@HZSsbe!Vws+O_I;JA02mUb|GC}sMMSh#6f9LSfL6w!z}?c@&{ZFr6@5eEyoc}%os}OZKI`~55nLK4 z?BM9R9P*>UH$K^ZvX-cvalP&vJFPXk{=0YiCnwzqwB~#;UF>a5{FPp=vQKxuU+=&0 z`Y^x!@1N7_Maq^3KRhM;i|aSv`l5e(UatTDyfQdA_)*#6Ht};R4>nsLJ}t2qGIYrx zB$3PXBm8NGu)O+X-#<*6kDv4&Fn~A!G;$xxxg_Qm`__fgM|oG3IasSSu()*}_%b6P zTKeB)e>*PbvXT;>DeUrV3qquxSN!?;_~Yq=CKe}d-t!;areYA^HD>yThGJQDI^C?%vfExbo)4&&xl#PyU_t zJ6=R|>Wh~f7X(z)o5sQX4lS!EDlOQ%vVc9^eMU*8=S>e^*}stS5pXnsjW(!ZaZ&ns zDLQ=Ri~O5hM{j4Yx*>ZO)#`>_EG1XpHhhB=A<%XkR?abT(6IOV@7#-Bp_y#Kj0VyB zOnygmSHMJ2^c%e7%*bz?Quy(jh2^aR3mFbm_3a*C8~EjaUA=bg+u!f@|4QH6ulgR8 z@$=^LeE->Evhwwxu3onb$S`O-_4zrUq@<+nma5vKqGuj03xC~tet!1H8_C&o=Fa`{ zl~-X>Gdq8#;<||rEp`(bgd}{Kj>TkWSLZV`FaBm$Sexsay!R1Pe2}s8<8|l0RXI-F zur0%+`N+m&a~i%kubcRKS=T9P{yUX5XPPH+CR}@_aXs>!&r|-1`;GOlPSVx&skF&+ zcX!X6*>H2>d2@3^v#i|QwJX*nzfy0`Iq~e(%gqnd_lGLY;`w~k5-q@Ul^a%mP~5BR z-v3VduX$e0(+B(2L%%$I&eft_4 zC&rb#oA+=F1}*;1quKKP-*2yyk`({*Q#O34^l^{gS8@OL?bQn%MWnYkt(?zObY#xd zT~=16p+6%}wyPhmDeOZ{MGm()60~AIm#2JnF26W8{7&8MS1Sv4UBAHkLygn##On)t zSH4p;GP1F;IdkuxpQlz#Y5uIpRTkfm%T=@dF*i5AU;jT!wfp7!S}$B|$zHfVUqYz# z_Rr+oyLR15eRFm18qq7OZ>L^)Ge6=*h5wxPiHY~)W%nL%%l$J^_Jr*_y4nzOue_=_|4%rQeG?f8-9?`W8%wu_(a?_?$^tQpE~PM z^A>|pK)1vF=|Nj}Ei@E}*pk7xhpUx$!yn6eQ@lp={#GveNR4j;>HyJPY?LR{hik;{rPl| z`TtKZ0w!FnZEj|Eytl9R`sefZxfvM(GEXK?n&rFQs8#av@^ka$Z{NLJ`{91_^>s^) zXLsSs*w}4boZX#ty_0$PLF!NV!6BR&9$pnzkcYaI_sn(+pfZ^ z@8d(=*u!IEV;LB$?p2-svohFu*3n18>gTjnuASe#h})<${SkCCmCfs^@bmbWvi;OI-U%8VL@>F#X#>}anc#I|1L+j3r+u^3I zd2>CQJnqa3*Sqy4x~$Ik_S3V4I;=4Do5!V+U30kW*ZQ{D;clDN? zyTezQURblz!1Lh$HaESTSY}zz%II+K?f0X~*X`|Gu|>1mec>60wr#(D#k}g*ST6bP z!>yawG9O(H7g%GtJ${|=v8NUR|0O3bUcTBr=0eqDuY>zv9eY`|yScQ%wK&xK-Fnx# zi}YCKmishLv9&+pbw6T%sD`FZ(6t$gVHs{ya^$32x=x3B^@P9X z^66XWuA8UCIoG=9-_K_2v&sIQw)C^zpAWGwHYFYXYh9MZI&aDES?%(7c7N|TzxQWG+3IU| zUdElCc9%(g=bnm>pI)`kSK7p8yr^~&xT-2B=~ob7i@kjJn$}sHZzb9%Ldwdv@X9U| zo&QVT#rpJ;BYUgD)^hIKeM{l%9e^Z8!LdDeF&*%$8qySkF;r|GTj z|M!XH7=2IwefOEO^kKK@?elEn%9pNBIPtb#Csoqpe=@7Ft@GZe_WBFo*2ivHzjxi_ zeLO$y_cvJovQ=G>V(qo{(VlIOt1j%V`z4bVoMEAKt?lUkqFs+7_D_1+U3Pxsx6Nv7 zPfUO8Td-=%tK@8v{hHGzURw3twLZvv`9}#cCcSwRA1n_HQ5NKnIpGmymN7xvHN+;v zl2udlZ^9#Wm#O|8K`l2r;47+pLYOY8h6|TgNx%QCcc^;ep_`FKZ|CnWkKS?Jmn)h* z^j_t2qYW!pt!lfTeSOly(py_Je#Y7V|H6B+IXxw1p5^2VH*Q#z>wVSPbEx_B(|NYl z^GqD&Y^<%#b#-O+@}ms=kJ%X-?%c3J!f#Rg)tg3aXHHL#zsE5%gTu(!xckNZioc@E z8ebWyOx(O_)5Dz1OvSw?R4do6diVYAY>5@tzs}8#ca+<+a;2ue{{BsqqWYh`@L0QJ z3BTy`c~eT}y0}_7$x2&YdGbW$)@(N8MZZC@*zirjz~pJ4r@^wcD|ZimE9;zRcicL| z@yHwYgy=mD9nrqJGO3Riig#L9yI%I)5HRILQ&&z@=OGV)hz+%3C!4#yT$hTUp0#Sl z*9T^fH7Bf%+c)=4);r&BW1;g!Z+A@G>{xC#t6f%`@6S26WsRTsm8Vw^drr~)DpbU| z{%N@OjLHIojO-lswRh)USvEy?>!ttBGx<8#B+Yr2c!~edm3HTn$Tg>)top^I_)%c% z!uvfAo}V5p+H`D__tVtrr;eCjd9m%F*~RxQlk|+%>wR9~uYc1%cp0a@sd7TzTQ@U) zjWg5!&2<0G*|+a*)BW=HwrkA{w>eo{*EZ~P>73?cv889q2Vo|8wyX74FZ}rs-N^$> zR1GGEx9WF9JWYH5zU;O1=F?#{-^=$uc^_LGKSOw*%)Qcxph+{TLz=hCmqoV3g|Clm zzW(?3|K9HIty{Jjm_NTX)!e~8y==;U`>k9#udn5A%el+SB=hn3{}jD7YF?kXPH$f* zc53_m8l&uMU*^r5mwGz9(ayLzDqU9K!OVH{+@&v{KL7vTf9AbU=HyKkJaBc+4OLUq zE7PAmJuSZP`+f6;bGWbMSeDP@id*~m>UBGXZ}IhC6La3^c!cW5f1FUn|EllYh8Y^o%6f)`+9Xbq4me4B_(C;>mGeIw^NdOwwld2smxuHQ=!pN zJwaTe;`!~_Jr!vJp3r&0vlDo#zaPuVpX_pct(xfJ#1f=gK_=rFf7X1K+jseA**rCy zY|qUB_b=R-;=cB(Xvnjxq2K?urI$HYMsTp?e%Qvl@Y;;{TScbNe=9vI{@A}U%r0!+ z=ev4)E`8s>@G{TK$^H{|{`-DU_=wS~vkCK6@2S-!+sbw8*$e%DVR>oSr&F>Db8eTc ze8n==B)not{akK!6W!nGir1%U>tqFFEUTKMS9qyf_e|^C;_W`gJ!LHaeVzwwd-`hI zk~8_AXL@b&yQ6vQ;iV}l%jTV^see`1upy#w!l!R5B&RgY_UE~=rNg!NYpPFj^mBoE zJ9W?37*6?p!pG`NPSNL?4_}CRnH-wsw8h2i2j{|i2a|s<7uYTU7kuC@gijiiu%&3- zn^*77t?N@YzFuL$w)guFd(E98UWaBR|9U9w-fMY9{HXWJ_Hy}3@8<3IZ=arec%fI* zzkk2$Dr~EKesz5ORm#8b|3R@^4{N{QuNiV+?DTMeEhil zg^h)UrtWGzU2B(WXGT!3>wpUOttz>~n{pxxFKummBot-5hiP?rhVVMp9j$jiu!Qbx zYjc}$;L>H|f)d`$7254GVzx=HfkGlqA30}bryOaX{Ni+X5cj5Y7tEXmw`?})N;A&N z3=ME|GY-F?U6N|BXIhNL($5bHB1@Ku1U)q8^7W{Vt_sdqm2tm5UyW%`+P_KsD+>hH zDC=tVf3N;`((Kx#8JZ`XFV{IJRAxEudbDKctHP+Cp5g0_x%YIOnyuh?@8&&z*%00H zXWmX{Z#Pi5$nSl$G@?sh)414_S$dPqJV~2Vvjhzu-khLY++C!9x?+XaPCn7ecWRT* z=)6gOW*Zaix;OY2+ndbK*IvBd?6^+n>aCX|aeT(L2)ry3z-`9U5epk(0j-NkU z14OF7y_tC?WXJB^(p$ttL4yyY0-j+zpE zU)4xj%SqlBtj}#}SJPPP#4~08p+!YW0X2JK z?gg#2I2*gtLUHH$FUj|29}){UH=e3+*lhtLe^q2{NI^fAjsUIqdBAmzVoD zU;pxDU;ayRy%?6>)vLe!Hj_Mko zXH0$ugc*aS1J4HL9c1k#M8~?_ir#8>;Dz$@!&fVcko$-!t5Dd(|MYCQV)8{?NRxE z>*{6uC9yZzU%sEB{XZZrhWq{n*CqP<_^cVX@4TX0&>5fvk7u?{u@?A_mf+;`ie5;DZ$!6#8Z9=T70WxBrJadd*-+#*N)~m&GLq_iW z9X6eZ?D3tong2v182qG7Os|POjY)WR@TJ<8C4ma6e_Afcvnj3kcUkeWpgw%WsL|6k zp=mu=`}d-sYj{5|e$UDnbiiuw?UmLaY~!m6+}$2NmFM59oE7+I!lp_5jMGm){Jiqj zI!4C2B@@gqSA2CYe|u-^^bC(nGjGB+xbP@9*3Q1iHE+Mg`z>3hxgOe<|3mNHJwxNi za&i3Xk}p0?^KUD3kqn2-{!Oa;dECtNvdFxx>-_HjO|xFkaA~{6^jnNgGK+Qzop@H@ zyH+Q6#qy4}iinRZAIS-?^f@KaIN@E|UhWmrr&Bgx4cjNOuB`C>s_4}js|+|~p0=~N zD6H=@=ju4Ea(Us@=RRxCtvfySno07vNz-!=tWQpV@2vNF$6a@AdEh$ZmDY@v$GM9Bom#(J>F<VT&&r(a^*_Sloelmv$w=leC~a2R{u$C zj(pt@d0l?d!{9}`44~ac|0}lMU;oiQzS{8h9Z9KkFYn&w77}`KI#FCDX`$2nT}A%) zBqXF7Q>&}v!@5gy@^cv*XPSh+5SS_@tiZkZ_N>(6-DL}}MBQCAWnamL%Q9NrI`tMV zfmMkMo4(JoSifUUkggzidGRU#|Dm_P6s$V_vTAZkX=S)~{5r{13k1TCvY+lq$XMB02+fH>TIze()U0rqY-#X?%3G_uj91PA{%c{Zq1K z>*;;Ue|yx9uy&^=B>LX%($Ec=!@hmX#~$Qa&BtxOyaaabveNyj7a(`*Z2PMB?bAFs z?G>H>zhvNtZvt^(@<>ox&y}CEE?HV)YFO%fs~=@kHzbsOkbUykZt^GVIojg;>~u3W zI=)}$l2yz1=+U!=y$%7!>fgfBj_3dTE_j8DpO0_R)So|D&zYTeaxWMDx z%*)E*QmtpU?6SFi?xcJQ_rB`A3mWFx9iQP=IW`B2gM z!DiEco15Nr82x!!F*9td-tOJIO23wT?Okz7{R?C3&aakJ)Kb%})^7RsRy)I4`>fNB zW#5(+%-pI!JxqvkR!2b9f~aMl50|cFe!AQ&_~YY@tRpvfr!QOa->Urj(G96;r5C)f z$BKCc-@9qen^9? z^5MB@tT4@IiOKs(ha|Ud-}>U^OK01qZ=NWBNa@_VSKe^-$`dV@57)2TyLrD}x7I{> zj0F5h?(l*P`+{O*li-XKD?gmM)SaKVd$(K2q7S}*AD+0~uK!MIMQEbgCI|J_pl?v7^yWLOD&LDe66X(baEpUyHmk*-tN>{8%4i1I3>3}@-gwAf9v*SorL99{0}W^ zo*c3L9gj{0ra;?w-6XL2fQxTkiMlIpYc<`X|S1HUhKo~M`vAJuF0^hi)ze(2%bka;h| z?ydT8x!}F+rKu0<|7?su_Ta&Ob^CuaF5RtsZZwI@zWiU5mseCl!Gp*B_FR@9Z{6Cp zVMD^E>C1fLx9ZKW|F_X{bNWQRl#`Vn9v>R(>(pi)Ztu^#xyxI(T}q<<`(6KRxjF$W zIjb-3zP`E9@0~B4xBdRgBfrYBs91irWF~|BZUsh_1)(fmMS4}g&ZcbkO}n#Y-$us+ zmoB$!ty4MFbY@?W?u5-7NstU1H~AU8_- z;GBdN?V*p-KRX0pzfpDhwX((EFMDThj+@6<`SABS-Su88&7MddV4Gy17s6@3bK(6R zIl<0CiAQc3Opo-F)s_7gyj1(L|DK|0k~R~w*IgBS*jKx~BRhJR_3o-&Rp0cjy`m3o z`WX3t;kTP@&fndN>W`hi=yEyaX34AndP0(Mq5YRnOgfbs9{+y(vsq7$zvlk+>2YiQ zbT?Z$zG>b2X2h56l;OD1dgD@}f$mEQhZSenZROn5IGa7de@+O`dHcAEr*A`+^>!6~ z`ub+gnwA=3>>!43l|+({2gOym@NM#YvtGo^6`Oxm$Xd*W|1~Y~)ar zX1sgSIMK1zGtt$}ukca8vOOg)lP<|#y7Q^-)901x6O@&fUGgozzgcV_`=O=t=bv8_ zd0COa?#~a;U7^N`Di#7p51y#Vm6Vod^zPsP{_Ije7YYvfct?k#s!%JQALb?el{ zi<#$KyLRQ)F+p3mym!f_iv^Fio&!yAurf_OdcFJ8x=}{{pIYy$ZkVg4qx7uctgftw+@9ULrC%|bUlB9bRiAgpOya|PUhm+{iLV7`m0p}L z8d)FwUvM)|1fSqH75=!%p`Wk6{coh*@_}jT)GWQaI@X@GwQD`(WG)?apJ(kTek+Yl zQ!m5!=43aM4OeDmpH@{3$dJDNvoB50rfAkJ@6{_WNNw_0%UzQ+r^NTeBIYZPBflgj zIUm*cSiR6RKvVVO?v$dJ5>FY5+iiYa&ApUP0=iHUwBz=P;)2IdUb&^MUhb`HbZOqyLPMR&^gsB zH;ydYy_&0CB$-(=!u3f&bF)%#cIyEwS$ps(grK0n9$y-i5wIqWfae-dI z;UmdAxsM+Th|$%w3DAl^uku54{si`>1;6`1%m3m zG3ON@*X;DFyAO{Uny)#Zf8zhnkh|{=C!K#jTYrD9;QeE}cCP$+Ec$D7kK6qC zQ7dd%1PV~tWMm_!CqwW<;ZM0oJrT~!5-=z>WzL?tSL5o7{$(BwjtWmN?bEcfTKkBt zjn5TXJ7_BQ*gS@1hmQZVJ-`7gka^}ZOmmaua#rT%D_QS#jPi27cL^e< z9hn6;Jc@P@4&#yU;C}@wD6AO8*GqVIdj!mLjQQEBYi9C9MeWa}OEK)4Z#GDN?~u;E zyzZU+DTj;2@=fp3`SY*d=w!Vg>)yV!>if29$I=3~zWR2=$e?KE&ZW%@l+3MJ=W7Ov z%iTiE4|pV;0qvXfc=6W4#yPgOAW&tU>?Gw>l&bQx} z%z0kBQl-@U)cs`1;|aBmOXc8v2yrj%SWY$9P8qNiV zjQcmtKC2^p_0_!_0dwslxTGiDJtr5ud-v`aAr?;#CW@SBKD?Uowu}G$19wgtCVe}# zq>DJYv4O+*peXC0o`?fRly#7 zstE|o0QXTK8HVvKXsev!Uh#OnTOPma>;668e08Str>nOsXJ(fyNlqwqKDs@#o$XUv zP6CVX9K%|<;HeffKU#nIAyaF%$7t$eh5qRlq3#P$^|<{KUi4_UWZuhth0na6T{&aH zHE&^9s+*6xhBBkB3d8vmhc9_3C(Fsso?JbnCa?AELsqxD#}fZ2?5J|%(E7Y-z5GIJ z&;U4tkc1VZGheuWf7I%QzYeDT$&y+zy*XRMr1O<_C*w-qOINRY>Ln(AT`Ii$P}-fd zhDN;eYyUr;Y|Z}Z@Rj!Sbq#0D60uUT5ZHZi5Gc8a`m zA%CIy`7>;1Ql=DyU%GK5Cb3*{>DuCsJ*sEs9=-ILH}=j&z3SlLfQfgnD6pzN=0bF; zK`H#$1cqgYOxx>cXN%meoLBv1!;(YHIW{$GVt=YV^9p+SWkIsW3hDIXXl|F=SJdR5 z&N%KpXH{}wx!bDhu)9G=%RjGcsC=+_?dHE%h2ym=bDX5pmi)SNF=!rxjg0YWJCFV4 z5BJw-XY8u3n#8j6@e56{*3AKn-mK*;*uJih??y>!=@ze9kyXDY+)OM^nQX0SSS}&4 z;h@hQot$h@>0(_T-rgT&^X|sZZPqIa5n{O8vNSM5L_?v`(@){&pY&MiVMFS)NdN``wEor1u>Zmd)jfxj^}_w$HibT%z{}sc z^YyRRy(O0&n&!=0xNcojlA4iMa&Y$i0}%y5VKdfv$;nJxw>;+0T+0`0T>TF$?Gc(b zbMhCf%Rvj@ypTF#_-gn4lUp`!D@~6R+PQYCWbngJ5dqDJQlrsOyiI6UbZBdx=2%d`tzP+|=}q_3*3KoG z3XjaVwcVrL?;m>eIYP6wlN+>kZOa6PNU_-9nXXBjHxw}~^wjvh`|qon@`uvyM9$xO`AV?qx{*^uRhJ0H&5=V@~mjRoS5H-?bTJjiA{@~^!nVM?FueA>EKn9CA0j{3K6aRGQRR>H#e1( ze0p`dZ;Gn+mYLb|FH#?-oVb*qk!uw@OX+;N%VQH`gQzD$iAN2y-(^^aPn&i7%KMMe zLL0hlM9=vx40V@xkG1wo7-fX^pxnnl1hy`K8j!oZV3~rz&bK&kYejap&N@ z1>Y~eIrq-**Wo)iG#A=0l{)Y7)#F0Z6^@Rm*?LoA)5;n=E{EZ(#9g@*)lnG=_8gq+C-kmvGB@yFmU=rFl2 z)GO^E{r^i_1$1(4JVR7MecfL?km6CV_2gy@7H^D+oHcQJ-K4NAg$nkw+Y3K5_APBa z>6$!?y?m?UjJX zWV#-(QD#0T)c^9PXZ;C_`#ZmwUikCoyx0 zE#^IAv23=uoo{?y$+zF$TPwe)3d-yVTYoOnS$jJBncuH2tykB&l))?b?$eCQ@DEN= zI}d25)u!KW=iD9KqPj(><-C|3_xj!I-qvTl%H0xoZ~3mmyyz_#=Uo17kXhz?aW&_r z?D`iE=kdkbUC!02wY;uemHXsvb#ULCv;GUF<{z_>$k6_)<~C*f&*PzHocFADZ{D_c zrkV6ki^St6GM?{yU+rD{e2Q7fzIh*HKObXjS*YZ1^}vy}G@XSn^iRu~ZC`H-6wBY( zU-0I%`Ewhl`6)tY*4Ax2{#oyz{#OOr*r=ZVo?tM|R1?+ZsZOJ1}K zykN%kHRp__p9?3-A@aTKdyNEICes%+fH-mZH5wCwwV~6O=$M@ z=sUcq$k==N!sUn7{t$a0b9#vZ=i(lgW5SFt-=Dl^AF)+Guk`yv72|+U?d1huWOJ0? zISBX8+ATF}K@0bk&uV5h)%g~V@7`qWp7!ChgKVeVtJNo#yy=Wv7_?IxZNHZXi&}-& zpK~wI$Dg%~iMaj!h}GX`l9Ds-$?W^)b#CG#tD9d8a~HeJcrVAf)`@fKl@n_;Im$UW zwtb#8X~~lIP0Oa7iYe#Lid{H&yIsCskj>;Jr@XMHClntz3upF8u) z>HEE}leX>MYUIJoSQY2fc&^a-;LJb2j3N%aJMlPLYNqqkFPc3eJN)w}o@%>xj4QBr z$>ys~hl@8CY)zi}@D6I}U0yVxwJHDy}BDmBfooXs}@fAJ5kmKlRhJ!>bKVH#4`teJf;J+qe6@ zPjIpHj>lSp-Co@5Os`EhJg#KUTvfGerk596FCX`}9h0v(?psT+g4gHR5~s>7TjhuOVn# z{VKcCSdw?{xxUT(H8#KUG%g9HJ@nGIZ%^=$v)QWkeJ%HH&v=a+oBlqV9=U6c`tp1K zp0U3C6|*e*somAy;?KRQpXML6eRyJXD&LVQ`9(a6*?aw;#QxIy%5ay(`~BUj%RXD4 z-G8&$y-S#NZS85TgK3Y?Otmo+*Q%bg%13>gvY`5c*$=Hm|HQ99Q}_H)f64;Uyvy&n zV-J~cjCvzdwj$)`gDu)*)*4OcSg<&EN_^Y-`7-67VNLuu*?Ev# zzP;h>kbL#RtTwwC2fFq=635p%+3-SL6>0fA!F`ynFSk zrTPy)@qOWYGe?(KV$R6g`6i&{#@?2g!fa;QYVXb$HD?0vdntay<*S9 zO2==T_m-WuO@DL7V%dh%8?^n!V-`*?b&j2KeEA(-XO_s_msfsZuX#S%5maM2$@bq5 z>c1F>^JN;D7xO4t_q58$L{~{{{Pn~cOs$3Vnq(-Y|O~s@SS7iTYMOw{eXXl?P z6qs7cWg9Fqp`lFZ!s89$dG}UTC&w=IE_m@Ozx-{@7e%XE6H-riO6B!uA6f%vGbL7;&Drl%GtB5O}I{bN=$gG#N_#l z$Iwit*T|*ehAd}~QRAvfuWj$fzT0P*Cicznv)sCsi^L*cNN-qvHhS^;^K6luoo=t2 zC-_Ntbs^QM0etxPucF1%F0T%W_~+&_m!q^+y&9?3u8>DyzD6WAip)FVpYSheJ=7U zvN<)2FBre5{c%Zk@BiJMDz6e*ZBqHFizdZws9wHgck_uSr2+GO9w$zDTUqmATF}JR zUHXC1$v_H4HLEh_}>6+YeI%_^W-FTlea{r}=z|N3C^IZpSQlmzT3hK6RMBK~X(NmWTUEX7jswdCJ0{ zFU+vc;1v`)|BoeF=Y)p)^GR2COIE-RZ#1a_AKtjaoe3UM3_=plOu{yzekbBi{-1u@ zko#+g>h#bo4 zPnCCEJ~iW)pK5)?Cf`$0Ek1u2*v&k4STEe{I^V0$oe!d)Rk>)IH2*T-`g(Y&%67>G zpSJcKX5$Q`uwTrWZ5#m;r^Nkj!zGWI-+sjLG>x0AV)M- zU)5M;_%za4HD;Mmk%nF4#s!51yo$=D|d}7 zW=Umt7wKh8V{d==w6$|@-V%Kk(YP74eJfdRp0gB~mp$?N=@}>ErDlJ6xuw**TjS%I zejn)@J~z%HC#Gr`>-hwRQiCO+7_C`rNNi&$C~X?B+< zY>Fp^Rfs)_;oxFAP=6lTp3oTmq#WfZ`+`4aDPjX6D%8}-*$CWSDJNza1 zK0QznPveg(o-?o1xmH!vrs&&;ut)CyOq0%O_077?{rODJlfN1vr)Hnbp7Q2{W_}M! zs4}hvmpt#TDRwJgm-wc4eeX2scuk!cp|2jEzJ6H`XU<->?$Z|j*3VwwE^dz8zxwMp zo#OfHj@0ccUQweJqbBW?rJz-4q~qJ0d-fQcTYzgy#Gc+JVe5Aa*{{TR1jny?72WdX zSdWU9GV|gmsK!dJ#extwFgD~vzhxqJC*=B0S9T+l&`4K|>; z--ju${@1)cnwr1=y>;`gMutArlO`{1WWM?@FFSYAO{TszGUDc${M z0vt88R8a@5aiqMy5yVG=r9{3x|{oUgh2A?3{Q`bqBYG z#`y+4V=+6m&(}Ad59t>9(6Ge+z3j>X?4D{b%o zPk(Q7;PHxe`*Xj>ZVi3E*E_6mqU!g&9>b$%M;kUW&gN5Gs5Zr3_h*LU1a%f6j-!n% zmvcABn%}9OsuR8U`@OKezkc_wlju(?ci;RfYHsDX$h(%`?|eI#mG=Ek@p)50F7|1D z>rc4<_>|an)2QX|kr!UCPTPj83%;{+gRxM1uK%5rrpT9*+QxFUr)-khrJmpF9V~aICH9B zT4tA@}_gft~;0YZ(X`|@!CDsK$oaluY-llKDnw0xmrGV+4*^0 zZHS8Gft8cH?Y*{|t={T3Ly&X*3GP2{A4#@Z&AxJ#`31kIB9lu4hXA9J14{>kphPeW z&rOlIu=$@qzPoR}EwjAEa>k#lH`m1ODpJ)Gy8YO2>7P|6n>FRn?PdtJxMV!n`Ns{} zw{zbb%scme_P64Hk;}gQ*fLvHOF#R`z7*vhvy@Ji=%Pub?OKSq1|wU-7gy#Gbun{aNfzx}6#J8P#sJ9Yoaoge>>&i{FBxiuTh z&FZ?zrBgg#MN8k!nd3T{_xzc-K0PUU&n(Y7s_dEMC$ZYBvLkGC(FzxcW}xBRuM_3GZDo2+)V6;Ww zIWsaVQ?lNwrn7C=q;u>1)_mpcnQ?sfp|^AY=&aolcs=y{O|!EnZMSS(*TcV*$+V)u zY3_gd!o*@sjFA6*W9@Ed&=yB zEUCGsQ=fV$Jq~kRyLWGEe@9U4ywaaB=_zl#s@2aKFIcp|AbX^bm`u>&OjIb3tf(nROJHR1ekP>Y1!83Z-(&DX` z;vd>ht$61Ag@M6=!PCVtdZZ44d z7GU{#cVyPu|Ig=4K6CHa^{4(yhd7Pp_Es02X4q!3COiM$tUdK#pQujEJ9vLxmFUN< z(N?$KOx?Ecn_PyJ;SqJ-Ln3<1=a;wZc7E+jINVZiC3Mhl-+TSPypv^4dq2s&^Sf(u z+H~K}BJMZ=1LbqpTDx~`_Wb?jMw(OfB#(H>Mwp}s-;Hm-nTpEM~n7e-OZ){;?>~`r=Ip? zWJITIJ-L6@uWfcw%9pm!U$VZNA^PgW>&3|zmog`RY)oqYT6ONUg{X*K_`(hIiqATC zI$9b3fS;O6&AP*K=_zeM!aOW(d-JJ)AsT--WS(tKi@vh7RpX$#7g99Yz(7&E=M zm*nkVqvpS#&uVJw&A(Zv0y#SOvF&DbHa1PLYg7Rg=GM2W;53JDLE6+ARuk-bV|&6zXW zyLCif&7Sw%x>wY5#mUP$(YFE^XTMLm?qK)&Zi~FroZ2~SWHww@yPZ><|E%*UW8tnt zt%vLWe!Thqy8q6&%w-RHpOrlet7)xL{WrUC#nQ!J&uMB4)(N_$B;NE+op~s@_HxlL z$wyC4>pVDgs?syTU&Y;yyYAZlxkZ<4{zve{WIXU&zh~Ey?-7+*1-~XvUHI8i|CVjb z)VtFwi`u@YShwoNPc<;y^7l{nqcGv*$&vdEzj=wqty$Q#>(cc8TCJRth|5K8CNi=Y zUT3|!+IUfFV%%JnR8FRT`5(Jo-npF6I=eq3YuSN+`Nxi_)!g}T?%cO0+`Dhxp4Gx~ zD_Uyf^kqADGU=g!WNrQ)8 zMkxP|d5q#c6_AGe!?UfH(l{Iq4MW-nsutsbbgty1{&Zf|JbhP&z4MGi&BY>rar zew}J}`K_o~=-JZUT<5qBmt|}5x3g?K6#nB(!KN+QZ!#>yN;H>mOtOetXw`YZMJ*si z<6GjI^{ZI~rpi8_V3EczQMhJ}Y(h|@>gq6qUiCK{E?qiz()NZ^;^nJ1!(t~M*YR>{ zJ-B3UjKyq`R!>PDXDw?njz2HYZokkg{{O@8jTdHqyutqFSm%$sYo2#5ugh7XebuV# z=4rVjajfp;^$SeHHB7c|=!!4lSE-DA&b8UEn*H2|PxBAmUbZpS>DbcM-qB3TVpTtU zo^^K@RqoVKEWYj2Sz)w7K$syX|IC%{{UM2wiT9gXmC_&g&fOoES$Va4k;VHUzo_}H zHZ_5F);$)>JCJ$kzIssdVOcg#;l%cZT(ar?w;dEIlGCr+6g_suBcW9%~}8X_NLk461P(#7BS^?M#e|m zoy?fDOjc&8miK1g`q+t!*XeE!h;Y7f_iki(?4r|re5~Hm>U+YcKA$KnBl7~`$BqU@ z0fpclj4>`!OvvQ|gWv;>EhbmzSbvK5RJK@NmURF49r;T4Q>}0JB-K4TxvcHgs!cp` zi)J=_wumz;d$+6RlB??{qjL$rTlVw_D_Pdxx)PC`n%dqgR?&IQ+@+;~L&<(qGft#;y}uFH`c0t!sYjSMpn9X}Mhzz*Ix&|&22HHv5ona3f) z)mZoTL9@(?EwXdg$1IbT`StgmM`T<@=lZf1;S_fH#0OVoki5}0wc+E2YkpIOq0I~= ze={H{12sTOnZoxT+HCB8_O_EYtG~SdiQ`f0x0Fq7fw;|}g+)hXnsbD7s4J+sg3TNO zh4lr$Bw%jFrUKN|X=oHy2;R{cpCBa%ae$8ilb;@6h3|xemkc`590fMyh4+K329a9t zNp{K`{;e}Tc(74TO)WCgGCud_t!vjDQ@*GcJo=&M?R~oXU9EM6`zIOmslDd4y%9*rp81i2mhIg zOx{<8pusC~o#ljx-G|fPkG(#>KB}~N!=KIP?LI#4x1abiIOdnl-P-Ht{$|^?WybAh-0bh(S(|FqnCWwJ%#942GOe=GUVmml=MC1^y}dnJ zr#D>|D)(Q}csT(Qt_dt07lPAR6oQ2zaq7TwOqS88mH)$}ztx|A7Zz^RxO45=w3#zo zTlVkYKXqzqNXQe0Y?BK~#%VtD?P^!Gx9@Jztl7~X7k<1lbyIogHo3YVc2CUA%+^Jh z*Sn?7(>uG-KqGyu+skdVMR%Ulqxuc8JX1_{3V(9?JzL`r5#Y} zXjXJs_M!1Y^^=LaXUf(*KX=#P{@^vd><4hF#)7A$)6Z!fcq5I^5>VTJ1i@szoO&)C||AI$tdReL#q&ACU5d}fx; z?mcz$vbmQ1Vx}~hZW8eRW^%a?6dHMOp*LOwV zxf63W>ucZK>dBKW^@|=peEYO3<8kFrRsTQl>UK=4|8+Bb>C&m%VLEect?g7TOHW_7 z|9t9`=})D<+xUF#U==+qQ4r=q4B( zEWAfrTAH1$pe?OzLtCBg>(AkSuUD*Ey_$PRz^7x<)BI+OUA}RpN@{@ z%Svpgc(b@oZ}{VHl*#vYZexX%kpH)Mo#&dyyZ*nwVkHR9^a6}Z22m_(6EyFvNLMtg zmgjkAxp}$f)HxrS5)VGT(rha|T|fR<_PyfszrK7q^Ho|dCu7I1&|CNR&WTU-_3dqD z7B4QlyX$t~&snDCg-i3|OpC+r6fic%HRHCe4oGPBj2I9htU#l^`xE9~p- z9{#+?)M2x8#4&X>FQqbR_Zl6I;z@RrY`Ho*c4uJmxw>Wg#t2NUGvSYd8$2ULKr+u9wd9}VgYYsHkG6+iO zG96od^}~rRzrW0#f2=KL^;TVX>Hg*Keq8_ZAbZO*FOKQDkEiW@{NuX%|5VP-@7wSH zvrl8!*VMEvd6AKoWi`dwxq1KZvdw9l($cdRyI1dJN@Q<8`sY*XxwAiKrFy%ZGBW*| zCF^VR{m$hLF7Z1i%$Rj6`(WkIrO#b`PusK#TI%Y`u7CC{Ev#kkrth0Rt)4fjruEmh ze?OngAL6@r;lhNeQ+fGouRMPG^yKxnBR@ZP?%esa!cfuXa_i0-E5(OOuSF+cxpwVi ztgnyHqpNHNO8azFK~-5Eli0Rh%4PiJw`*U2=YM-w)YyLU`Ih-hedp&^=!%%xHC_(b zvUMxxdoSslSnz?uLey5XX8J{fcmVLPRaei3==LM^ZV(!%|c9?3ME_#}swYIm# zC80-P!hcUS-?h_o4wSP!%Ji^tO`ceBs>9^v6nJ3HXkg*Fsa+HKhONGz?zTU$GOV%*-UpRfPF7v<(It~e-fU&tu5w@*B-X7bDbj}LDb*PFv0{PmUT zcaQ4}@?UFB<&xR=W76mJ{?`mZsd!WNaTai;xfyvqLfUVJ|YuE2>3Mu4v`7~K*yUoWJ6U-Nex9Z6q zywJXX^NJ}mEw|6%uud_mym3X;BV_8+S+k-wM8cLWd!|&&Z1lxr`5lZ(f_aM6BIpDJ3csqb7|*+2xW!;lYM46SEnn?YdaGB z`G3^)V7K7ODzM6|;{c0F!lF%^(+``O8A%5}-X2=0SH%1E72|iUD=*I|P{&nny6%?Z`Xmeb@U%J|9p@*lZx8Jz~D{FjzuKj#9@AkIZ-`{F2j!v95?d+{pD^}Q4 zezFnfd+_(y)YE_d)JRK93s%O27haoi=9;rJ(=^@ao>kx8+kZJW`~BzARabpiu6(5M zM(2B<^)(ACt0_~b{{QfpFD~Br;{)dM*)wM@U9v>ZMx`u7MC-KV#DMviTpBoDcptbD z^2a7C`{}jm2U2uaoeEi5k?~oqSXI1Px_$2I=nr>(cHZ8;F?i4MHBncuUAhw$f8M*z z^x+GsBR&srt~~C#(0FMr;c}?BdQ~c+jykY$0*Lq=0V*lq|!^|FSD0T zs>`erFGvHF%|GE$&1#b$-j-XKO;XkuOt6}aIJDuxgp`z&b2AL@o?m}`w~SDkL3xf?;F7g#dpU34wiI`o zJx^Do`m37nq{S=qU$6LcWu;x)jBD4zA|8g?77Nu}K0N)x_OJc+_iSqBE}YfLASfZu zvaa?M|1q^?y1cLa z(p0{(!~{{98ggdz&&x^Bsk8cbaOUaiAC98oDY^E)J~q64;OiR`6K>hQR`>ahbz$Zm zQyzZg?VLLG#@>l5OH0d@8Rg5$xF&N6eF#fieyBY2EaR4QR>#a6J5)WSUvg|byP7xD z>Hf9JdWDVG!6ODr!W{Fr3mm@x^WJP<(W%e(|9y7jdi;ZuU#CI~Utd$+`03~L1orUo zu$~@S8Qoi}=6s9`oz`4_@#5XPteq1#X5N@0dd$tO)O4xnQOyYlrs!WXc@W$hv~*MX zQ3mc!OCRNlrp<3CLC)@#7c1I#o z>{5RO1~;J;&i}=KysGTi@7>QlU92s3v0S~4|MUg+cm76ty_@;}&9CIz;;H+>ZSD6b zJUe4CWzz0Z}tsHxJwXR&PC`!(y!CDF)s{>^KW z|NlC1)LbFu+MT1)@`t)P%m4q?pT*klEF@qzKXmr|La)vLzHLjhu$Vn-hSl!E+fK{W z3L5PSX3Wz3aPMa2)%!c$Z6=tji?fTYUiBa?`uHfTlLLRRFxxQOeAhFmiH7~K^j3( zQ($!d#?{Xs|8x5B@5a_YI=ij+-TkhoSK#;YY5uo=OZP6;J!JpxtNXU~v47U=3$OkE zFK+f}=I{4aW4G;7zhL?KoLH8XMZq<$pZA^~+Vtu2;ak77_3gjDi;p|{`tA1r{&mwm zCWAuSU<->8zucdPt#;c#%E$kyz4cMKM!S9g50&RP4F8ox2(O#BaK-+mOBZf+_5QN< z*|F7PujYIS-s2gbnAsh)R=4-?t?e5Fc6N5|Y~IXY6*Ob@UA{h#*PEx=9eDFVm38{% zNqi2H?>>7dB<)z-{eF$A^k3(l5$T^yj~_a)lHbKa9GnDXZdo&bG*OxN=YRNqf62JX zLWugoMJ?e(+{0JLvRZehCPw~xHh2BhtIy8#?`w^HsqA_G@z%PZ-o}f=W_;iA_~lnl zi>fy<+1b+H_Qq^}w(R@=e>+`nKA95oqdz)2`sNLd)`y1fZNf5%<^;4c5S`tq^e6MtX-9(TXwzOc{R z#^3L^Y~1Krb$i>}nu8BLdh%ZX>EPn-o={d)^yTx{6Q}hbK7DMSSfMPn|9VrEA4`c~ zhK{bcuI}Zj)-px+7f7zTwP;Rp)a(4bHF^2@hUVc>Q7e7UPC27(qmm{WtaoPTf~`~H z4sDc?Vr4G;$Z@RN({a-}N7E})=U9Gmjb2^R%)4y<+#-RT$aOWovAGlHaP+V;eX>8f z#BvLy!S?h2)4KUad(=RUL`W~AvD4?k1mA}{)?J#G_cErlpLNc`$B&Ydlb7qp@6&nt z=+K%qIU2uz{?wX!>Wq)xS-y&<{QLj*OXuy#`}SrgJAaPrwC-;242^VdhXWtiZ~OCQ z^`i}cWba=4saX5#W&O>X1M6b#L&CzgZr!T*dgbysiFdyG^DCb{Wc~bnV;&3JFJ(bt z;l0Vf!+kbYf4{fCe#6YAUu)&p3ckGUe@UImB|(LA#hbeqWENF=n9Q17rKFR_DI)M< z@xFC2nVHtiRF8ZYV$_Pmmkz}w&N69bX-R3xTzKxp2N!Ll`9`y9 zl|hX=NTxBk$RV*qGp52)JBzExJILr$_4K9Oo7pDB?SFXDJg&a(>MCi0U&=N%J?kGP zT~f8{WwANp>+2i7X2-F$&l4hFzLb1zZle++z3j`2ZRglRqi=usbh^Ik)5+tZTG8q0 z>S`rr&lWA^e=R61ds62?jrOr)ZhKUe7M0(%ov&?g7^OVvzVjs&CYOX3&L3@Ol61wM zdDtdtd0*CKli@t0O{&|+`EzF>hM3(ZHcZ?u|Jvf6K$7Q$QmynUkNJ^SCapK?m>$hdS)kSr!>}vvkY$DWY-HzAd?&y~j`Q zj?QBVp2?3y1x~9m_`Umo=E2|lUw5S}Su^#({ChRrpTz%vs9w8M1w7=?!Eqtjjitp8 zGMLjKxFCP(7tae`2~3r(*|Ps1v_|jyxAVT2!0IQ5`TyTtf6G9#C-3!P6OC2Vz~yos z=aeVBjwv4sO={jZtbCE18)|s-;PH#0i`NN(2Ct5BgT^zaYrFmh+Xd==i*ZcQtkH>K zeZ?#Y$t;Y`suN18ZmTFSzP8M&Syqv^2V}AXi(5Z~ZK_+p?N%R1%giO=kDx;Ej~~0! zt|Y^TZDz3?OUMwpc5)e;-acMgNa}K6X=`S%O>Jw|y6Oqlc_4sOLi*K#j2M`T4p2>t zl_SAu)OuOH5#j{g%6R55%sj;Vhw1VMBsB*(X3Ur&%ms5C-24MAsty|uslQpeyb&^D z+TbWyP*ixJ;#kg+4;NG4J`hSsNm1I*pS$+ev0p`;wc63*-r4S)tY_D>-I{c5T9vP* zzTx%_MuuA-_(v{Uy7Jf+pOsrS37@=~SsuPT&ao(6f44mAdHLVFc5aNyZY^ptD|@%l z@U+xx?aM;tAAbo*NzcA^UBu~;ti*G1o5g!njFmI>{U)ZSIvf5aw$A#+$b+J#7RP>F{~kBn(zx#bzh?bCKbDpJ z^`3sF^2bA8JG*;nX>;!1!7?Vg($D<%x^rcjmzO6VmJD7z+a|X)?r&z!3QvoDu8TU# z50s>DX8?KrPS0gm%jYIf_o$ep?VQ_eU4Kf*y}bUx8$l@%J9m}m{tFK*Un+cXqW8iJ z_wHrAe$YC7*{7-6cb~RSH+ib$=^VQC%!`rt%pL2JO&eUBZY?}NWAHrAW{QdiP$?Hf1`*(l8-xqvk zVVU{y$7ExrL{(73=|BT#i~sI(x7t1*y;ikltJz%3Es;~6zO%k+o-u3YOr}q}W=--= z-_|AaqT~n1mFr(;t`57obCK>ufp0O|*}v<42ZRUbq^*0p@o3J?Qq!fgOifmDH@9p5Yc7*Y4HUIkCs7y4rj4t~PtuL-$SwS7^;G``TCZnX9#L=T0Syq;-0!mdgsJ zy<+rbqFOrs)zP@(1y|S_Y z^k)$g6W8sVIEO=^QBdu|#cM_<-O^|Cew}*A#-;ZI-wZ92Jil2-EYI^@bzB&@#Ivlt zTy^fRxE03lZ@Cw030YOQw7cqyt;+PBE~@fy=}gi66^nQ7{B&uiyy@l?uOR(D>sPOq zJ!NQ|zjbTzL+`8Cr*JV(a_IY49{=*0sEUwWT`=?EH0Oo;w(VKGH`4j?iIZ3P4_%w) z*1N`z$-?wE+i~ZTr(Ef4`Dg9fuyo_H)vJxIp9aX=T3vKI03JC$anj3fjqdyu7ew{| zms&j5pdoIVkLCCCYhNzi&K@_< zw2Zq%WX`u~GY`}o2)A9l9wu5|w#6rb$D~9F8i^2J7|a5VfY!uaIlEoI-fH)YOS|Lk z4Gx_UjNY?)wYL6#mbdpR4qwW=oFp}S7N3ko)kCLEE7U)kzI~hfE~>nH_UzSBYwv9= z&MzvOwnFP)NZs6X=e}*fSG9O~yS381=g-CKe&7AJZQH;4L*9IUs( zim7dp^d6@&zU^zLT}wXvYj(49)2h|0{{KB{zI7{aROyp`kL<#U7tUXAcCFjKVS_`M zyF2^+tFvxd?c-;bo4CT#N|njwzyr>TUq=&qa&ylv`paW&bxZ1M=*^SuQL|ppu-|lW zvBVjMxpxmAN}3kHBN@x{cJ7}Etq1P!5}8r+YL0aT&-^u4gt?YZogVL#a6?C8rQm|a zn-}w5JSiJnv!L_k%q5p=W1ejdyL~BS_Vm)QDWywifA-1GTCp|Ia^sqJ^BDB^Ncnwy ze_+1f8J4sYXU^?>E!sZ`>>_+*p`%s{%Cc`QelH`whW+PG3J_H{tYj{m0zMqt61r zB~PyZyth1l_qSWCOBa25KEFQg_O`d@ZU0-{pYZ3%^=LcS0Fk)+SFgHCB>dfYeBPb< z2P*SV3pFPzQ!S$+h(Y_^bZhdVI z*pzjZx5DrwY~)z(Ku62fpjjLOjhs%Pkz<|yr$-OjvhKV%|JMJL8=MM?9xj?D?Kfk} zyyf2nj!iH$GrMTTyfD;aO}N*tPd87$n;uZ`wd)JNYlo0>TKL-adXoa&!V2pq`5)MF z<%q4$itW3$$q4gw>=xQyz#P9p;df2lGtGY?N{r>tp0!1}hyU+cvNxdkb0;6q{K=D# z*Mg}lHH(3tc}{{!Fcn5Tn+{^oK{*9?dM$Bh+4}jpu(Hyji66dyKWc4r`RArXE>4GBw{UnyL}=X0&VGIB zboKX8Z=XF<>}3qfLff1`lizJq8(P=;&W`$RH7PMSd~?zN06it+zCS!4TjVES-p$hZ z;>YanBH=8pB9qcHx@#UB>bZ04RLt3!nUmhl*{qe{D~dsnXpMu#48SG#y6Wm4Cols!LUl#?I3m2Nd)vFzErXu-EVo36D?|1Tnv zzN|nyn?3nTMvjn4<;JbK>%Dhe3lb>Wc+3C$%<5}PGb@U!)9YI&MQcr;UFT$C^>cpZ z&smGqUQ9u6RLyHpoPB8e(Yz1eJ3j3H`1Gf1w%y;Km33!4?reK;ll5}Aa^u}@wj8eg zU#DHI;9Tt*_2h-_nO509z2^6RK01Bxe&fBkb%lkOTexbXoGu@8t33Smy5r{;7lrw4 zeu!un-Ct%suj*By#QOI;{vOkf|9|GXvX93V&aJQ4MfZJw^5jZ%-rW<^ouWTlRh@YK z!fVfmN3(Ta%ggWETkvqtrAwQxX1}|*^vshR&DjSJO`SS*t3@n)1pKq*=H;^Xg{DhZ zZeV*U==k|%dR*!+$oTez4QmW)pYLkF@$zT!@5HVDpH64i-}@(hA(JGTZ83$V4RE+`*Q^)vo#N$Gv*4hX(Z@9#`9ma6QeLEh)O)Epw^Rv`3e37z@AL z;rjiPP~xJcEBD=(H5KlwUtD_IC(J)OyLrXoL%+5zOKf*_5s|hw_ja5wxKZj@$(w1N z3HK*2kL_;aeWg|Eo0%FJ<}53I-uL>&I}yymI~Hdblvb)eQ(tgx#kwyadV@4}n8-$P z{%Akfv}fnaxY*0byW{_*$L(8ojc>vP^!RlHb%KANtG#&h>c*mX%N*9PxGsNl-jqt7 z_WgYkr@fT-(sKQ`|C0RIZ!KA2CvZ$HCL?F<@qd5YE37Ro>3ecfhvh zep1-lQ~o;wrpF(E3|6!7N16mqnQX9zH*;;xo_04)?Hv_McWW!;Oq<($e2uEK_UTQA zKE}mIBmb7sd@>!q!lktf-=_SLqumht6Xni#F4v*T&NtoBycbPf3%58sGAs98I0 z=Dfwb?JtBre3QyQW5x{5Vrl6tEpOx5S3Zr@mX==5cM zu3vZW-d)b0cWu}17&gKUX#_a1bTmu=v)sVnnsHYnyie3Z&>_fAS;&zqC! z-}e4aH+v%5Z)mddCVYr>`C-p-K_03bb#&U1;TrnK-?$=tEh-jfsoX?;Uz2vah>k*|KMc?o9OAJa=zt?(E3eiTuA!eI9nI z?{i%5<8x-=&0DuLE;`3XM{YWxwpO?H(ChH2sSi{GrB5e`A38LtyCyd)?cnR~GI>)& zLs8GJ&-%}o$UXb2?%}jBWaXP!!y8RGZ!g}HU>C>mGH|FPG1`4hOk7u||IIwMdTyNYirPl@PYv113 zZ&&n@mt&@B`Nw1JAChEm{{6nc$ETm^(DTi0pN#!_eYzKg&55yazxz>6jrF6|pQfZ% z=jQjtZ}wOxaGcw*C4TzqO#$&rYd1aanY74xb-)s1qvblg{L<6ev(H`G;-4eTA$ae| ztz!qT-3yM5e)G^?bkeV^l{@YQbaX}cHW;sQx>#Ls#Ac7np=;OF0=EcHWb%9XG54ad z>YY0=7w_Fu3Q4eSahY-84U<`K%3@(H|CW*jmhxzah%y(8i>7hl(A#xO3AC&RULr6E z&UgS?FfJ#*@6)zaf2}jRg}weUcLF2k?fW)sc~O`CeBXPUt1c!B{ww5AsC#ngT0Y+k z!6zwK?Df8Wjj_moCaw3Tr({jpUkiG!_jtTk4D6oycl(kDn|7^J(y1zp_05b7%=+-; zXP@3a4u!ZW)rRJ=@3}#P0~?bsA37p@pG(o;|7+pj|5Qb+cCog)=?Zzd>6~43Z~mXB z$=8z3oQdA(xOBtUrZvTa!RtBuVoLtKsJE$6nix7o`)M{$>$`P&EKY)9NoRDoKYDiF zwa6o3?X=CmQ>&wmofd4cik`iG*EI7h;cD~#h()(4g;X8-sB5@6@30l8%7&-O1uWuA z@)pT{xKm#6?cfUT;;pw2KCOTDG~3VQgh?DI0AQU5Mx_PfpwaWy(yJ5M^>w`O@Aoa{#|A6_myXN6>Sxh8F$GazEbfp3l{N>gS{E{N0^PP2WD)(>KQ@F;Vc!oH;UoYivJ#`W<;( zcln{f>!8rH3VOvLC{e`JyEJv>(p2ZTwJxpu9E~IL0?%1;ZcRKZ^?qTX)GM!ZGQSqB z{km%UMuG6MQ;r+1PSw70#pm{|-Ods5*4O4cZpizd-RZY~NB*rjx97zCHCVK#WUuqb zA8lun7OpP7yv6!@dHL72^ovJMc&|FOIePZ%w{vQ$Rons#v-RfwoOpgIi~oV6$`UUp zHKoKUd4a=5#MBXW38+g02M4FbCQX^gQ?`|OKj$*c^u1qoI%|(hZEUeeM{pi5JnRDk} zzICfh;Nii8jVrWNRZ9~@KP|QU`(^)`vi;fD`KL||&CSg{QT1x2cw6=5)9LdnD-Qm- zC$OTiagKe0kI$FBxA7bM-rn2${k?onT4wEi_iUG6-zI+n&3=0_X;qt>v}ec(_1?1n z?e4eVZ$-lM2j{klxg23(#tfhpH4{A)1UY+guA#kbDzch3Fn?()j%6DRkL7Z zHgDx#pXci9nCI!`_OEhb!HLV$rp07@O0=E#_pdx+?jm>T^7J+LFYI=|!w}`~eY&Hw z)2X0f&yPpb^_Sn-TKAxIQ&m>BbX?s}e}B85JG@d~zjvlgceyZ-4?eeMGW0p$;M~?wx#jQ+=SDZ~M37&U}=UC#;(s5vg@P>en`R-p!=hOzImY1%}yJBt0X8qvFp|eJP z-wiaKxy62qZQ5%Xtp6`4be7(#IXj=o-~G$@WaH=SpQ9ZgZLxSCNM->O7Hm9HNW=k4)}Iyr)+8H&wmSxcki~YWm>*@)t%bqEUo7=1Hy0K zC^9}JE1N6cm%Fi?lVj$YpR$j?JX$FqJ!{L14S@o?cWw&0aKvxPOY!fYZttpMKWc69 zv!t|CLQ+ywCs&j!`-RHfyK(Wlrhf(YS0uZ;SJ##XMkX4?TnTKEZP2`9Cd{(z%r4%4 zKO|4O$Jjb;$YCmUTJm>s{o2e+psAdfpsAcVqjy4aj2r(cethxm$u&Mk2el;J`uH()9-hgFR1zSF#mtug$504a_!&BI%sM+u|Os? z5^{tNOq=m_>(j|SYMHkVUVM1!QciF&>+7Anww+mYT#QG$B(}7(#@eDQS>F5Mhtpk` zT9zAK?%20B;MM^?zFPhN5@D-${#<2jef#|8ZM!VK9{O*v?$aUf-mgy|2Com*%H4A7 zU?8*3-7|cBf=|=tH0;}8e9Z0GwlcjvEQzPMH=XmkeQ@EOd=>eeRh^lP8A^$nx$S%# zJ~*0+R@Owb&HunRqs#DGnCN8LI}g^I@}l%R6KVt%)_?d|x_8BPaI@yHxe7rK5VvwP1W^>bU7O$0Y4I~o*)7hKSI{P)$TxZ9?p zwN3L(&BLCj&beS}#c6HP^(C9V{oI4M4_3IWzHhra#!r4hrboL$NsnT0_c2&3K}V&J zSu;NVXvgr%9=$3A2LZBU7&DntxTZgI-iaxLqD;x)9p?ninmh4MQiz5Lw2i%s!SRWdlndEt1)nplNSZKa8(*W4Wcf(F5v zgcd|R6#lU2@&rg>aDajH!nT>tMS-Dxb{CEFGV)g0*MG5h4pIztVmZdwc%TEc;Lq~q z@>AUUyrJu##s1bDgVJeO;l>LjrrRdXXf%x3FF(wH*b-ZssF~$xwXCvm(_%* zBnF3>tkUi-I`?tyas7KYE?%s2Jnw$>$5Tne+@*`VPK%yLR1b<(eW_%Jd`f;=OxWnM*(M&YU{Gu;|Kf<0RRJ`zt0+{C2dd z$gHs^c7x-}E$2M!y!KbRy_v+jK+!v!J$QzwXc)6bx6+4CpZR75Mu%$ry)B(}(KugE zbH|{&B!TUQ=E z7eBw?&+LagIIXK&Zd+EQ#NE@k-E#7Bn1^;pk+gJ{P+z_v=Wkox6IR^87RwyilDXRN z9yzM*v~mmU)n&(Cbu9{sz4IjdQR9nu(n~{DS>1}(^14)dH{EHm`0N=S^&87dwwRe- z(VA+;nE1K)Qs0p?yUvC^<%tgZRH>V0=j)<3Z=oIIWzZaG^DeEAN6eKTu9)+v{?tv~ z15r;^Oj>8p*8X^DyZz(o>dT4~xcE8d+R7{|NH8&b_H1d?tgNZiwpAR=I&$R!9y*roh-?(w+wU}5Z|NjyxTg_G*u0H;{ zq_p%AW8+B=4=d|iEs=AUY>tf$KBE17cE`M(Y*rh#^~+jY{CfOOxN~;a{<-2>SFTR+ znsMl1qhiQ4ZCyiIX^D*|ZrTd0?_R!rWp?+^Eat<{BX1Y%cX2s>@$M#{hl-P*cXN5o z|MuJ0VVA{Ip~gfdg?~wgK4&E&wu*_R$=83$i-}oN5Tn2EM^0SazpmGL;#}MF{+>E@ z%E;34>$GTduBWqJ_W%2tSFz*V^2`dB?-mzrn3(>AYDL%A+n?3(*tNcd`{}!Pd511f z^vvH={`S#PZX+Y3_}`DKcSRjhVc4`r=j%VC<*S`0#>^60zbB#Y&hPj856@S6_~eO( zO=_m4@+BQtA?UEmme*VAW~VggX6IUcUTxj8cXE8d6Q?q2kK zvf4$fO*Vg*J=nW%+4a^5YcIDqwoPqH{`!)6_VRO)+}YQ^{CvIS_#tzX+JCW&q%BT2 zEmmFM%n~DR=qe?)SK{TXt~;~3WD?h1E2)j$8L)0s@zIqY9h^Mc+0_eYtuxtUZE9** z7BTr@7|-I(D-?1rTryL;Fk7aYIc7sEYxAM!+e}uUm;D)}F=c&ckmF?W-PP@%kM8}G ze5F)W+`sm?_YarCxJe%tt(LLT5eefhoobnRJn8w}hcCsSzIfK}efi$KNz-=B)>oao zVzri$`SvLn47YD6Dfl)qKw^L7Efs@o!+|C@4kfzOMS}Pvs=xsznPhQ!X zt*f)+fAEdDGlTa8FuvTjZJIH=y)dWy0~70~9CC59cwLXi@jqNzY;CgnR+N-U>mHW> zQ)euG>Ob+o64n={rdQ3)LmpqbbnN0OuhyFvrY*Bt7*(CU_(RB)bAFZ`(K9Enj`#Va zbymOVhuWRLuag7s-;T}pe*Cof_Qg4qqbJ0F=(%+0%A-9la~?hH=681d#%dE;U{(36 z)a&H=%_|OKfE`0FFUvS{69hc6Z7lenZLe%f8|O8rb&>webIfNg_eDI_^!=Fb>G)^ z`F;E^j~zQ!`ug4Vr)~VL8x559od+%YODN$i*|OE=(x=oN+k+$cPL`iPce2}j@qs3T zlP5Z6^2ORV`S$n*3;#c!&1Vr98fg4rQ(No>YlCyv30d*`os>Hjr;Eq7Y07XFWS=@e zN2BlIkGt7Pi|%T0+6QpPOffCw5J{W+?ZZr-+b!h*HkYE<`J4@8CQO`K`m-U7bz#7Z zBJ0|Fu@Su!Cv6CSVr5=^v-!vKshPHkQ*T=r$DcUey>tE}iwEa=zArm=Yo75t-fMa~ zx7))l);uiG*jMo>+Ri(iN$&WW%#bAtLQyxZ+THY8`~ve9$yi(b+d0XvR)5Xp$Ql=? z9W#wT?+OUczI9sPYx+yZyrZ*RZ$(Kj^?hi#Ilq~cjkU1b;faPy!OVj73|4`;pG^Nc zKR37?SzcbA>}=|D`$xddTRV@O*%O+=d;G36-@h6kp6Kdxdu87qS}C?MpzE{VL6N@n z)w}jhigmg2=Bd4^YNCH><+K&mhl0<%_4I$gv%>ec3vyqF989P_(bRS&IldD*e@w5w2=mhHjyTRD1`Jd;}3&LVU! z-{BEQ?@qRz5tfXP*K}K_FIcbf^xw-33YW|))N)?WohxfP^=CvE%jCT1=;B&m&Z67V zvp?K>&lvyd!^-PY(uLEd7p^n;syk)diMQ{B)}Oe3MW(2;JH4fR!IfE)4s=vTN$a*W z{e(d8rSUt|J(NZKac0fe|Wmwhx7m0Wd0A!nY~R+O^=?7(Axi|ZMXd7 z^)`v7sX?E}4|dCq49GsdT(1X(w*anc#Bbw8hc? zcE@jhc=`EVgZSFXv+cvCbTj?@$f{{r!So>_aq5-p(^kK+>_1({|A(hdzIS%{kt2q! z^KX1NJZzNuEow(qV&ct@w*Oy29V_|Mv+F#dy7R`uK?KOGOQtb8@cnrrj<&qAF?UNX<`Dl&EczU#MN zVp#!;!-eY!imPfSEPr}bGJIXk#C1pR-JD$+2{+!{T9vdmKHF9F(Ur^fR{!2lNVq)nLyCLLqGfGaVh7UCner=Zt0tN& zEq$>2-5OQt!qq2t)dZYtW_!5SY0)&L2GOhof1Vzld_Q|yP}HrX=i=F0=a~F`b4bJJ zhjZrAO>aJyyt`J|AF6Tma?6=z{Y^q^*<17rzRx_Py+LV)QOR4*Lx=qe>py?!epu?o zJ*m|9*as_*6jk{a&spUR?JZv?Y;WeCn!3y|wduj9J)0JH>&N|BxjLOA?$63IEpsR9 zzpLO_t0vp%o0hx8jpdgr`X11#0JhiJ)ASjy?b>CvP*z6h$j*Bu_l5ro$v@`Wey1ij zB!p$3?R>l9y6fwlncwL;&A)xCAl|~qcW0zKivl;#gSz$&%GU1BFIOE#U59fkTxg+I z#i{2{i;GX2-`h}fYs-vd%a$>-_r>j)@F$zQ>d)8DUkzT)l-jysgM*<<-E;6}&hLo_ zlrJsTUg8DXODyo=&ChvU;t~&<B{4OjF*Wnzmeqd;TV#=CY4m`XV>Z@-iJxU_ZDm zS*l_2pH+o1LFZo9gp54|$APE23GrqQj{_)*Zl z+|?;8-G9m5f+J5-?PiF&YR=lv{PSAuUZv`9dAowbV|6y(7ZfvkeDdCa*)V53LqFd!B(?pl##V5RWW^Hs`U@+s5;&JAX46frTDY2!3 zeV0H>kAs=G8Lx1~At%?t{W_BnZ|;KQMqGH2nn;(}T)yuR-U`+oAu zR(%h7)sw>K=lxKhsp&pbOzfbGPb3?&_MZ36i^U#GPv5nzMbhM>E&IfMf)9_}HIKS} zAwu=XDgmjzQ{sCA=hSmbTUPz>6}(%sz-(*!F(%`_L)EP*_3d`TtNXZ$^=5E3eSO?5 zu-k2U-A|U&%|FzvZ_V1noP6_gqj;8@LFnN(clW;j@I!WLqY(< zE#8yO{CQRXTEn;HiT)|ie{QZB`HP%FKez*C&-G=&zmA{jD)@1JI3zIDpgR1KHW`6uL$zhc#_dWlpr$SD3(dutrnVpE_`r*7#@krmA z)u*0E&)HcrdE>;5&+bKXJk#b%5)iRt=bI-v{oBXX$V1i|=k!-xzbz}Yrta;Vs3}GN z>SviLq$RYTYZboldi?H@=%o+iT0ZpUyj{DZ=wb?>mGVEINez??~dY+Gn>O6B(z=-Uc1&emG##C(}|^)Ik{zg@2V`@o-dPiSBUjJ zZf^4G#iRc3Pq=qSu)95ty-R4gUsxvo2^RVuIz2VxS(caH+`FqZpQv(7I3)9@^5hKP zgl{GmS)8|z@OPh|RL%6m>r0uO-Ck7y!zg3YK|7Gv3AbE#*$p2oi`-d z7;mbGxY@n_Gx7fhx5Dtu%*+JA1A_T?tsA1}9OV0^yG1?B;(KqaYzO;V?FtdILkG99 z{HbF7b2;(2tIxi35e4Gf-7C2(6b){h%Lw|M(b?d6c)e)$f9}OQbI;FX=_sEv@eB8| z56a8pm_yV1CtUe#{oquCNlS3r-JW}g{1@HNUbT1T!pTZ4-2YuUwkk=Oua@5Ec!NjV z`e;Fv)X`v_T~!mORxL8IXWXfCP58fRZ|=GVP4;c~IA-5xX+3`}lymaqXr=WRZrwSs zY@$K?*)pDUo^Ora`+oY_9DO8~+cKYJQCer3)z2!YSCGU0@OYS!1w7OMsr}*#c6Snhrh8u@F1qB8gW(Kt^ zUVQk~)zhZDTZLHL4PO8Gd8DJGAmqjS^SZ8yv+eko{Zp?i|If@{;BU7zs`lffRAb}I zKYmz9OV2(#`@d!V!P2QyxBd#_Q!a9Gk?{KYqar#gN@?P>Y3IU&!s<-x^z>`q?OZ-_ zUx8xa%8(7RF7b6QD>)C%um88uYiU+ShKBz74VyL{TDBlAu6A3|G*3;%he}Z{mlG4` zZ1&8}jjgPFzu3y~Otw)9+&IQqJ)8-JgH9 z*VMp1@xDsS)1$jr2xV-Nzx_cb{Q9)R2W77L1PcfM@SA7(rByHg!iycDF+cqNT*%Bl zzzEQrMabKR^!j}G}h+MEl_uPpdTBU7peXvd(&;crnb(i`?oGJ z1n=P3dg&CS-!dH&7vEIHJPR-F>OjXiVN;)5?E3N0jN|&1M>ggxeiyG8GH`}*f1IZF zuihZ=pn&w+$Fj?9A~POelS^K4t&lgaL)|Vdcte2Omhi?nQ{^ibHJ@8uL;CD) zm~S#LcU-S8#9`i2ZyM>Jp ziz9B{>AC!3M!knk+zlIJ>!%v>5&<`g*S=h0Y=7{~*=sWsWpD3Po|+gOeqcY#!vLd6 z{STNDw^xcBS?eHi*EKs-S>cN5<;dWPXa84k%1bzVf#Zw&4VFJeE%$_S*ZLZ?J}y4` z=0RoTbC>y_o8Ek$k+)?-fZwJ7uNvc=yj9Jq@izp#AMd;SdFzL+UHiO@#V0*m_HV7{ zjgnhuwfC$saLwjbQDJiSb9lvO{-n3Jp!IZ;`HyOsANPNJ`m@M%{=YwaHhjG;X1hD# zSC+?vipA?S|HRL)*;w(>X-e(i|9d~(zHe9bFXqs`TK2N{lC##w7`$AztmE#||9{{A z-}Cbtd&Wfjd3Dc1r~Ui6T)(-wx#H&mp>N;J&hXVeI-+TJth=}O(~F6*9g?4}&DPJq z^ToP0yYAsp^T>(yH9x)`YW?ixeL8IYPWN3xf1br}C{Q)toji-NLRaU^^E30x{v2Ul z%bLhu|G2k&-;Yh#m*2j9+Eg!ZRzR@YQtRtqe;t}{zg<4;cYK}YhfAmH#jYK`#4Ir} zO;rUn&CX`Kt3F@MUAr=9NYdpP%MrC3*G{#~{rlD~|(Szw=p(sK*n zvz-c!?Seh4w0?^kL@ibOGV^fSJwu*DCuSbH9sM!-ii|yv+pB_io%{^Hiw=F%t(pD9 zc5%@Q8#c->?oRL6qG@APCMv_G zn)OJxL`k;u!RCl%xgWDW2j&9sR&`<^>54EVQg=9&-> zlWWqkg4Y(z+!kn=$>l2K!jK@Cu#vH?{)JCYO?c$A#aG`(?Nhrw-QReQx!&p(r9U2j z-ff+-Oh$OtJ(pP)x+ixF2$-!`=kc2|yS$?6Qr%j%9V;YPzm>5y>CT?2aEt%{!FB6( zy{+#4UhKU3(+Q4`t&F^#GU}(hE*b0pzm(5@OyB?6C;gsDI#M~A|CTzs-OtL;c9Y4? zj#i)88E|F75zXftY<`DnJdjpkN;mekX7|5yFk#A;Et{sxn`xzds)^-2xKHQIA@#(B zWgqT*Nv_~rEfsO=uK6+R1gYOl4Z0=|&z@BZ2%rAE#W%7qpZmu1%}XoA( z)Z_j4fBPbin6HsX3d-h}eB{{l_NH6t!Us0ov3n1_t)3S5@U=|*{%b5TZ$5@3MZRh0 zy!5nndQPZb#IfZ+72BKb-#=cz)k*D1vu%=>+YX~Mo74+zzOBs-`fk{I*m$wQ?=>#1 zhje28zL{bt&yc9MwDZ6OVS|+5ANz{qOUg>u%-OAZLHpHL_igH_Mcb>jm%hE-|88qg z(Q6t1_*;LE&WoG9sb*6L=fCUw|NSlBU(Mqh6C1N`cb;Y3d*4|l4-PC;pI^0dt?!%1 z3j3d3yj1gRW!!1Oe0Fz#DXFM`KhOIozMgNo`n+)Y538NIx6AJRl{xpopmp(D$kNsK z_v<^hH#=|Muwu`KjO!mt`?J)TToQPM3_`W8?f=afZQ1?qrQBOYSDuQ0n3U+N;HKMsKYYsM3;E6mCwk9h_YEt`-*B|9+e5qO`vnbyVgsF>bfM0bm*%|c`pXXAK3F(wdf}{G z(bZcm?p)Y<%F|-a{7U}$9s=uDzk2?T-GAeR6RbN558lucY~Q|lv+t8VJ*P}#r<#@q z1YB=Dc5n8q$Qy1E!5o}r{KBVFXB~R@_~abz!#iRnKM2WhsGn=QB~s_@dzs0_d3F;c z+8JGESQ_cQO>g}1Tie{|r&|Wwzl+Y7B$ApAADB-uZmw5Lxp4;acyE1g=wy5vh zT5M-)C6_<7V{P1=?&p{keaP1>a}Yu z_0cU;514E_6D!HJNqjcV~<6AztqS?-o(%k2jeP?L6`FN1K3z;rd-V z?7vTX$7==_R~T%p2%ez4%lp^S_=44fFMF zp~d$->tf>g+Ih?zk9toyz-7FrMQBaTlzc96u_Y#g5{yi3Ib7`fFjgKn^WU3X?3a7* zqgQeD-va{mUq05?-}$v)z3#ifnIn05>()#<+AS_;xhMHw(`)u*-u$eruDKga681_J ztkeu%6nV3@dcSzsq2sX|HyB8tShn~Ux5Bex-6pVoH_lJ#dv+|Un7zD0$FFa%;QqR;yW$q9o7o;TnK)zPhj;PT$4V!? zU$Jh-`Buxn|M&dd>NK}rckNF3I^W){PP`S>o-+cko%$jgLah3SwN&ZYo|^$zdMhve#X#?&G7sNA~rbUM=w~y5w(o+e(d=BSIGb2|i-iRP^hq^WPsrXS&EW)`$JvwvM7c1I-x=W*JrIKI+VG zN!_~aX2|?g7lr#?nTEE`if7xfg4@tJ!0huU$F-&zT)DD)6kJ-p=AZO<&@HBQF(ldA zu7UqM zpEsDT-LtnXS4;ZJ)N5Mlds^16$o6WfzQVn^P1Dx$n_9QrdauC0=iVLk5t7^q9X*55UauPc0i^6&hE*9TVf8Z}D#K1>eZY}UGMVdg5s z&M!-!sJE1es#^+5#4@qvaitf)7LUJ*DvOGlxb^Rg76ImDYuoruBtht#s4#1 zuG^1c^VY3_eg9+gesRt?lxTI`v~$tdT`fmrxWC8E*c|!c+L|kmZ=@O2ygGHIWp310 z(^jSH#%Z6fZP~Nqx67dmD>bjL2~gR%sr2F5YiG}X{d2F-hxhgFhi`grcdwZ8O!JSY z?A+SiU$^3CNG)UR@!dI-;iBS3&gCA$Jd4x2bYET&&N`{6>TI8u#yXnz=YJUzIW<8VE%b6JeKvjh6vcayGT<4@kx4Z3A zwmIZ^FoX5?+}^#}msZNRP8Qp1ef`@OuHV%WXC}>iBARH%)0^xw=_1Hs5@+dqcwhOuHw2E(RKJ z8k$~6a?EO+U~RPaVTQT>-;f_uQaKVzW0KWxtf)BW(RIM0NbYrD;p*1YM+F{hsDC|i z#d5p%440o*EqCSZS&*KWH*qb z>(=hqMq{t(Zl}xl2`V#3&dCb@SXb7Q5HH^I?~eEssvhn`=jBK6%{K@No8_@blF*nSW$= zNEob@`dV}FBihRT@Bevs+~?l;xt{%FleekQNsbp6*Z;c_vZKcG=e67Kq-WPIjuQkO zOt3}h#Go|3C4ysqHZm0{3>`Gq8U= z#pUTYzq&WqP3x|!&i@_h6ntL?bgGWz`@|rVaKp#Tli%#I-XJF*$kBGebXokKG+Xcu_`Yb4-NXA_ZiW%^HO^^=T$Gk`yg701U84qTv6*q};cu^A3BG#r z>bbO$cSgdbVCj+#H`VT$ZV#Fg8M8X7;EB?S>{VS4dNVRtI&Jy5<_WWp@Y#(E*o%xQq!ep?rCjO;Fy{$p3`uDX~3oYS#LQtgDz`{yv(po{dX~HbDK*<=&U~R zX-g8c&Yhe1+4ySdi%TuR_B?vCHZDJNraJaqX{bd=QOyJYxBpGF>P){E%&ZST)cM|^ zyd!sJp#7WQ{}PLfFZb=Na#bo2P*G()S(RI15i(cC{VR{T|C=T3QdZ^S?<1!kdKAph zCbj>rNt#zm-Q@%4o;&Vmd+Vr^bE8$&s$s3d4Q|7D*cY@mE~HwQgd`mM<@M%>JSJ^2W1?Z{-35XD#jKcRKQ- zdh^xLsc}VjY`45RerC_rYv){dD0hdM`YPHL{RwuP(#`czh|_g>)yzYYkvgo-S418f z3r5F(sj<0Iq!#{zFJX0@-P)NSxUZepVRNr2e}8gm>&dFGj+SNtG%=7uF#g@p7EiR9(3z3b||>rKIPBmV5z zwO6iQwY0eL`s(NKxVl&WH-DG?P$+W!|F&N%=lbpaeoysu(R+zx=7S;ZPU?*UL7wV= z8Ylb-;@ICJQpeK7*(l)jK7sk&-D#z8X=L3s4rpQ?|=;~newVHVK3ur4ZpMkL)t#5R(iA^b^(+%Sd{^g|*iIqtvp;uSk!27Q^p;xH za?tGw!|~JUDR-j$?|WGNeWG&kJ70%-*^@(?FWWQ;vXx&I{m$OLyuY_s@^{fY`AJM? zI|P?pSQqFzZ{I}&%Y|MyfBp@-wk^WXdCSL*7ZcxXZ1`M#YgVj=)03s=wq-SE3F=5I z9`!u8-SoCx$c_r$RgbtQRIuL3Klm|oKkMr4b<9b}_y1szzj4?+NRdr&YKteUiC)mD zjRF^!=9hfv4oNN(_dcXM#kjTm(mqQIKb`gyEe;1AHbf=ssk`JD9|}s!c@^4mq;;#? zj_s3gHiZdHkm{azL|?RR-mLEBx-AP;uN+$*pPQS!-bmrA)4|~9f(&c2^wK<))b7P8 zXfA8({GfJ2rdee|(YgyvswtuZ`%fITj4#=Cox|&B`vgs!?c$j#8{hlJXr0uw`2BEG z>Gs9pN2besUpFPx{Na&AThD^~6V~|6aNQZNl*#hu_n*`3Wo7RVu&^xFKKHjtUQDKc zlR@kD?O~s1a-H(b`MBor`iWPR{~hFfF|XZUq*u6O&#Wi2Cf&Z|ICXlw#lx4v_pZeR z##i$BT#9CWZE2=>wo`LXi3GFy#T6O%$IXf^H1K5WnFdc zP-gvv9Xw)kGa^{ydb+y!`YTgo7!DphxNo!jKV9{089xoo4q9Bgdb3C| zU4NSJ1no|T#vez2xE;y=#NXHMnO*kb;3C${6$@v5uQO`iX6l#s%R9PaqP2JLvc7Ap z<~+_%&D$e<=*Z%(E3dAZA37@9w=QeSjM!!Cqx+}(C#f$wl_?J88Lw!-ogLv8hI>T&SV@p&cHjnui~cu_b1jJ zirnnxK9eT>(N{I~Fe;5Z_~7x@m`ztA)Og%DbQ+>g?C@xF)(N#aZmlSH(0Orho!P040**Dijz$I@ zJ=>{zdzQB61F1I?FKrLIq$R~$?*IB_#xfZ%g+HxQzwPX=aIRhLAoBc8=EvSX|NDIb zS3IupY_<@akdUJCefyio`CrRCOWYhE&JnlF-aE1Ee1f@{z`To|cUtdU4AbAC^g`ln z23LNjMrm2(!*{Z7z7IWKU%C~bWWJtj^YI3d^l!XmipzdT_t>_ao6o4IQOP^=&ga`Q|AZL{FL)U;XLy-B(*VLtaL&F5H`7 zF1_ZPhSNXkic`va&19AT)NsZJ1lDwHjsALi`=eKz&j{MEUcB~H>AIzrr%T%Mt*5$| z=UnKNyxz!PKH<{m-zyk6azYPwv)vI8oyPv-QhaUGLl1n|`2TzJmoHoF|2&+Te0=lD8`I9NzP3KT`|zPdKW9i~ou5~G z|M$J^7xuF6uY6uyU*B3bTfqPS`_1Ne4%{s+e_!`(-|v0r_4mF!CVTK;WBs4Wf}Rt8 zTy?yDrQl!5ym@j975{A(D2TbROO;Q5zhESj>axj-2HQEln3#x}nw(3McTx*_w7_-a z#g>x8j*ER`YmGDZoKKhkyI%FzwhPXSOUkdzxBJmGNAg|LL&gN{@T@W$&l^c|bas{} zeAS(FV8^|G7mr-z6g|0nZ-jHkmsiVfIX#MU_1eq)nDgNMzr813rf@SkO?fuIvVT2$ zzE{Q0wK|sP(q~OyxBlGc56{+2<9O|8TkpKMHBs+puM`jC8S%J~EvnCI^252mEjuu^ z=bZV}2}z=-ZeG27?OxzRt#1GQ)8^Esq_+6FuaOiseg336;`3pV*3fkdua#MIx9nQ2 z*Dyo$$-DU>L8=pH>=d5#;o1_xzzerOM=IZ#viS288y|M|-<$bn?~}WE^PH?OLAGlO|b4^Vn^jYT5GO|650<39}9@_+WA*SK$8rCM9EM<>|_kIxTi9#COg( z_K$N@p39=>irB9LLA`0RRjhS8I09M4T`kTWN)3t$Jkw}j=X7kV!D*!l(_MLA+|XsY zy=b=kftk)i!VypF=Kkzgcf4`eS|(D=F@6gF9hauZ-RC>J_?W6HDp<4Ulv=#s@jqcd zx1VjSpi7iEi-7&6=l@lQmFW8L+W9XcU79(4JN33|7N+RF#6rC zmiW^zng1U-u#uCqd%f)2uRoXXbT@K|cH+8r>eA&COij*rq#x?|e=z(Z{(6UszQ&_P z31>xbw^`kOJ^NLcfKAJiG~JW4)8x%pOyE-R;XNm!^Csda+vcSUZyfaOF3nD2KhDhH z%i*|MLgkacZ#9ePiOFZyF)cfk_-`ivU#75{Qo{Y-}2zUuSbWr)|3v{AJg~SsGIqG z*7mw&|LDw#hIsQdSG-n_W{{Pi~&#(RF=k2Zian}Eb{Pl@1mrPe%;PNm??OB$~sl7={c^KOc+);9< zxg&7ad7X3xo9aBiZQH(ad{Gq0UZt;l@^{REn3ncaVa88`Sa+1fC4Q)R$gS`||KsbH zjc=~}oBqSKrERr1$C5Q7;*zH`vsbxVN)_ARkwC^@wM{?S5CaSQsK z71D4Ha_EZ;D+oB)@?3a|$lML{uIQfn$u#@*(=Q3DQsk?RORx5BXh`sHo+Tj6DLh^M zPSMj6siXaV`zECr=vi%_9@etC#^ukG!^Sf{)F1!$t=rZ8DvQTLFO7-ipK`spIlgW@ z2*!s1G%WCiPYYHby@ zjL183t3X?()9<@a5sZ1uvqrH|{_9U{*%P7cZmNNkQw*n0;TTeGv{kIN?$~w8=3kP5ER*7g%}a|;OmNqV)e~*+<};~t{wSK! z{M)iFB5}9QnSUIaa)Di`#*Jrk{}ae_T9U8 zO{I8q9)&c=-T!zadH=bSGk5L3pk#UI=26de9*y%S&GFEA5W>cmeX6s6{+{qlK~8t- zjx+1O{TH2-w5Pe*c3;z_^nZVp&fLHM|DSVNS=s4nzP4gLeSPz6POeR#^`~*}-;c-Z z|9&`p|7uq3%cjQZ;@@}WUF^@)So!i&Os|LU{TV#Y4T=Wp916ZNncJ7ed{b|Gk~{6S z+?&TmuHDn@FI>O4GXKcE+cT9H2!ydt+;Q}m;JfBE(KA%_DhuDqiErO6V>0J&$BYRN z_P&r7QSY5v z!M!IoFBc;K*gNIb)S%RJdI&Sme(h%OT&SiI=VJeqWi@pMLN{g1RY9p z1UB%j`f~p0BEENXo|ucT)-T$;?!}$uDL>txOYTUze0RsuSRI=+8%$cyC0o4cjC2am zyxUyU{OI8?-qsYx1|G3`_FvVvqE{srFS%}!Wca;wJF~xvO-n<<%PC$&XQXFnPdN+9~!rg-V=9u z|MAn~xX;#6**`=Si3ehI{Nm$ z+IHQy&vG_+rwKYNnSSO>s-NFEU0vPych(^R3LAf4)!{W%1&;za@G(}iKYSmU&YoU* zaaZNZ(8L;r2QSaAHPZWHp!xY>NLt=5c1Dwjoy$J3<>lBX#{kO|AS0_m|Fnn5TnGw?=I`xR4-TbeeUb_`p1^hNu=uWTkR9nA$()uF? zKl*z6!{RL5zJB0L;dN!RJap3XXI^>xmvytQM#~@A#_ajGCU?O@PxjTS9j7+wxV-1= zir~ARoHaXd)$RlQ@p?D5=xK;%ENj=goThc8s!1zC$s^Ba$!6j9k53=Z-1v`gU#95F zt3MP|GuJyP2r!=1E_iq3cG`aa10Bzv%D;W_q%U;i!-aM&CDW}VU#W8PeW)q9u;bCO z8zJF3`>v~|-fNJx`IES6NurLP#qWtr3@&ymI4yn7FZBM?g&C%c7b|a@qiHGGc*y_l z{j=*if~_N;9~N3x6p~S4xO~kX#=yI*Qa^5mT;i~A%{32^D12LO6&)>gnuTwnO0uG; za?7D*y{%2^lYT5+`)E_p*YBmNvUQ;b2Trt{?K`*Y<440k9|XM%9lxc&J^Pf&@Sx(O zNr{2%3x95WxoY+1m0K51S*OaOvT@Ec=@UoqsLQh~3jB9+pNhp>`~O<=!V+F6=%n?Y z({9=SrbVVRqV3^#G4lx>ExmDd6R(Kz%(e?VsQUhVhQN0#^>BadYWFvX8$`d}*?66G z?}MiNBWs;^B`@$=XYtfp;^IoX7wr2jS(b!Jb~rCp+`3Nde!{kNpnOCr@+hUKVNGnOndfTJ^QvwyTMAPt)i;O zRqNe9SU65|nD9&D5J$9rMqmAj=?6bIS3LUIs_V6RxmWk0-33$imDU%0p10)PyMNdJ zEf$eIx##TJS=(xV`#-SGKD)6^f(e`x8WauI3pBKfKXbCVKl>tIAEViI(MPM(mS`RA zSoQ5$_m5S9*=3S@YBl#a%-rj`?1Bl)hg)avmoLg+YkZBbuk4{pw$tmPvgk!zeJu(J zZ=E~}>pE%zogXXpnItDUu8?9h>DVoEN2pUa=F%D7&1(BLSpH&H_sh9%aHxO%x>tKj z_^0VK3Z6~aqhR5xB*Y=ZezdG$MQ-MfjO6DIU&>!f3)--Jxp9Nzj4)S))vkhlR}1>4 zZHX7iRr}v6+1E1NbcN$yl@Lbu)b?xl)`dy5@tA3eIb0AmoLn#=n{irL-IJsnohu65 zvw4?t2G01)DHGMaI85XALB%((3n_QJ%+2b5q}Abkb@i80Ke01;%W!(S&xrVLl)>J< z#Ot%RN@9T9iPBB~7}X<%-#qwpPs)3DC7ZCdMb5RaSD(+$QduUvjK%5I!`Ba`Wd$bn z%uT#ewrpy=$ssGrvxyA+d4o#?Hu}o;=^aKU~NtWa|A~&?BrrA@2Cx3Hujuo(=8Gi%4=@BfUcO z`-FmmuM^KsiLhXFFG^NjdwP1{Jt^B+l(zuXWqxsas3No-Vh7omE*swU{3q(wvkp5^$kU z;<7<`m$CUJEspF5Po9K@R(Tz4@@zfpognFdQ!VM0UO8G242kNxi4`ly=#U(uC#Rj@@fdu1NZOSZ=l8+e1qQr*cg^B|3?NXHn3+ zu7$DUbL+kL3NnTVieLYC!J)q*o;5UkLc{ToLE73Kj{Mg|cu$>gVt9AYdz#+V7=4ws zkN5Se9>}ZR>!eZX5%uC$%dN(LaZfc=Uo5<4%kXf?t=W1~E~55&vA?RO?ss8f{PRQV z_v!8m&9hrsxt8-+^G%q2{l=4ZTwKqc1*=!h;ZIML`_?$~(DR3$iLpuGpz4-lOyi2b zvV1;o-9_aCaZzG_MG_bcSi~flHh3On-L%iBy7BAm(&>r?JCzTtJA3*skF=b8^(vE| zi;JctFb0V|%}j4j0~-X*uLqz}z|$zHoUkVUS&wuI7br7_a%{*B7WMJzP~7lC;DOnp z4gAgxig3lCv2Pq)y=0adntzP!cIITDTLIB!aGXPdXG`zev_5Oat2Rnf%9|I3biTB) zu2$dt+GMZ!8a22@%aji=E%7PG~jh}98SsfE&d%CO7 zG~_8qSK3blm8{VE`1Imf#Y}U(+}(Tkwk5C%m))AR{#)7BdFOv6sO^o~_%biVe$(^F z$x{_3CZ9U>x_Z~`&wl%K!-S?*wB{%Hct1Y+XpZfpOFvJ3{L&q1WOwgYgwyWr?^d<5 zu~+%a*UT)E*f>Kl;r#sWpKmsAJa5NzMkG(>SzMge+}U9}&t13>kQ%yoQ_qesPdfMV z@$qfHQxxu*xpc>r^Oe8&`{QfBhE55NjotiqRrHI8e-!)Vf1kN{@#DMdeKRK~$B5-j zb*^~k@4qf)XVU$B%=;_8l%EP+cVpADgY56Q`S><%-P-&_eSS^ahOSLcVW+?m&B0Xg zY16icc{-EkU!1tG@tp5DQu^JM zj5*$1eI{3*NQnWp0L2Ivx`mnQC7fx(2HnThS z{KwCqpZL?;_kEuiJKH>8LsPT%Q+WNPY{70ejiRD0SNslq_+Zc=CMMPuCSoUjE&bou z_5Pn(ZES6Sw6}yAD;4Sr zdoIhDZM)7fqsh6q?Y3!tdiu)wMUKU9CMYiGad5a{#6RC)$C@}D8x75w6AE>&ZFF;M z^RM~I6o0_pL|3WI)FO7S?oRc4PsC?VPYahh82tDRoAt6|p3CLpitpyGS^KtzIr(B< z<;N+iQ5Sf%U!M$rFj4TOX`oVmYHoJxR+sE($I^p|0WQnFs%#>V)v>wL416ES}9e88K(|+7k!Yc%0gy_WDEio~^r9 zdT!d_W@5c}YIbVPNs~J#?mGKbFn-iu{`@>Ei}mYQk6apN9_qDzzjkddo3@FWndFq2 zod^8uK55r1d^mA_{qsKSyxE;vlV9t_@5{Nl$)vvCo;7&K$y1?WZ(cm8(LEX8s>N^j zqjbu*FJEfjX5SC^uwy>Ia*zb8HmDYOrp|P@`LFjA=A5!}zskE&@l^|tu6}$QztHP__CxgS+gCeu%_PmwJN)XpcmMiUmJ|LG+%SV6V&(nC>=lj%Nf2llu!oLllisCBRZIT5{^#q$@a zB(b-ro?TQ{u;tu3iy|(!Hg?Az9)6E`g55@kA769)|KY=eeM_BoF1*!sr;FQNG$r9N zUvJbp1&6LDDNTnivy^`9-k-v`FIb*6xZ>PL^A&eCm2OY&UH;+D^IYGhQ#g~pUSm&a zHP_yAq?N4CuPE1zWCm$B@@xds1P@9h2I;C0SL2d2rlY_5){@F8+ zmF3-@*SE8xI5|0+ZY*?m7k%>}>x+S88-sI$qJrClnrpw4)Z)%0O)_hYk1d-ujgSA^ z)Od@?@WN?D4olYEYn8qHZo}2AElmMhHHL2ISr~t`C(W)&J$Z85GNVQ82ZN@q-7Pxr z+`N5>75cI=FT`HAzn9Aq+voU^`>sgcpKkFJFHWQ!KDH^jdcJV~^g;=y>Do+jbsH~o z-U+rUyO1vbw|{B-$#oV@1@~5#>+o)!Q}@0hLu9L2+!SF0>;3z7U5NOedx<@De#z$V zlQmlAn|4fVODcNt?&MyL(%0&9k8ldtZ|2)SYumGHv$`+KMP;mPu-IC^y`fp-#)A*J zU+NTB9^tze5q*YVR(VCo;f4#D(|m2zCf+d85Snplqgk7N{9(c3*@wrnzZXXCs=C~3 zJ!#UTjlS=%yxcs$w(LU6T#l*lULAUCxP9ZpuzzdUtWi3&^SSz`K#A*f%kM~t%HF(o z>)ZP9dlz1-_V8U`2<^~yKD4I(=`HO`mxHUSs=vRtpENhNU?r>azxK8t(b2_ow`_l& zbS`0MQ`y$o`;Na8K6UiG;1W*dFklgrWMW*XDe>_^g1o(w?whqt))pyV=jA=(YmGTr z1jMK78Qo|(+{&nJmg?Kk33vs^bxDX}fv zWL-h%v8I6H+=Wh0;^!Z5@d=#st8bfCU6T2#UjHAhODqZ#UraA6H9LKDqWhH`o62wB zi*|-m#k3N0m_R3hHnX%%O>8eL(T5W5TI)#!}P0qdd2X+>~GA_aWtt|x$ zWt>!&6u(?4mBn?Mt4DgVgMY~-Q9DM%gli#BLOwaI;68HV;n7FJ+sf{|p8Y_1n-o9q z*YCcc)8&Oi1m(}2^l>w~x`LCR^OF3JJ<3aX3TGZa#gb8IfAD2ien7{Hz6X!Jr$s$v z>z-hJqdX#fVzYdb|NV@)=>g^8-pQr&miSHeS-j47^85K*+rv`lYPla2ckZZ}zrZDj zOGZxaz++w}#*f~dxrZiCJyj?5H9OKXYSVG|CipS zhpmk|nEy`t{*LF5j&@bQt6i})HA`Je^v1s0(|6178&+04J~#j0mxu3bp1ZsEr3Er3 zcjni=UiZGHTFdA5x>)nSpE}>YuX%oax!>$hZ@%8YUaqXc^|rac|NC)y|D(}=zHOJk zxBWkhuBwZR%Z<^JjzY^bL)?>Jm;GBSE`+Iru7>mty8c19!V7V^zR_&TivT%qRBz)jJrub^9;Q1$ZnEIBgs{V}k*{HT+!HF{$PCT9RQe4E|XxGXQ zZ!u`@Wtr z^V_$+ErB^NAGANwJ8|#wy_$*Bf}CCp`tVflIG841JNMJimRZ*>hu!ay5RuLO`ZIl* z?&*WuHq|hvA6}(lxm4g;(|wuwX5YFVU!R*A^*83!of7{4KBZ+QPv_P|--$lmd-r+J z4j$8P;l@PGf5M7;j@qiouy9S+7Wy!8#bg!@@edo>ihmvOW?HtnTm9XZb9!-1|8!)e zX17l+@zncn?&$R{oAudo;_1Z4%Q|f* zPmAwQXZ*OUBE7n{@SNPvYe)a=oORN}?bVAFqTkyKQo4^l%_{iQ^ELi(JPXsNtxu=% z{%IB#{L;3@{D)rW)k+r5PZuH{-3lrGuhOsm=#9?1dk;BZ-oE(xO>+JxS@$b1Tyz)P zoe=4YZP?z3s&xC+Yclacw`mdgacYn-;CwzWw^`+uDYPjMpBP->Xag z+va&%XTNR|yM5s!k(}c*XG(s^sC(GDf7!CN@-i~dx>qi#|L`~ePW}JA0`FeEI(61I zKRbJKlIybW?$f9B|JVL)lby7!BK53+((&WPof%Ed%`&n(%O^d0w6Y_o*Ecw@aOJ%D z|Npoz*u1&eM5=v<#kzNo`)!rXT7!Sq|NH!ZOH^)od3kW~=E;+VCpEFLv0VrfNI(B< z!kMLsi7_jzE0%rs)~tOOqZf4~F(-%T->a`5*36FiclPAWD0TBhmo1!bpb?VK?2Ogn z(+}#slvUq+=j4VNSC_b&N{S~3Z5MpF?byj1vYYSR{&_9tQxS?(bDM>~RVly9e)TH) z$m~;!o^Kv(*sZ&DQ8+Ar)?JYAA2`pRFQSw0An+WaDzBvLhVJ{yFH!h$pI-&hVJ* za4K}$y^B%J8i!Xrdhx_f?vH`D{?-jC85`O!O(`h+>>YQ`Hd9$7|J%j?Tt0FxKB=$I zeqCDUSePew`H_KxbHS4(o4Yy73PQ}^i?WMfT%mIAf9ykM@0m~Zgj=;Vg6yX&@Ysi? z>y|I`FzQ)qls)sAto8o?>#W|@onG1+@I5g9P1rSeo0>vFUVh#nPxfA8UtuQwH;s*P zM;%P9gO2Nm_o{_HBqaML9-i65@$AtF-6M9<(KDk1YeFtm&JB-vv`O~y<7B3NKLpAOYO~koB}Gb= z6+~1N9zSe+%q8Sk=A*RzW})}&H!kZ-=j4BXA#<;z#GDBt1rHXV-QK+Vie|vlnhtmO zB=+ZK*=1XkBANKKD{gtl)&^f&7=E<$s!im~YyMB;o$LJ1eEoCs`pJwo24{x`(E6DB zvkKE^)Yseymy3_DKKzbf{$c;0X}po2NqbJ1aOKL5>;Dh`6#Vll++MlT=;$FX8I~x;vJa1s z```QiRbIeTT>R|B!}AXw`uF?3ZQc9Y`{66}BENLI*Sr5X|Mz~dwE4R1jEp`1+IFj| zY6`FSls1d=J@f8*{QkG*|3A0v-FtV|)tATn(t{=K7_EQg*Z=;ay?&qb497;hnKNJ3 zG|K<|G5yJh!~Ai*U0qMaKfb)Z@0)K?g;}=zns4!SJO6Jy{{Miu!WK?tA?-w&HU{Se zKEV&(K6`$iylHKH1WB!x3$TNZDu>SgoSbQj6==JJL1J=e71^O{iH!wPU?X7 zGFkn~>2sDC?O$iKedDqQc_)_WD~~U4Q~brX$Zg9N@y^xhvUO|suV=0a|9)V~nb#k- z2np`@EaKl+j`_;MmmGgPHlC;h4GYcn{d=jlO_feUz@>^Qq)%5+j@}FA$GCO~9w9k)PhTLx(KYzXL`$q1--Pz}-EIvEcZ1Su1>#gD=i{{92HBAtB z9~-sGhsAZ>hVA8xjK1;7ynQR)`aygOXVZ*@%ED&B&prg~|0N=x!ngSLmR%c_kl}r|7$eQAM=&AXKDI3;c4%*wjwK~ zuU46&%`A22_NeDy+4_95ox-QahmwgGwFRusm|H{|P3rkHdFsi(AwNt-c#g!JpU<@Q z%>=~*0uPi8?)yGuJlOc>UDLYN>qHBl@a;cQm6}rWH|pZx#mug_spv-5pKef@s7#f_J@-w>($@IWw$-F@wx zIZ8kDmoHOWAHCacfy>#4{dE?Z{ENy8uDp8p?*H%m<@N8Y?+dl$1}?r}GV}ZW{~f#L zAC{C^R`4QnXVK0%nl<HjPL-z|BdX6JtCq=h3mI?Qx}V{Y3`Gik$EAqLe=Wz{cX>*ve?<#i)Z`gFOGWt+Qeh>Zv(;Z zj`;KPvTIlGD`jxfb8hKdxU4`Y_~1Im+^gc}1LnTs6kcbM`qY`(_N{Kwx}TFnH$GUn z#${8;)0jzXv`?D+zVUPI1Zy)-?-S4UEv!t8obRO9O<=Fnj9j%)X6mv6ar645x&2R# z(jRF5;o|6G^8DTS?w+24q9PT=&57*pyVtFoV;T3sF+;>o-F&)n>+YSIKYgZ}XbGLvx0^4v zq~A?sQy6?J9@d<34tiF7&A`cIbLhXvM*ILV;w2H?~(@v^^jGj0=Sk+$7W?fNV ze!ctqeaYR0pRcj+do3)u_3K0V`j5x$1x~)b|4*>U#isV`w2A*R)f5}$ie=gSY>NAP z`?s1-J@xWPT7Oe)UK1$-AkO<3nmHbRJ2IY?Mi}(7(^oGxvw+ zqq9p?-g6h8vl2e=k>B?-?-QomF+cXz3pweeD!pb3-(9m7F=^=t6u$c(v3Om61eow{q*6jscG#IQ%+cD z*>AAB>|I^KZtoL6xaRD*E#-a0xA}3=$^MF;$9C&+76{Mtt2$D$?7HmD6PGSVReJ_q z{Xgr|=At6Et$NE{8u>48*d0FMoHE1Avq$b-aN8OeJyCe}9!W?03B~FRdcilxjxdPZ7ne}Ry7 z`wF>D`dptU6mcj%c zo4V5b_oNw2s*d+>@4nW3r#{Bk!oA|j+N1F|^aTGlYaQ|J%-g^md{e}3eVUQcyGL%^ zvrQ`>INCit^v3_ij%iWR&2sX_G77DC`cA}Fm6S}$aOjz$>YdYjtJOhk)qb|{ zLlcfK*dtFUqB_wVJO)0+aVR%)d(`Ns=L$g*DgTXS;WI!0b9{d0Z)ubaK* zaq2gnRRjb-PYj%Ve$D#)C-PFRm(NUYintxpIe)$QSBK?3n}u_8gAd9qoRjm_=}+*^ zwojk*Ply`Yf#CDO=3{^Rj&JS>5#eHO`Amqkmtuo*uh*^X8QwzhtkE`+FjJ zLv#67lk9}$A!i9TL!C?gsk)ocTO^e?<{a7RTc=ry=T^48cU%k$I zDlZgnVb^}?m1?G_<)LG3#dYD5p$>kWk7xHZ+`Oj|B#fx{1IpTNJ96G&b<+dGr zDsK64yefbB;DOG~N|$1xiW06p;;PvnXWo_N{o`zx>-751t5YTKUh;2PnRIq4|0At# zzdILh1t{%km$+(GmhYmT+jP`GB}?_B^{UTLoX(nDo%Q3&u7-6MQ;R)+mVEHmwJN)_ zG0|~aOzM;GjTfxm2PFL#78m4uwm)W~ihNqe5%qbTZ2iOBlE`{kqtbLT^Px)@&g<1Wd)IPa z`zPpSZBtfT`7!ZQ<)7jm6*h=j*QCO#fkiFGZj%u3U9v zvvTWWZ~eVp7c^Y1PCP7MT~%cNip}18ZQ3r0ihnf+&u*`|o8asNnTXYKVY&DJum0rq zH{!}(2)z7rvEJsrtdQK^thdjq_jl?izWaIm|G9G)4n4j$xowq~->MwJ_3I0I5Bf#i zU+`n*>_fJD?_PZ8#6P*|d)C2Y1v?s2*X$OR_TG3t>cREJLg`A&KJ)bY-ei(``HyQ8 z&u;%)w+gR4TzF7(cU$0D!Nm6s6-n&wKYrZ$lq2QdI;-qa1xDt@{i5P4@l@dNKIEj=00*CEFZKZ~-f2mg{*x!yRf zEvPg)ahaw;mY(lw&FX|_8$aJ}RFzs*Q1t!TXVpw~b=`fFT$dTn&g%XlxRGV!A>YfL zQ8&69_RqMgmMsw8<8_B6tvXxxo0WxCmSFClH3{~D&cz)|cy>KH#98Rd*#0k0`LwFh zyG0`9a*aH;XKoZMPgC;He0oAKcJA4IjT`l%aLSCdxd z%o9gjs~1fCg3F4Gdi(nL%FE|&-Yocz$KU3kb9lI338z!l%n2)k z7p`8@0iOMjU~0U?%>LnNcHQ@j^WW8c+P*PcZSAG@xXO)xR-9bMwJ+)L7JmnxmJRoX z_08Ir>U$pOM)Z?9hTobCDJ?ypy$I}E!zSSohsfp@&Ue<<6;@Q7{m zoRf_03s!VWhA-Z-e!KDWBeRQ=YV0b--_9{u@$skI#X3jM3Gxyz&d$BJ;?TKNcX9uH zuLWeDJdFy`*IeHCNsvKmS&&QGav8TXOiW^PKfkE2ytt%);p}t|pHE_U(hFoi9yc|) z(*M~{=E9E})~LrOy3^-2Ejg$p9A&;DSmDC0h)egRp1+giV2`-5UV2s0%7%?=lk-ot zBro2&yPRd`I?+qYm&2ZLY>qGd5dZB1-?Z$$4;!asG-=McVrnsc%Is&;XVt!`c^do6 z?MG6!OsM1zLF(Qmi$3Oga_A2aa-x>AgORrj@+o^N!w{O~Z zG|6~TZ+|ae@^y}+NlVt=Y;6;mzGm~l=HVEqV?V++j@@!3NLnUv{GNK9rga?F}shOd*8ZTuxwc( z{)Mp}RDr%(zC*RLFLIv!)2o|>KrJul2SsMB^J!7Y8N#E)0CEw9&$|O=huH zT-~9HxoxsC*OqcyR&Sj1;HkX(v5pHYKOh^*j(hLsWG*lI=oMROtHsCnZLaO7d{e0e zEA=Gat3R`zJWV;ledB=xe|&;c#nLzH^mEMeSIx}2a`Y^F>Bm<`uRCOXo%CoIlVALH zxpP%bZ~qizig$eEe|%}(w=bNkGOXqfb)s(EpkBMU$(4&yQ;YwsJ=0s>^!~*gM_H}i zUwaEUTBBDu#C;Mt?NMNGz3^e!na_r+N-sUR;qoEo9b225t+RMx zW^bpc1&^F`!@y(3hR`foj9wyaf!_F zH_6Ykln3SG2tS%=FkJ)7;~jBXpUSBX*tHh~j0Cv6&OYMyKPMZ> z**1IsFNPOQ=QL!_UG-_ZTw8I*t7OflFFbGM7R^b%%W5w6z9HhsX{Ga%otrPulUcFh zDaY6A%Qk*Lzv|LtgVx2*BfMOG2@CL=vNknJteza=zAL0r?w_KWK*DAwv-n##vVPp= znyaRC?(wv9;%ZwCs%u~VrJ3i^m%Q$|zvbVa^8yJR0xT*_6|rU)M6S-Mc_hEP?)&@Q ziR|M1udeR(UnEk0)b-h;E!&Huw+jg^+w1x7=gj-sFYa$hjCJ|(#M-T@%E^oC`|QFG z54IfW+wyCk?Qf3156pL#OCNmx^8egB^R`ugH*Q)qJMYfN5B%@M4SKECtX$b@b7)UX zn~xNC&ni3qR{i~gXPqZRPM2WX+~(l#oP6=#-py(3$9Fg>O`m+RGoG8%-Y8MHDW6Ng z!Oi+mrulJgJ*UMwFJ5TemHoP$e;?EPOwaAVx?9-;y7n{i@X9IWIlOu`Tam4N`J2W6 zO*Q;kJQBh}3^RNJ!#p0X-jT6$!;CM&)3?pNl*ClGTk5#y@o1K7D*C;-zM{qxMXrk$ zyjuO4gZ-dYM&OGha+OsY3LmmGA8c?=7Tj=Qj=Q%xuEq+>fy?yZD@a36P z)O4R;>-r(TWLAriUHg(Z&AnKRXa|%xa@RIvXXlLVv?8gzu5&xHM8A0n)U?eYuEqG&N+4K z$5O55Li@Xna=&f-Jo~VzshOam=;>7pw+lr2>=dK$RNB!NJ`R1?i zN~w;!mRg0(FVi0{UCwtaH@S3D*Vpp*ZsbjGxC1r_oPD;uX@&12) zL2{n@-d-IFb9{7j!2SY!jAkPX`Ax zPd3%4d z6E`S)@lLjDzlX7go>5nD{M6-Fb6wdt6z|!*meJDtjt!5#V)~l@QqP2n;xbE*s29fQ zom}qdImv9oH_6@CSDubLck+9JnUd>=e&+|?IWyfKg!|9`@bI8r_+0%>8!vYCY!F~N zwK-~M$U<+%#FYgBty{SrMcv%pWb}@@h`sSV?&V)vSyoVCwM$)i>(-8?6^@#v~FuK?ZDkk>Amv8jB9!U{{6 zWn2^c+4ftc>)S7zu%q(J9d!k=SCCJj|NV~cR(8+Jo4<*NS?-K@xPh6E z_sgsclUq{n%#J;+T0Ngr>PP6J$?H{q?(IJJc4j$-%#8Yfct;mE5YlEI2=0JEr{dwL49_b|^pWzJ2}2j~Ncje;u}*f70N!`uaVL zjjJ|91yA@oal!M5vx_`CWn)V;=AKNN?XdIadSBsp^KWcyOivKv5BQXMlVz@invivJ zEaPd{g@LD=m5q-bcl&U->5bzWvFCry6*k(AmyDdwo5W_a4_+b=b43M3nW)~R*BP+h)r@2*ArHY|Dd zX3gAB=k2U#%=&a|Zh4*W;)`oychB1L`+nuV4OE%4f2Vr=N;gL6mG-?WxZG{>3%Pojau_Zv30_3A9Jh#zjG; zO7vx@@6I>=KIQ4ghP(&nPh??K34LG_S~2bOu_Sp$rh@-(u9&}yo;qy{Yp4pp=dqlV zN(QRQWy%H|8|R%@P`{Vb%n=$B{<6FF;0DLT5BmzFv$zsHr*I4K-;b8~?eWj>M1No3 zob$gbg6@8)&r%U}@=UO8ThHGq{4m8uhtFH>oBnpw@G2i2&JRZ=axGcTm_EPgyhHd= zsATb(bFt3WJeSM~VXpjXgiQ zt>#wU40UK^JGd}#?P9$g0aey^<)rX~<)Vqg9cH&Xw>U7JZ907DjsFQR2`Aw#W>Y4Z zvL#2i`#pKD`tjOoFMrNi3hmSHi!QIz+qvWCr1_S1MpGW2x-FA@akp>f$G{nf3~du0 zhMGNNYg_u@^!v3>GwO2mbc$DHa!mBObp2jetwh8p&2u*wFYDUm6Y@wbX3vC!u2+_> zXKu_8RScY&^0NEGMVWPiH@GHGkH3BYerWVW^>n={*{AhYLf3U1`8z>-Y0a)UZwrsa ztCw%~++Y0bRPoBy<&8UY-iEK-P?eOIzA3-JDAwb&?a#hLM(RN|kFV)U9*g>$z&dH# z4zu39^+}gielB#p|4M6V?uBEoR1Tl9{u=k_)A|J6`PD_jtrvGs`2I6UJ6r#LL3Ooz zji+eD`9SXmAP>C?f4!Q0_wL_Qvr4U|?|ye@>E)eIpNj7J{4V{}AuaFi zIdM~G?&Qf{0NX(nuxyi(a58u)_U8VEz0DuKuh_hp_3njTm1pbH&)iljvr@jKw{72{ zOxBGSv$ME=ZsbnCF>Sf|cDbDkl9!wjU@Q)8e=Trt`YWx^6P(ZJSP4pBG1p*xUYoFB z_d>1scS|OJ|G!3gy>ji$_<~8jYn{X0l|9|nUbIztI=VKu8^~4uYEeAk;O(sWRnk1- z^d6@D|L(qD_~GLGzgPBTi7zzU6LNS-YbOhLQE@Hjp64PC4S{FQ+8(%djPFqH*{2*E zS1Isp?wh>XTBj=FN%Q%wO-oKGEshFb8N7GKuWKiZ1btc}+v7sl$2%6;#T0VgUF_<1 zRyE3d=7u{rk|(Wik=Ixgd(&OLypO9&XntL}Y}xCsnB5I2U#Rlm`;yE*wq@T4l4v+JS|VX zqNBdl|9tKJ>g!=!$$NKRJ-zDj?sh#>yy+yLHgE^rfJLXB;m?K+?P*VECCp0iV7SMS z$bS6ys-15hyx@=f-P_g3T=`G0x>a!H^pJo<`c1-1H_5BU&$#$mhVf*n$WFWM=J7|u z*M%GqZ?6>Pkf_cGd3{XFTx0#FQWa5^f1AICrfv8)IZk(q_PG^1Qv?dU`(^7Eyt@}8 z&7;7 zpIqKUu7k$~J)4i6RVZElUv_?-MzzVtBcW^aSLd#<_qQPOe-sW5(o}Ju{MDa8A(F zw^Dg0WMR;hyzA1UX~#ZUZfRaD?RIy1$UXa)FRlorUFQ^UnYHP1)~vRkU+UVQzXkvO zynJ;vgPG~IP~W8y;g%+9#{?2M0$65f&T%@(0qWdB!&l6jse(D=apbleiAj&sIds%m zCP-a5cW~yrckTpwG5NeiP-eLcWdhgQ{`uEDVnL=U2>2nNz6v!RFe)O#WIrZ3p?VFyMoGIpdy<@NKTR*=%<-;*unwRZTS=l$9_DW!H z*IKgA!1v*sD_Up%NyPn(=$(+g?$aT|nu)I^7TO$HJbCreAJZk7wC3ASWKuUUS-N`h z&OI+aq_#0g^EoG&2_`K2fV};NhtXT{fYag#uE=H66%PnR9N{=%H`QX|t_cYZ7N@%< zdQPXjczgEg4=EX*o=IA7C;htSRkPBmQE<+uUL~Ge2YWB6iG^)z4z8S9R&Zi+%a$#R zW@N0Ixl&l0$=vf;-mKLHagPj76^guhu%&D1d5hc*L2jwWhYK6KkE*}Y-Yot_BjULv zQ*j@IaqIb?nhCOyC1RktCeTp&+t&E3)K-UwY#RggGXq40L9$Z>9sccVY?O0Jm6dw2 zR`dL|?I9YcHwQsuFrt7%XNtPEib8C`jZJ*UEYB2~K3^26yX_YnHEDM9-caY$58A7v z&cz$lieI&HnN!*cjdcko9>%r?iN-aDvxGmcoG!BckOfE}kmJDI&u0$SONO>`8bfxW z9603EU>m8s=JV02(*iRTnLeKr`L|qMsfkI}aMi(wf%fsozo^_5Z=W@<{%S$bHc6&~ ztDqV|>wa%bqQ*810a@;44{{(P(c^K>hPcS|CF-F82;J@13&&KuPacH}JT zJpJ{~hC=v;7zbOXdH3E4qzN9rs;s8D@+*g5TwQK_{_~s4|EGy0_&@^%6l`D*LYxq> zfa8pQ@Q*H@?++&$Cb6?G*!%af{eRz|`~S`QF*SD|dtbZ>@1q3{^1W`0-@nMoyr`Ys z!NI?2+qP*_G&R>wdD2{Y{H?i*tLuWK$jG1darOBH1yLRTtaJbV{cF0_wK2}y&^Sd?cmoTK({rNb1ikLw8{l8~}mlw01x&QCk#JSe%pBmSH znk>KP_d9mbwwx&|7i?a;w)X9=)ryfJ7C)EOetGdR|IUBsDDfl9=l?O+(Mc&OF)_^4 zeR})$?R_8q$Qv6QYp@?#-2ZRMrcGJD`__M&nHBZ?wDBxxV(oEK(5r3V%>BGu_ot-H z3)|WcsV9}BqkW$pv-S*pxAOb$FL&ZJzHMY-l2ZTPQ@WkYq}_FWM(!#dtM8o+{(Rcr zey-|y9DM%sg@Y@=J32PpTX(Iym0flB@fkdguH6j>S1T(%j#`u4+ru;e$0t@huYU_O z1vjof`0`cGNn6F=2hOiQ^x~$Y!{aw;xw)$gFP(KN{&;#_;E$8bWGyy5kMvrt^2T(X z_cWc-f#i#mELOY_B2@3k82N22rp3D><)xbSe+tXrzy$C{g(E_}XlL15nuBi+2%$azzZ zAi20fqVeIuf2w*(-&B_G4!(BR+OD=e$26EbB16RU$(r08&ia&;jEoKSFE1^Xa(}n| z-eq+Y#mBpv*6<(M3fVDH=W%7#s!KP^c*={vU*Gl9A|qyoW=u_%MEXSKgj-i13Quu) z^I)%SRT}&J{NRj~3V~QDNx2!VeTvMlgnnMlzSP*rC^orR%#Ss@2^y&qOvRlCY>Zm} zKbtt=5O?fJ&O6UiYi#byZp;1Tx<^E!xqN?^>kP5KyNj-xE&6}fJkH0<%Yk9?m(C!M-e^0=?3$E3=mbNjuz_ZyGTThz{VMA-k&m+Je~A&v>x zb*~Fq|A_zh=?>^{Bd=x4j@3TD{r$4D-NJpPb-HfQ;;O-s< zuy@iT_P>`E)Y(g(6-eMXBfzM?|Hg}!uK}0t-=ACi#oqYJu~oBW8}|gipAox!&Y`9? z{6AeaSLYR8t!hkelD3HFUn~hJEkILT5fL0`#Djk! z=bU-jhb>*!$3OjebmF|9&2O&1-_1QYGb7{4?Sls^f6w**Xu5X(-o3R2TKkJ$i&g)Q z?^;|JJU2Y=Hq)-fi<|S=+11sTzu33_e?ylcEH<8TH8xxjF|oC{p)NkrOsi0tMa%oV zfcw#%ub$iFi?gk{F+~@ADo?sdRcXP3=mhrT2fUG%_BX|cPvs8YF?TMr)6EmEqUKx< zM^=30>$#i}vyr7~&YZJbTNf|BY&OTS;mF+hT~oZHnckPWr}N)(iT9UYs+pMn{)zCE z6&H8#@bhmmnewgVl$t{0s*fKFg&3}%w(pDXl$6ZMUMJ%I?4VU|_ciHmlY;p<>-H<$ z)O*=*PW#MZ>ynz9HA@%ztZ6TsBw*Lr8PHvuBb!^gwpX<&sLVsa;QE~96;g9bLxfra zpP$fIIePLY^X4t9H}71jHh+ElrB~^54_@=#VdY_dXRmUBG^4G>jrpY+riw6wraXRr_}o1~pOYt(H_T@-FB1O7?dhB`ec{4=ZL9WFn%-n_P6ID40af?{5dxrC zuEEdNuodiwIY`;uqOICSvJYxd;DPrlDTZ*?X+Y`HSO`YhLo0%Nnd=xA{f zkq0kc{5bO1{@+jK{Sz<0d3DIq(8@|p{Q%^c#s)=$c8&*Er?)8OuF{_N#x3Vn+3Ka< zw`Z+xJF}Pj_T|~#QFT9CJR`ee6MszDFTCXEzaAdnXJ=flh(3aK^Uo}aI2c*3+-fmCR9<_?K_xUHxa~D?BfbjVyC3 zn7Lw3>4nA1N|V>~U9w&4Uf}dAbV1?Fbotu3-)8!+$h6JIllX%Ot<{9Uh6P>^ELI{tG)?*ykFx0_e^n+-6P4}n ziT2f2?mXK1hxu%2>y*PBNDY@B*M`qK=Y`L|I&)s&!*w%X2G0v%ZeIN1=nVV0cXpKu zzdU0fS95mH-m0&m?+jxnbOm2v>HPTi;lsMWUnf6|VPSeR0bIW7II+BmeSW$tGvdq> z?bRvAT-R0$7GGR}{h`E!D@*C7QrL5d|X2zA1_b+U7)4SdN zB)vkZY3(^>k4+)A#wJy!mM@wA+P>Aj8o}CFD|yw_Z?fRSj#YivX3cw7R&)HATKMjh zia)JQEOPT>d`_7qu^)fN{Gx?t$^o;vyJl_PbE0;Varq%WJ0q(mz6DB0WYi8V6Rr$5 zj~Cc*t7MD$g2{Jd*IKx+9lCOLy8z=a@rS7$vNFfMf14)7;QHo)FwT$mGX`MP0G;B$`Gsz>c_No7A^i4^NMcRCb~?H2V3i*L^-dzwm13`grku zE_o~etXh65a(R)SZ<+U9`^)dPTzI>RTcNDr&bQ5ce`nkO?Rl%dvAMj|Wb$vDDaTcE zL$24V!43fcHMdQeJ|ESN*;Oul=jSs9Ss@{xs?`F=Tny&S&fg^$&-nSQ`Tfd2oy&io zN&olf(e?dx+qZ80x_JMOd+Yx{KmSL`>;L1U;b!^wIu76eb6yy9g5SBcxxXKE|E>Ne z$L9WS@ut$9ufP*W1}x8vnd&+_&Fz0yyX&Tht~qtO@-I98+1&ijxhuTY64P>Cg_ZE@ zwG`hz`Sa$k`~ohf@{H2kf+9;_+>xtOKb&;!K36QSm9zBEPj`vU-|tb75Mx)b`8GYF!Y$$p7fZrNUcbCG=N_L9zIE)F7+)K2=yMMC zZx-Tl@~^atrL(zyOuTGcqXb%2?8TF`YReVziBVSP16m^4V}t9~ZF*vnn6_qrvx>56 z`xE^z{RiQEQf8Mwr>iY~wfM8P+5P5T!HXxu&lr5+-M^OAD?0Do>N%xubCp4>hS{XD zyS>l9Upymuo`t^;=c$LnT$}xBU%y()yR{Zn|5RNF*&eG4-6?{gWq8 z>hwlM79VtAoo{}xLV8Z@?v%Z|1cd+0O#kPW^|PY-{f&)p{p)|#20rXe|Nkwzx;nbi ztkwDCHPI)Lpe17m1ZK2z+{lo5_v7*ABYb`DT2+?c`+Vs1pSopl?z}9^{5$FVb^+eW z6Q+um=dahDzs2m6G*^FY`wDSpX4_-C1U^mGxWLi7X~E*fCSlK~&9P_64+gK6y?)r5 zrFrYt;)tD3?ULB@S7@HG|GSQf>6Gg{yTlU{+}=J|+Uh>VpQUqdQgZhCHP;eWP8EG# zW?*D;V0sJZzO|m$6zA$|E^9t?nRBy6xt0LqF{bm`A8MH`XU~1YzVF@@KEB3(`}&^U zV$$N=ZFZlJ&#ubC?A*>3yFcnY`=@wv3D?%<#{os9>Lyj96PJ_~$Rs_uni|6*>S!RQ zwDZQlC|Fuxd3ccXkQxJM3GJE5t1eA0dhVBaP4eolIdz_PJNYLs>iF?>lF_T^<4aWe zc5=LT4&Awn{m`54!>?WnHm+!3GBj!6RWdylucM%uF+qFNk;>=Wmll{!G+?myQ;-qa z))VHKBIhRG{5|> ziob2yNA|sX6+Jzt&k)@FJg~`Xc z__gQnZF{GG2bZtEX>`f{tU%^MK~}#T8i}?!*^kVwe6~MPFDh{ArhxaSZ=ZkmJX2=S z_o!)Bn%3@?TJ_UyK?lntQ0MIAZ>Kq%`{J)F#757|PJiBUWy-lI{Ua-SyW*E${TX}W zZuW{tTRP<1T2xQ{uKEI6F52sJ;s@8A%-d;mOoaTVXQY*sxIDOY@A6f)@ZhZ1;^j-1 z{aG{PyR8G1Hojalt;kOJ&sPR_aajwF-b3px3|FkJ;merrUAO+UdC|#Z z>UJ_SL;|w2cT3K@?75cv=ls@Zdrn2BU$ImQUcGqhHH|%@X>$YDN?+9wY+HNtuC~W> z!G;9oB>wEhQR~;9==8nUvzDLZjq{sH2Ommiuol|$JnIqtE|cxYnr<)hIsM$mBbCp; zdp*o?TC;a0)9*!h)WZxtb^lLL5%~M}u3CCkh~?`ZtJ!}9H7}^$$;{1_)oWcYf6nq> zTf|PzzgM>V?tXOa*zt|4j~z=}mc8ZCK0`ima}ksj#Q2#$9~Jp`%>CC9i|acw!tK8F z`u;tv{MgFedhWHp^Yf#3ZNI+B@{#?I2ajK`+x@K1I&ZRZYw{0W-Cyo?AETQNU$r*7 zTmRqo;>Eyq>(=@Ie>naA&v(kj-Tc{e!7Ya~QcQ)f%@(co|MzA0y=T)l=kHs1ao5_v zujZa!ckkxSD;2-ChOe)CVtC`=?xf0s?8cvEUN>NI(SewBVIBS3Z zlWWD_c@CAIoV50XdHyP0J$<{RCbJO5w#>rgZEA<=qoYsf3BG<8P^8o0wdatzf!ii; z4s*w!M_8I)7*0`kPvB?KEGj6NHpOwztbg~)PA)on%;Yzyec)r^e#0l>%FVzTrD|K& z?R$CWvb@62x3je6kG$zcTF%MD;;Nq8#AyUt&RMql^Ajc+)2nd-_m@Ua*m9>*eQvnL zY=ISe*1W+M4DMk(pS+*Hdd6E9eAg$?$O06NTK+AZm!v-|DyZJhVnKQ#2~si`wxurW>Qh~1EWZ}0K>6*liZcf9xbLFnk+FJ5LCBiR3dbE6FWoO*bV!$XsoEc< zGjA)~4^|ayxPLEBSYjGmu!YmZ$rrCqnL9~(S>?}H=b!o93f*_v%zR4X>xQuB&3k=+ zUCXMwQF80Wn=g~ijw83!Svbxp2k%%vy|(^;hnw-ur5i7*>fQ3(wJL0boSwwdHFEE6 zmv)CVf z!i9$BX5p_Fg}DWU3!b+)`o?7GEZ>u1bkOwD+@mV_j=NPPc5SRu@c(mA&^0#TdYRH; z^-c*r4Wo(|;!_xAHa6b7Bb@)Kb)v@KUw`&&V=N*plHT~VSP-}XY!M4&jzB3Cdx49W)6zx5+ z)P0%n3Beou4UwiKtugSy>Z)OGmUf#NUi;Ml$p4O8FkTEduXs~-i)^Y9m z*6Lf@cRSCn>oBQ(Q<7bLd%?9_BfV3*rp6^YZj!D`-?`nrFO8RnXVFFJydCG9eC_`o zdB5Q1-Pq?(pRU}*xmG*fSyJYS)iFEpxP5}5@`r!P_Wy6cHLv?{Wip3g-J|$t@f)Tb z341N*<5Smb+B%6T)%Ezqs0shquAg@%A|kks@6Wbn-IGMc`Gr5)Ca|}czC5$AK3-?e zmoLXZ2+n%&L;U>p@ZtkeC*A)Wh-ut77})ycn8@|g!q4hw+FsQZzVxw>T^nk7{JxTu zOz~X@AJ|y{b8U~TI(F>XfhWgR1WyQgT@K^Y!YiLtnJYR!Y3z@_Z=tyJ#`Ovfg`}iw&-UC|WBf{H;>x9u zoBqsro*eReiDz?o`;K(hNO^&@lsi$@)$dlR%O^*>cN8=+Y|@+*Yj?xG&~s+t|919j z>#x3S`(%BOX-=JFcDizUkk0AFoFN+%9=th}8 zPSx)nF;8M`okdL##~l3gW6zgM`!5JCSyS`O-~UtfmeywV>9jR647Iyr9o3mbh(p%lo2j9PL@eB(&?~*ZvRaSQC?#2JU91MRj zRp(_`{l*UrULV746Wy1l zIOmziYgq|@FIE3v2V8X0ek@VGsug-jJxfpWK~nJcK)YiZ#)3ucGc7fPZ5#E?aY>6wzvmJ4&(Cuyb6 z4SdeAQhA|dcHKqK%Rx(CJy^pMvs`wy;m-XyL1Tz&erN0*krnWVNQE_^Ec zUzLAfZ`{!6RaUvVf=Rg1<&)puwSRI~8Kw%J6M7z-sPITBqOtR%l~y6MX6~A24#qzh z7hRiKV8&aPsa77fad` zD{tS+s+!DFr!QZ>>Yl!#lyK>A{QqAs&)ZGz$b9kOfq<~^*GJvwzkUB69)6vVpa1ud zis0uTm;d{azN%43ZHwP)BiLZ+6@`Yo-vyja_)QP_xbZRmh&?6RxIRWJJP^EsjJG?^ z(8k8V#)Kn1i~VehfniSCa=V_USMTmEe4wy%X+!>@M2Cp1fC{N^N56l4;qLAxGiQsR z-yH9)F?SkvosvoFaBnGbDQrB-d+4ZTb><-}7r%^k`mSr(=DIsPTUK4Q`}SMeIUTdL z9(^_mdp_&J$zQ8%*5+w%Vp@0eTS9s?vs2^lN}pdxxwjmw@sD!nX(SQyWjBGchyxx3r%aKa+ZU22skM6iX=|*tT{?0_h?31=)r##tMbqsW( z%dIn4?_4;gO+4K4wU5PYw&SLUt#dv9O^@n#h>^X>`Pw>q^+}=is=qP|6u^rRL5*Ng zVZaC)J+aZ7Rjt4E*Z((j*=wX-za4*^xAWxQ?^4zq=gM8OR%w~U)gqu!FlBMMMc?U* z)t0yZ_Z5|Juy80YiRIE%X=(ZL?8x5lYUZBn{%@B{-5a-cs_yMge{T2NojNnyJU?yU z?u|>f=ie{!TfWEX>)AG`RsS~aJ#+QwynX4xmIe;b-e%Sv-#fed`g`+*@-ZzZPX$S4HGH-8;i&`B%#h6%-YJQ+|%j!R7xh6ds;!ax#2UU75W1Pv7&`C#$!{@%7zRV$+**D=u)B@2M%Z^FApU z+gBtz^Ukkoo#kVtv9u^q?>pdcb9bANU&Tw~GvCRk5- zm&c2sh24`^bHxX7uViD-qqhY zz6Ve2oA}uD{Dpv>`p<4!a@=!ZV1b||LK`@{dy;m2+jh4u)zdUVZSQmWehInf7gyvg zU80dv^to@6|Nh5yR&{^ht1R7g_Hy%Qm+8|q@Bdc$UlX=@<}s80nWEQYMa9LF)6=KV zoA>prx&6%JD-5;Yz1y^U-5hyo>8B4KM1+QJ&Az@bc)6*)S8u7SHAe#jM}!rldiuxb z6Bli3Qq%qZW%HMn{4f60{M}h2)okVNb^qh%4_pgnj!!w&{BFJWY?0K~=Ngav?-z(< z`RYAebSL=c^ZLunlUL2T^ZLn#ZRG`O@o|-fyEaYSz54Fy=Au@1R~2jP-P^aV(CFK= z(eLq>_ZuUN1lihp6}+@UYd!B}GI!h%SSR=IZSL9}U6Jp;PNyb#&t%-9?$Ft6yVv>6 z8`;dCp-*q>aIW7bHQDC>C+(1mSDnYdYC7mmZ14=9>-k*6z~<=Hv$u~Oo0ex9cxT;_ zpArGanI+6+Ro7H%zg!FsyYN85ul?&(*Xh1q|3wfplu{>kPBEk{=S{kmylFm1!h zH}#n_3pbw+wn(4i2#y8`#+ubfrY@M=@bmGSQoiJn7muIbxOD5>p9hweyl43R_VUTC zn`P(u`1gVXAC^?iT)Okn^}5{`U%WcAE+BsW8}seEZ>sY*gK`H0lfs4bEU&Vr^-_BLsM*Od{-!{& z-1x4x{2njQEq-62^;Vtf!M|G0{+m9HH}_kmSG=GJ#A^--9l{$#!gnlQoEtiI)||T~ zc1nkLPnuTqEBEIa&3~d(wd~{5vu@QcsOy+-pd$}WlMV~sb7-vkRDb(h-Tj-}nNDBq zysWw3(et0=v%=4u9n$koy{!#e{$Sp{1B^`9+#9yBm6nt(7P*m=d35LTwfVgff}jM} zzyKn71evBi;D_STy{Ayba5JMReng>p3i~TZiHCwJhcCx;{0!yp3DwlU=)C$ z2b>C~T>VU~cB&4NAsN3N`=3N|p4|MS@z$-V^YZskg$FV)u2xhi{wQPlt*Fj&%BlJK ziy6RW6NEH)!4c7={pT#d_oKx!&wRdbl1%&jb64I;z3;MFi>^n@e=BgF=UDD#kO;Mo zfl**X1k=%hbbxREflg&!0bin(~=fbZV%n>DQXjkd98i z&6_t1b4G@)3=tEVmTBJF%61~fXwvfZCt2IsSby%^z0+u?AM0DaAQjsg%SATty?Arm z*WCvW9GD(g^>=sa{x8S)el;<0G#q%K^gw6w?VHxGW*0|iZ}wXGAUbv3y=~j9>{6p< z&3?gH6UW+5vS*a3d{MuEk=2_(`#y{o0y*)qSsk>Zd(dTJ*_skKs`}1J! zjT;gRH*DKh_W4=v`TZZZ1?>2Jowu=(QN^ zm*#8{j&2c}Qtgv)rzdoqnVIO)$mjk!uQv*ourxZlFkJtmU==w-3n?ZJGzd#*aR%M8 z;%~cs`0(RC>%71FzsFDW+rKB~c-_3!`TN?oZ%BE0wI*`6!@eA~FJHdI?EJLr@BZ9H z4TW)MuZ0zh9qW@l%e~Sd`B+HU)~#CwPw`GEJ$JdmZWbhMR5>qjU2Ae-Z(LyFf~omg zd1fgs;l@!bbk;AM9%!l^#?dL|T~`p5@a*Q^usuFHyet(=JDydFXh72~B$+gbFons9 zo)cJc_3UD|^9fHiCm-MQ@tAkf&f{w$jhFe#e!u-sS;^=A{|}dEnXd0@v0DB4`FZ6^ zUte9-L`myqv&ExV#7D25VNz-5xo6Lv`G0OVhsSI@aQyi5$Nl%>Dvo}2G7a08_VZBb z{rqL0Is`WsRfcDY=lZ_WIkPL5 zD14rDTKycXzet2Wg0$J$v@dUcW;tgDrojpPwh3>ih8P z*@y(m^Q%{{-n(~a&-N*YQvd#%KV9dg&(BRI7gZ;#-KqZ{{HMUrkMI8a-zs`~(>&Mh z|99)|?uR80J}zKjQZTr}dBgfd#J3Nd7Tv3hn%Q;f`uyM0dA628R>5<5q>riEOe}Ia z(x<-F%C&V#6w(GVqOf__q5KF@Zm=Gaur8}S4c%g#g4rjn~Q2bzua71%y#E) z>YCk7f!C9*Pfss?czfy6)Gu{!Zg5uZe=>oRTyAE3>ETI*k0-^( z#u^zJxpLJ;m6n(Pf4lwuO@Sxu`wJ8wpP9KiWN*nmnTI>>*ZgJt+&tfU`nLT0b+?*J zuC00L#0qW@Ecnl9QMu#${{>d=Jzf{y6wR4l7qGl3-{H%Sm#dGR-=UfMXj8+6l(|;U zVKa;hS8m_`Dr@EOwsA-}UlZ<>$2f1&6&Z zCEiZ_`e{ndpNrOK4t>A>e@SlE&rL_a2%WsX_V>s6{`>Vr`sD4Wt#a>^S^Rx|{XL`V zZ$9$!`KQ?YA_H2otWH0DD%#k{cx&YW``>R~&oo}H8&z_2iBaD4b7yC7udJ?qxy8Y$ z^YfMKvC;o7d9Pppx9oAd{2s&HTTlA>`dqtr^5|whw+QD`)w8dj_WAVr8Fq@AniIL+ z@Be?W`hC3{t5w(m5Hik^H*k1w29eQ8JKa=$gTAKul?Ysj$p z|G;8WK+YzQbK#L^EJ9C~YJcbA==QQL{NVm%o0>x7!<9wr9d=%Cy7lVOmdCd%l0rJX zW(Y}{c6nxL6}9-Tk(p}H$>rPIwd6)2>v`+$tjv+;@`uSsy0P);`eQ9?_bP*Kd<-6?Nm>XeSg3G-ut^GCu7;= zb!+F&4Um%;*PL9q@TvWu55{}*?|+oe-ut<4c1zZ;Q(9BK(l0MOtUlFC_2(>g|7UfV z>OVZZZgrK1!}YVtf|Yl-6pL@~nlwqMYW=E9HnD!0(JQQqSf2k|_W0N9>6fd$y}94B zFMqZB{lE6?!;GN9;jiC;xMzD_o&7#jehyo|hve2@htIcc_K4kb!dNLrVB^boUt}}dHc3)*OsZdx@MLIzI)jeyY&b7v>~+PE&4 zMM&U9J*WtsZ|TMYYkn~>3P?O+(b3SFGq>|~xK8EMtJ`1xoxSz-?OV6_WUc1J2NZmG zu&`5D{n)W%Uw-7ca>OW#eEM^JzukiK{Qv&)+pmrMyy{_N*!q3*G$(wXCAw)-QBu;P zW&ZPjT{`_<@?G|h(*OUOMVS2e{r>!1U*g&nBmcuR7*iay@>@cG;HIHP$ge-~N(V+FZ&v*3*X7lgN!eLMAQSAYKg z!XIaDetx$@UqLPJo^$`ZiqEEI$C|3;7VciTKHhM7=*r)6OKvW^l&N7-#q;;}y^C?7 z(-!9b{4Ois(Xw*7&i4;r)Xnvte!n33M(5J@Us;h4C97JG_VoA6ejt9L_Jy&0d1_?d zua&a7xo537UHIp_72R6CWtB0Jb&BKvCh_R z@nrRQr`q%Ld>`w|*!*Chs;s$oe1#i~To# z?K*YmOY^Tk56&4`KAoxf^pB2SswAI)L=Q{BS^i_2+bof5VIDUoF%i*sZ{D~Zd?3=W zr{<@TlV_^9xOja0{qOf`brUO`@*k|adN%*>Yvr_uXSe_R`+8k(NZsqR;_IRdtNG7d zS~vUOzd4(ek94?9^NP+$`Fi$x&99qQ3te`f*>(L|(N1@lQs*^U|-i+V}6?6_XXkOD_C5oTTn{tWS8|#k+^g z?`#nbeOmc)d3mOKXus#94coVF4OqvSs6WLqVS~T{Q~QW>BF{KM*&EswOkicvIU(xT z_x}0(`f2H>rt9yoVbssb%zS#a>blH_FPdbHze)6baT6_1KxZaI` z`-b)ZViw1}X<(>-HE-t3rI$}+um2mpe9!Si%m4q}(B95onxC6p`)1?ws+@MKYuB$= zS5{`_|F|hV`vth=VXHP_W3~C-mb=&Ozy4U19xr9*`grQS-$#$%{QaF<^w&BWfkSt< zhHAYI^^z^O;_bhE@h3-LqU`GYr9Q7ep4|NQ?at>Dx;~%3a&;QN|IFCv*r2ExV&+L! zOZk?wJZiO6FV-uW8(?kip1;4~lKzQC`8%)l1+-%n)>UfTtc(x>x8@+_aYHJo-}&R( z+S@&8;w#tZNBn-vzxVmiU$6h~e0gl@w{PG4Z9YDDq-s7ptKi!k$tz3u>`6LyW_3%g z{g3n6Q?zYVRUK?7=y-u5Vb^du!+SSFa{?C28@Si4AYqM~Xtp zQi)b@}3Lub$pw43fsE50McA44h7n zebR#Vo@=vo&e|W{ekx|Kdtda4RY!NKS5)?MH`|(e)#iSDXdAyG^s5zT%G}$&Ij(E} z9V}cDGp9Z*U7W-5z#&jA`a!tmB}y$S&*)v)`rY_u`uytdn1?1W9{1aCNO_s{@Wtur zy33{qI6YmqY}v-g&jJ#Umd^6{AYcC{a>?e()95qMRb5)Y z^KD&VdGPAC=b5WaJAbj)*}Fy0Shl2R&+B!!PpZd7Mw(WYaNYI}Uc|Zrk2nWwQ!BFJF1JY}+-lYIb*bcgbKi|8<*IhQ%LOR28q><(e(N)s6r9 zl7lC$w!1Is)Z*X~V_LL2$|N^XdtIfVdDm=ULbPV>&4;EWFHjwol!Mxz@W?UtN921n$p7m@{_$NaNNyVYe!O=_)nW z?4qKg!osPa>puK>ll1~d&VSn`mB0S#8eZ;~)=ZQ2ByH2red+7#I~!d#$715Oa^{0Z z91RBqR31Fh`DYV!B}{;Q=kogtmN{12{9;qzv8yuJ?aj7rVy>m0shPVZ!=JoG2CI?YGIx<4f|{w zH@vtQvTmhLll+|YrlwNi1E%?RpNp?%M)Ktgd8UHa@BHaKOt4Zk!Hj7+U#>)2^5QI) zzS7N#t%_L34Gc6m4v5-m_zA1Q%3n7Qo{3j9dYqDNryH}gbMVMA?ReIHZs##Csn4oSF9hyZ7Z)VtNU3a6pE`7#OqF6JFjaOg zr$6LdLZ_$-efa*psJJxo)J;p*wrW>5mW$^8bFH+duG+DrYu1yZaQXc6-@bjjvqvcR z>;nb)^z`(&$Y{s;Dpp!8vre5_y=A|B_H7db|Jv*nw+xP2uAHPVssDbxUdPADxo+*+ zyFX5s9X|Ez+M(0c2LAijKWysXetgqX?&jv^S<$nMFKsrrdFtsM{j7fO zwJdQV?^eM#x>ipm6Hv zLmOUP*H@rh09`@F=t*fT3nWhck(i9NT* zHh$CP6MJ)`5>B)zM6*X%t6Ou&tXbS$Th0BQw=3<0Z@$L8RR-rXfA0St*W24Gc1ub9 z{WRU%@wI=M7fzUJ{^!foZ4*3Kty=Z`Otjxq>2TfMU#Cg0+>r5+_tw1+XJ_x*ym6yt z>8mrn=J(g1pW^rP`)j+cn>TA~Xzcm)YIX~kvip&`ud3!(Wv%OKzkC#rnA})-ujsYy z@xJApx*I-CJNr7~;l|_FbM6;9L^rDV|9d+xdz${U*?IeZwmm-Xzi!`G#kftmugx17 z8AEK#-W<`bubeU6{`Z-~;jxvLp|@{aAM2TY8WN}bnEpys{=X}JtY2O`q~g;__oCv@ z;<}N#B3{>*#JOG9+hVif=~d(Auyeg1i;5Rz%A8;G=XA9?dzl^b_-vJrL%D!g|Jj+_ zlQTN@PoAv&bL(gBQ!dkFW!_ZSTezmI2zk}V_fA~;+I znaPX?Pf}x7OkcF^;0KLMro6XbE)MI={yt^*?p>!eX07aBmMFGabn4cWW)xYl+^f9F3LNYGTewi+_9e>ImcfxcBM;w#HfpstdLTme;H}v9I|| zqsr3dr(BD?SG?Qx`TKhH^~=LQ7V3T3vCZMQG4C{k^NuF$pDt~>SyfdPSO3>^cUg@; zT&b`TdP?*JF2kT$wv>o{GH%XZhP(+F3?XwO_Yx_f*%>DS6tpy5-b) z`|m2DvuDq)w6_R-{pwYV)uB!29%L*$d$fC5^XI|Yvu63tvx$v5Rr^rh z*6Qo!|NBn9zHuv~Zf%dGahG^d;;}C)*Ld|$;Cj03T=0$X_@A@a-%GD6Fsl2Ob8DvY z^BWoyCq7)Hcgb|=n%I;YhgIeAV!v){8^4R3a^%kgllF6`a~+;FIU)-413WA`=2~~o zp1pazy?gfQ)B4xqR?#+;#9D^~{2 zXze-UlpEZp8aMBqkK;-sqiH3+hn2qtuAZ{B`c&-A7reZCed8~?uYPcL(&l-Ke*dl6 z+vTlQsmKK?xfk>wn5itiW%eKENkWs-C)(J5-mt;cE_Gh+a&E=HZ}x3!x?D3keuL?b z=ATb9#6w9JRV^n*#(P)u@14^f zzI5@U1!<*gR=uzNt^50nX=L9W>siIu!%_=YS{8hn;gGvF>De3AD|0W+w)uAAF1L8x zv&WDB&i`Lsdva6s*4%#|9=AP@uLzGmr!wWhl4Z+|oj#p?XNRHE+^45aw5b8t3Mpvy+3aE#Fz2)cc)IAC>T=r@8|Uvv1R9D&a8^B`{?)SgrL{b ztV>f`omc}a|JCRP?OwL*+Ue89Z*Jtp<-`?#eH9uab}Y>P_ZR2z*sT-xJPcmmbF=<| z#QKw5-RtJP_igwwVzYu9AGvMH4kDBk?<@tq%VuUaJ(k5uOD-Frt1%9RnLq7VvEclUhoTpYBwih0 zct0T_-ktqj9N*tVDjVYF?K-TLxozLRBCDxO9>0Hnbk_^%xk7IOgsrWAl_-FQ%oq=| zI4K2tI)a9Zue+2O)#}UZ>E;LC%UmcwM@95#rq98s`}gn7+W%zf;XU$mrbS-8^6j1f z^BYy-@rw#oxu^T@`=lYy{!Qpqb>z0tKC%>z2AoY{WE^NS#gD3*k+Q+_7z{x9*jJ9cEgW?m3FL?pRD^=H#slL z_Wwrp=Xd@;c(L^Kn^d=H`IA06E?g5}^6#(w`ntcS;x}bwWi89kJ(2VIeO~$a%IO;K zxjc{cKArPeRn^uevfuU}PwDpU+n3iUhb(x!u>IY_?~C30^Y47gUcR6`|Kv&K70>JK ze>UBdm6e?=wAi&RFQU?P@}3`;X4hPvwcPZnbXd{M#2?K`$5!qsIT`9x^jcSYrGsX0 zMe9E8GqaE1y|qNd+{xa;^W2=XdtT2|Z++}_-6ZqvA;n+&*JR|_&5A2L%l$jQ?zhYB zqswJ})k{eTA_k<-xE^p?d}!}pTN$gpb68u|YBoPF*I2aVye;#Qm}9S=O`Gy0;@cL@ zo19!<&Ysl`D)BG!t;_Y&(v^AoWF=2xXhdH(Z)xGfn)@X`epHy5Y2AFKv7TG_)B8)N z&dz^p_v~CzS@z>sQJV4QBOhOO9^NVcVQFe8{e!1N;pQ^$VrnWk5xA#Y`z=@7$1kZanHPwig^6uPuXYu`8 z>`RwSKe@xi`j+nmP;<6Hgy|cf(KOpVYhLULus5laS)I20t?;R{5#PRkSIeF@Q`xn8 zrgVSk|HawuED<2$>{qzn%j(iHFiy8DOsY-ntC z?ecrGEs`R)->7H49XQM6eDeG`K5-LYC|s_|>D`_CWu5wWvG*$;q&v0#j6d_i`-`ww z&^muT+t6)~uibBP+V{pd?()avH-!&ZnVwfYJ45$P-OX73d3QFJzCL}PyVZH& zpIg%M>tZT(*M9jW>RtZs*WD|s|34f)=KZ`-<#?}b;gc7U`>ao2EPQc6FfJ*m(Wt-v z|6zYnw|38M`Pwf}=a%2`$t&FS;ezsilaqZOmB+q(^!ar4y8ZvB*Q7P=&+uwbxBYo# zwbRy@mrb^_@clozbLY?IrX!hw%5Ewjg|sw8w&VvG7q1U4J$1$Ni{DN!m&o9j*fXKu zK0L^q{jw)skXJGG@{+CHZZDs|db9uaKj(|*FCQoX)g_RgM1mfZ*efpebnUehgr=?t z36h_+>_%=8d$9hmRa#GPuQGGIb$Bi(GxLs%CpYJ$`xlt~GeT6(Fjhh|FXdB<3@Z!qrO#TFPHo!xy*-(iQ#ktJ(7`hPFGW8pGMP{F>U z)_L-3?ziPx&a>mVVjt=x@~vGuImq~_*_4xWcFY!-oB6bK-NbdWlVeV1zHu??wE0<%6mRQUftUHS=Y0RT zhlMHq_a>vC2Q@G3{*p3t^PQ9C2d^KymiFuN zuX!#tUVZjHrFI~QNZ`;bC_s^7so}X;r(!W!6>)M@bpSY}?+%9}N z%jn@gL+P?V{byVD8r$z!v|-)8j(`0n6|U90f9(t?`rLWG*Xj$utm9U}VyBhLx9x*7 zma{)yx&7|JKM%GpJu14>@=MK(aE}%76-&wu%#R4zc%43#!G2=KjCIjQmlQwzd*m7( ze{T1o<^Hm#b!z^e?Z4#PX%b)YaqrLRXG_b=>p%BCfBYzE#n-vz|D7kkQ8Jyp%6od8 z=dUesRrY0nuDpr7tnPQk;&`RCqT-!qqbY7}(>FL37IN-9bocI2|KGAU6&1&NR$ho*tdR<94g2O%3mnnCNjbcU^=<@#SZ>Y;F_RWxl$_%X4a0Q*+I{%3pZd+oM~at<)@B zGxgBq%GsApH7~M%W_td7`UlOV71pgDr=N;!zx0%A`B|-*wVK+G(v!9*6uzzLu!_!i zbj*zXexj;Qp;No)bHq*52o-IASTW$Y> zEz8>K{$%sbJae&O`E9p^he`Z@xS13ZF0wSa^~L;qqper;E-d5TO5tqzJ=F=zU(28Q z+q(YszC<<8Iac3~<%DUgpNl(d)HXRl?em9kyxM1MXBOA}Kj63fzk9r8`{x~3O-2)8R-^+Ubx0%kn{lV&)PagNpl~yfjZTEfnF5-gYyw|$gCN~w;Z01hd zU~up3{i&Dww`lFKURLsE*Ix$hHNT1_Mfvs=t@v`?OYLHv{qOfYRTFD2>QA4@y-#g> zSaiF}rRS1{(VyqKOWd$~z1Q1Gy=_wM!a}bzMNbw#&Xg}JmfB?TaYLtKd)v!`*5B%d z-ycHKiV+TBo#6imo$0`E}FM?iZ?y zHop5kKd$DbZCu5xD>J7|nV5WR$)`!$>*n~Ua!o&X;>`0_TMe7p9oBEB^`lRU`mIYjm7)G+gE8ONud(r$X0Uqc>zN##ZvX#L zbWv-3?bWBLsZUkzY)aj|eY^VoTjj0(c0Xr5^Zz!#{+;~yyVdGT`)=+me!uQ+eMY}V z?By$4v-?#i{rL53L$9rCQR3DO8zj~hC<=gQX~E-qZEB3s+%enc&sY2C5xVut{Nob^ z9d*9Um~;B-r0ULV?=J03SL3X-x0tH=Y}UQ6?SlNtaYbDXVv)(A(_EIWTi5u#dGZ`H zsid`AIEC(eFH)`cy{=$Zy345*SLN%}<`leIp$CZI>-epsshadS_VR5KohNH$1%l(0=#4hKuR!MUb)$L~f{JnkFH7uBlR!v(4q#mi#3# z@7zv({o4Dh$idt2=$w~I{@S-2gMVjR8K3y?vNE*8Im)BaXrA%bMT_zpU%j1MeQrbf zGKE-Uixd5&nK?nb;=dRfb5<{3qF1w(UHj2*DP4&;>-%%>8&96z!F4J8$fMVh8hiH? ze-AXedg;*LXUos1e_0n)=%-rt;bgj7Ux%jV+6A>?`r9OaUEf%2?;rR{%vtsV8; z0+wg?Us~4A-MeYuyT`ZlW$cQ-?XiBx-O)Mw`-R2U_iB32W*@ZepRoS(SM6t8;`XrL zT>hE2SI+s{ZR=aBPwfy$nCtGF`f#SQ{pMwl?o6Ki{I1*64>vxVir&3c@bXQz?4js1 zHr*K~?#}=5^!uLqeFfW%xAwQ&dn=1?*|vv$(v7KtYpx$m@8jCNI{WG&E$!2XpM+@G zgdB0N58kR7|9D@{<)RDie!Oa|UP}+{lCFFHeao5KkxSNQUb5M%w6k(jaePMmru6|^ zH?DEiSYnjRV)ba5#r+C-_i}Y>c0IO3SIKRvn`;+V`@5j3a+h)HT+6ez*3l=Q-mNioHw)hO{!7^EE3x*6mS$H~6xheV z?C85>BXf4upWOwkY$F8ok7peGaO>wg9izFMN9dF1#iPm-KU&P+Ev|Y|ihsU-byaMdy^dLy*z>aRYZ|UUcc<=~)jqAI zxS-^z9qT3Y>_Z14C*Nl|dpK)r)Xtb1>%}KTStq^Q=BCG!$oBetdG(%7PTyI}H_z!_ zsc&)N?9y|Kc4ileo>8ct|LN9^7c+Oww_I^{y=c~^q7$kA)&y+Yd^@%#*H?3r@A~}} z*Pq|N_;=L}&gmDjZv-EnQrb1ydi%S|`yJ=QyFMOVp}c!XXH~zt|Hj)-HqWcSDO1#{ zA6q23dGo{Pb8XE|SgrecJUL{6%3YBnyPAqalU%2-yT4)YaWi$_Sq`=5=U)DCaHUV? zzLTQo%6>dKdr925D9hTSV0&Ck`)1+t%GWk_zyCxRwFV2T#$H@`q1>q|?CZ;P(~qB~ zpPTJe-FWQy@k@3y_4j@_a=714T_(IMjsKdS3=?=36FTnSC?~wZCEOyq$KX)-{ZBvk zik;S8P;`wmL{C{ zc{xkxyy?v=mKoANqi)^0`DgB2Q+r*XsEGw{+fKJ#KleS=@0Hr$k28eAkEll_PP^H+ zz{dHO@#H6qrapTzt)g=0@1IqtQtsqy{n{!jCgorr6)~y2uC1mqFK`3<(^ktnKf^>E z7+7>B9B2!l=Q(q?Lb3jIlZL*(^0g1-1!e6sU%iQbw?texulhZ2*o)WSUmSUv)3N7^ z{g&fdnb&MStq&^u9I|Ta*TYZhwwa}!2!Hnaz3=0wqP{7Xo}ZTINw`k7i4XXF{r{Yc z%dUtE-BO%<=9*~N-mCi#TgII2jV;frRWAy>S|XDB)JX8N`-`J2kCf7_G*2A1d>kry z=boZlf5XK&(<7H&nES`^m(AVw?7Xh$e}i;nt{k2H+b^hUQTC_H_ht!LPTeg$VW+b7 z-{oso*_&RMJDnd_wa0t%c3~^k8K-nl>FLx2^KZBZotfwOe&^20OS@lR zsrmTxc!b^L-|`ioCbqv{^jBBD`c0$U?n9a4Rbs2>O_|~m>EfciE~fD6^=xyag-(Ib zpFQ)sUD{Q$XOGE$Vb`KnL2Q%G+x`uWx(%oA>nsQN&2bnt2URcO7cwm(H5cR#bcJc&qXJ zqQtnXL}U8i5ap8Ch6L@Z*7rRmHqN^_T$aPh96`~J|JD__6*?r}*!_@XOfk?6mD zOwxPz8S~7kD>0mXXsQus>x9i3MV~s`+Y9~wO-hfMuWfT6Uif=>;6mj9zvo*NSr`Q* zBABM+J5DJ|{2Sb_U-fP2!(fxRmwV%D=Iq@+zwRe{i;4j6(oJzs-lTO*={22qFIKXy zGLOyd)6>#5iwz8KeZ6|ya(6E2ecx^`ide9Hm*bn;yi!L~Ej4VfKIP+^eXz1| zYySk!IeXNWyY23j)VI^ooZeAx@?BH8>(;Yeuleaumd<+dX`9;Fv-w6&kpVMS?m64K z=7GqI1AiX0|F8XWNZs;}!^G1jcZk_*?(u{Zxx?;*NlUk_l7$#lv}G98}B~O{qxss`IDmO<{UkFQuzemQ=XR_OAn8DTR(+sPglLW>Hcryv#)XyakZaK*{Wj8Zpz&2j*W?t z5fXax^5k8AAvu*LtG<=Z@_8QrzD6rV?!c!`;pHbq-#uIQlQl6x?ba;Y=-rKqOw51M zO5eTBbZCEf@PKGu13z@}umb~28wca*($?UH&HIWS;CN6Yku!Gv2WzZGCTk~^vbU;FcSJtt8d;d<8a(yS9mw*25OC4Q{<-&oH7h4x> zP|5oAt$&@~`=XYUF_zymyuHIu>CC;$zPli$mU=6wPkm}!o6$nUb}bgplh-GEYrZGy!LbcD}v|Gdph-R+iQ>D$1`8O6FgIE zHRt|>gV~#yOgB0mTxJwF`P?dZj`Zb5YlG5PbAEnv$2j<+?K#_`Mzvfi`CW2_Gmkyh z{nC8-LUNvOs<1%SrJtHRSql`Om(H~=_~cY9`su&TpO<&H-`icrEcvCcZ`HYXMHgmg zb9it2{9XNh;^p7JW|!K|Q?Fb-d-iP2^(S7vxp=D?POD{hq&XzGMbR*jP_~ z|NHItkB^TB{hZXfvR=mOq_%s>yV9CxK2rJ{&os5q`OUpROvUDkxqq(j=Jk6wZF=?M z;^SXx2cD?=e?7k0qWu2WSKIdAIpenH{l4Sh-oD>2J-=;we;BBB45^(GSahz7o)gN- zp092m{(Qy^gGs?^6TbA=cyX>>dzqPYXVS{)L08X8K74h`Hv7&c+soUx6}GOkHI8)m zI`vTL1h=Q+$unv0?(G-&cohRT@7+819V5$+>hMs_7RL))T9>u&jt(qj&EICO{is($ z_eHez>lb%s&TKi=$M>}_x*G^u))>R0q3aywnOT3%QmM>FdY!@Vj&p^tv%8~j zN4nUy8=H*_q&d-{yMQpZ|>Z#)oAG51pJR{nBRy*GG9lN^k z@^g1(d3D07f@2f+miL4wYp%T+(OUO6Dq2GZnZ}) zNqC-Q{oVB3`sJ%aQ|pw!aaJ8GT$gK=qUd*OQQm#|88Kh-vX4Lgbn5=%-!b0vmxqg( zpPw}+YGLhv!F?gQ0&iw!7Ju}!S-btw0=LR*)zf$U+BG39cB4fy&*rq}2XgnFWx6qQ ziTV4>dHX+KnHDe78td)1^N&|rfyd8-hn@b)*E~3PbM8-{OP7ty-&@|1UUj^G{-Px^ z=J!rm&eANI_09Xu#;u@9ySLMR{^4H#*)6W(-b`x`%8Z3Ay13sUqAN#{rnD2&wAWnaWv|2z9QC&{3i|xVD7Srv#hl;UbZtIydqq0& zEDZ3PS$R)-H~Wn2>dBgCENyH$B%jo$w3VjaQ&418OMfo%`EY2cb`#Ui`O8i!CreAu z*65k_=kA)R{$CERx@3C#^lE12+LM{yW|`?1r(HAis?&Y_<#G3?Egms~_R))3Cto|a z=40LSBS$p4)-0E1C|NT1yl9_lq3fZX2Sxnmn+~)$ZoaH?=DFW?84lB4SH|DDxh*Fg zukHM+`Bh8fitKK2nIlWztF)z6o^aUV02;bzV`AJXKD}w>wp#&S8+LpNnK^m);(Vs|xtm(v?|a`+ppef0ZkdJb<2-3uL)|q?SE^R@ zJQMzZUA;cp_t5vQW&Q11aQ}{%@lnNZZoa&3tX_W=y{7rjj{Em@oqOrJ)SH)FzX+L{nwt8~ zTeY+DfO+oCO><)&#>L%XR(0?1vOTwF)``mpiR{Wg4DQ*8R zuJE{BU%Z}iE&sH9ew}AA2S2}cNrAVUo0#f^kDnj^s}5@T{GPpfhl%FrC(jgr7L=9s z_4oh&&|H5}{C!gsQ&;@L%3r^J88AOldamnveTiP}#Q%_}buYh#hpcErT7wj!#`tde%B8YrmY)7I`&M!C!rixu)C^}$j!Jj8Q{Rzs z-}RHxWV`q0mu_0VoV{|(&F*h|(yvu?W}FjQ9vpw)!|KVg((-cE=Q+WXC;i#1BKS09 z;=0JNxZud8KNou2JK5Zcohd)(Owb&Q+5CIG;|1>@%{aJZt8sQ#skf`EsrKaTc@>|` znSVS~S=aG$v%`<>lR+Uq9_Fqq0)j7#K5%=NKPf;@r0d}Fgo0=6;VWnB$DGK`d^pRc zefo0gxh_J_o=O)bcUCO~=iVL%mp>|D8Yk?s8azGkoxJ0tmv-mcwZ2bn#`YE2*`H4y zwl`nBlb<`=_Knrzg#f|2b3qF} zSWPT;wpD)e=BePDb5HiPyq$bf+k1}sd|{jNV8#2>x$+qA{5gHT#P){E%%Hx1;g5ZI z<_b7vtK0vW*l(gKa>~Cflk4rDUk~0-eL2;~$ui}|)n$3&S~EiQ&OVB{H$}!K>&uQG z-pdT^D?E?2u9w?!S;abRfe)v%-M{zGxy9Bk;ZeCXUAe^e^WM6Dsp9efGMjHaK4UJa zyXW66W1r^}-RFyG)_;Dh-#T~WAAb89{rgLbjn+*4Usitf(CHa25)NmhPrUQ#G|qdI z5%=la?)ht@x6fN8(qdPXBJ{ag-m++oIpevYXD9s~ydIvo`1tD8m>0ISAJyjn=n(8Y z!ygwMT>YQfe?v9HnZ6R=OYgh4dPnxHv^v*URj>Q2qJ*{KbJ}6$Lq9&{v-8xdeab@G z4CYV|+KzmMCG7U?GS;du&v~{S=i>Rm$MIp|ANBNeX_?#ktT&AsTR;F7^t>!-9 zaq=qC`FU1bE33=fWdDC%En{EISvT?G zxp#73L#IA3%Uyr=w0X|ApVN0fc({CC#hndOQES^6Dldhv+hJv)GT~e4ogE+RSzGVU ztJp9h^^)3KZt*i+ot=x)c~`GmUH;y#Kuv4`)q&;WOj46McGbU-zk$KcCsQt{QgTuv*<>Il~ zQ`GEi@1E%Y?6^h4N0f!llyP-=>-UQ{8@2bCCp@n)XgzcwQ~y+N_im#(UydpK`Zj~1 zZp#-5S*xZ44Z;sDKkJb?tQvR_yx&BDfr;6J;l38Tz&=&}v#Jamw=_>m2>2)9)%5VS z^71#5)68to9G}H;;_134?8{rUJ~T&P&O90g+R1PrglXQyc~@Mwh%a5~wt%0B*{`A8 zMQYy3&FA-RsylpP?`vkSHvN9N?QxrP7+>#x_j`U-4vQ?*mkcZp3;a1j9u%81(bC}j zhi{tM65Ab^xLhBf40cZXtjFEbui41JF=IMI(Bdy`%e%Mt?3zAr!;v#Qdn3hvH8uD# ztzEm;>Zt%1XTzikZBsU_pYNX;8Ce^9xLbd(x~seuV&3$^e-@o9qWAtQ7%0IOiX51r z>aa2}=WWg>GjYC1ridM-0uEtZn$HENED3nkc<%q--`Z7Q7bx!n&5T1oT{VoY*_3sXUB^rOgwb3lQ~z~XQq z1iaMKXsh@aL@0r0GC&FvIs_b=rYIII+UNr@__J`qw3Bb&$bP+<3toS@CUn;_yX*BdmC1j z_#Q6%5f)#cU-dCm1Zp-ssKJaDmIqwommDO)V*@<+L|?H;r@flpU*cOU23ZD#9{ z?kq3$4Bov^dAu4{RU9}V1loS}#c}DH_0{D^7s<-+ah}WxnSA)OM(ob#6LaUUQSGxf}@Io<<~6m&ZZ-{cQzTHIks%wqCKH27iHPY$Q9X3E!|{t`$fbh z)81K4OxQf%Ai@;8^N^z7AcZr`h3yngw?H}2|w>8*Rd*zwM#d}Ee(;7^Vqd4(HeQy9%0)A zzXjS_G3oN>r0?BIN>29fE1JtC09~@);3{}y!?XN6$Hqj>m6E@|eT&W#+39a_S|u#1 z;G6+3?$|q^!sHh7Fs1ubb9u=D{yd>j4M-uyqte(m>C%=-m;TLhba_)@7L)i*@Gxlk zRgU{^(UpN)W;(zZ`y&+$HjV38H1}|@!W})CpA6_l1Ihpnaz&_on_=8Dbqvvbn4la-f$!psbX@#&k@0$&5sS8K7VW z<=)q-6Q@@J#SF-e2TpMw5Y2n4E^!*$5`VZq z1ss}0Vwgd00v)!)%Q8XgsMI;7Ye+!@mAjD6A|QG%JtI*9TRk$WNdOzMQ4z@7|YJt)*Y^VhPzWZ-(1xQEyisJp@tz zYgWT%pS<+m>sSHgo=H#Y~rYFT~KTkp!7$eo~t+qSQ7|KIDh z=-4FrImgc0?B4$0{dkAMh5sxCXTRGQ_n@XkC8iE8aYjejaQzY?i{g~#ZL8`%f}B3- zOnRO!_bfWr_D}5HJu9Ljr_C;_F`mrcqI$f4hTqeUAKsfTfBSgKd7*%q?W}2$!I4*P zCA}y+6yVTk$|uOQ<5~NO&SOT%tqq--42urw{(O^=2lZ9IgGY*%Zxm<&>iaIP@1J}D^=xEs6+>^?$uWhA&W$~VeYi?b__@n##+kH9)9V5(mUcZd3FGabed4rcz?7+{yY)Xf zt=6yj>1uiUPR2}2@j7u`v(!gH`TsK}P6(Bq8#7s5XW_mzTCu^)O*tGF*mFLZ_1^!M z1oB}83@khXFQ#eVId<&Y!HXxC8Sh%PckkMzH+dYh*p9ggLrbAAlN>Ibn0R`A?NQfv zo@LNxz+RIZr{DcMSeVlIXztdxDRckZru|6yzaYYV=lo3zj~zUD)%l%!)>GA)p{X-( z{ko>r={3W}J3A}nN~k#3bqR@iTHm>*RO_x-tLJCE&-aGNR0CC$ERo4qLY{bB7m;$5 zWMKmZP@__RAxdO8xE%0%ck=Bc*Y>r&&679x{5-hQE3Ys*diS?&*UlaDV~w`n*8{5? zzX&o`SF=w{-MXq%<=nY-N;~(S`FZaAHLFuGGwv8G805YBBD}vOH+AN&jEPYjIlXoB{N|q4JY_q#@>}@kGs{;7dg^bU z@yc2`?^DBv85>jVAK%G8)u#2(fw#+OtC!|?J~fLi2NT@gcdwpZ9vz+Sa)(8Ke!wLi zO^fn>N5Zr`u8Z7quiPLo!?s+`_v`h!*6gQSc+U%8-;-#ERN*z+DJ7)d*ZSRYNxr26`d}eyq>>6NL6a>Z^tW42YJ^?;qc!+t1Ij zec$(+De2KO+b=Kr%;S?cM-`k`F3J85T3DeK`q$w6g%^eW4{`_*>1W&{^yD9@uoq_zPFJq`aj0 zL4AWZk(j$Xq`yr~-T88P*_N%QpJSI~e~JxYXk=7u$gXqPkQ@L>*HH3;C#bQOetc?l z)D_v10Fi4O(u(?u7go+S1-F)Tt=58ORKUg9Uw?(dTRu`H#Ln(V9GZOUQckYO6tOu! z4}RX<_cS%*Qpf4qL&3-5J^4O9T>Nal{Y72Xj%}>k6D&?YDO%4E#o=hMVNTN;`?+)O zHXSnQyz=_BR`F$iuD=gz;$P0H5f^d5k$rlEUKF(krxiq7@Ba3!>{|Prha%E`{|e73 zo?|-J&YPX<_^n{E$3n0t6fXF)nCzONf{`U9P&w?-%MCYWK79u5C>%lgVz!tnw*(AHRNAdwS|q z*2~ymk;ZDB&R4Eo3z}6QbpJx)Mq%aT=6BpmIFZX#p`Psd5e;yqDnSO5muc=eAS9`B; zxbIf;M_KLcPF1Z$EzYIcd=IJ&O*kIRI&KOHL>M{E4b%vHe#RodBAYvRp(mk6pjYW3 zqt2;fb1r)}J1z8l_d-K>tJTRnE<)LBkM+$sIW25LrN?ur*;jqPs6@?O^ypCM|BG)~ z_CIi078W?iZT*ZTkp~}oZY#SpZPg{yQa%<%jxYR-F`xe)mRt<)zJeGHCE!M6hqvrK zM*AHlOD;gR<4Ghi@v1J(R9*Vlva{pH$DAF9-Y8og4-J{VK~_O?E&F@!Y1Qq4iwZ8~ z=d8GR^R&uB)#YvrTYs=0zgS)PFS53K)p}jKJ>7uMv z#RXFg?JY`A+SNP6yT6{`&VOAc=)BeT;|IFF{5%*bp169$n%oNotGC&yC?u3I?Ra*! zPlOGnOkVJvWsSXM=6lJ5u##5-+-lt_mLhMupzP5D{wbbwL{f~(_2&ybV_)uTp&?$D zn>w$tFYJPdkmaPXz`$9mwYsa8MY;NB`ljx?s0Xc%XLK{Ht1O<_A`4A`kY=2LGNViD zY(X`jMsO#V$Cat_CHu?NlBd0;Pu;)OJ$&`>y|~2d2QMyNyX8}NZ`P@p#j>heKYkVO z4AU|2^~(6NBk^!fM&8Wy?_M$7E3aOkHplAAVv*XHvx3$*va?kAGhDBEFL?;&5m1ye zaC~89+;Ql4lU5F_i;>6lL}2;!cV^mC+g$sXO}!c-wso7v+LF!)F^&@DhU|UPd?Aq5 zB9uIk1Zu+?E@u*k#Z$IQ!K({xiDqW2jzwk1$XsJ#vf_9!>pCN=5{hRxFfbLgMj!Az z0PFoPxXz+uV9FI8oLuX=UYwm-!C(f*gIV%E>Dxk9TBm;}V%Q1U>45<~b8Ov88#ncmHF^gH+NS?rd@lULu&omJkp z$5h;0-rCxIpH&L?IisC>n{!U+$#b(q)1^QH%LHwia9HR@Ff+#2=SQ_2P2<_lb|iwC zgQtke(9`&D^W#nWe*(h_ZpC=dH2a@t^!4_(?`Kml=#(ivQq!C*^3?VDjIBp5a?hEQ z;{Lt<_LTrvy*C$w*JZC+ZBzW@`Mn;~!fR)*>^0~9{;hx8#*LHh*tf1)=NPCKJ3q5e zX8(!P>Pz-mJU+DY?XqvOJ-_7Q6jut1Z{51}-@eDp7yq>P&wKyw;p~}Z3sl~>tjIjD zdhh>wlc$%gvsZVYm%F)Q@3aFSuIyN{GW6-r=Y6@?@>*HIO?L(s^ds%gxHUMPdGqFL z+pVoz%~nt0x|r}q`>wgcwR`shSFX`MX?^w9A`#66x7=AY>Mq-xc&dHQ$j{HtS)rxx zlbxOYZt+I8xshe6+qZAkn4RT1`MG{k4y&e;vBYY`+eXqOsGuIvLoOkb_WO`ZV`FIUU!?eZ?SzGP|#Mk~g z5Vbffd_IS@-s$UEMU^5EQ_WT0Gnh{EIwc-jasRDZTgt4he?MF-w#B47f8R9m_!aA~HVDmp zb$Qj=xzF4)vR5yRp0uruZ=0Ffm%Iw2L_^_>#yx(uxtg(!3j-HhSy@#idZcho)jnOM z{CMa7%r^@za&PXBSyTFZ`u>Dno~j`{B_*Zbi_0cTZ+IvyuzGXo)g=aB(^Br7*m>`t z;2Ejk@z0)08x?+A>vQSawW5^w;#S9-1fWTN%{+(e)7e(XYl*>P?wRU^MPYoeKjn1i zUf0%YS#-tow2bUr&r3G~oa2hZ%k$OM#s4q3@oih!W|s+yrd<=ZZJ1;AQz|_C+;0c_ zf3u}_TGST(zID@bg0jyFvlpjC%Dm!?Kklo)Z`O}1opM`P&q`*SG~f{Nk=qddW0MFTc84y=&J~ z-4Co2FI+vWpZLTlOZSYW;!%FTg|C?U=UVSi)ND;py~p&kc&5CK`Q!6VYt|V*dM7C(Xnt@{ za^z&w{R-W6NdXXbla_G^Y;$?Pm2d-sUsKv^+|Co5k zy(aLUWG&aPCsSfNobHrvdsQRVaD7Q%O=O6K@mscUlI}%SZ+i4jPZRqpzuV5PdW&|Z zMfJYr%QksDpDs4h5q6r&fmfiRtgp)@xFOB67vW5b6IYtdi@K{)Gv}tcL0ijPS=l}E zb5=<%FW=OAeT8@2G5Nn%-re#&F&hsDKmVd2;L^ZRdTxVy)IH9bfUABB9Mn8p@6Ry}iD37sbT8afqACfAZ0kUcFMEwIXPn zn3t;m>=}Vk(GyKqNy*KfG;cG{*PO$)Qqy}ryG59#?7OwOX?s1-!R7n1wq82m{H5)= zX7Sf$J6BHqdMhuCOJ>;@jdNHWuP+b3cJ=+W%5>_^-d?Vvy{jY2Igfy)TE{N zt+apKqEL?LSN8?EC7q@jsoKgie+5-PyOyjajAAJ8`41Qu6M3$N9Ou zvvOybzI!3&y0ve?o|Y-zk$N20<}&@!oMdJG`8?A>X2vT=V&;TceEa=fQ+VC7E&F}u z2i=?|vLtoh+f&MlsU@ejD3>HyTAe!Px3^Dn$@k62w}nnm|9DkS3oi{pEYawuT;E{{PLl82j8zi@IaHDQ4<_C)ll@aL#dUe8#fudF-y|?0%hI z9ArA}+3RoLFS@Pd>r%8hRR6xm+a=m=)n8X0 zx>y@;_`Brn1!tQ#&r%DX%-E2jmuzSKVw!}gRdMV6qChjBr8yHONp)Ozhm8}j0nOKi z6>Du207aw3E|xhqcdnK)e>2!Op*o}~&0a5Xk4o$DNm1_Zy8~uwPd;+2?^~bn>Gl-c z)JK;N&3SAk{ASXB6UDDaS!-9GFzoc*ysB_UyZ+DL8il@<7cCFhYToJJX{oWgBGy;Z z_M~5&Q?>Q)DV*W5_dd63oHD9Blr-(@)R<#a#OLjKtCtsd?%cYf^9r(YpU*W0tLZFS z{v%pbA?9jE`g;et+;qo%MV*VUhlvH)C2e~C=v8L+Y1jXzSBeildy^4n9;5T^NqqVF z1yeqT2l;y?*5v%(ZuR+_$DH)aLbrg3Ma%mw4D%}9xR@FLIN0eNAM^Q7XL|XAFFy}@ z_Rfo0_~z%7h1OPuv%bi3zxcmkpU6q||3Bu?7;rZ0)TOz1+oqqHwC(czchk~L z*T2*$Uc7(H8n1atyL4SFAN>pq=HBvBHI-fH+p9f=I~FKB-|^zx@&NbUKAF2;D0@cx z3019g(#X#Xni_w}Js{re zf!x%B&PBU+EHc^c6@E_m(>7!K3g1w{`PQw6{-~de^F24~$CRCiVo#r#e(=wODIyUs zoR>V|$&=f6=p^^)C+z2ae(zOzZy|gt#^&x`=ZMH(FJ3M$-=wE}Ebir7ale%neeyr* zznx>=xAxVq#}jisr=>`;&d= zt>4F7-g*1b!nG4MH_Y4i#wxAy+X=qOjJ~g*M}`&jI6ZvQ%k`A|HB%;2vWI7A_S%_~ zl5hK5G(ErIqN*3u(j!r65rH*r-;%#@_X)=xI~MzFdH&KVLCLo34W}Q?S!iSUcCY4i z?Mi-~g7@MQtL60Go_!rDVWn2dy#C_rEl*pI|8PFdwEJGxvG{8<%d1P?|NpotU;($a zQkmv%Sy%JAPn*AUM6aFll7F3Y^*=?EG?o50|7Hhd{@pfnP57NA-;%|lFAhe03z+%W zGd0J?b=JvSW|Qa3&CN)3Rk)aHIWc}~NV`_<(XS=~f9t-7NM1U*cTe-nUrX3K?@Y;h z-OaQySK@~yck2m@!*iXMEjfL7TYyEbTUMog(8C22pko0E>jWi4xu&0V|2Aov{kf$^ z;lD1=RdVxRKjC55hb`7KH5zWj z#Jl9*^HI65phjlStSz>=U((DTOT|7Z+r)SC>gheM>T3my9<*<$o7d8J>FUZj0nwQc zogdD-rhylD?9E-V*mC}s@5>4&=QD&S9sO}MZ}o>NZRdv}Jab*`g)eEC_~d`|zJ5K~d6vQ7 z7knJE{m*w#*xk3St?Bdcn`R72?~RMk*fzTKZ$5M80iV^XQ`S@F@h&==S#7mq1&4g% z^4A;IueUbK+!~+2b60K4re}4oA0v0^`>}34p?KHF^yHbtr|17W>u`8x&ejD>jw`(Q z#-BSo^HSQyRnaHj?wl-Nz9nR)Ndu&OU|@O1)HpF<$LRt?->Qn}<*%n&SYEW3d8uDq zwJc{r{CS6fLl&I1du@vwxvxPw1_r-5bLu_>{&HRV_D#jldAEhH<=*(x{QK|XWOLge zpZDt9oLAfb)ZKoz`our~PWAr1HPL!^@b+~RR=7T?DahEcb6UFlhS zHTC)K-O*R=6lXPPOuQ*$zh?a|hx}Ww`<>1oW_(@w#ogL4bCtgKiF?{S(f!;aqK`7x zoaFD1efCsZYr6D2$>kv*dw9#|{CH)*Cz`*Mp?G)7-SYik&$CY5@xLwpP}QlB;HHvX z-^bl*A=Bm<{xJG=e}|j-E8Ce}(~f!N`>wd9KBMgDlFN!kw(BaxrltiRGt4zSE`F9n zy6#W>`u9)sIXeHG-+EYc*E3G#+glp)p7eB1F`A{R<<1`$!O{9WwKnrj`qQP~FY!k# ze`xz_g#oLi%-!Gq$;QRACjEJAbkjqOuliH-I?0K9H_rPPB6xF#bx)7^o%=?8w>ImA zTs#pIpKN>g%@WFr8h;d3>gpZVl+o_^K%B>u;&f6Ii=o_dn# z?!DD>?_8~yC+B&tf9hMheX;Nk?#_2_CGDGn!zZqN{njgPOKYC}lC?VwU%%L$yeQec zD8%j{SF5{s@Y0XLUSAFb{F)Vg(*I#Ab8=NOTgmi^EJZ6gyww`(dM61#-WOunHsxR< zH&c`F*In~2&AfO2YDL=x*jSN?fCOjj=HvXD`#V4FYiDEo_57uV#ktgXJn|dYCQJ~r z<6X|;3{e~5sZjXUYSW)T{rg|%W!P+f9NT&-`>OAD{e4N<)wOp|n#b>dv{7Bd=kmoH z2DXI-vpB0WcCB1@q~f1Q{?Z#Re`@j$9Tbdt$n-5McU92uwu1eoAAe1ANd2g}bG7wS zA!FBccj2kG9xz|KAJzN*f5_{_5B|hmJ8o`Zr&DFfdNWgfz07=GE^fjjKGl!E`$GS8!@mKF;_|*bj&C-qs`-as z-#i=g?*f}*>dCD+hncyT)@9Up7ps?Ds?Fo~kJn!O&}8zaKaV>zXTL~$D7d&OR#P%& z;i_G$YPk0s{n+|pcbEZhD1-J%+bso8eGmQi*<&dDBV9k|M^tCbL)a+6n~4WJH0EeE zTU%Q5^GTI`S!93uWqkP;e}6rNb3(`3%bm6#cTGA68R_BkOnAO|e_X-IX^WcKZ@%K& z|E0d}VPN6u@Z24*6fmbpZ(_TULP*kxh|=~8apQJ`<14p>lDi2^X`#_o|nv`TQya| zugqKb?5)}1WA(7xDf!xxubjJm7TIiG{Qdjuo57!d-u$iU=KR{rZ@<>>Bkaexx75w^ z-Rc_R5HBp@yYRz-4nA$ZhXw%6N9B zrRvt5{&SjN7ZsRsw_ZMP{(nJ5{Qr&*JDx7`ZElr4d-BU22Gfh88RrhL)xP=j^~&m{ zE4$`-7Ze8vZvD8ZJKoUVL`Hj7$SnJ^vxWcEgQC54wQZ8}t_Kv&+xM;P)EU|9IiYd? zj(t;`dMxdj)U?|vYv#|DS{mf~-g8cLZRonzsE3cGGz@HX)e2Q#JE?v>&F%X?EuHPl z4DW9z{a18w{XH~mf6D5UuYwj`oc)R`N=o>J;G9#{DxYV%n zL|6I6z6D=8n!|3NlzX6Y?&q;5Wp}htl?5A4O;7tWL4iPyEhx~^Zv$dg+Mpi)-f{Gcpgq zIP`9^cCq2CWiI*m1-w=*{ZX3x)5E27&*vpQbzH5%MXlT8OA<7`KiT(j*`~?XmDZok zT(8$8-FnK>s5R}%wC?WS8O_1bm4<4CQ}Z2qOcxgHf1o1fe$LWZi*XK@s{Z~b^ERw- z{qo%@^&)3+^2{sjy!X{!zRWVcnbmyBT%oUN(v^)b4ux;|FL>8)an|Ez=3kTc|GGt66f2wb!!9-{&5Lv|=ZIZ<<4diT z&93jN3oofFw3+=A&GI?5<*fco%~P41O2xER29+hpPHgS0%s$k7I9Nx*Csug(acBFF zpEiBlwmqcJQsCy3ly`nn6_U1BO_muN@2Ge-`RMBX&ipAGVv2Sd1y9dkK09d2ZM|Ju zIu>VaxA$*jSs)!%!&+>#Z2rSK#gidfI`8kqB}NwfYUgS_6g^9;#f$sgMHcAt89vVh z6_LE6N|Ardq8@&kGGD_h?~;TUto>KWWIg{!YjuLdi>51woY|MJHR(E~-qRZJT5D%P z=T!Y$%llfBvQKSZ`{vPsG?AP|78XZ6KW{F({FbHZu$Z^2*$dgZTjq7G4fe?1#IeKU z=@YLVr{mMJWta8^F|9Op4q)g$)w;UYd9C5LV`(vqjoWn`6NA3Rn9h56^s@Zs16-~i zvtM`pICT8+T=l#E64y8_7TcJ_8mgM!+dg~ttd%>Sy-XKeT^A5$Q8u+e&id}#1;W0D zM*CJ--&YY@agFz?xRR>t-=6&6bFKd!d=zRPC}o=JCO&7o*7B)8!?O>0WaMdx@}}Qb zY!$hg%)52!&Jxy1!eWbg`XU}Yd-r(dx5ZjYx8{{;XYag_#=Iz#Be`#p+H3yWze*=} zY+vpi(%exgpt*KU;7jp24tIB3&ssbG*qNxHq@w8HovoG?D}0umiQX{(NYq2i^zzEz zt|HHCauY2Ah2PGcGg;H{o{me3n$404=67c-ylr;c{BJ|ujLW@Zr{3F@zp=f@s_;f> z(o044iEXk)vrSIi4Vo5HT62H@n(t|sM5jPXUWEqmHyWzK5LpI@_|_q{dfTYqsf$XWrdo6qV>{wwRXYO5UDe`S`(s`7*KQTjdECC|2PU*5~I z@=EB!Cs~hueyx6R$nlwR;YHCs#nR6rZEKbvzkIrX&INl-&8Q>QvmYM{iTQk2w)aa8 z*W35u!P6}D_LPWsz2otGnkQDJs=KS}=)uhA%#)vQT)x`!^$+(ajg$OeRv$k#FQVYY zESswb@844sWS@KdP4=aPf0x!SSzZ&jPl@a7w?mnF?Q>UM4VKxn-oN_S1!=Y4QHyi( zSDSy|w#<6-#}M{O7w$zR*G@OoHDxqRakku_92J%CF!R~U)AmrvmP8Qm{D)BdVvR_;2r z`EOnu2!_AlhUGE0gq}48?9sZ@V5>QU9UF4bxz*Q27x3q){nvl~-8uD%+%xN- zFAEnv(V1Gl!1t2I>&<(&)-8xvFAlwQ`E|2uE2)#aGIICYo0(@bYJ|C> z7S|UvdA;@jeUQyvFKxEWEravBy58Jaay5rx^6XQyB&yG?(22h~E!x*@sB0T5jnN!zxuuk73+f!^@{yA17((c#o?g>+-{P&yq>(QOV*F-!X z&i&x@?c?{#NR^0q?Ky7`XFhzl^2BD-_BU;lGwg%1t)ez`c0?bmIJP{l>YMcK_wo@E zNkX${z3Mu9m23I&;F7&}CtF-Q5_|ExiB+NL)CrGbFRC35;p*}FKINILS!MW)?OWUy zOnSP7-Ds}eD)E$u*4e$n*XL zL!`J4eJYILiVu>JQMLAUT0G_Xw`;quaV|ICwl$>sckAU|cCWtVnagu_{Hu#S_>);= zn{c0BW=V*6-&MzD0vk50T)We&!o$c_Qsc!lc@gi@SdFT4+qZ3x+NFGGwj}psskys? zLv}rQwD3jCQ;~Pi^|e->2?|^PaltfG%XItYJ9use+`O~w7L(Xa+4nXpt>0F8UA^b2 zp)V89nR)th@rG+BcRl0m-pQLbH+F}R^Pyc1HqP9I1yyrej~>)K|Lf37|J~NoKjYL` zjV$*4zt24_ckyix!HwQgQm4;YF85OF4!i5)AAX^C&Ym@~YI}RVC9Ug~YgJcr*P3Ri zI}5!Kobchs(l>2lmoFET*k1N;X7yfoDa(45*XGw+ZmS+f8_VRb$T-&%ddlefsn?5z zcKqhLd-v_0W1Akl6m?*GQ#Rf5^ou21uUxri^eTOpW%BE$Mu`~o6r9^q~0nw2O5tDSU43#>>L|j@PRX_!FA4nlnM8egHD|N z<@E5`Q)vlrS7x?fzh_CGidd3%rY}tD$FIVZr^{lMU3Ua|<({+q*cgyJ+h_LOi?a{0 z%vxY!zSR8f{T0{026!eFue>Z$zQDZo{GL6{b7NCP^78UDUS_Q~x2$sfCRfGYq8qTQ zI=Exu=Dm$ZD|>aNvp>JcU1NT&C%fcTTzuucd&laItrleKY^l{;^y}=p{amI#a_?=P za`C_NNIc&8+)~Q_Sfa_~(zD$S7y4?}aq)|}F1>Z|%B`zuzpBJapYXq2V|sP^rN=t0 z;@>`dojz1>vwgef18WPD1U){#-EEiTn$P%uJK3*my=7UbS~y!v!O=~pyZkPHS2C&M zv^6_;eA(;OpMQROI>p>~>ExzElSH}B-&hj*MQmH2Wth*SQ---Z5*K(I9(0Hptf9=wr?q_e_Shjqv)C=~6SDRk1SreXjTVwL=ZnNKC zxx3P%3on$Ow)T&#nmc*(=?4z`r<;e*bz46Boyx}(Kc;5+?)b1Q$2m8!@SNhmd!npI zxaX!`Jo);?O68?NuHRE%-d+86QT4`W`)W&6wQXxt569>3ntED)8t2(k)?N3`ZFy>b z;hs>)mnqwJDAdOHZ~dO&a<2d~sKLP0=g6?I;J8J91S@!vWP_~0g>PFz#rg$*Z)i@v z@lZtbf`c@3l>NtPZAn$pnf|N}Yqo5;loPW!`em`!nKg6Nb3;xP6=k3Dy`w&(tMlOU z2@H4tJv?nVao^UJv$_-)sx>nd2;B{1GU3BM3FP>(aeOtV&))vE^U z%yUiXwd&P)VaIqiewO;o^rFLukKIZVpQ*k1UB=qQO@1o;eBZX4UXOV2^4UZ0w2+9y zcWQS}e0W^HvWoq(YDladXZc;1=J2WWw9ne>1aa{@FP?s>nboP+v}k|a@vdDH3UiO1 z=$G~1YHurd#O6Y?W9ZRoa!)p;E%i7dY~M z7gg=F@O>t{^^Ce5q@m)Fu!rTzx<~ij-{#)={6kCsY(?x@&1&}3T=RpUp9>WCp51XO z=C0=IiLFV#YBQwQ|6O_C*89|P`Pw5pIA=wAD5z-7O>^bvx?x~m6>uy`;11jL#k(RR zm2Mu^zHld5Y4M%2v)x@kzf2MfTP!9S@Q{1yrEV_9K!@NfCRf`-`0sAJ^n9AS*vt)D zDoR@yuYGvu<9D6tzSUQ6>vyf%cj0p2BCSv*^%CW?xAiB$dv0WG{`76LcbBHz_I|l= zqDA3%h7-d|jP07eMo*U* z9M~eTK;!wJhi?ULW`yjnjA}coaM}03HU*2>Pp2F^n8~rvWmBp~*s+5rPtLFonsw&$ zR-0cbTl*w+w&g19UANuheT47L$lJSSzrK1Yz$w3Y?Vf$hSI@lnXCZ6jyix(XcLuJG z1!j!WO%K*6UzOUl(BsOH^KWK+{j1yH3R^vFA;;KqNcR1lBv)u_6Li_yVHP&FFMIdK zhQvo)za1M{v~BCgk~a00hmH&^$NUqNwp$&um#Tsmte^`TrwBV})SMDbW`Oo17#Ia+ zWV5_5{1Y6%-|Bi;?^kH=6SjisOd3<$T<5gUB8KJ&9gxLQXygnI#+F0sPW-j7q_j<7 zL9X%5b2B%mI?Z5*h9<%#3>;_Fm^3CB%|6HW3{qbw%x9S*C*>P3$7h|)i|boWdT#AG zUE%mo@$=MoulDVm_iNRhy1mzKFi6ejJP~Ufvt(U~Sk8@EXP!A;6l6)8z_9M8WJ~`- zsNLXQ4{!PyoF0~ypMqZS*%;|^;AZ5sNrh|Mm+RXvU$7x%?>>F?;&69w(e)`#2|o_a zI3l{oNUJU~HZ<;G>)$(e_PNuyOdZ2t@G+G+GF;yiJ4ek2HjE2e zJ&-U>z@c$UWRa2qFC>t5t4!EcW8K~SJ!^N=EUn#E(HW9KD{V?&nu@53>z!0r=<9DT zFEjc2{q;&y1rbyu6|B;KXiCK0j{FT@@D?r!}iK9GJmUaJIjb zgAEby3``0NDh+E26nRfT1r7L^y!`F8N}Xh9y*gcFY@crX*E#snq@6FyD(-BWbjNwm z?iWXT zeGKa=cUz~Rx-ZCGDx6;Sb;^Qjj#Jkl#tyI&tbz8MeHPRrdHMFUcbnHeE?7Ca^7D=+^=o?$B~P0W>0Z5f zTeV+!=f*bCoHR|wn9rNP97%|R7!D!(oI%x-<@5+nP#~O%WGeFSmyE1V_SKU%*D-6J z&#+Y8c;`a3`Qf`Ne|#|FEDG=6@WNH)3m^Ms(Y$$wu32j9pSbbtt$1qDPoteisV_^j ze3zUIyR=(t$8WBsKiz$$95|UaPi9#6^JZn7fi@)ZKuC90gH>TWTpp=RJ*j%wQnR_4 zx$3Tm)$GOV{{NHBd0P5Wu9aoiZu8$UKYLp9oVC>44Ttm%j>Rs$ zShm&csh9uMwr3XtqOZinAMtn7dYrY8ho>Mq^^$htL$O~utF%ieo{Wv1s;9Tu?1qfl z%wl=Q#Fbj54PJYfu3f5TUOc_v@5GPV&PtbOEZe#<W zu4h*>(9)jsPcFT%AZo+p$(uu-X~kG({s>zr2itCVhJ#VO^>wcPpQ%R=W->kGzVzYTpSs7rkP>%g$vw#{d5?7QJ}3?9<2Ygu)N6Hhup2yo}quue-}5-mizPbp7^|U?wg+$&p$iaqptqxvAW#%BD%VIsgvsqWGt^| zSyxqCY5u-__pr5vlIv69Ih9XxFD_UkQjqg0>XLZekEknMhkvc&{=(NACnUH#L|DMr zA);sZ%6)4Ts^1@IIH$5`Ma$JC?_a;pT&Hc2$)L({CW>jtv-G!Um(s~h0u3jtu6g;j z*7=Ln0v%IX*5k1;OFp;t-`ITn%A*M)8#ExPuv%rpjw)H--=FROt=qlusYpMGEn!jDg=Pj_Pa8Evb<>O^=*zxtg1OArn9q)=@Dn< z?9ZEcTJ2W6k=W`rCr0SfHVaRcT-~j_S1p$;46u9M>OT)M;ZU8hV&Rjjbob!MgNDV0 z1>vn;OTR7OSHe}nFW_)sAIF1P+t+_?y@^~y-Oz8Gxc|UPqaBkse}3>lA)1Hvu%`K( zM~`D1=BW0ZpM8D3``3csw%2D^f^x5gRO8(9|EdnEMtAYarEpvSx%B^MtNT&i_^l7u zRlhttWp~`4!&h{z4NPWQK7ah6a@aEB*MVt?|wVpZq zXHsd&#Gblplw4UVr*V=kv9Ktq-)l1=+=$8gt}X3eMg>UU^6d>K1S{E7NhnBW%Y> z*TRb*>h`)kIx_E#Sthe8D7+gOIL^qS4O6Z&;fmH*Ri3OcS<~h8bnQE?vv!^8KKM{` z$(xsX>-B68N3Gn!{5gUk(^PfhY%QJnjJX`t2ZEWf(HQStfe{Jt6@&t+CJqW)B0?WbcMYn;92&ZsAp%`t<{U z-}|>OC!7+|;qzmj`b1WcFMLw9iDId8=86mM)zNzoot?aN^7XK1AHD{)d@BwRbKT|2 z;<7f7GiGW*mFV*t+duU+4Gn(<1!Au*V(o}3Jlp-_fdJIU5c`jLCoJiCSy6pR4koPd z`o`}l-RURR1XQf3ZHkncv+ddfuIstJPo3;~w^lA?ZL^Zo(ytd|&tIXdwX4D*gKehZ zi?^BC`D?RH`(Na8I>tV75$7lwre z*6@3{59p{mFcwxtTkf2-$#Vry-qVP_EHSCA6Q*6e71DdlsIRJPqaJAUqLP9IbOjxB z#l19#f=C{-NeN`>52(@X$#he_?A&CJs@=EKii9MjS(Z&?SobrtrFS7Dm?5OhGzO=K zeAQVS4IqJG>3Sg1YjH&JwO!WIYYm%uIV^Y>V?Oi2)_y^o)QyrV4Y|hY0(NbXQy8Ga zP`7|h0<&lEF>$o|v#0jJ&h~(K8l5y~W)a|;@AyLkval5?EkYa#B@?VT6h!P~rx>z9 zGp~;+W4wKSmH$_b5ByB%hB7dfc`@8hyY;g3@D@n&P7`EY+U_PSqCQ!h{a2|jZ|Yj# z)Xf=1o0GLJO8s2QUF%$PH^XXbukCx;mD%qkXUGQ6z7~0;);#GXzu4CB@|v>0`!Z#3 zbR7)azriG_bjF1bUsd08^D9Ps9yss+cHI-b(yFOb!i*2zNq4DOFnRLJJ5%o6yEp4r z+N%{onzeiOvaz$DK9$}3mxcA$j$OMx9XrPM%p`E)!7Cy6FWk9%S9_D;x6H(O&#s-A zGaGtD? zr=E-NuG}44VZFXA{_%{@{Bcq*R7#5`TK8vK{$(>VpYHfqq1W5^iJN=CxogEAwu{g2 z?(7u$woKu@c>J5nci;c3hZt2pch8s5p1Ex2&eX@tUVHBQyX|fH{Zebr==Fd53Qhie zK7Vhjhsw>H;(7ZYt3QjGd1e3h$D--!&rh9NwR+vpbr%xqiq1%{FAl!D`}^wo_Mf7! zr6kTPzx#Lb=9xR4_P#)IS#Xq*L_?py>9=s{9TXx-lwF^+x__+ zd*#`6`uDj$d|!TebNc3YzwSBj`FAqjzT|7@^ve$)Jh-svi+9Cd-p@~d`_0~9==t@+ z*OULUS__W+-(^+!{^cq6`x*jTipU#jeDoMEdumL!X4?bms)Ks#i-kO5_su?xw>0|R));m^+l>{5_h<))ZUV#%jA!!tV<~T@KCMuLPAqfurv2w^%p5xyR`T7 z{x19S;?!&F)w6fq&{!SJ^uO-?pOq4YvufSecJo9$xPO1`_OzIlPF`uHH+io~_-{V@ zueyKV&PUEw*HemC9?zZn_OcD*ap}HATXF*9;^Ma7DLVXb@B6?*JHDRI$uPJR^Ygp* z`%Q7?cXxzdv9;5i^XS2nJNy4HSdn?=@bh{9g>SaBwcV-ueAVgk^19Cwvad?4?D+iJ zWo2cZq!+HaJafm7R=f3gtCn7vH@*JTW9tZ+J9@|W3ty}e)J^~^S|8C$k(TXpr-moKhLvtO?_ci9+U z``0<}@{IQP?|#(WE)73=^r+p31GV}4{&H^K|8&`EC8a~2b+1-FZ;*D@Hj;ifs?b%i)Y?iyEZp#>-9%R7caW8=jq(?du~OMkw>q+ewT7ez5eTi ztKspE7e9n*i_6*-P1~Mw=vubk&7X$hksWyqD?;Wi3M;d?S2?T1=<2C^`y6-a|F|U1 z8b8nY&z%KHn(zN4?cCm8lbaRL5gr&iTlRm*oUAJ30#sPBVa=+ofyGnsccQKNU0Vd&w- zk6F98Tz`4ooE`2LQ&3jiwcohtz8 zt5ozU<|Qd^?=MVsymIUQ_49l;m!FuohckL=U`(Y9_xg|LjFX~2Oi~XLdf!*poi#(xDeuo~(M57E!$E+@_gR*LvBX{q+9c&YusDoBeyz*m`^0 zZjECR`=;i-J+Wr*-rxJ{f8Lp$Uo`V>V4&dioS4MJPyFYeTN1Zum-Z6R_%p1>v$Jz; zBKhq)S-l>Xs}~2|i!{_qXY@U_1mu#yKJZEw8_PX7(GT-fc9VWCQg)2WH{?Pn=Yj(Vw zIk`q=j+%e9`k$xDhy9OcPd~e`dg-^)M+VXGI+R_ZVNC(MMP~$LGu?qR0xw>_O=ITg zoppn8Q~Z7PIoCe^)#v6{kNx)bv)cU4o87uTCe60~J#*9U>k1sX^LE)z6goCD?W6y9 z?%6rhrrYn;NW1J|p!f9Hl3$;Bk6GTjeeb!J<&(NO^DBI9?8S0Rem?F0W;#FXx#?}Y z9hV}_gv!e%?vD9=&7qHt(N{-IcdYsty^au3H&ZCc4t!1^w|C1-{rTY?Rm4=<>-@5{O+osKC?X* zT37eT+%wypV|UEA!|dnY{{3|NzO8D=Io6*)?f-nf7M;Ij>b;`R+iHLJ_m!V~zyJTq zrL!$2z3iUsKY!m!Z19<)b+;EcTfD!ZToZn?VUAGo9645ZRheq zI?yD|lCkAb`cKY7Um%T@V`>WDzr1+;LgsefCS9rQB4M$DgDW@O==oE;duH|i1ZS4z z3)Sj3v6sly@88>eT4L$-d23#-IJEKQ)2oatV{-QH-L!Y{uFKj^PX!-5od3wZ@Og6o zT-kXX-vu`uoN&Uwu)yofI_vQFt0w(E_S?CPoU?dx`~Xk{sFnLfEY*njduJ%i2Sby1f7c1vq7#;#=E)p%=- z)z0N}LQNy9{x6za%{yJ`^O;8v-iFG~z4}E~GRVDB+cYoaj?H89O!c}F(&}Er$9J*CwSv9S`OLxa*jn4S43e_8Mc&7%IBND( zyf&TwdEp1s)ndl{`TM>$eqHA>ZGY{rqi3hbK6?1@<01KZUA?`_uG#;8DnI3Ly+~yLQ8dh!g|ScE?7${r|2iPqVH6cJp1o{oEGoviq^p2iUBFZ)8=vY(ehb z-tb8X(TGthoOJ3*&ilCs4>ImucRp?Z3)oE1We0_Ve}7)jues*?)HQi(divhK2Wl@H ztSIPsv}f@}PVGNT$#+7fa>G}Ltoss@d1ArBZ7X)}^T^=**b__rFqm^@z!_`QrcMJn}E)|NUabb#VJM z$yLnVI&0^r|6p_0h@5d_-@l`&ijs;?;$KYT+B~OD)A;-R#Z7HG{x&AcKaSW4FD%%v z@~+Myd*@3}uDSi1=i|3`=45eyj^F!T@7|2OnARn_Pxzltxp3ox%ff=muIskv-}cHE z?3I0A`{j$V{-;xK-f(k^or&L~G4*;(<<+!Fnz6e!tY`|qrSRw3%IEW|zj=zW>+9+L z{jUA~<^z`-sV573KN~OK$^GWC|NbQb7FRu&&w6=?!}{@so0|K&J)a(JU03n*>h*gI z1Adr>$H&E=e{1#osCf0By?cLL(-f+yt(~T0cqiY$+$A*h?2?v4`FBs;ZC~)VmH$cE z3~BBvc&RDC(8wty0bZuQ%G}D*(mQosRjj@}LuOkWo95#7>kiN!OQWQUgKK+!^~3dk z=i2V4LPz=Hc0T8;^MWl?pC2%x<9k}r3eD3?oHRaazP@`Wx&+h;CRql5oIeTIX(QWBJ}Wu zU13a@`QC4LswZ%Mnfcq#u=SAV1)Y!U?wYUq=5fGEJ}pg=@7!<2#-l5x^=9$SinjN^ z@8bMz*MJRT!+|+L3);D`Pd!9;uTCc*? z;?B>vE^hRzYFPZ^LP@>#nwf7+Ze(n2b7x;8bGq`2<*ohy6>C=gxT5hNNt8TBz`t-|o*8JN}y5Nu_`8#hU+r z*suTDFU3gnWYyk^`V;r|P26u+kt}pB?U~SlB}-Vs4E|0HzhD2N@jWN+GN*N`^KD8_ z6x=^O=l|s8Zu4{u(~Xz?TRA(q)@G;uk$-;{E=>9{BQ}A73DE*fU8$9_{ny= z`kLDtUb27x?S4G3>cjKu_=vhjdoOAwv za;dD~rCoJ^$z7oq6ZE_+@4+aAtq^?;pGM z{9kY5b@V@6y>4HV@Z;cg-PzY`c6RTc`{4O;+vZPC*X^BV-FMTAFH&ZXPW`tFwdQv} z9X8w@VesSKTk}m}Tye$=mn<>S(klA;YU-hY9jyG%X7$Nf)}2&+ohf{7&mN{?v&pOT zGcxx6U+(X-&ObT7d@c8#Ne8di{Qr{i(|!5uUfu=p0SM3-gbtJP!h+`y*I4b{*Z*lB zY?b)CM@mZ{Sjg_&QP5blq{UyX{0BtS42#AiS6`nv`Mmy5Y`OE}nX>P-LgH5E?W+AK z8h!iqYrlOze~I?nIu?HDJJz>-DnA&$5@+?=}wJF>}YOV_Um!Md_7Se@>Ow6xsK7*XfA+ zbLHPlPZv&ptX}uiKy#&s`O-P+Do5hCf4#(C{?5>BbI|6RpFx|>oHRUg&~us5eG#s7 z>zYq~C04eqTbBmCyVm{TsaAzo=nD0p_UrdKFDwvR@uF+R$vXb^zTbD|XA7PEpAz?X zeVNG3_f7`Ih;nlSHz>+h@w|KT#>T?p#53QA`KNf7|F94jNauX~<;vQ7E?iS%*9cFB zj6N0EFj>$4`?bk?n~1E*l}#3C1J>7ezdma3bN3Ll^W>Supnl}rv!ZiledXruv-`Kq zc|x#>e7ya-9mn73Pd+s5|DEHh*VptEOtp;PX;b3m{BVo2TJMcO)xNObY6Uw=?pm42 z|N3&);EL?Je?=iTr%lM`?&xlMdBs)qm}S1+lWj+2K0n<*q1)*Dw%K(K2Y*#osT5e= z{i5}MOB?5#%V%eEZ@!k=>r?kte!AP5NAFlf-s)=pSh3^h6h*IduV!CQ?Th$S=M+{{ z`L60MhuFT8YztR!d$-g3`nvxg{TGSpF1>4XviVT();GImUtv5b|Myd%w9Kt)aeLnB zM{@TBF8TV}s_@a5Yiq-UW{SMpRCqY*myWjf_IUg6U~x2eo1E!&@QBszcJue00j6Hk5n##dghntNSyQ{Zae`z{mb@#)9t z{5oHMuOL{fQvI0Of2=dF%nHBxE&l(G zgmXM7)!<1b10k;EwMH*>zuaE`{GUM5%||L16y-I0uIW$Z31hZs;hdkXra*a=rcJ|L=q97IeIAdzWhM zt~%A2`F_Q^UzOP}S6nIDw?6k!L)+bsyN;1*lHq+t7_6=@x3@2yCV%KGzkdhyMt^0i+y-|qc+quzdR{eAa! z>ptDL`MEjX?qjvI&Hc*bykel+OWf?bVuOQ)Wn^AFJ$lz)XrG5?`?1@ZMzjC_{jBZu zc;z(aKc=s*OV=%&A>I8h_l6kP`HmpFa(}CxYja-g`pUmp%VH*Gi-favc8SZw{8gfQ z6D>LyJyGjj2+2qcjgsmcK6l3NU}tO5^U$>^DR||zcta{5&kN59Cy%)DKjHc=`P|^Q z`h)e)c9dpXwk^8;|K8j!R!29OKX^31OW51>*fjo^Zu=*9PrlN-y5`XS6$aZw4rXqO z-+4mYlru&o;>#zmuOE0msifXJ=k!=@*~&j_;;y88x?nux^W`boRo15p&lFdjKlANk zQZGtrob#qV&_$ z-|zl?yls7Qd3i{R_eqb2IwAL&Ti5;jy-|MS&b>dc@y%BEyqGoD&%M<3-Jf@5{QU2p zpINx`UjBW3{pbBECfiH)eE;WuJ%9hVRodJ4e7nUbmAB{I_4SXL zj`Q;NPS4xtdE@)}`l_O{ucqqEpZk)(wz%Zvq@ybOr#)=z9?9g($jP~VYUNh`8v0|| zOYvE=W}TmNa*NHoojK8)c0Om{{qOC+)F(SP`tJVvQg~YXf@{C)DsF$@U(Rm4`~Sao zep$B{H9lzu&=w(R+*+9B7)NU`|GJVN7gppH3g<0=OlUJSMmjf~GJ10Dn(yI~ThUol z-^HxT&+u_eov1i%;U~VOP7jy7d1o}y-KKQ&tgTA}>q?z{YG;3YU+wl(;s4)zKY5OB z>{hP4X(RLV!@Ise4_#XNR{6cpD0SeP-g`Q3*3-Dt^}AZ0+$#RMcBZmneCUpaI?j)5 zFYf%WqsE+WzTA7>^G8*CbN9ODcP2N*ZH&p79oLnxb6eFLt|Gs4LPqO;REe}FvfpX{ z`Z>hC=yQ&4?&IH+9!!7ZJwLX|)>zf=PQjN&U(MuwH_GO1 zzA}?#+JSQ>!arkb&+ok%Uvn$Ybkh^o>G4Np&aJel$mqDwY`?esaDnaQrlv*A@$=_! zTs?Eb{LZgCHwp_jh3t@;_ebI8 zjsw3`&hf5;bi6_2fsLF!vRvhFz9vK0VJIXRH$Ixf%($@APsr27XIFheO}>A7$g+gO zf=`i?%3WSWuI-$&|3}fnW2dbPzCQcy;j9|wSMxPLakBDB?nQeK|Fzv$Ug?w_9-rRt z_w13(gR`#5FRxy?_j`YQhx3^=nVanN-<^p4+bt?0zVGRYt)(}=7V*!_w-Gm8ruC5h zoqTCij4kids7w3z8`PAp*yk?4<-~-U|E?zAmpH%tV~%lf-u=qZ^5F9c{|{U^aN&6L z_Yc#i1n&DaJ?5JF-Z}et->v`S%67TrT3OxS#}hA!T|0mIZ2RKy`gM&z6X)yL-3u}j zlGp#cr06C8&MCLg*6&;LD#cWg!-x05a>XqozcNqdfAashd^UTC|C~@B{d?h6uTFk; zl75}4#l7#-vD>?%W=Ws+{#sl6>VSf;g}C}!ZK35iqTgTc(^>hYeMN*>Zq?6t`#(ER zs^;9^Huc`3W%K6E6J5z5ZjG&Bq}zhCk6_WQbHv-;%zT{(O9!&%i{r-!H3zq8){w_#Pql9CvK%udH$aAdE3)3x8?5cYCYsx_jKy@Ouod#h3b8^fe{fttSKf^?_PU$-f-`~ zxx*y;*~zoXZt?az)xusqatRL5Os$?5r#|cSy~|Vk_XR2hJwl#VObFsA_|I_|Hn-|< zz>F#K_`T~{JI^Gw9&&yweCE-EEnD5!`BQ_mfI>8QhZZ^rc>7q{uRh&qLQ-WetQ z#p<8BUVP}}2d5uB5aGM`_d@uew#kRp+WOy3y7y+g{sv~Rt=~94B&Y^d%7h08LU;b|{AnJO@nmX<=F>C#c@7?#u=L=TvaaU& zbteN}Eqr*VL-l@Z%X&GBTOk*Yn1ndD-D9q8c4{s>S$Z_)#O$tScRio}=xvjZ9?a(c zeE-MGy^lXVW;hmN@Z@Rz-d#_-qE|9?emyPNHt%Pge7#kW(Tl}%A9Wb#n=SkB`+5D@ zoVstH<2!rqS3SSH>S4^kcDYBJw%t7aBqd_9Puh=%Wlu$8ZWdO}wMl*S>e|=bSG` zxo7g${(Gta|K`TW&O#cTwg3L8h_Tz({(9pgv~lfBb^FGdLUY>Z3mTg5+I{GVe)onC z0r&pRR!*;wgbZ&&R`ng&!QtVgyyJ^Z)lQ3El_~>X`0CGJoF`(B?(e^Kd&h>kr@W2- zPTpALvUK@;H3c`VV(Ayxx0;1+F`F8Bvry=4&EFlXbT)7EIkhR{d+Nt~c3M9lJP1fl zKJR|~_3UakwZfm%Vgq?6zsNo;dhgS_s@#|9Q;&W15&NTJ_tjeS$&;_D?@!mnJj(B# zq_vdy`K(p@R&0M$SrmPI&hv`)#b=F^x4Uay`84g(wny8wj^C~gdNbK9|C)=-&Z>$s zGxMi!?q3eGJ66o#|F_RuuI|r8ceT_KiHR|DRW_FC|4_rwum53Ln_7_4d~n-@8u&6;fZ_IAjc-{5%k& zc;W6vhxa=-Ma{lexo1cBJ+soeEq)@u_kCEjw266^$~qn9+uf5stvvt#=A@4*J|}v* z4WDN&n;%&kKlk3JLubFu+0!;BcDLGjNgGQA#<_m~Ppy7${os&VsizK~pPnH5{E&48 zY_R^g!E?~W>vKO#F&>zZg3<%ghpti=J9Y3!* z?Uc1Yc8;D;(IS>#zkjR0V*0y$V#E4l%h&DOw|VjFm+5hHjwKq3fCB=YVk{Iu>+U`* zN%Dk`AUsxbSQI@Y^7--cu0i z%ZATAC#)0x@gz#5d&cYQ4A=OP+|IzH@ZbZ>9GSY-2nB=-kj;Uz56GxDtodMB$C&UF z;;J&A2M7M~U&+eb@4h$g|HJnAHlN?ze^X&N!wQQzKB|l@hn7#$tAT{j8D1tY{$4c= zKeceaFR!DdPDRZ=<+JX#>#7gjej&T6gR}k1vty6`Y-F74>8(9=x!>H*?rvT#t_deo z%_Cx zR@K*EYb|H)wu)L+vGkU?Qrf?$@bL0DXQz-qn|{qY)Rc33b%_0+7njdGoOpP?*4;aL z`yU0h9BOSrnIVM+34=ew)fV-ekMe-2I==z1BW&9Qo2P$aLwZ7e{91-+S=*LCS^o*TtU8q)n?+?5rv0 zfVvcU^oxNh*pXr3hm{Ms=0jwQn;hcv&7+@Rc+l{%>G#*#V_plFtz6e2Yh~@-S~WYh z(}OiF{!gx6U%mL9*3{=Na~9}hc$I;}!k{tG)m2qUeWlO2qEl?_>|b^nUb-nW zgL@jRxVR>ufo+ASmDv?B&eIELl!%J$?fCNd@2+jjiYBvN)BgPF)8)(JzkdBXa=i7q zxX+c@UmqVrUb_s7*f*^QRs`(ud3Wmk?UN@XwTiF4u4P z$w#Y2X#S>qXMJ7s-4^e9Q{y==@vu*EP15str|Ptz?2AY9&)key|Nk$y{@RF%f})~7 z-&y}Zck<-xH*ZdzJo)!>{J-SA>=DTG0?^dkkSr{rIpLAg)Ix7rsY_oozaD<-J6A*I zol(% z{evHPnC$e8*=h!jS*s@OTe@=7i${#f%+e+c73=2LeKIS3^X^^U->>DTv{$`)EuH(J zSr3vDQOE}?SlpZ*hDhEyd9$*hV1n+{riVhOxXPC$>^L*Q!0)S1`{tvv@KvV_OplcW zBxap?13HLhn|XLinMq~EuU|jhrK*E}yQwV6Q>jg_eth)udcjs~XBw1qo_LbC zrDCq1w$p;wZxwqdr#zXiWBlO**R1(nzKuLy)=}mA#K1&l#;)sEuPgJ}3HHEu zUqVd*75N8P+%_HZ)tl7a_WiZ0kUH0t*+?g)=rlGm+OD1ZwobD}A$$in|I9UCE-caA zJ?+W9ee;a(?@7!t5bWLDZCv{P)th-8!e5Y2aD{a{+0_*MB8=m-uClw!+>zjhm0uU` z1)WxCIhOLlB;?-05REx}yLU%bE?w<3b=#vVt0lIU=j7_zuRFsGLCO3xc4 z52eJ-Bp@{sG*d8u%s!*Yl$_prUQr@K3&d?;;OLp`aBbuFt+)JMEkP{WJ8}Bos1+jma}(1_@9ftea=$8U55U*_A;$gHK-U?BZGJXCgUFqx+8M}`^{F9OvIXN|jYWWxK$p3%PvyfXI z5ufnV_CW=U09W~*;x19h+8rNrMy`&_)w|s1n&6gR_z>{=*M1BtH(oC+f67B|40 zQs$=cI%eP8vl5rFq>n~PrG{MNbPk&~ERu-gUSNhEXvLj)W)Ez;FcvR^w&gg3R@^Cm zkbqSANP&&wwS--uhF|OwLq2Gt9Q@XIER@Sh&94rRbOG9!b#YSMI6;GCOBHdaz~JqHcEC z(%uMhl)yVv#B_pdK9W6qS*FaZ^C~wz>p$bpTlrZ+)AzJZc?e$CwDIQa-&J0p`opKs z6Vy}7J=!bc_Wc`gVd#ng14j=wBMu9>#tSQ2x2(9fENhBu(VofO&@>E*nH$`U=~KnO zo7?Q2qte!5TGrtYO@lK8ne^_f1$V!GMo1HWI$@0JTGMVrfBmbR_B ze^-|Gp`o{@rG>?ffVroad0lO^zMbWI`CU#v509@}YJ7Zu;p!6-?Ft6&9638ugx7vq zUHe-mb8p>2&$>p#EsMI-?aalwuCu7=#FZ{SWxZsf)%j1i5;?tgIGqvP@b7lwIp!~r zsN{1#p!h%Zr9pZ+n|KqGrZI=SGo*OAVcxj!Uf#Bbg$EfQPyO=xch6KO-~Zb$7O$G6 zbj6vCQJ}zp>7k)_hO6w`-dlO=+9ticUhs2D<>wtrTFHA7IK8In3&tp3i&2~=xd?Vq ziNgU$P6d&73)IC(T;Ra8giP!&>B4=N_^(LA}md76Za<~(XSI5(_yx_3F`DCUU}3S~dhf(@X6%)TCn0`UP*4e&Q*L6F$U4=oAUtzR z?AoB6mx7lnHn-hc^sGvl@!gTu>8E4v#>S-Nd5L}wjw(15bCBhs1L$;>H&Yx^{uIr! zd^`R2#URxfCx?htUa|{<@-wqno;-U;HK(g5UQ%f0F_X2uOE<8n#wh*~Kk_V}{XHYR zmwg~cO@Xu5KxOt0i=8K9@3!cyII%MB`swUBbLO z-nA3$LgQjOD#i9`@Y>j=?kz|HPlEGEr*VB%S%0r)en9Yy&%Uv)+X9#+S)N%mF0cEp zBUth0qPt9U^X^mi4?n%#GHuS9y=O~V7rFjyKhy6O;rIQR$Nh-#HJlyq-epZaDH^m= zJ2OkWxJyqiAvOV&TGal%6{uw_gsi}LaDzqWP5jip+D*%fOO+QEgzucbdGqN#3l-h^F)G(R-+y_? zvV!j)mK|PqZrT~;eChc+PPBv?&78ftyJyphr$z7hOrG{XpMN_yhm)7%j4u=4H}}&` zJ#V$P+UNFGe>$N3%r7J3%cPEoy!EmFu7<>iP1swW;IOBgL8sD1r9o(&z{9Tzf44i| z;D+obYG4*F*iav8^ zEG#W2niyQLKO20c0FqFOyBfN?L$<_~iC?=Ks(I=B55=d=?Z=l~Hqq)j|7_mhJqy*= zulwXwoxQidz~*V`uARS4Bt;qXvsY!T)6ZL@yF%}DQQ5T0Rm@Z5?$z$S^{%f?GM-P3 zfoXH&fi`cmizfSSZO=FJNoy)D2wkyAC~Si8s^%+K`!2}kWm+@2iDx1=>0+k~l8j16F)qa2Ne#t*(iIJXv z%!fr)njxmH3mPEN$iOt&=Rn`4L&4Ky-@kn7{`!B~_4rniOJR3jJbY-uer3^@&CNs4``S0pWf#)ywtjSiwhvZl0E7pxgygpAf`xBQUHY;)~F(+%=aoS9=?`W`Dota&g^^FVP%VPtH+jKI&QC) z`?DkeMG-?Vn=wlyE7+%lgsPn@$i>ctzK{OolNsu~ZuX8zcI_=hzI zFSyQVNER-bQ6FmcEpyhttG!;o%;e@Bx*wuF=g~usCh5bCY~Ttkp-*^0ioX(Q!-~uk zu6565nCgdpUAn*i$wZy(s)KAS4hi3cJ3Ql#t3^hZ9D0&dT5DVKykCy%B9EediaMPGmP_Xn+yIX4ac~iFtSod&- z%7uGDt4p45yR*Q3^UsXwr>v*EopbZ<)l=WQJmc=Ke!E7?@@%$;hu{8LvfE6iaD4>@ zu}nile1Oi{Mav#7+PSr4+toRLer?@%&u=o@QA_q0>TKDqsv45}h76 zy6VIZp1u3R*S}u;>%qGFX?@?new`vN_gq)8fq}z

aivX^!Yo%lPPszMUNUua$B{ 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 diff --git a/doc/image/vscode.png b/doc/image/vscode.png deleted file mode 100644 index e3ffacb54ac4845d0abbc0de51405e50caa844b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48523 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 Wxu&MVz^VdJqa2`Q3WdLlv@8Q&%e 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 0; i-- { + if p.out[i] != ' ' && p.out[i] != '\t' { + break + } + n++ + } + p.out = p.out[:len(p.out)-n] + return n +} + +func (p *printer) run(d Doc) (string, error) { + commands := []command{{indentation: indentation{}, mode: modeBreak, doc: d}} + newLine := p.o.NewLine + + for len(commands) > 0 { + last := len(commands) - 1 + cmd := commands[last] + commands = commands[:last] + + switch v := cmd.doc.(type) { + case Text: + if v != "" { + s := string(v) + p.write(s) + if len(commands) > 0 { + p.position += stringWidth(s) + } + } + + case Concat: + for i := len(v) - 1; i >= 0; i-- { + commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: v[i]}) + } + + case *indent: + commands = append(commands, command{indentation: cmd.indentation.add(p.o), mode: cmd.mode, doc: v.doc}) + + case *align: + commands = append(commands, command{indentation: cmd.indentation.align(v.n, p.o), mode: cmd.mode, doc: v.doc}) + + case trim: + p.position -= p.trim() + + case *group: + { + gcmd := p.printGroup(cmd, v, commands) + commands = append(commands, gcmd) + if v.id != 0 { + p.groupMode[v.id] = gcmd.mode + } + } + + case *ifBreak: + groupMode := cmd.mode + if v.groupID != 0 { + if m, ok := p.groupMode[v.groupID]; ok { + groupMode = m + } else { + groupMode = modeFlat + } + } + var contents Doc + if groupMode == modeBreak { + contents = v.breakDoc + } else { + contents = v.flatDoc + } + if contents != nil { + commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: contents}) + } + + case *lineSuffix: + p.lineSuffix = append(p.lineSuffix, command{indentation: cmd.indentation, mode: cmd.mode, doc: v.doc}) + + case lineSuffixBoundary: + if len(p.lineSuffix) > 0 { + commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: HardLineNoBreak}) + } + + case LineDoc: + switch cmd.mode { + case modeFlat: + if !v.Hard { + if !v.Soft { + p.write(" ") + p.position++ + } + break + } + // A hard line printed in flat mode invalidates earlier + // measurements of enclosing groups; the next group must + // remeasure. + p.shouldRemeasure = true + fallthrough + + case modeBreak: + if len(p.lineSuffix) > 0 { + // Print the pending end-of-line suffixes before the + // newline, then the line itself. + commands = append(commands, cmd) + for i := len(p.lineSuffix) - 1; i >= 0; i-- { + commands = append(commands, p.lineSuffix[i]) + } + p.lineSuffix = nil + break + } + if v.Literal { + p.write(newLine) + p.position = 0 + } else { + p.trim() + p.write(newLine + cmd.indentation.value) + p.position = cmd.indentation.length + } + } + + case breakParent: + // Handled by propagateBreaks before printing. + + default: + return "", fmt.Errorf("doc: cannot print unknown document type %T", cmd.doc) + } + + // Flush remaining suffixes at the end of the document, in case there + // is no line break after them. + if len(commands) == 0 && len(p.lineSuffix) > 0 { + for i := len(p.lineSuffix) - 1; i >= 0; i-- { + commands = append(commands, p.lineSuffix[i]) + } + p.lineSuffix = nil + } + } + + return string(p.out), nil +} + +// printGroup decides whether g fits in the remaining width and returns the +// command to print it. With expanded states it tries each state in order. +func (p *printer) printGroup(cmd command, g *group, rest []command) command { + if cmd.mode == modeFlat && !p.shouldRemeasure { + m := modeFlat + if g.brk { + m = modeBreak + } + return command{indentation: cmd.indentation, mode: m, doc: g.doc} + } + + p.shouldRemeasure = false + remainingWidth := p.o.PrintWidth - p.position + hasLineSuffix := len(p.lineSuffix) > 0 + + flat := command{indentation: cmd.indentation, mode: modeFlat, doc: g.doc} + if !g.brk && p.fits(flat, rest, remainingWidth, hasLineSuffix) { + return flat + } + + if g.expanded == nil { + return command{indentation: cmd.indentation, mode: modeBreak, doc: g.doc} + } + + if !g.brk { + for i := 1; i < len(g.expanded)-1; i++ { + candidate := command{indentation: cmd.indentation, mode: modeFlat, doc: g.expanded[i]} + if p.fits(candidate, rest, remainingWidth, hasLineSuffix) { + return candidate + } + } + } + + return command{indentation: cmd.indentation, mode: modeBreak, doc: g.expanded[len(g.expanded)-1]} +} + +// fits reports whether next, followed by the pending rest commands, fits in +// the remaining width. It mirrors Prettier's lookahead: groups are assumed +// flat unless forced, hard lines and line breaks reset the width, and +// line suffixes mark the line as already occupied. +func (p *printer) fits(next command, rest []command, remainingWidth int, hasLineSuffix bool) bool { + if remainingWidth == math.MaxInt { + return true + } + + restIndex := len(rest) + hasPendingSpace := false + // output is only used for width counting because trim needs to look + // backwards for spaces. + var output strings.Builder + commands := []command{next} + + for remainingWidth >= 0 { + if p.fitsDBG { + fmt.Printf(" fitloop rem=%d cmds=%d\n", remainingWidth, len(commands)) + } + if len(commands) == 0 { + if restIndex == 0 { + return true + } + restIndex-- + commands = append(commands, rest[restIndex]) + continue + } + + last := len(commands) - 1 + cmd := commands[last] + commands = commands[:last] + + switch v := cmd.doc.(type) { + case Text: + if v != "" { + s := string(v) + if hasPendingSpace { + output.WriteString(" ") + remainingWidth-- + hasPendingSpace = false + } + output.WriteString(s) + remainingWidth -= stringWidth(s) + } + + case Concat: + for i := len(v) - 1; i >= 0; i-- { + commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: v[i]}) + } + + case *indent, *align: + commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: contentsOf(v)}) + + case trim: + remainingWidth += trimTrailingWidth(output.String()) + output.Reset() + + case *group: + if v.brk && cmd.mode == modeFlat { + return false + } + groupMode := cmd.mode + if v.brk { + groupMode = modeBreak + } + contents := v.doc + if v.expanded != nil && groupMode == modeBreak { + contents = v.expanded[len(v.expanded)-1] + } + commands = append(commands, command{indentation: cmd.indentation, mode: groupMode, doc: contents}) + + case *ifBreak: + groupMode := cmd.mode + if v.groupID != 0 { + if m, ok := p.groupMode[v.groupID]; ok { + groupMode = m + } else { + groupMode = modeFlat + } + } + var contents Doc + if groupMode == modeBreak { + contents = v.breakDoc + } else { + contents = v.flatDoc + } + if contents != nil { + commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: contents}) + } + + case LineDoc: + if cmd.mode == modeBreak || v.Hard { + return true + } + if !v.Soft { + hasPendingSpace = true + } + + case *lineSuffix: + hasLineSuffix = true + + case lineSuffixBoundary: + if hasLineSuffix { + return false + } + } + } + return false +} + +func contentsOf(d Doc) Doc { + switch v := d.(type) { + case *indent: + return v.doc + case *align: + return v.doc + } + return nil +} + +func trimTrailingWidth(s string) int { + n := 0 + for i := len(s) - 1; i >= 0; i-- { + if s[i] != ' ' && s[i] != '\t' { + break + } + n++ + } + return n +} + +// propagateBreaks marks groups containing a BreakParent as broken, and +// propagates breaks upward through enclosing groups. Groups are mutated in +// place, matching Prettier's traversal. +func propagateBreaks(d Doc) { + visited := map[*group]bool{} + var stack []*group + + enter := func(d Doc) bool { + switch v := d.(type) { + case breakParent: + breakParentGroup(stack) + case *group: + stack = append(stack, v) + if visited[v] { + return false + } + visited[v] = true + } + return true + } + exit := func(d Doc) { + if v, ok := d.(*group); ok { + stack = stack[:len(stack)-1] + if v.brk { + breakParentGroup(stack) + } + } + } + traverseDoc(d, enter, exit, true) +} + +func breakParentGroup(stack []*group) { + if len(stack) > 0 { + stack[len(stack)-1].brk = true + } +} + +func traverseDoc(d Doc, enter func(Doc) bool, exit func(Doc), includeConditionalGroups bool) { + if d == nil || !enter(d) { + return + } + switch v := d.(type) { + case Concat: + for _, part := range v { + traverseDoc(part, enter, exit, includeConditionalGroups) + } + case *group: + if includeConditionalGroups { + for _, state := range v.expanded { + traverseDoc(state, enter, exit, includeConditionalGroups) + } + } + traverseDoc(v.doc, enter, exit, includeConditionalGroups) + case *indent: + traverseDoc(v.doc, enter, exit, includeConditionalGroups) + case *align: + traverseDoc(v.doc, enter, exit, includeConditionalGroups) + case *ifBreak: + traverseDoc(v.breakDoc, enter, exit, includeConditionalGroups) + traverseDoc(v.flatDoc, enter, exit, includeConditionalGroups) + case *lineSuffix: + traverseDoc(v.doc, enter, exit, includeConditionalGroups) + } + exit(d) +} diff --git a/doc/print_fuzz_test.go b/doc/print_fuzz_test.go new file mode 100644 index 0000000..d7c9e18 --- /dev/null +++ b/doc/print_fuzz_test.go @@ -0,0 +1,187 @@ +package doc + +import ( + "testing" +) + +// FuzzPrint checks printer invariants over arbitrary documents built from a +// small bytecode grammar: +// +// - printing never panics or loops forever, for any document shape +// - Print only errors on structurally invalid documents (never for the +// generated ones) +// - printing the same document bytes twice is deterministic +// +// The generated documents exercise every IR node, nested arbitrarily deep, +// including mutually-recursive shapes (ConditionalGroup states contain +// groups) and documents with no group at all. +func FuzzPrint(f *testing.F) { + seeds := [][]byte{ + {}, + {opText, 3, 'a', 'b', 'c'}, + {opGroup, opText, 3, 'a', 'b', 'c'}, + {opGroup, opConcat, 3, opText, 1, 'a', opLine, opText, 1, 'b'}, + {opGroup, opText, 6, 'a', 'a', 'a', 'a', opLine, opText, 1, 'b'}, + {opConcat, 3, opText, 1, 'a', opLineSuffix, opText, 4, ' ', '/', '/', 'c', opLine}, + {opConditional, 3, opConcat, 3, opText, 1, 'a', opLine, opText, 1, 'b', opConcat, 2, opText, 1, 'a', opHardLine}, + {opGroup, opText, 6, 'a', 'b', 'c', 'd', opHardLine, opText, 1, 'e'}, + } + for _, seed := range seeds { + f.Add(seed) + } + + f.Fuzz(func(t *testing.T, program []byte) { + build := func() Doc { + d, ok := buildDoc(program, 0) + if !ok { + t.Skip("program exhausted") + } + return d + } + + opts := Options{PrintWidth: 1 + len(program)%80, Indent: " ", TabWidth: 2, NewLine: "\n"} + first, err := Print(build(), opts) + if err != nil { + t.Fatalf("Print: %v", err) + } + second, err := Print(build(), opts) + if err != nil { + t.Fatalf("Print (second): %v", err) + } + if first != second { + t.Fatalf("Print is not deterministic:\n%q\n%q", first, second) + } + }) +} + +// Bytecode operations for the doc generator. +const ( + opText byte = iota + opLine + opSoftLine + opHardLine + opGroup + opGroupBreak + opConcat + opIndent + opAlign + opIfBreak + opLineSuffix + opConditional + opTrim + opBreakParent + opLineSuffixBoundary +) + +const maxDepth = 6 + +// textChars map program bytes to characters spanning the width classes: +// ASCII, wide (CJK), zero-width (combining), and control. +var textChars = []rune{'a', 'b', ' ', 'x', '日', '本', '😀', '\u0301', '\t', 'e', '0'} + +// buildDoc builds a document from the bytecode program, returning the doc +// and whether the program was fully consumed. +func buildDoc(program []byte, depth int) (Doc, bool) { + if depth > maxDepth || len(program) == 0 { + return Concat{}, false + } + + switch op := program[0]; op { + case opText: + n := int(program[1%len(program)]) % 8 + text := make([]rune, 0, n) + for i := range n { + text = append(text, textChars[int(program[(2+i)%len(program)])%len(textChars)]) + } + return Text(string(text)), true + + case opLine, opSoftLine, opHardLine: + switch op { + case opLine: + return Line, true + case opSoftLine: + return SoftLine, true + default: + return HardLine, true + } + + case opGroup, opGroupBreak: + inner, ok := buildDoc(program[1:], depth+1) + if !ok { + return Concat{}, false + } + if op == opGroupBreak { + return GroupBreak(inner), true + } + return Group(inner), true + + case opConcat: + n := int(program[1%len(program)]) % 5 + parts := make([]Doc, 0, n) + offset := 2 + for range n { + if offset >= len(program) { + return Concat(parts), true + } + part, ok := buildDoc(program[offset:], depth+1) + if !ok { + return Concat(parts), true + } + parts = append(parts, part) + offset++ + } + return Concat(parts), true + + case opIndent: + inner, ok := buildDoc(program[1:], depth+1) + if !ok { + return Concat{}, false + } + return Indent(inner), true + + case opAlign: + inner, ok := buildDoc(program[1:], depth+1) + if !ok { + return Concat{}, false + } + return Align(int(program[1%len(program)])%5, inner), true + + case opIfBreak: + brk, ok1 := buildDoc(program[1:], depth+1) + flat, ok2 := buildDoc(program[2%len(program):], depth+1) + if !ok1 || !ok2 { + return Concat{}, false + } + return IfBreak(brk, flat), true + + case opLineSuffix: + inner, ok := buildDoc(program[1:], depth+1) + if !ok { + return Concat{}, false + } + return LineSuffix(inner), true + + case opConditional: + first, ok := buildDoc(program[1:], depth+1) + if !ok { + return Concat{}, false + } + second, ok := buildDoc(program[2%len(program):], depth+1) + if !ok { + return Concat{}, false + } + return ConditionalGroup(0, first, second, GroupBreak(first)), true + + case opTrim: + return TrimDoc, true + + case opBreakParent: + return BreakParent, true + + case opLineSuffixBoundary: + return LineSuffixBoundary, true + + default: + return Concat{}, false + } +} diff --git a/doc/print_test.go b/doc/print_test.go new file mode 100644 index 0000000..7597bc4 --- /dev/null +++ b/doc/print_test.go @@ -0,0 +1,541 @@ +package doc + +import ( + "testing" +) + +// T builds a Text from a string. +func T(s string) Doc { return Text(s) } + +// testOpts are the defaults used by most cases. +func testOpts(width int) Options { + return Options{PrintWidth: width, Indent: " ", TabWidth: 2, NewLine: "\n"} +} + +func printT(t *testing.T, d Doc, o Options) string { + t.Helper() + got, err := Print(d, o) + if err != nil { + t.Fatalf("Print: %v", err) + } + return got +} + +func TestPrintText(t *testing.T) { + tests := []struct { + name string + doc func() Doc + want string + }{ + {"empty", func() Doc { return Concat{} }, ""}, + {"text", func() Doc { return T("hello world") }, "hello world"}, + {"concat", func() Doc { return Concat{T("a"), T("b"), T("c")} }, "abc"}, + {"join", func() Doc { return Join(T(","), []Doc{T("a"), T("b"), T("c")}) }, "a,b,c"}, + {"join empty", func() Doc { return Join(T(","), nil) }, ""}, + {"group fits", func() Doc { return Group(Concat{T("hello"), T(" world")}) }, "hello world"}, + {"empty group", func() Doc { return Group(Concat{}) }, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := printT(t, tt.doc(), testOpts(80)); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestPrintLineBreaking(t *testing.T) { + tests := []struct { + name string + doc func() Doc + width int + want string + }{ + { + name: "line stays flat when it fits", + doc: func() Doc { return Group(Concat{T("a"), Line, T("b")}) }, + width: 10, + want: "a b", + }, + { + name: "line breaks when it does not fit", + doc: func() Doc { return Group(Concat{T("a"), Line, T("b")}) }, + width: 2, + want: "a\nb", + }, + { + name: "line breaks exactly at boundary", + doc: func() Doc { return Group(Concat{T("ab"), Line, T("c")}) }, + width: 3, + want: "ab\nc", // "ab c" is 4 columns and does not fit + }, + { + name: "softline is empty when flat", + doc: func() Doc { return Group(Concat{T("a"), SoftLine, T("b")}) }, + width: 80, + want: "ab", + }, + { + name: "softline breaks with the group", + doc: func() Doc { return Group(Concat{T("a"), SoftLine, T("b")}) }, + width: 1, + want: "a\nb", + }, + { + name: "canonical indent pattern", + doc: func() Doc { return Group(Concat{T("a"), Indent(Concat{Line, T("b")})}) }, + width: 10, + want: "a b", + }, + { + name: "canonical indent pattern breaks", + doc: func() Doc { return Group(Concat{T("a"), Indent(Concat{Line, T("b")})}) }, + width: 2, + want: "a\n b", + }, + { + name: "inner group stays flat inside broken outer", + doc: func() Doc { + return Group(Concat{ + T("a"), + Group(Concat{T("b"), SoftLine, T("c")}), + Line, + T("d"), + }) + }, + width: 4, + want: "abc\nd", // the inner group fits in the remaining width + }, + { + name: "inner group breaks when it does not fit", + doc: func() Doc { + return Group(Concat{ + T("aaaa"), + Group(Concat{T("bbbb"), SoftLine, T("cc")}), + Line, + T("d"), + }) + }, + width: 8, + want: "aaaabbbb\ncc\nd", // only the inner group's own line breaks + }, + { + name: "inner group breaks when remaining width is too small", + doc: func() Doc { + return Group(Concat{ + T("a"), + Group(Concat{T("b"), SoftLine, T("c")}), + Line, + T("d"), + }) + }, + width: 2, + want: "ab\nc\nd", + }, + { + name: "hardline always breaks", + doc: func() Doc { return Group(Concat{T("a"), HardLineNoBreak, T("b")}) }, + width: 80, + want: "a\nb", + }, + { + name: "hardline breaks enclosing group", + doc: func() Doc { return Group(Concat{T("a"), HardLine, T("b"), SoftLine, T("c")}) }, + width: 80, + want: "a\nb\nc", + }, + { + name: "break propagates through nested groups", + doc: func() Doc { + return Group(Concat{ + Group(Concat{T("x"), HardLine, T("y"), SoftLine, T("z")}), + SoftLine, + T("w"), + }) + }, + width: 80, + want: "x\ny\nz\nw", + }, + { + name: "forced break group", + doc: func() Doc { return GroupBreak(Concat{T("a"), SoftLine, T("b")}) }, + width: 80, + want: "a\nb", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := printT(t, tt.doc(), testOpts(tt.width)); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestPrintIfBreak(t *testing.T) { + tests := []struct { + name string + doc func() Doc + width int + want string + }{ + { + name: "flat takes flat contents", + doc: func() Doc { return Group(Concat{T("a"), IfBreak(T(","), T("")), T("b")}) }, + width: 80, + want: "ab", + }, + { + name: "broken takes break contents", + doc: func() Doc { return GroupBreak(Concat{T("a"), IfBreak(T(","), T("")), T("b")}) }, + width: 80, + want: "a,b", + }, + { + name: "ifBreak follows referenced group", + doc: func() Doc { + return Concat{ + GroupID(1, Concat{T("aaaa"), SoftLine, T("bbbb")}), + IfBreakFor(T(","), T(""), 1), + } + }, + width: 80, + want: "aaaabbbb", + }, + { + name: "ifBreak follows broken referenced group", + doc: func() Doc { + return Concat{ + GroupID(1, Concat{T("aaaa"), SoftLine, T("bbbb")}), + IfBreakFor(T(","), T(""), 1), + } + }, + width: 4, + want: "aaaa\nbbbb,", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := printT(t, tt.doc(), testOpts(tt.width)); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestPrintConditionalGroup(t *testing.T) { + tests := []struct { + name string + width int + want string + }{ + { + name: "least expanded state fits", + width: 5, + want: "a b c", + }, + { + name: "middle state fits", + width: 3, + want: "a b\nc", + }, + { + name: "last state breaks", + width: 2, + want: "a\nb\nc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := ConditionalGroup(0, + Concat{T("a"), Line, T("b"), Line, T("c")}, + Concat{T("a"), Line, T("b"), HardLineNoBreak, T("c")}, + Concat{T("a"), HardLineNoBreak, T("b"), HardLineNoBreak, T("c")}, + ) + if got := printT(t, doc, testOpts(tt.width)); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestPrintIndentAndAlign(t *testing.T) { + tests := []struct { + name string + doc func() Doc + want string + }{ + { + name: "indent applies at line breaks", + doc: func() Doc { + return GroupBreak(Concat{T("a"), Indent(Concat{Line, T("b"), Line, T("c")})}) + }, + want: "a\n b\n c", + }, + { + name: "align by columns", + doc: func() Doc { + return GroupBreak(Concat{T("a"), Align(4, Concat{Line, T("b")})}) + }, + want: "a\n b", + }, + { + name: "align nests inside indent", + doc: func() Doc { + return GroupBreak(Concat{T("a"), Indent(Concat{Line, Align(2, Concat{T("b"), Line, T("c")})})}) + }, + want: "a\n b\n c", + }, + { + name: "nested indent accumulates", + doc: func() Doc { + return GroupBreak(Concat{T("a"), Indent(Indent(Concat{Line, T("b")}))}) + }, + want: "a\n b", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := printT(t, tt.doc(), testOpts(80)); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestPrintTabIndent(t *testing.T) { + tests := []struct { + name string + doc func() Doc + want string + }{ + { + name: "tab indentation and measurement", + doc: func() Doc { + return GroupBreak(Concat{T("a"), Indent(Concat{Line, T("b")})}) + }, + want: "a\n\tb", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := Options{PrintWidth: 80, Indent: "\t", TabWidth: 4, NewLine: "\n"} + if got := printT(t, tt.doc(), o); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestPrintLineSuffix(t *testing.T) { + tests := []struct { + name string + doc func() Doc + want string + }{ + { + name: "suffix prints before the line break", + doc: func() Doc { + return GroupBreak(Concat{T("a"), LineSuffix(T(" // c")), Line, T("b")}) + }, + want: "a // c\nb", + }, + { + name: "suffix flushes at document end without a break", + doc: func() Doc { + return Concat{T("a"), LineSuffix(T(" // c"))} + }, + want: "a // c", + }, + { + name: "boundary flushes pending suffixes", + doc: func() Doc { + return GroupBreak(Concat{ + T("a"), LineSuffix(T(" // first")), LineSuffixBoundary, + }) + }, + // The boundary schedules a hard line that flushes the suffix and + // ends the line, matching Prettier's boundary semantics. + want: "a // first\n", + }, + { + name: "suffix counts against width", + doc: func() Doc { + return Group(Concat{T("aaaa"), LineSuffix(T(" // c")), Line, T("b")}) + }, + want: "aaaa b // c", // suffix width is not measured, like Prettier + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := printT(t, tt.doc(), testOpts(80)); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestPrintTrim(t *testing.T) { + tests := []struct { + name string + doc func() Doc + want string + }{ + {"trim removes trailing spaces", func() Doc { return Concat{T("a "), TrimDoc, T("b")} }, "ab"}, + {"trim removes trailing tabs", func() Doc { return Concat{T("a\t\t"), TrimDoc, T("b")} }, "ab"}, + {"trim without trailing whitespace", func() Doc { return Concat{T("a"), TrimDoc, T("b")} }, "ab"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := printT(t, tt.doc(), testOpts(80)); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestPrintRemeasure(t *testing.T) { + // A hard line printed inside a flat-measured group invalidates the + // measurement; the next group must remeasure instead of trusting the + // flat shortcut. + doc := Group(Concat{ + T("a"), HardLineNoBreak, + Group(Concat{T("bbbb"), Line, T("cc")}), + Line, + T("dd"), + }) + got := printT(t, doc, testOpts(8)) + want := "a\nbbbb\ncc dd" + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestPrintNewLineOption(t *testing.T) { + o := Options{PrintWidth: 2, Indent: " ", TabWidth: 2, NewLine: "\r\n"} + doc := GroupBreak(Concat{T("a"), Line, T("b")}) + if got := printT(t, doc, o); got != "a\r\nb" { + t.Errorf("got %q, want %q", got, "a\r\nb") + } +} + +func TestPrintValidation(t *testing.T) { + tests := []struct { + name string + opts Options + }{ + {"zero width", Options{PrintWidth: 0, TabWidth: 2}}, + {"negative width", Options{PrintWidth: -1, TabWidth: 2}}, + {"zero tab width", Options{PrintWidth: 80, TabWidth: 0}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := Print(T("x"), tt.opts); err == nil { + t.Error("expected validation error") + } + }) + } +} + +func TestPrintUnicodeWidth(t *testing.T) { + tests := []struct { + name string + doc func() Doc + width int + want string + }{ + { + name: "wide characters count as two columns", + doc: func() Doc { return Group(Concat{T("日本語"), Line, T("x")}) }, + width: 8, + want: "日本語 x", + }, + { + name: "wide characters break the group", + doc: func() Doc { return Group(Concat{T("日本語"), Line, T("x")}) }, + width: 7, + want: "日本語\nx", + }, + { + name: "combining marks are zero width", + doc: func() Doc { return Group(Concat{T("e\u0301"), Line, T("x")}) }, + width: 3, + want: "e\u0301 x", + }, + { + name: "emoji count as two columns", + doc: func() Doc { return Group(Concat{T("😀"), Line, T("x")}) }, + width: 4, + want: "😀 x", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := printT(t, tt.doc(), testOpts(tt.width)); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestStringWidth(t *testing.T) { + tests := []struct { + in string + want int + }{ + {"", 0}, + {"abc", 3}, + {"日本語", 6}, + {"aé", 2}, + {"e\u0301", 1}, + {"😀", 2}, + {"a\tb", 2}, // tab is a control character + {"\x00\x1f\x7f", 0}, + } + for _, tt := range tests { + if got := stringWidth(tt.in); got != tt.want { + t.Errorf("stringWidth(%q) = %d, want %d", tt.in, got, tt.want) + } + } +} + +func TestPrintIdempotentDocs(t *testing.T) { + // These docs must print identically at the same width twice in a row; + // break propagation must not change the result on the second pass. + docs := []Doc{ + Group(Concat{T("a"), Line, T("b")}), + Group(Concat{T("a"), HardLine, T("b"), SoftLine, T("c")}), + ConditionalGroup(0, + Concat{T("a"), Line, T("b"), Line, T("c")}, + Concat{T("a"), Line, T("b"), HardLineNoBreak, T("c")}, + ), + } + for _, d := range docs { + first := printT(t, d, testOpts(2)) + second := printT(t, d, testOpts(2)) + if first != second { + t.Errorf("not idempotent: %q vs %q", first, second) + } + } +} + +func TestDocMutability(t *testing.T) { + // Print mutates the doc (break propagation); printing a fresh doc each + // time must yield stable output. + build := func() Doc { + return Group(Concat{T("a"), HardLine, T("b"), SoftLine, T("c")}) + } + first := printT(t, build(), testOpts(80)) + for i := range 10 { + if got := printT(t, build(), testOpts(80)); got != first { + t.Fatalf("iteration %d: got %q, want %q", i, got, first) + } + } +} diff --git a/doc/rename.md b/doc/rename.md deleted file mode 100644 index 9932cc6..0000000 --- a/doc/rename.md +++ /dev/null @@ -1,15 +0,0 @@ -# Rename - -rename 是基于 AST + 语义分析实现的 identifier 批量替换功能。 - -以 struct 为例,通过 rename struct name,可以同步批量修改所有引用该 struct name 的地方。 - -在实现上,Rename 依赖 codejump/find references 的能力。通过查找到 struct name 所有引用的地方,然后对指定位置进行替换即可 - -在 LSP 中,rename 有两步: - -1. prepareRename:client 向 server 发送 prepareRename request,server 根据当前光标位置,计算出 rename 的范围。如果不支持 rename,则返回 error。 -2. rename: client 向 server 发送 rename request,这个 request 包含当前的位置以及要变更后的 name,server 根据这两个信息,返回 WorkspaceEdit 响应。 - WorkspaceEdit 响应中包含了当前 workspace 在所有要 rename 的文件和位置. - - diff --git a/doc/width.go b/doc/width.go new file mode 100644 index 0000000..0dde55c --- /dev/null +++ b/doc/width.go @@ -0,0 +1,62 @@ +package doc + +import ( + "unicode" + + "golang.org/x/text/width" +) + +// stringWidth returns the display width of s in monospace columns: wide +// characters (East Asian W/F) count as 2, zero-width characters (combining +// marks, variation selectors) and control characters count as 0. +// +// Width classification uses the stdlib unicode package for categories and +// properties, and golang.org/x/text/width for the East Asian Width classes, +// the same data Prettier measures with. Ambiguous-width characters count as +// narrow, like Prettier. +func stringWidth(s string) int { + // Fast path: pure ASCII without control characters has width equal to + // its byte length. + ascii := true + for i := 0; i < len(s); i++ { + if s[i] >= 0x80 || s[i] <= 0x1f || s[i] == 0x7f { + ascii = false + break + } + } + if ascii { + return len(s) + } + + width := 0 + for _, r := range s { + switch { + case unicode.Is(unicode.Cc, r): + // Control characters take no space. + case isZeroWidth(r): + // Combining marks and variation selectors. + case isWide(r): + width += 2 + default: + width++ + } + } + return width +} + +// isZeroWidth reports whether r renders zero columns wide: combining marks +// (Nonspacing_Mark and Enclosing_Mark, matching Prettier, which excludes +// spacing marks) and variation selectors. +func isZeroWidth(r rune) bool { + return unicode.Is(unicode.Mn, r) || unicode.Is(unicode.Me, r) || unicode.Is(unicode.Variation_Selector, r) +} + +// isWide reports whether r renders two columns wide per the East Asian Width +// classes W and F (golang.org/x/text/width). +func isWide(r rune) bool { + switch width.LookupRune(r).Kind() { + case width.EastAsianWide, width.EastAsianFullwidth: + return true + } + return false +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ee3273a --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1786020313, + "narHash": "sha256-+SRq9fKS489zHzmsBxTKD99TMqST2LnWSM5EsXAzLpU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "addb9f5fb57327a6712a409ba4e17efb7e3ec1c5", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..81935b5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,76 @@ +{ + description = "thriftls: a Thrift language server and formatter"; + inputs = { + nixpkgs.url = "github:nixos/nixpkgs"; + }; + outputs = + { self, nixpkgs }: + let + # Evaluate an expression once per supported system (flakeExposed), + # so every output stays in sync with the same system list. + forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed; + pkgsFor = system: import nixpkgs { inherit system; }; + + # The language server / formatter binary for this module. + thriftls = + pkgs: + pkgs.buildGoModule.override { go = pkgs.go_1_26; } { + pname = "thriftls"; + version = "0.1"; + src = nixpkgs.lib.cleanSource ./.; + vendorHash = "sha256-5xOCOOr6/zc0d8BcTdBApsdnSiQtjpR//Ux4zdorGrw="; + ldflags = [ + "-s" + "-w" + ]; + postInstall = '' + mv "$out/bin/thrift-ls" "$out/bin/thriftls" + ''; + meta = { + description = "A Thrift language server and formatter"; + homepage = "https://github.com/karitham/thrift-ls"; + license = nixpkgs.lib.licenses.asl20; + mainProgram = "thriftls"; + }; + }; + + formatterTools = + pkgs: with pkgs; [ + gofumpt + nixfmt + ]; + in + { + formatter = forAllSystems (system: (pkgsFor system).treefmt); + + packages = forAllSystems ( + system: + let + pkg = thriftls (pkgsFor system); + in + { + thriftls = pkg; + default = pkg; + } + ); + + devShells = forAllSystems ( + system: + let + pkgs = pkgsFor system; + in + { + default = pkgs.mkShell { + packages = + with pkgs; + [ + go + treefmt + golangci-lint + ] + ++ formatterTools pkgs; + }; + } + ); + }; +} diff --git a/format/annotation.go b/format/annotation.go deleted file mode 100644 index f21f2c4..0000000 --- a/format/annotation.go +++ /dev/null @@ -1,55 +0,0 @@ -package format - -import ( - "bytes" - "fmt" - - "github.com/joyme123/thrift-ls/parser" -) - -func MustFormatAnnotations(annotations *parser.Annotations, opts Options) string { - buf := bytes.NewBuffer(nil) - - buf.WriteString(MustFormatKeyword(opts, annotations.LParKeyword.Keyword)) - - var preNode parser.Node - preNode = annotations.LParKeyword - - indent := "" - isNewLine := false - - for i, anno := range annotations.Annotations { - if lineDistance(preNode, annotations.Annotations[i]) >= 1 { - buf.WriteString("\n") - isNewLine = true - indent = opts.GetIndent() + opts.GetIndent() - } - buf.WriteString(MustFormatAnnotation(anno, opts, i == len(annotations.Annotations)-1, i == 0, indent, isNewLine)) - preNode = annotations.Annotations[i] - isNewLine = false - indent = "" - } - - if lineDistance(preNode, annotations.RParKeyword) >= 1 { - buf.WriteString("\n") - buf.WriteString(opts.GetIndent()) - } - buf.WriteString(MustFormatKeyword(opts, annotations.RParKeyword.Keyword)) - - return buf.String() -} - -func MustFormatAnnotation(anno *parser.Annotation, opts Options, isLast bool, isFirst bool, indent string, isNewLine bool) string { - sep := "" - if (!isLast) && anno.ListSeparatorKeyword != nil { - sep = MustFormatKeyword(opts, anno.ListSeparatorKeyword.Keyword) - } - - space := "" - if (!isFirst) && (!isNewLine) { - space = " " - } - - // a = "xxxx", - return fmt.Sprintf("%s%s %s %s%s", space, MustFormatIdentifier(opts, anno.Identifier, indent), MustFormatKeyword(opts, anno.EqualKeyword.Keyword), MustFormatLiteral(opts, anno.Value, ""), sep) -} diff --git a/format/annotation_test.go b/format/annotation_test.go deleted file mode 100644 index 08394d8..0000000 --- a/format/annotation_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package format - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func TestMustFormatAnnotations(t *testing.T) { - - doc := `struct a {}/* xxx */ ( /* xxx */ key /* xxx */ = /* xxx */ "value", key2 = "value2", key3 = "value3" ) /* xxx */` - ast, err := parser.Parse("test.thrift", []byte(doc)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - type args struct { - annotations *parser.Annotations - } - tests := []struct { - name string - args args - want string - }{ - { - name: "normal", - args: args{ - annotations: ast.(*parser.Document).Structs[0].Annotations, - }, - want: `/* xxx */ (/* xxx */ key /* xxx */ = /* xxx */ "value", key2 = "value2", key3 = "value3")`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := Options{} - assert.Equal(t, tt.want, MustFormatAnnotations(tt.args.annotations, opts)) - }) - } -} - -func TestMustFormatAnnotationsInvalidCase(t *testing.T) { - - doc := ` -struct Foo { - // 用户名列表 - 2: list strings ( - custom_tag_1 = "1" - custom_tag_2 = "2" - ) -} -` - ast, err := parser.Parse("test.thrift", []byte(doc)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - type args struct { - annotations *parser.Annotations - } - tests := []struct { - name string - args args - want string - }{ - { - name: "normal", - args: args{ - annotations: ast.(*parser.Document).Structs[0].Fields[0].Annotations, - }, - want: `( - custom_tag_1 = "1" - custom_tag_2 = "2" - )`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := Options{} - assert.Equal(t, tt.want, MustFormatAnnotations(tt.args.annotations, opts)) - }) - } -} diff --git a/format/comment.go b/format/comment.go deleted file mode 100644 index fab669e..0000000 --- a/format/comment.go +++ /dev/null @@ -1,82 +0,0 @@ -package format - -import ( - "bytes" - "strings" - - "github.com/joyme123/thrift-ls/parser" -) - -// TODO(jpf): 多行注释,换行上还需要优化 -// MustFormatComments formats comments -// return string doesn't include '\n' end of line -func MustFormatComments(opts Options, comments []*parser.Comment, indent string, innerIndent string) string { - fmtCtx := &fmtContext{} - buf := bytes.NewBuffer(nil) - for _, c := range comments { - if fmtCtx.preNode != nil { - buf.WriteString("\n") - if lineDistance(fmtCtx.preNode, c) > 1 { - buf.WriteString("\n") - } - } - - buf.WriteString(formatMultiLineComment(c.Text, indent)) - - fmtCtx.preNode = c - } - - return buf.String() -} - -func MustFormatEndLineComments(opts Options, comments []*parser.Comment, indent string, innerIndent string) string { - if len(comments) == 0 { - return "" - } - fmtCtx := &fmtContext{} - buf := bytes.NewBufferString(" ") - for _, c := range comments { - if fmtCtx.preNode != nil { - buf.WriteString("\n") - if lineDistance(fmtCtx.preNode, c) > 1 { - buf.WriteString("\n") - } - } - - buf.WriteString(formatMultiLineComment(c.Text, indent)) - - fmtCtx.preNode = c - } - - return buf.String() -} - -func formatMultiLineComment(comment string, indent string) string { - comment = strings.TrimSpace(comment) - - if strings.HasPrefix(comment, "//") || strings.HasPrefix(comment, "#") { - return indent + comment - } - - lines := strings.Split(comment, "\n") - if len(lines) == 1 { - return indent + comment - } - - buf := bytes.NewBuffer(nil) - for i, line := range lines { - line = strings.TrimSpace(line) - space := "" - if strings.HasPrefix(line, "*") { - space = " " - } - if i == 0 { - buf.WriteString(indent + space + line) - } else { - buf.WriteString("\n" + indent + space + line) - } - } - - return buf.String() - -} diff --git a/format/comment_test.go b/format/comment_test.go deleted file mode 100644 index 8adc5af..0000000 --- a/format/comment_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package format - -import ( - "strings" - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -/* - * multi line comments - */ -func TestMustFormatComments(t *testing.T) { - doc := strings.TrimSpace(` -/* - * aaaaa - * aaaaa - * aaaaa - * aaaaa - */ - - -// aaaaa - - - -// aaaaa - -include "a.thrift" // aaaaa - -// endline comments`) - ast, err := parser.Parse("test.thrift", []byte(doc)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - type args struct { - comments []*parser.Comment - indent string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "comments", - args: args{ - comments: ast.(*parser.Document).Includes[0].Comments, - indent: " ", - }, - want: strings.TrimSpace(` -/* - * aaaaa - * aaaaa - * aaaaa - * aaaaa - */ - -// aaaaa - -// aaaaa`), - }, - { - name: "endline comments", - args: args{ - comments: ast.(*parser.Document).Includes[0].EndLineComments, - }, - want: `// aaaaa`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // For comments test, pass empty indent to match expected output format - assert.Equal(t, tt.want, MustFormatComments(Options{}, tt.args.comments, "", "")) - }) - } -} diff --git a/format/const.go b/format/const.go deleted file mode 100644 index a5834b6..0000000 --- a/format/const.go +++ /dev/null @@ -1,46 +0,0 @@ -package format - -import ( - "github.com/joyme123/thrift-ls/parser" -) - -const constOneLineTpl = `{{.Comments}}{{.Const}} {{.Type}} {{.Name}} {{.Equal}} {{.Value}}{{.Annotations}}{{.ListSeparator}}{{.EndLineComments}} -` - -type ConstFormatter struct { - Comments string - Const string - Type string - Name string - Annotations string - Equal string - Value string - ListSeparator string - EndLineComments string -} - -func MustFormatConst(cst *parser.Const, opts Options) string { - comments, annos := formatCommentsAndAnnos(opts, cst.Comments, cst.Annotations, "") - if len(cst.Comments) > 0 && lineDistance(cst.Comments[len(cst.Comments)-1], cst.ConstKeyword) > 1 { - comments = comments + "\n" - } - - sep := "" - if cst.ListSeparatorKeyword != nil { - sep = MustFormatKeyword(opts, cst.ListSeparatorKeyword.Keyword) - } - - f := &ConstFormatter{ - Comments: comments, - Const: MustFormatKeyword(opts, cst.ConstKeyword.Keyword), - Type: MustFormatFieldType(cst.ConstType, opts), - Name: MustFormatIdentifier(opts, cst.Name, ""), - Annotations: annos, - Equal: MustFormatKeyword(opts, cst.EqualKeyword.Keyword), - Value: MustFormatConstValue(cst.Value, opts, "", false), - ListSeparator: sep, - EndLineComments: MustFormatEndLineComments(opts, cst.EndLineComments, "", ""), - } - - return MustFormat(constOneLineTpl, f) -} diff --git a/format/constvalue.go b/format/constvalue.go deleted file mode 100644 index 9d03d61..0000000 --- a/format/constvalue.go +++ /dev/null @@ -1,172 +0,0 @@ -package format - -import ( - "bytes" - "fmt" - - "github.com/joyme123/thrift-ls/parser" -) - -func MustFormatConstValue(cv *parser.ConstValue, opts Options, indent string, newLine bool) string { - buf := bytes.NewBuffer(nil) - if len(cv.Comments) > 0 { - buf.WriteString(MustFormatComments(opts, cv.Comments, indent, "")) - } - sep := "" - if cv.ListSeparatorKeyword != nil { - sep = MustFormatKeyword(opts, cv.ListSeparatorKeyword.Keyword) + " " - } - - switch cv.TypeName { - case "list": - values := cv.Value.([]*parser.ConstValue) - - if len(cv.Comments) > 0 && len(values) > 0 { - if lineDistance(cv.Comments[len(cv.Comments)-1], values[0]) >= 1 { - buf.WriteString("\n") - } - } - - buf.WriteString(MustFormatKeyword(opts, cv.LBrkKeyword.Keyword)) - - if len(values) == 1 { - // Single item: format on one line - item := *values[0] - item.ListSeparatorKeyword = nil - buf.WriteString(MustFormatConstValue(&item, opts, indent, false)) - buf.WriteString(MustFormatKeyword(opts, cv.RBrkKeyword.Keyword)) - } else { - // Multiple items: format on multiple lines - buf.WriteString("\n") - itemIndent := indent + opts.GetIndent() - for i := range values { - item := *values[i] - item.ListSeparatorKeyword = nil - buf.WriteString(MustFormatConstValue(&item, opts, itemIndent, true)) - buf.WriteString(",\n") - } - buf.WriteString(indent) - buf.WriteString(MustFormatKeyword(opts, cv.RBrkKeyword.Keyword)) - } - case "map": - values := cv.Value.([]*parser.ConstValue) - - if len(cv.Comments) > 0 && len(values) > 0 { - if lineDistance(cv.Comments[len(cv.Comments)-1], values[0]) >= 1 { - buf.WriteString("\n") - } - } - - var preNode parser.Node - buf.WriteString(MustFormatKeyword(opts, cv.LCurKeyword.Keyword)) - preNode = cv.LCurKeyword - for i := range values { - distance := lineDistance(preNode, values[i]) - if distance >= 1 { - buf.WriteString("\n") - newLine = true - } else { - if i > 0 { - buf.WriteString(" ") - } - newLine = false - } - buf.WriteString(MustFormatConstValue(values[i], opts, indent, newLine)) - preNode = values[i] - } - if lineDistance(preNode, cv.RCurKeyword) >= 1 { - buf.WriteString("\n") - buf.WriteString(indent) - } - buf.WriteString(MustFormatKeyword(opts, cv.RCurKeyword.Keyword)) - case "pair": - key := cv.Key.(*parser.ConstValue) - value := cv.Value.(*parser.ConstValue) - - if len(cv.Comments) > 0 { - if lineDistance(cv.Comments[len(cv.Comments)-1], key) >= 1 { - buf.WriteString("\n") - } - } - - if cv.ListSeparatorKeyword != nil { - sep = MustFormatKeyword(opts, cv.ListSeparatorKeyword.Keyword) - } - buf.WriteString(fmt.Sprintf("%s%s %s%s", - MustFormatConstValue(key, opts, indent+opts.GetIndent(), newLine), - MustFormatKeyword(opts, cv.ColonKeyword.Keyword), - MustFormatConstValue(value, opts, indent, false), - sep)) - case "identifier": - if len(cv.Comments) > 0 { - // special case for iline distance - if lineDistance(cv.Comments[len(cv.Comments)-1], cv) >= 1 { - buf.WriteString("\n") - newLine = true - } - } - - if newLine { - buf.WriteString(indent) - } - - buf.WriteString(fmt.Sprintf("%s%s", cv.Value.(string), sep)) - case "string": - val := "" - if _, ok := cv.Value.(string); ok { - if len(cv.Comments) > 0 { - if lineDistance(cv.Comments[len(cv.Comments)-1], cv) >= 1 { - buf.WriteString("\n") - newLine = true - } - } - - if newLine { - buf.WriteString(indent) - } - val = cv.Value.(string) - buf.WriteString(fmt.Sprintf("%q%s", val, sep)) - } else { - literal := cv.Value.(*parser.Literal) - if len(cv.Comments) > 0 { - if lineDistance(cv.Comments[len(cv.Comments)-1], literal) >= 1 { - buf.WriteString("\n") - newLine = true - } - } - - if !newLine { - indent = "" - } - - val = MustFormatLiteral(opts, literal, indent) - buf.WriteString(fmt.Sprintf("%s%s", val, sep)) - } - case "i64": - if len(cv.Comments) > 0 { - if lineDistance(cv.Comments[len(cv.Comments)-1], cv) >= 1 { - buf.WriteString("\n") - newLine = true - } - } - - if newLine { - buf.WriteString(indent) - } - buf.WriteString(fmt.Sprintf("%s%s", cv.ValueInText, sep)) - case "double": - if len(cv.Comments) > 0 { - if lineDistance(cv.Comments[len(cv.Comments)-1], cv) >= 1 { - buf.WriteString("\n") - newLine = true - } - } - - if newLine { - buf.WriteString(indent) - } - buf.WriteString(fmt.Sprintf("%s%s", cv.ValueInText, sep)) - } - - return buf.String() -} diff --git a/format/constvalue_test.go b/format/constvalue_test.go deleted file mode 100644 index d22c08e..0000000 --- a/format/constvalue_test.go +++ /dev/null @@ -1,234 +0,0 @@ -package format - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func TestMustFormatConstValue(t *testing.T) { - type args struct { - cv *parser.ConstValue - } - tests := []struct { - name string - args args - want string - }{ - { - name: "map", - args: args{ - cv: &parser.ConstValue{ - TypeName: "map", - LCurKeyword: &parser.LCurKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "{", - }, - }, - }, - RCurKeyword: &parser.RCurKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "}", - }, - }, - }, - Value: []*parser.ConstValue{ - { - TypeName: "pair", - Key: &parser.ConstValue{ - TypeName: "string", - Value: "key", - }, - Value: &parser.ConstValue{ - TypeName: "string", - Value: "value", - }, - ColonKeyword: &parser.ColonKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: ":", - }, - }, - }, - ListSeparatorKeyword: &parser.ListSeparatorKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: ",", - }, - }, - }, - }, - { - TypeName: "pair", - Key: &parser.ConstValue{ - TypeName: "string", - Value: "key2", - }, - ColonKeyword: &parser.ColonKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: ":", - }, - }, - }, - Value: &parser.ConstValue{ - TypeName: "string", - Value: "value2", - }, - }, - }, - }, - }, - want: `{"key": "value", "key2": "value2"}`, - }, - { - name: "list", - args: args{ - cv: &parser.ConstValue{ - TypeName: "list", - LBrkKeyword: &parser.LBrkKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "[", - }, - }, - }, - RBrkKeyword: &parser.RBrkKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "]", - }, - }, - }, - Value: []*parser.ConstValue{ - { - TypeName: "string", - Value: "value1", - ListSeparatorKeyword: &parser.ListSeparatorKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: ",", - }, - }, - }, - }, - { - TypeName: "string", - Value: "value2", - }, - }, - }, - }, - want: "[\n \"value1\",\n \"value2\",\n]", - }, - { - name: "list single item", - args: args{ - cv: &parser.ConstValue{ - TypeName: "list", - LBrkKeyword: &parser.LBrkKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "[", - }, - }, - }, - RBrkKeyword: &parser.RBrkKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "]", - }, - }, - }, - Value: []*parser.ConstValue{ - { - TypeName: "string", - Value: "value1", - ListSeparatorKeyword: &parser.ListSeparatorKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: ",", - }, - }, - }, - }, - }, - }, - }, - want: `["value1"]`, - }, - { - name: "i64", - args: args{ - cv: &parser.ConstValue{ - TypeName: "i64", - Value: 1, - ValueInText: "1", - }, - }, - want: "1", - }, - { - name: "i64 in hex", - args: args{ - cv: &parser.ConstValue{ - TypeName: "i64", - Value: 26, - ValueInText: "0x1a", - }, - }, - want: "0x1a", - }, - { - name: "i64 in oct", - args: args{ - cv: &parser.ConstValue{ - TypeName: "i64", - Value: 1, - ValueInText: "0o1", - }, - }, - want: "0o1", - }, - { - name: "double", - args: args{ - cv: &parser.ConstValue{ - TypeName: "i64", - Value: 1e11, - ValueInText: "1E11", - }, - }, - want: "1E11", - }, - { - name: "identifier", - args: args{ - cv: &parser.ConstValue{ - TypeName: "identifier", - Value: "User.Name", - }, - }, - want: "User.Name", - }, - { - name: "literal", - args: args{ - cv: &parser.ConstValue{ - TypeName: "string", - Value: "value", - }, - }, - want: `"value"`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := Options{} - assert.Equal(t, tt.want, MustFormatConstValue(tt.args.cv, opts, "", false)) - }) - } -} diff --git a/format/document.go b/format/document.go deleted file mode 100644 index d9ca9cf..0000000 --- a/format/document.go +++ /dev/null @@ -1,252 +0,0 @@ -package format - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/joyme123/thrift-ls/parser" -) - -type fmtContext struct { - // preNode record previous print node. we can use preNode as print context - // if preNodex is const or typdef, and current node is const or typedef, '\n' should be ignore - preNode parser.Node -} - -// formatConfig holds internal behavioral options for document formatting. -// These are not user-facing formatting preferences but control validation behavior. -type formatConfig struct { - selfValidation bool - includePaths []string - currentFile string -} - -// FormatDocument formats a Thrift document according to the provided options. -// This is the basic formatting function without self-validation. -func FormatDocument(doc *parser.Document, opts Options) (string, error) { - return formatDocument(doc, opts, formatConfig{}) -} - -// FormatDocumentWithValidation formats a Thrift document with self-validation enabled. -// The formatted output is parsed back to ensure it's valid. -func FormatDocumentWithValidation(doc *parser.Document, opts Options) (string, error) { - return formatDocument(doc, opts, formatConfig{selfValidation: true}) -} - -// FormatDocumentWithValidationFull formats a Thrift document with full validation support. -// It includes paths for resolving includes and a current file path for relative resolution. -// This is useful when the document contains includes that need to be resolved during validation. -func FormatDocumentWithValidationFull(doc *parser.Document, opts Options, includePaths []string, currentFile string) (string, error) { - return formatDocument(doc, opts, formatConfig{ - selfValidation: true, - includePaths: includePaths, - currentFile: currentFile, - }) -} - -// formatDocument is the internal implementation that all public functions delegate to. -func formatDocument(doc *parser.Document, opts Options, cfg formatConfig) (string, error) { - if doc.ChildrenBadNode() { - return "", BadNodeError - } - - buf := bytes.NewBuffer(nil) - - fmtCtx := &fmtContext{} - - writeBuf := func(node parser.Node, addtionalLine bool) { - if addtionalLine { - if len(buf.Bytes()) > 0 && buf.Bytes()[buf.Len()-1] != '\n' { - // if preNode doesn't have \n at end of line, set \n for it - buf.WriteString("\n") - } - buf.WriteString("\n") - } - - switch node.Type() { - case "Include": - buf.WriteString(MustFormatInclude(node.(*parser.Include), opts)) - case "CPPInclude": - buf.WriteString(MustFormatCPPInclude(node.(*parser.CPPInclude), opts)) - case "Namespace": - buf.WriteString(MustFormatNamespace(node.(*parser.Namespace), opts)) - case "Struct": - buf.WriteString(MustFormatStruct(node.(*parser.Struct), opts)) - case "Union": - buf.WriteString(MustFormatUnion(node.(*parser.Union), opts)) - case "Exception": - buf.WriteString(MustFormatException(node.(*parser.Exception), opts)) - case "Service": - buf.WriteString(MustFormatService(node.(*parser.Service), opts)) - case "Typedef": - buf.WriteString(MustFormatTypedef(node.(*parser.Typedef), opts)) - case "Const": - buf.WriteString(MustFormatConst(node.(*parser.Const), opts)) - case "Enum": - buf.WriteString(MustFormatEnum(node.(*parser.Enum), opts)) - } - - } - - for _, node := range doc.Nodes { - addtionalLine := needAddtionalLineInDocument(fmtCtx.preNode, node) - writeBuf(node, addtionalLine) - fmtCtx.preNode = node - } - - if len(doc.Comments) > 0 { - buf.WriteString(MustFormatComments(opts, doc.Comments, "", "")) - } - - res := buf.String() - res = strings.TrimSpace(res) - - if opts.TrailingNewline && !strings.HasSuffix(res, "\n") { - res += "\n" - } - - if cfg.selfValidation { - if err := validateFormattedDocument(doc, res, cfg); err != nil { - return "", err - } - } - - return res, nil -} - -// validateFormattedDocument parses the formatted result and validates it against the original. -// It uses ParseRecursively when includePaths is non-empty and currentFile is set; -// otherwise it falls back to plain Parse. -func validateFormattedDocument(originalDoc *parser.Document, formatted string, cfg formatConfig) error { - psr := parser.PEGParser{} - - var formattedAst *parser.Document - - if len(cfg.includePaths) > 0 && cfg.currentFile != "" { - results := psr.ParseRecursively("formatted.thrift", []byte(formatted), 0, createIncludeCall(cfg.includePaths, cfg.currentFile)) - if len(results) > 0 && results[0].Doc != nil && len(results[0].Errors) == 0 { - formattedAst = results[0].Doc - } - } - - if formattedAst == nil { - var errs []error - formattedAst, errs = psr.Parse("formatted.thrift", []byte(formatted)) - if len(errs) > 0 { - return fmt.Errorf("format error: format result failed to parse: %v", errs) - } - } - - if formattedAst != nil && !originalDoc.Equals(formattedAst) { - return fmt.Errorf("format error: format result failed to pass self validation") - } - - return nil -} - -// createIncludeCall creates a parser.IncludeCall function that resolves includes -// using the provided include paths and falls back to relative resolution. -func createIncludeCall(includePaths []string, currentFile string) parser.IncludeCall { - return func(include string) (filename string, content []byte, err error) { - // Try include paths first - for _, ip := range includePaths { - candidatePath := filepath.Join(ip, include) - if _, statErr := os.Stat(candidatePath); statErr == nil { - content, err = os.ReadFile(candidatePath) - return candidatePath, content, err - } - } - - // Fall back to relative resolution from current file - basePath := filepath.Dir(currentFile) - resolvedPath := filepath.Join(basePath, include) - content, err = os.ReadFile(resolvedPath) - return resolvedPath, content, err - } -} - -var ( - header = map[string]struct{}{ - "Include": {}, - "CPPInclude": {}, - "Namespace": {}, - } - onelineDefinition = map[string]struct{}{ - "Const": {}, - "Typedef": {}, - } - multiLineDefinition = map[string]struct{}{ - "Struct": {}, - "Union": {}, - "Exception": {}, - "Service": {}, - "Typedef": {}, - "Const": {}, - "Enum": {}, - } -) - -func isHeader(node parser.Node) bool { - _, ok := header[node.Type()] - return ok -} - -func isOneLineDefinition(node parser.Node) bool { - _, ok := onelineDefinition[node.Type()] - return ok -} - -func isMultiLineDefinition(node parser.Node) bool { - _, ok := multiLineDefinition[node.Type()] - return ok -} - -// constHasMultiLineValue returns true if the const has a list with multiple -// items or a map value, which should be treated as a multi-line definition -func constHasMultiLineValue(cst *parser.Const) bool { - if cst.Value == nil { - return false - } - if cst.Value.TypeName == "map" { - return true - } - if cst.Value.TypeName == "list" { - values := cst.Value.Value.([]*parser.ConstValue) - return len(values) > 1 - } - return false -} - -func needAddtionalLineInDocument(preNode parser.Node, currentNode parser.Node) bool { - if preNode == nil { - return false - } - - if isHeader(preNode) && isHeader(currentNode) { - if preNode.Type() == currentNode.Type() { - if lineDistance(preNode, currentNode) > 1 { - return true - } - return false - } - return true - } - - if isOneLineDefinition(preNode) && isOneLineDefinition(currentNode) { - // if preNode and currentNode has one or more empty lines between them, we should reserve - // one empty line - if lineDistance(preNode, currentNode) > 1 { - return true - } - // Consts with list/map values should be treated as multi-line - if preNode.Type() == "Const" && constHasMultiLineValue(preNode.(*parser.Const)) { - return true - } - return false - } - - return true -} diff --git a/format/document_include_test.go b/format/document_include_test.go deleted file mode 100644 index 841a512..0000000 --- a/format/document_include_test.go +++ /dev/null @@ -1,364 +0,0 @@ -package format - -import ( - "os" - "path/filepath" - "testing" - - "github.com/joyme123/thrift-ls/parser" -) - -func TestFormatDocumentWithValidationFull_RelativeInclude(t *testing.T) { - // Create temp dir structure - tmpDir := t.TempDir() - - // Create shared.thrift in same directory as main file - sharedFile := filepath.Join(tmpDir, "shared.thrift") - sharedContent := `namespace * test - -struct User { - 1: string Name - 2: i32 Age -}` - if err := os.WriteFile(sharedFile, []byte(sharedContent), 0644); err != nil { - t.Fatal(err) - } - - // Create main.thrift with relative include - mainFile := filepath.Join(tmpDir, "main.thrift") - mainContent := `include "shared.thrift" - -namespace * test - -struct Person { - 1: string Name -}` - if err := os.WriteFile(mainFile, []byte(mainContent), 0644); err != nil { - t.Fatal(err) - } - - // Parse the document - psr := parser.PEGParser{} - doc, errs := psr.Parse(mainFile, []byte(mainContent)) - if len(errs) > 0 { - t.Fatalf("failed to parse: %v", errs) - } - - // Format with empty include paths (backward compatibility) - opts := Options{} - formatted, err := FormatDocumentWithValidationFull(doc, opts, nil, mainFile) - if err != nil { - t.Fatalf("formatting failed: %v", err) - } - - // Verify the output - expected := `include "shared.thrift" - -namespace * test - -struct Person { - 1: string Name -}` - if formatted != expected { - t.Errorf("expected:\n%s\n\ngot:\n%s", expected, formatted) - } - - // Format with include paths (should work the same) - opts = Options{} - formatted2, err := FormatDocumentWithValidationFull(doc, opts, []string{tmpDir}, mainFile) - if err != nil { - t.Fatalf("formatting with include paths failed: %v", err) - } - if formatted2 != formatted { - t.Errorf("formatted output differs with include paths") - } -} - -func TestFormatDocumentWithValidationFull_IncludePath(t *testing.T) { - // Create temp dir structure with separate include directory - tmpDir := t.TempDir() - includeDir := filepath.Join(tmpDir, "includes") - srcDir := filepath.Join(tmpDir, "src") - - if err := os.MkdirAll(includeDir, 0755); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(srcDir, 0755); err != nil { - t.Fatal(err) - } - - // Create types.thrift in include directory - typesFile := filepath.Join(includeDir, "types.thrift") - typesContent := `namespace * types - -enum Status { - ACTIVE = 1 - INACTIVE = 2 -}` - if err := os.WriteFile(typesFile, []byte(typesContent), 0644); err != nil { - t.Fatal(err) - } - - // Create main.thrift in src directory - mainFile := filepath.Join(srcDir, "main.thrift") - mainContent := `include "types.thrift" - -namespace * test - -struct Entity { - 1: Status status -}` - if err := os.WriteFile(mainFile, []byte(mainContent), 0644); err != nil { - t.Fatal(err) - } - - // Parse with include paths - psr := parser.PEGParser{} - doc, errs := psr.Parse(mainFile, []byte(mainContent)) - if len(errs) > 0 { - t.Fatalf("failed to parse: %v", errs) - } - - // Format with include path - opts := Options{} - formatted, err := FormatDocumentWithValidationFull(doc, opts, []string{includeDir}, mainFile) - if err != nil { - t.Fatalf("formatting failed: %v", err) - } - - // Verify output - expected := `include "types.thrift" - -namespace * test - -struct Entity { - 1: Status status -}` - if formatted != expected { - t.Errorf("expected:\n%s\n\ngot:\n%s", expected, formatted) - } -} - -func TestFormatDocumentWithValidationFull_NestedIncludes(t *testing.T) { - // Create nested include structure - tmpDir := t.TempDir() - includeDir := filepath.Join(tmpDir, "includes") - - if err := os.MkdirAll(includeDir, 0755); err != nil { - t.Fatal(err) - } - - // Create common.thrift - commonFile := filepath.Join(includeDir, "common.thrift") - commonContent := `namespace * common - -struct ID { - 1: string value -}` - if err := os.WriteFile(commonFile, []byte(commonContent), 0644); err != nil { - t.Fatal(err) - } - - // Create types.thrift that includes common - typesFile := filepath.Join(includeDir, "types.thrift") - typesContent := `include "common.thrift" - -namespace * types - -struct Entity { - 1: ID id - 2: string name -}` - if err := os.WriteFile(typesFile, []byte(typesContent), 0644); err != nil { - t.Fatal(err) - } - - // Create main.thrift that includes types - mainFile := filepath.Join(tmpDir, "main.thrift") - mainContent := `include "types.thrift" - -namespace * test - -struct Container { - 1: Entity entity -}` - if err := os.WriteFile(mainFile, []byte(mainContent), 0644); err != nil { - t.Fatal(err) - } - - // Parse with include paths - psr := parser.PEGParser{} - doc, errs := psr.Parse(mainFile, []byte(mainContent)) - if len(errs) > 0 { - t.Fatalf("failed to parse: %v", errs) - } - - // Format with include path (nested includes should be resolved) - opts := Options{} - formatted, err := FormatDocumentWithValidationFull(doc, opts, []string{includeDir}, mainFile) - if err != nil { - t.Fatalf("formatting failed: %v", err) - } - - // Verify basic formatting works - if formatted == "" { - t.Error("expected non-empty formatted output") - } -} - -func TestFormatDocumentWithValidationFull_MissingInclude(t *testing.T) { - // Formatting should still work even if includes are missing - tmpDir := t.TempDir() - - mainFile := filepath.Join(tmpDir, "main.thrift") - mainContent := `include "nonexistent.thrift" - -namespace * test - -struct Data { - 1: string value -}` - if err := os.WriteFile(mainFile, []byte(mainContent), 0644); err != nil { - t.Fatal(err) - } - - // Parse the document - psr := parser.PEGParser{} - doc, errs := psr.Parse(mainFile, []byte(mainContent)) - if len(errs) > 0 { - t.Fatalf("failed to parse: %v", errs) - } - - // Format without include paths (backward compatibility, no self-val) - opts := Options{} - formatted, err := FormatDocument(doc, opts) - if err != nil { - t.Fatalf("formatting failed: %v", err) - } - - expected := `include "nonexistent.thrift" - -namespace * test - -struct Data { - 1: string value -}` - if formatted != expected { - t.Errorf("expected:\n%s\n\ngot:\n%s", expected, formatted) - } -} - -func TestFormatDocumentWithValidationFull_BackwardCompat(t *testing.T) { - // Test backward compatibility: empty include paths with self-validation=false - tmpDir := t.TempDir() - - mainFile := filepath.Join(tmpDir, "main.thrift") - mainContent := `namespace * test - -struct Point { - 1: i32 X - 2: i32 Y -}` - if err := os.WriteFile(mainFile, []byte(mainContent), 0644); err != nil { - t.Fatal(err) - } - - // Parse - psr := parser.PEGParser{} - doc, errs := psr.Parse(mainFile, []byte(mainContent)) - if len(errs) > 0 { - t.Fatalf("failed to parse: %v", errs) - } - - // Format without self-validation and no include paths (backward compat path) - opts := Options{} - formatted, err := FormatDocument(doc, opts) - if err != nil { - t.Fatalf("formatting failed: %v", err) - } - - // Should work fine - if formatted == "" { - t.Error("expected non-empty formatted output") - } - - // Test with self-validation using fallback Parse path - opts = Options{} - formatted2, err := FormatDocumentWithValidation(doc, opts) - if err != nil { - t.Fatalf("formatting with empty currentFile failed: %v", err) - } - if formatted2 == "" { - t.Error("expected non-empty formatted output") - } -} - -func TestFormatDocumentWithValidationFull_MultipleIncludePaths(t *testing.T) { - // Test with multiple include paths - tmpDir := t.TempDir() - includeDir1 := filepath.Join(tmpDir, "includes1") - includeDir2 := filepath.Join(tmpDir, "includes2") - - if err := os.MkdirAll(includeDir1, 0755); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(includeDir2, 0755); err != nil { - t.Fatal(err) - } - - // Create file in first include directory - file1 := filepath.Join(includeDir1, "common.thrift") - content1 := `namespace * common - -struct Base { - 1: string id -}` - if err := os.WriteFile(file1, []byte(content1), 0644); err != nil { - t.Fatal(err) - } - - // Create file in second include directory - file2 := filepath.Join(includeDir2, "types.thrift") - content2 := `namespace * types - -struct Item { - 1: string name -}` - if err := os.WriteFile(file2, []byte(content2), 0644); err != nil { - t.Fatal(err) - } - - // Create main file that includes both - mainFile := filepath.Join(tmpDir, "main.thrift") - mainContent := `include "common.thrift" -include "types.thrift" - -namespace * test - -struct Container { - 1: Base base - 2: Item item -}` - if err := os.WriteFile(mainFile, []byte(mainContent), 0644); err != nil { - t.Fatal(err) - } - - // Parse - psr := parser.PEGParser{} - doc, errs := psr.Parse(mainFile, []byte(mainContent)) - if len(errs) > 0 { - t.Fatalf("failed to parse: %v", errs) - } - - // Format with both include paths - opts := Options{} - formatted, err := FormatDocumentWithValidationFull(doc, opts, []string{includeDir1, includeDir2}, mainFile) - if err != nil { - t.Fatalf("formatting failed: %v", err) - } - - // Verify output - if formatted == "" { - t.Error("expected non-empty formatted output") - } -} diff --git a/format/document_test.go b/format/document_test.go deleted file mode 100644 index fca13f5..0000000 --- a/format/document_test.go +++ /dev/null @@ -1,1070 +0,0 @@ -package format - -import ( - "strings" - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_FormatDocumentServiceCommaOptions(t *testing.T) { - doc := ` -service A { - bool func1() - - bool func2(); -}` - - // add comma - expectedDoc := `service A { - bool func1(), - - bool func2(), -}` - - opts := Options{FieldLineComma: FieldLineCommaAdd} - - ast, err := parser.Parse("test.thrift", []byte(doc)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - formated, err := FormatDocument(ast.(*parser.Document), opts) - assert.Equal(t, expectedDoc, formated) - - _, err = FormatDocumentWithValidation(ast.(*parser.Document), opts) - assert.NoError(t, err) - - // remove comma - expectedDoc = `service A { - bool func1() - - bool func2() -}` - opts = Options{FieldLineComma: FieldLineCommaRemove} - - ast, err = parser.Parse("test.thrift", []byte(doc)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - formated, err = FormatDocument(ast.(*parser.Document), opts) - assert.Equal(t, expectedDoc, formated) - - _, err = FormatDocumentWithValidation(ast.(*parser.Document), opts) - assert.NoError(t, err) - - // disable - expectedDoc = `service A { - bool func1() - - bool func2(); -}` - opts = Options{FieldLineComma: FieldLineCommaDisable} - - ast, err = parser.Parse("test.thrift", []byte(doc)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - formated, err = FormatDocument(ast.(*parser.Document), opts) - assert.Equal(t, expectedDoc, formated) - - _, err = FormatDocumentWithValidation(ast.(*parser.Document), opts) - assert.NoError(t, err) -} - -func Test_FormatDocument(t *testing.T) { - ast, err := parser.Parse("test.thrift", []byte(ThriftTestContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - opts := Options{} - formated, err := FormatDocumentWithValidation(ast.(*parser.Document), opts) - assert.Equal(t, expectedFormated, formated) - assert.NoError(t, err) -} - -func TestFormatDocumentTrailingNewline(t *testing.T) { - doc := `struct Test {}` - - ast, err := parser.Parse("test.thrift", []byte(doc)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - // Test with TrailingNewline = true - output should end with "\n" - opts := Options{TrailingNewline: true} - formatted, err := FormatDocument(ast.(*parser.Document), opts) - assert.NoError(t, err) - assert.True(t, strings.HasSuffix(formatted, "\n"), "Expected formatted output to end with newline when TrailingNewline is true") - - // Test with TrailingNewline = false - output should NOT end with "\n" - opts = Options{TrailingNewline: false} - formatted, err = FormatDocument(ast.(*parser.Document), opts) - assert.NoError(t, err) - assert.False(t, strings.HasSuffix(formatted, "\n"), "Expected formatted output to NOT end with newline when TrailingNewline is false") -} - -var expectedFormated = `/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * Contains some contributions under the Thrift Software License. - * Please see doc/old-thrift-license.txt in the Thrift distribution for - * details. - */ - -include "a.thrift" -include "b.thrift" - -cpp_include "aaaaa" -cpp_include "bbbbb" - -namespace c_glib TTest -namespace cpp thrift.test -namespace delphi Thrift.Test -namespace go thrifttest -namespace java thrift.test -namespace js ThriftTest -namespace lua ThriftTest -namespace netstd ThriftTest -namespace perl ThriftTest -namespace php ThriftTest -namespace py ThriftTest -namespace py.twisted ThriftTest -namespace rb Thrift.Test -namespace st ThriftTest -namespace xsd test (uri = 'http://thrift.apache.org/ns/ThriftTest') - -// Presence of namespaces and sub-namespaces for which there is -// no generator should compile with warnings only -namespace noexist ThriftTest -namespace cpp.noexist ThriftTest - -namespace * thrift.test - -/** - * Docstring! - */ -enum Numberz { - ONE = 1, - - // annotations - TWO, // endline comments - THREE, - FIVE = 5, - SIX, - EIGHT = 8 -} - -const Numberz myNumberz = Numberz.ONE; -// the following is expected to fail: -// const Numberz urNumberz = ONE; - -// additional comment line - -typedef i64 UserId -// comment1 -const UserId default_user = 0 -// comment2 -const UserId admin_user = 1 - -struct Bonk { - 1: string message, - 2: i32 type -} - -typedef map MapType - -struct Bools { - 1: bool im_true, - - // comments - 2: bool im_false, -} - -struct Xtruct { - 1: string string_thing, - 4: i8 byte_thing, - 9: i32 i32_thing, - 11: i64 i64_thing -} - -struct Xtruct2 { - 1: i8 byte_thing, // used to be byte, hence the name - 2: Xtruct struct_thing, - 3: i32 i32_thing -} - -struct Xtruct3 { - 1: string string_thing, - 4: i32 changed, - 9: i32 i32_thing, - 11: i64 i64_thing -} - -struct Insanity { - 1: map userMap, - 2: list xtructs -} (python.immutable = "") - -struct CrazyNesting { - 1: string string_field, - 2: optional set set_field, - // Do not insert line break as test/go/Makefile.am is removing this line with pattern match - 3: required list (python.immutable = ""), map (python.immutable = "")> (python.immutable = "")>>>> list_field, - 4: binary binary_field - 5: uuid uuid_field -} - -union SomeUnion { - 1: map map_thing, - 2: string string_thing, - 3: i32 i32_thing, - 4: Xtruct3 xtruct_thing, - 5: Insanity insanity_thing -} - -exception Xception { - 1: i32 errorCode, - 2: string message -} - -exception Xception2 { - 1: i32 errorCode, - 2: Xtruct struct_thing -} - -struct EmptyStruct {} - -struct OneField { - 1: EmptyStruct field -} - -service ThriftTest { - /** - * Prints "testVoid()" and returns nothing. - */ - void testVoid(), - - /** - * Prints 'testString("%s")' with thing as '%s' - * @param string thing - the string to print - * @return string - returns the string 'thing' - */ - string testString(1: string thing), - - /** - * Prints 'testBool("%s")' where '%s' with thing as 'true' or 'false' - * @param bool thing - the bool data to print - * @return bool - returns the bool 'thing' - */ - bool testBool(1: bool thing), - - /** - * Prints 'testByte("%d")' with thing as '%d' - * The types i8 and byte are synonyms, use of i8 is encouraged, byte still exists for the sake of compatibility. - * @param byte thing - the i8/byte to print - * @return i8 - returns the i8/byte 'thing' - */ - i8 testByte(1: i8 thing), - - /** - * Prints 'testI32("%d")' with thing as '%d' - * @param i32 thing - the i32 to print - * @return i32 - returns the i32 'thing' - */ - i32 testI32(1: i32 thing), - - /** - * Prints 'testI64("%d")' with thing as '%d' - * @param i64 thing - the i64 to print - * @return i64 - returns the i64 'thing' - */ - i64 testI64(1: i64 thing), - - /** - * Prints 'testDouble("%f")' with thing as '%f' - * @param double thing - the double to print - * @return double - returns the double 'thing' - */ - double testDouble(1: double thing), - - /** - * Prints 'testBinary("%s")' where '%s' is a hex-formatted string of thing's data - * @param binary thing - the binary data to print - * @return binary - returns the binary 'thing' - */ - binary testBinary(1: binary thing), - - /** - * Prints 'testUuid("%s")' where '%s' is the uuid given. Note that the uuid byte order should be correct. - * @param uuid thing - the uuid to print - * @return uuid - returns the uuid 'thing' - */ - uuid testUuid(1: uuid thing), - - /** - * Prints 'testStruct("{%s}")' where thing has been formatted into a string of comma separated values - * @param Xtruct thing - the Xtruct to print - * @return Xtruct - returns the Xtruct 'thing' - */ - Xtruct testStruct(1: Xtruct thing), - - /** - * Prints 'testNest("{%s}")' where thing has been formatted into a string of the nested struct - * @param Xtruct2 thing - the Xtruct2 to print - * @return Xtruct2 - returns the Xtruct2 'thing' - */ - Xtruct2 testNest(1: Xtruct2 thing), - - /** - * Prints 'testMap("{%s")' where thing has been formatted into a string of 'key => value' pairs - * separated by commas and new lines - * @param map thing - the map to print - * @return map - returns the map 'thing' - */ - map testMap(1: map thing), - - /** - * Prints 'testStringMap("{%s}")' where thing has been formatted into a string of 'key => value' pairs - * separated by commas and new lines - * @param map thing - the map to print - * @return map - returns the map 'thing' - */ - map testStringMap(1: map thing), - - /** - * Prints 'testSet("{%s}")' where thing has been formatted into a string of values - * separated by commas and new lines - * @param set thing - the set to print - * @return set - returns the set 'thing' - */ - set testSet(1: set thing), - - /** - * Prints 'testList("{%s}")' where thing has been formatted into a string of values - * separated by commas and new lines - * @param list thing - the list to print - * @return list - returns the list 'thing' - */ - list testList(1: list thing), - - /** - * Prints 'testEnum("%d")' where thing has been formatted into its numeric value - * @param Numberz thing - the Numberz to print - * @return Numberz - returns the Numberz 'thing' - */ - Numberz testEnum(1: Numberz thing), - - /** - * Prints 'testTypedef("%d")' with thing as '%d' - * @param UserId thing - the UserId to print - * @return UserId - returns the UserId 'thing' - */ - UserId testTypedef(1: UserId thing), - - /** - * Prints 'testMapMap("%d")' with hello as '%d' - * @param i32 hello - the i32 to print - * @return map> - returns a dictionary with these values: - * {-4 => {-4 => -4, -3 => -3, -2 => -2, -1 => -1, }, 4 => {1 => 1, 2 => 2, 3 => 3, 4 => 4, }, } - */ - map> testMapMap(1: i32 hello), - - /** - * So you think you've got this all worked out, eh? - * - * Creates a map with these values and prints it out: - * { 1 => { 2 => argument, - * 3 => argument, - * }, - * 2 => { 6 => , }, - * } - * @return map> - a map with the above values - */ - map> testInsanity(1: Insanity argument), - - /** - * Prints 'testMulti()' - * @param i8 arg0 - - * @param i32 arg1 - - * @param i64 arg2 - - * @param map arg3 - - * @param Numberz arg4 - - * @param UserId arg5 - - * @return Xtruct - returns an Xtruct with string_thing = "Hello2, byte_thing = arg0, i32_thing = arg1 - * and i64_thing = arg2 - */ - Xtruct testMulti( - 1: i8 arg0, - 2: i32 arg1, - 3: i64 arg2, - 4: map arg3, - 5: Numberz arg4, - 6: UserId arg5 - ), - - /** - * Print 'testException(%s)' with arg as '%s' - * @param string arg - a string indication what type of exception to throw - * if arg == "Xception" throw Xception with errorCode = 1001 and message = arg - * else if arg == "TException" throw TException - * else do not throw anything - */ - void testException(1: string arg) throws (1: Xception err1), - - /** - * Print 'testMultiException(%s, %s)' with arg0 as '%s' and arg1 as '%s' - * @param string arg - a string indicating what type of exception to throw - * if arg0 == "Xception" throw Xception with errorCode = 1001 and message = "This is an Xception" - * else if arg0 == "Xception2" throw Xception2 with errorCode = 2002 and struct_thing.string_thing = "This is an Xception2" - * else do not throw anything - * @return Xtruct - an Xtruct with string_thing = arg1 - */ - Xtruct testMultiException( - 1: string arg0, - 2: string arg1 - ) throws ( - 1: Xception err1, - 2: Xception2 err2 - ) - - /** - * Print 'testOneway(%d): Sleeping...' with secondsToSleep as '%d' - * sleep 'secondsToSleep' - * Print 'testOneway(%d): done sleeping!' with secondsToSleep as '%d' - * @param i32 secondsToSleep - the number of seconds to sleep - */ - oneway void testOneway(1: i32 secondsToSleep) - void test() -} - -service SecondService { - /** - * Prints 'testString("%s")' with thing as '%s' - * @param string thing - the string to print - * @return string - returns the string 'thing' - */ - string secondtestString(1: string thing) -} - -struct VersioningTestV1 { - 1: i32 begin_in_both, - 3: string old_string, - 12: i32 end_in_both -} - -struct VersioningTestV2 { - 1: i32 begin_in_both, - - 2: i32 newint, - 3: i8 newbyte, - 4: i16 newshort, - 5: i64 newlong, - 6: double newdouble - 7: Bonk newstruct, - 8: list newlist, - 9: set newset, - 10: map newmap, - 11: string newstring, - 12: i32 end_in_both -} - -struct ListTypeVersioningV1 { - 1: list myints; - 2: string hello; -} - -struct ListTypeVersioningV2 { - 1: list strings; - 2: string hello; -} - -struct GuessProtocolStruct { - 7: map map_field, -} - -struct LargeDeltas { - 1: Bools b1, - 10: Bools b10, - 100: Bools b100, - 500: bool check_true, - 1000: Bools b1000, - 1500: bool check_false, - 2000: VersioningTestV2 vertwo2000, - 2500: set a_set2500, - 3000: VersioningTestV2 vertwo3000, - 4000: list big_numbers -} - -struct NestedListsI32x2 { - 1: list> integerlist -} - -struct NestedListsI32x3 { - 1: list>> integerlist -} - -struct NestedMixedx2 { - 1: list> int_set_list - 2: map> map_int_strset - 3: list>> map_int_strset_list -} - -struct ListBonks { - 1: list bonk -} - -struct NestedListsBonk { - 1: list>> bonk -} - -struct BoolTest { - 1: optional bool b = true; - 2: optional string s = "true"; -} - -struct StructA { - 1: required string s; -} - -struct StructB { - 1: optional StructA aa; - 2: required StructA ab; -} - -struct OptionalSetDefaultTest { - 1: optional set with_default = ["test"] -} - -struct OptionalBinary { - 1: optional set bin_set = {} - 2: optional map bin_map = {} -} - -struct OptionalMap { - 1: optional map str_map = {/* comment 1 */ "text": 1, "text2": 2} - 2: optional map str_map2 = { - // comments - "text": "text", - 'text2': "text2", "text3": "text3" - } - 3: optional map int_map = { - // comments - 1: 1, - } - 4: optional map int_bool_map = { - // comments - 1: true, - } -} - -const map SIGNATURE_SUPPORT = { - UserType.DEV_XIAOMI_SSO: false, - UserType.DEV_XIAOMI: true, - UserType.APP_SECRET: true, - UserType.APP_ACCESS_TOKEN: true, - UserType.APP_XIAOMI_SSO: false, - UserType.APP_ANONYMOUS: false -} - -// comments at end of doc` - -const ThriftTestContent = ` -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * Contains some contributions under the Thrift Software License. - * Please see doc/old-thrift-license.txt in the Thrift distribution for - * details. - */ - - -include "a.thrift" -include "b.thrift" - -cpp_include "aaaaa" -cpp_include "bbbbb" - -namespace c_glib TTest -namespace cpp thrift.test -namespace delphi Thrift.Test -namespace go thrifttest -namespace java thrift.test -namespace js ThriftTest -namespace lua ThriftTest -namespace netstd ThriftTest -namespace perl ThriftTest -namespace php ThriftTest -namespace py ThriftTest -namespace py.twisted ThriftTest -namespace rb Thrift.Test -namespace st ThriftTest -namespace xsd test (uri = 'http://thrift.apache.org/ns/ThriftTest') - -// Presence of namespaces and sub-namespaces for which there is -// no generator should compile with warnings only -namespace noexist ThriftTest -namespace cpp.noexist ThriftTest - -namespace * thrift.test - -/** - * Docstring! - */ -enum Numberz -{ - ONE = 1, - - // annotations - TWO, // endline comments - THREE, - FIVE = 5, - SIX, - EIGHT = 8 -} -const Numberz myNumberz = Numberz.ONE; - -// the following is expected to fail: -// const Numberz urNumberz = ONE; - - - - - - -// additional comment line - - - - -typedef i64 UserId - -// comment1 -const UserId default_user = 0 - -// comment2 -const UserId admin_user = 1 - -struct Bonk -{ - 1: string message, - 2: i32 type -} - -typedef map MapType - -struct Bools { - 1: bool im_true, - - // comments - 2: bool im_false, -} - -struct Xtruct -{ - 1: string string_thing, - 4: i8 byte_thing, - 9: i32 i32_thing, - 11: i64 i64_thing -} - -struct Xtruct2 -{ - 1: i8 byte_thing, // used to be byte, hence the name - 2: Xtruct struct_thing, - 3: i32 i32_thing -} - -struct Xtruct3 -{ - 1: string string_thing, - 4: i32 changed, - 9: i32 i32_thing, - 11: i64 i64_thing -} - - -struct Insanity -{ - 1: map userMap, - 2: list xtructs -} (python.immutable= "") - -struct CrazyNesting { - 1: string string_field, - 2: optional set set_field, - // Do not insert line break as test/go/Makefile.am is removing this line with pattern match - 3: required list (python.immutable = ""), map(python.immutable = "")> (python.immutable = "")>>>> list_field, - 4: binary binary_field - 5: uuid uuid_field -} - -union SomeUnion { - 1: map map_thing, - 2: string string_thing, - 3: i32 i32_thing, - 4: Xtruct3 xtruct_thing, - 5: Insanity insanity_thing -} - -exception Xception { - 1: i32 errorCode, - 2: string message -} - -exception Xception2 { - 1: i32 errorCode, - 2: Xtruct struct_thing -} - -struct EmptyStruct {} - -struct OneField { - 1: EmptyStruct field -} - -service ThriftTest -{ - /** - * Prints "testVoid()" and returns nothing. - */ - void testVoid(), - - /** - * Prints 'testString("%s")' with thing as '%s' - * @param string thing - the string to print - * @return string - returns the string 'thing' - */ - string testString(1: string thing), - - /** - * Prints 'testBool("%s")' where '%s' with thing as 'true' or 'false' - * @param bool thing - the bool data to print - * @return bool - returns the bool 'thing' - */ - bool testBool(1: bool thing), - - /** - * Prints 'testByte("%d")' with thing as '%d' - * The types i8 and byte are synonyms, use of i8 is encouraged, byte still exists for the sake of compatibility. - * @param byte thing - the i8/byte to print - * @return i8 - returns the i8/byte 'thing' - */ - i8 testByte(1: i8 thing), - - /** - * Prints 'testI32("%d")' with thing as '%d' - * @param i32 thing - the i32 to print - * @return i32 - returns the i32 'thing' - */ - i32 testI32(1: i32 thing), - - /** - * Prints 'testI64("%d")' with thing as '%d' - * @param i64 thing - the i64 to print - * @return i64 - returns the i64 'thing' - */ - i64 testI64(1: i64 thing), - - /** - * Prints 'testDouble("%f")' with thing as '%f' - * @param double thing - the double to print - * @return double - returns the double 'thing' - */ - double testDouble(1: double thing), - - /** - * Prints 'testBinary("%s")' where '%s' is a hex-formatted string of thing's data - * @param binary thing - the binary data to print - * @return binary - returns the binary 'thing' - */ - binary testBinary(1: binary thing), - - /** - * Prints 'testUuid("%s")' where '%s' is the uuid given. Note that the uuid byte order should be correct. - * @param uuid thing - the uuid to print - * @return uuid - returns the uuid 'thing' - */ - uuid testUuid(1: uuid thing), - - /** - * Prints 'testStruct("{%s}")' where thing has been formatted into a string of comma separated values - * @param Xtruct thing - the Xtruct to print - * @return Xtruct - returns the Xtruct 'thing' - */ - Xtruct testStruct(1: Xtruct thing), - - /** - * Prints 'testNest("{%s}")' where thing has been formatted into a string of the nested struct - * @param Xtruct2 thing - the Xtruct2 to print - * @return Xtruct2 - returns the Xtruct2 'thing' - */ - Xtruct2 testNest(1: Xtruct2 thing), - - /** - * Prints 'testMap("{%s")' where thing has been formatted into a string of 'key => value' pairs - * separated by commas and new lines - * @param map thing - the map to print - * @return map - returns the map 'thing' - */ - map testMap(1: map thing), - - /** - * Prints 'testStringMap("{%s}")' where thing has been formatted into a string of 'key => value' pairs - * separated by commas and new lines - * @param map thing - the map to print - * @return map - returns the map 'thing' - */ - map testStringMap(1: map thing), - - /** - * Prints 'testSet("{%s}")' where thing has been formatted into a string of values - * separated by commas and new lines - * @param set thing - the set to print - * @return set - returns the set 'thing' - */ - set testSet(1: set thing), - - /** - * Prints 'testList("{%s}")' where thing has been formatted into a string of values - * separated by commas and new lines - * @param list thing - the list to print - * @return list - returns the list 'thing' - */ - list testList(1: list thing), - - /** - * Prints 'testEnum("%d")' where thing has been formatted into its numeric value - * @param Numberz thing - the Numberz to print - * @return Numberz - returns the Numberz 'thing' - */ - Numberz testEnum(1: Numberz thing), - - /** - * Prints 'testTypedef("%d")' with thing as '%d' - * @param UserId thing - the UserId to print - * @return UserId - returns the UserId 'thing' - */ - UserId testTypedef(1: UserId thing), - - /** - * Prints 'testMapMap("%d")' with hello as '%d' - * @param i32 hello - the i32 to print - * @return map> - returns a dictionary with these values: - * {-4 => {-4 => -4, -3 => -3, -2 => -2, -1 => -1, }, 4 => {1 => 1, 2 => 2, 3 => 3, 4 => 4, }, } - */ - map> testMapMap(1: i32 hello), - - /** - * So you think you've got this all worked out, eh? - * - * Creates a map with these values and prints it out: - * { 1 => { 2 => argument, - * 3 => argument, - * }, - * 2 => { 6 => , }, - * } - * @return map> - a map with the above values - */ - map> testInsanity(1: Insanity argument), - - /** - * Prints 'testMulti()' - * @param i8 arg0 - - * @param i32 arg1 - - * @param i64 arg2 - - * @param map arg3 - - * @param Numberz arg4 - - * @param UserId arg5 - - * @return Xtruct - returns an Xtruct with string_thing = "Hello2, byte_thing = arg0, i32_thing = arg1 - * and i64_thing = arg2 - */ - Xtruct testMulti(1: i8 arg0, 2: i32 arg1, 3: i64 arg2, 4: map arg3, 5: Numberz arg4, 6: UserId arg5), - - /** - * Print 'testException(%s)' with arg as '%s' - * @param string arg - a string indication what type of exception to throw - * if arg == "Xception" throw Xception with errorCode = 1001 and message = arg - * else if arg == "TException" throw TException - * else do not throw anything - */ - void testException(1: string arg) throws(1: Xception err1), - - /** - * Print 'testMultiException(%s, %s)' with arg0 as '%s' and arg1 as '%s' - * @param string arg - a string indicating what type of exception to throw - * if arg0 == "Xception" throw Xception with errorCode = 1001 and message = "This is an Xception" - * else if arg0 == "Xception2" throw Xception2 with errorCode = 2002 and struct_thing.string_thing = "This is an Xception2" - * else do not throw anything - * @return Xtruct - an Xtruct with string_thing = arg1 - */ - Xtruct testMultiException(1: string arg0, 2: string arg1) throws(1: Xception err1, 2: Xception2 err2) - - /** - * Print 'testOneway(%d): Sleeping...' with secondsToSleep as '%d' - * sleep 'secondsToSleep' - * Print 'testOneway(%d): done sleeping!' with secondsToSleep as '%d' - * @param i32 secondsToSleep - the number of seconds to sleep - */ - oneway void testOneway(1:i32 secondsToSleep) - void test() -} - -service SecondService -{ - /** - * Prints 'testString("%s")' with thing as '%s' - * @param string thing - the string to print - * @return string - returns the string 'thing' - */ - string secondtestString(1: string thing) -} - -struct VersioningTestV1 { - 1: i32 begin_in_both, - 3: string old_string, - 12: i32 end_in_both -} - -struct VersioningTestV2 { - 1: i32 begin_in_both, - - 2: i32 newint, - 3: i8 newbyte, - 4: i16 newshort, - 5: i64 newlong, - 6: double newdouble - 7: Bonk newstruct, - 8: list newlist, - 9: set newset, - 10: map newmap, - 11: string newstring, - 12: i32 end_in_both -} - -struct ListTypeVersioningV1 { - 1: list myints; - 2: string hello; -} - -struct ListTypeVersioningV2 { - 1: list strings; - 2: string hello; -} - -struct GuessProtocolStruct { - 7: map map_field, -} - -struct LargeDeltas { - 1: Bools b1, - 10: Bools b10, - 100: Bools b100, - 500: bool check_true, - 1000: Bools b1000, - 1500: bool check_false, - 2000: VersioningTestV2 vertwo2000, - 2500: set a_set2500, - 3000: VersioningTestV2 vertwo3000, - 4000: list big_numbers -} - -struct NestedListsI32x2 { - 1: list> integerlist -} -struct NestedListsI32x3 { - 1: list>> integerlist -} -struct NestedMixedx2 { - 1: list> int_set_list - 2: map> map_int_strset - 3: list>> map_int_strset_list -} -struct ListBonks { - 1: list bonk -} -struct NestedListsBonk { - 1: list>> bonk -} - -struct BoolTest { - 1: optional bool b = true; - 2: optional string s = "true"; -} - -struct StructA { - 1: required string s; -} - -struct StructB { - 1: optional StructA aa; - 2: required StructA ab; -} - -struct OptionalSetDefaultTest { - 1: optional set with_default = ["test"] -} - -struct OptionalBinary { - 1: optional set bin_set = {} - 2: optional map bin_map = {} -} - -struct OptionalMap { - 1: optional map str_map = {/* comment 1 */ "text": 1, "text2": 2} - 2: optional map str_map2 = { - // comments - "text": "text", - 'text2': "text2", "text3": "text3" - } - 3: optional map int_map = { - // comments - 1: 1, - } - 4: optional map int_bool_map = { - // comments - 1: true, - } -} - - -const map SIGNATURE_SUPPORT = { - UserType.DEV_XIAOMI_SSO: false, - UserType.DEV_XIAOMI: true, - UserType.APP_SECRET: true, - UserType.APP_ACCESS_TOKEN: true, - UserType.APP_XIAOMI_SSO: false, - UserType.APP_ANONYMOUS: false -} - -// comments at end of doc -` diff --git a/format/enum.go b/format/enum.go deleted file mode 100644 index 96a4ac8..0000000 --- a/format/enum.go +++ /dev/null @@ -1,48 +0,0 @@ -package format - -import ( - "github.com/joyme123/thrift-ls/parser" -) - -const ( - enumOneLineTpl = `{{.Comments}}{{.Enum}} {{.Identifier}} {{.LCUR}}{{.RCUR}}{{.Annotations}}{{.EndLineComments}}` - - enumMultiLineTpl = `{{.Comments}}{{.Enum}} {{.Identifier}} {{.LCUR}} -{{.EnumValues}}{{.RCUR}}{{.Annotations}}{{.EndLineComments}} -` -) - -type EnumFormatter struct { - Comments string - Enum string - Identifier string - LCUR string - EnumValues string - RCUR string - Annotations string - EndLineComments string -} - -func MustFormatEnum(enum *parser.Enum, opts Options) string { - comments, annos := formatCommentsAndAnnos(opts, enum.Comments, enum.Annotations, "") - if len(enum.Comments) > 0 && lineDistance(enum.Comments[len(enum.Comments)-1], enum.EnumKeyword) > 1 { - comments = comments + "\n" - } - - f := EnumFormatter{ - Comments: comments, - Enum: MustFormatKeyword(opts, enum.EnumKeyword.Keyword), - Identifier: MustFormatIdentifier(opts, enum.Name, ""), - LCUR: MustFormatKeyword(opts, enum.LCurKeyword.Keyword), - EnumValues: MustFormatEnumValues(enum.Values, opts, opts.GetIndent()), - RCUR: MustFormatKeyword(opts, enum.RCurKeyword.Keyword), - Annotations: annos, - EndLineComments: MustFormatEndLineComments(opts, enum.EndLineComments, "", ""), - } - - if len(enum.Values) > 0 { - return MustFormat(enumMultiLineTpl, f) - } - - return MustFormat(enumOneLineTpl, f) -} diff --git a/format/enumvalue.go b/format/enumvalue.go deleted file mode 100644 index 6ba3e5a..0000000 --- a/format/enumvalue.go +++ /dev/null @@ -1,101 +0,0 @@ -package format - -import ( - "bytes" - "fmt" - "text/tabwriter" - - "github.com/joyme123/thrift-ls/parser" -) - -type enumValueGroup []string - -func MustFormatEnumValues(values []*parser.EnumValue, opts Options, indent string) string { - buf := bytes.NewBuffer(nil) - - fmtCtx := &fmtContext{} - - var enumValueGroups []enumValueGroup - var eg enumValueGroup - - for i, v := range values { - if needAddtionalLineForEnumValues(fmtCtx.preNode, values[i]) { - enumValueGroups = append(enumValueGroups, eg) - eg = make(enumValueGroup, 0) - } - space := " " - if opts.getAlign() == AlignTypeField { - space = "\t" - } - eg = append(eg, MustFormatEnumValue(v, opts, space, indent)) - fmtCtx.preNode = values[i] - } - - if len(eg) > 0 { - enumValueGroups = append(enumValueGroups, eg) - } - - for i, eg := range enumValueGroups { - w := new(tabwriter.Writer) - w.Init(buf, 1, 8, 1, ' ', tabwriter.TabIndent) - for j := range eg { - fmt.Fprintln(w, eg[j]) - } - w.Flush() - - if i < len(enumValueGroups)-1 { - buf.WriteString("\n") - } - } - - return buf.String() -} - -func MustFormatEnumValue(enumValue *parser.EnumValue, opts Options, space, indent string) string { - comments, annos := formatCommentsAndAnnos(opts, enumValue.Comments, enumValue.Annotations, indent) - - if len(comments) > 0 && lineDistance(enumValue.Comments[len(enumValue.Comments)-1], enumValue.Name) > 1 { - comments = comments + "\n" - } - - buf := bytes.NewBufferString(comments) - buf.WriteString(indent + MustFormatIdentifier(opts, enumValue.Name, "")) - if enumValue.ValueNode != nil { - equalSpace := space - if opts.getAlign() == AlignTypeAssign { - equalSpace = "\t" - } - buf.WriteString(fmt.Sprintf("%s%s%s%s", equalSpace, MustFormatKeyword(opts, enumValue.EqualKeyword.Keyword), equalSpace, MustFormatConstValue(enumValue.ValueNode, opts, indent, false))) - } - - buf.WriteString(annos) - - if opts.getFieldLineComma() == FieldLineCommaAdd { - buf.WriteString(",") - } else if opts.getFieldLineComma() == FieldLineCommaDisable { - if enumValue.ListSeparatorKeyword != nil { - buf.WriteString(MustFormatKeyword(opts, enumValue.ListSeparatorKeyword.Keyword)) - } - } - - buf.WriteString(MustFormatEndLineComments(opts, enumValue.EndLineComments, "", "")) - - return buf.String() -} - -func needAddtionalLineForEnumValues(preNode, curNode parser.Node) bool { - if preNode == nil { - return false - } - - curValue := curNode.(*parser.EnumValue) - - var curStartLine int - if len(curValue.Comments) > 0 { - curStartLine = curValue.Comments[0].Pos().Line - } else { - curStartLine = curValue.Name.Pos().Line - } - - return curStartLine-preNode.End().Line > 1 -} diff --git a/format/errors.go b/format/errors.go deleted file mode 100644 index 3e7c599..0000000 --- a/format/errors.go +++ /dev/null @@ -1,5 +0,0 @@ -package format - -import "errors" - -var BadNodeError = errors.New("syntax error") diff --git a/format/exception.go b/format/exception.go deleted file mode 100644 index fd47529..0000000 --- a/format/exception.go +++ /dev/null @@ -1,47 +0,0 @@ -package format - -import ( - "github.com/joyme123/thrift-ls/parser" -) - -const ( - exceptionOneLineTpl = `{{.Comments}}{{.Exception}} {{.Identifier}} {{.LCUR}}{{.RCUR}}{{.Annotations}}{{.EndLineComments}}` - - exceptionMultiLineTpl = `{{.Comments}}{{.Exception}} {{.Identifier}} {{.LCUR}} -{{.Fields}}{{.RCUR}}{{.Annotations}}{{.EndLineComments}} -` -) - -type ExceptionFormatter struct { - Comments string - Exception string - Identifier string - LCUR string - Fields string - RCUR string - Annotations string - EndLineComments string -} - -func MustFormatException(excep *parser.Exception, opts Options) string { - comments, annos := formatCommentsAndAnnos(opts, excep.Comments, excep.Annotations, "") - if len(excep.Comments) > 0 && lineDistance(excep.Comments[len(excep.Comments)-1], excep.ExceptionKeyword) > 1 { - comments = comments + "\n" - } - f := ExceptionFormatter{ - Comments: comments, - Exception: MustFormatKeyword(opts, excep.ExceptionKeyword.Keyword), - Identifier: MustFormatIdentifier(opts, excep.Name, ""), - LCUR: MustFormatKeyword(opts, excep.LCurKeyword.Keyword), - Fields: MustFormatFields(excep.Fields, opts, opts.GetIndent()), - RCUR: MustFormatKeyword(opts, excep.RCurKeyword.Keyword), - Annotations: annos, - EndLineComments: MustFormatEndLineComments(opts, excep.EndLineComments, "", ""), - } - - if len(excep.Fields) > 0 { - return MustFormat(exceptionMultiLineTpl, f) - } - - return MustFormat(exceptionOneLineTpl, f) -} diff --git a/format/field.go b/format/field.go deleted file mode 100644 index 529fd52..0000000 --- a/format/field.go +++ /dev/null @@ -1,160 +0,0 @@ -package format - -import ( - "bytes" - "fmt" - "strings" - "text/tabwriter" - - "github.com/joyme123/thrift-ls/parser" -) - -type fieldGroup []string - -func MustFormatFields(fields []*parser.Field, opts Options, indent string) string { - buf := bytes.NewBuffer(nil) - - fmtCtx := &fmtContext{} - - var fieldGroups []fieldGroup - var fg fieldGroup - for _, field := range fields { - if needAddtionalLineForFields(fmtCtx.preNode, field) { - fieldGroups = append(fieldGroups, fg) - fg = make(fieldGroup, 0) - } - space := " " - if opts.getAlign() == AlignTypeField { - space = "\t" - } - fg = append(fg, MustFormatField(field, opts, space, indent, false)) - fmtCtx.preNode = field - } - - if len(fg) > 0 { - fieldGroups = append(fieldGroups, fg) - } - - for i, fg := range fieldGroups { - w := new(tabwriter.Writer) - w.Init(buf, 1, 8, 1, ' ', tabwriter.TabIndent) - for j := range fg { - fmt.Fprintln(w, fg[j]) - } - w.Flush() - - if i < len(fieldGroups)-1 { - buf.WriteString("\n") - } - } - - return buf.String() -} - -func MustFormatOneLineFields(fields []*parser.Field, opts Options) string { - buf := bytes.NewBuffer(nil) - for i, field := range fields { - buf.WriteString(MustFormatField(field, opts, " ", "", true)) - if i < len(fields)-1 { - buf.WriteString(" ") - } - } - - return buf.String() -} - -func MustFormatField(field *parser.Field, opts Options, space string, indent string, oneline bool) string { - comments, annos := formatCommentsAndAnnos(opts, field.Comments, field.Annotations, indent) - if len(field.Comments) > 0 && lineDistance(field.Comments[len(field.Comments)-1], field.Index) > 1 { - comments = comments + "\n" - } - - buf := bytes.NewBuffer([]byte(comments)) - required := "" - if field.RequiredKeyword != nil { - required = MustFormatKeyword(opts, field.RequiredKeyword.Keyword) + space - } - - value := "" - if field.ConstValue != nil { - equalSpace := space - if opts.getAlign() == AlignTypeAssign { - equalSpace = "\t" - } - value = fmt.Sprintf("%s%s%s%s", equalSpace, MustFormatKeyword(opts, field.EqualKeyword.Keyword), equalSpace, MustFormatConstValue(field.ConstValue, opts, indent, false)) - } - str := fmt.Sprintf("%s%d:%s%s%s%s%s%s", indent, field.Index.Value, space, required, MustFormatFieldType(field.FieldType, opts), space, field.Identifier.Name.Text, value) - buf.WriteString(str) - buf.WriteString(annos) - if opts.getFieldLineComma() == FieldLineCommaAdd && !oneline { - buf.WriteString(",") - } else if opts.getFieldLineComma() == FieldLineCommaDisable || oneline { - buf.WriteString(formatListSeparator(opts, field.ListSeparatorKeyword)) - } - - if len(field.EndLineComments) > 0 { - buf.WriteString(MustFormatEndLineComments(opts, field.EndLineComments, "", "")) - } - - // remove space at end of line - return strings.TrimRight(buf.String(), " ") -} - -func MustFormatFieldType(ft *parser.FieldType, opts Options) string { - if ft == nil { - return "" - } - - annos := "" - if ft.Annotations != nil { - annos = MustFormatAnnotations(ft.Annotations, opts) - if len(ft.Annotations.Annotations) > 0 { - annos = " " + annos - } - } - - tn := MustFormatTypeName(ft.TypeName, opts) - - switch ft.TypeName.Name { - case "map": - return fmt.Sprintf("%s<%s, %s>%s", tn, MustFormatFieldType(ft.KeyType, opts), MustFormatFieldType(ft.ValueType, opts), annos) - case "set": - return fmt.Sprintf("%s<%s>%s", tn, MustFormatFieldType(ft.KeyType, opts), annos) - case "list": - return fmt.Sprintf("%s<%s>%s", tn, MustFormatFieldType(ft.KeyType, opts), annos) - default: - return tn + annos - } -} - -func MustFormatTypeName(tn *parser.TypeName, opts Options) string { - comments := MustFormatComments(opts, tn.Comments, "", "") - if len(tn.Comments) > 0 { - comments = comments + " " - } - - return comments + tn.Name -} - -func needAddtionalLineForFields(preNode, curNode parser.Node) bool { - if preNode == nil { - return false - } - - curField := curNode.(*parser.Field) - - var curStartLine int - if len(curField.Comments) > 0 { - curStartLine = curField.Comments[0].Pos().Line - } else { - if curField.Index != nil { - curStartLine = curField.Index.Pos().Line - } else if curField.RequiredKeyword != nil { - curStartLine = curField.RequiredKeyword.Pos().Line - } else { - curStartLine = curField.FieldType.Pos().Line - } - } - - return curStartLine-preNode.End().Line > 1 -} diff --git a/format/field_test.go b/format/field_test.go deleted file mode 100644 index 1c5d4c3..0000000 --- a/format/field_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package format - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func TestMustFormatFieldType(t *testing.T) { - type args struct { - ft *parser.FieldType - } - tests := []struct { - name string - args args - want string - }{ - { - name: "identifier type", - args: args{ - ft: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "User", - }, - }, - }, - want: "User", - }, - { - name: "map type", - args: args{ - ft: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "map", - Comments: []*parser.Comment{ - { - Text: "/* aaa */", - }, - }, - }, - KeyType: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "string", - Comments: []*parser.Comment{ - { - Text: "/* aaa */", - }, - }, - }, - }, - ValueType: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "i32", - Comments: []*parser.Comment{ - { - Text: "/* aaa */", - }, - }, - }, - }, - Annotations: &parser.Annotations{ - LParKeyword: &parser.LParKeyword{ - Keyword: parser.Keyword{ - Comments: []*parser.Comment{ - { - Text: "/* aaa */", - }, - }, - Literal: &parser.KeywordLiteral{ - Text: "(", - }, - }, - }, - RParKeyword: &parser.RParKeyword{ - Keyword: parser.Keyword{ - Comments: []*parser.Comment{ - { - Text: "/* aaa */", - }, - }, - Literal: &parser.KeywordLiteral{ - Text: ")", - }, - }, - }, - Annotations: []*parser.Annotation{ - { - Identifier: &parser.Identifier{ - Name: &parser.IdentifierName{ - Text: "key1", - }, - Comments: []*parser.Comment{ - { - Text: "/* aaa */", - }, - }, - }, - Value: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "value1", - }, - Quote: "'", - Comments: []*parser.Comment{ - { - Text: "/* aaa */", - }, - }, - }, - EqualKeyword: &parser.EqualKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "=", - }, - Comments: []*parser.Comment{ - { - Text: "/* aaa */", - }, - }, - }, - }, - }, - }, - }, - }, - }, - want: "/* aaa */ map /* aaa */ (/* aaa */ key1 /* aaa */ = /* aaa */ 'value1'/* aaa */ )", - }, - { - name: "set type", - args: args{ - ft: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "set", - }, - KeyType: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "string", - }, - }, - }, - }, - want: "set", - }, - { - name: "list type", - args: args{ - ft: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "list", - }, - KeyType: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "string", - }, - }, - }, - }, - want: "list", - }, - { - name: "embedding type", - args: args{ - ft: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "list", - }, - KeyType: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "map", - }, - KeyType: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "string", - }, - }, - ValueType: &parser.FieldType{ - TypeName: &parser.TypeName{ - Name: "string", - }, - }, - }, - }, - }, - want: "list>", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := Options{} - assert.Equal(t, tt.want, MustFormatFieldType(tt.args.ft, opts)) - }) - } -} diff --git a/format/function.go b/format/function.go deleted file mode 100644 index 42c6378..0000000 --- a/format/function.go +++ /dev/null @@ -1,160 +0,0 @@ -package format - -import ( - "bytes" - - "github.com/joyme123/thrift-ls/parser" -) - -func MustFormatFunctions(fns []*parser.Function, opts Options, indent string) string { - buf := bytes.NewBuffer(nil) - fmtCtx := &fmtContext{} - for i := range fns { - if needAddtionalLineForFuncs(fmtCtx.preNode, fns[i]) { - buf.WriteString("\n") - } - buf.WriteString(MustFormatFunction(fns[i], opts, indent)) - if i < len(fns)-1 { - buf.WriteString("\n") - } - fmtCtx.preNode = fns[i] - } - - return buf.String() -} - -const functionTpl = "{{.Oneway}}{{.FunctionType}} {{.Identifier}}{{.LPAR}}{{.Args}}{{.RPAR}}{{.Throws}}{{.Annotations}}{{.ListSeparator}}{{.EndLineComments}}" - -type FunctionFormatter struct { - Oneway string - FunctionType string - Identifier string - LPAR string - Args string - RPAR string - Throws string - Annotations string - ListSeparator string - EndLineComments string -} - -func MustFormatFunction(fn *parser.Function, opts Options, indent string) string { - comments, annos := formatCommentsAndAnnos(opts, fn.Comments, fn.Annotations, indent) - var firstNode parser.Node - if fn.Void != nil { - firstNode = fn.Void - } else { - firstNode = fn.FunctionType - } - if len(fn.Comments) > 0 && lineDistance(fn.Comments[len(fn.Comments)-1], firstNode) > 1 { - comments = comments + "\n" - } - - oneway := "" - if fn.Oneway != nil { - oneway = "oneway " - } - args := "" - if len(fn.Arguments) == 1 { - args = MustFormatOneLineFields(fn.Arguments, opts) - } else if len(fn.Arguments) > 1 { - indent := opts.GetIndent() - args = "\n" + MustFormatFields(fn.Arguments, opts, indent+indent) + indent - } - - ft := "" - if fn.Void != nil { - ft = MustFormatKeyword(opts, fn.Void.Keyword) - } else { - ft = MustFormatFieldType(fn.FunctionType, opts) - } - - sep := "" - - if opts.getFieldLineComma() == FieldLineCommaAdd { // add comma always - sep = "," - } else if opts.getFieldLineComma() == FieldLineCommaDisable { // add list separator - if fn.ListSeparatorKeyword != nil { - sep = MustFormatKeyword(opts, fn.ListSeparatorKeyword.Keyword) - } - } // otherwise, sep will be removed - - throws := MustFormatThrows(fn.Throws, opts) - if fn.Throws != nil { - throws = " " + throws - } - - f := &FunctionFormatter{ - Oneway: oneway, - FunctionType: ft, - Identifier: MustFormatIdentifier(opts, fn.Name, ""), - LPAR: MustFormatKeyword(opts, fn.LParKeyword.Keyword), - Args: args, - RPAR: MustFormatKeyword(opts, fn.RParKeyword.Keyword), - Throws: throws, - Annotations: annos, - ListSeparator: sep, - EndLineComments: MustFormatEndLineComments(opts, fn.EndLineComments, "", ""), - } - - fnStr := MustFormat(functionTpl, f) - fnStr = comments + indent + fnStr - - return fnStr -} - -const throwTpl = "{{.Throw}} {{.LPAR}}{{.Fields}}{{.RPAR}}" - -type ThrowFormatter struct { - Throw string - LPAR string - Fields string - RPAR string -} - -func MustFormatThrows(throws *parser.Throws, opts Options) string { - if throws == nil { - return "" - } - - args := "" - if len(throws.Fields) == 1 { - args = MustFormatOneLineFields(throws.Fields, opts) - } else if len(throws.Fields) > 1 { - indent := opts.GetIndent() - args = "\n" + MustFormatFields(throws.Fields, opts, indent+indent) + indent - } - - f := &ThrowFormatter{ - Throw: MustFormatKeyword(opts, throws.ThrowsKeyword.Keyword), - LPAR: MustFormatKeyword(opts, throws.LParKeyword.Keyword), - Fields: args, - RPAR: MustFormatKeyword(opts, throws.RParKeyword.Keyword), - } - - return MustFormat(throwTpl, f) - -} - -func needAddtionalLineForFuncs(preNode, curNode parser.Node) bool { - if preNode == nil { - return false - } - - curFunc := curNode.(*parser.Function) - - var curStartLine int - if len(curFunc.Comments) > 0 { - curStartLine = curFunc.Comments[0].Pos().Line - } else { - if curFunc.FunctionType != nil { - curStartLine = curFunc.FunctionType.Pos().Line - } else if curFunc.Void != nil { - curStartLine = curFunc.Void.Pos().Line - } else { - curStartLine = curFunc.Name.Pos().Line - } - } - - return curStartLine-preNode.End().Line > 1 -} diff --git a/format/identifier.go b/format/identifier.go deleted file mode 100644 index 0708b4e..0000000 --- a/format/identifier.go +++ /dev/null @@ -1,20 +0,0 @@ -package format - -import ( - "fmt" - - "github.com/joyme123/thrift-ls/parser" -) - -func MustFormatIdentifier(opts Options, id *parser.Identifier, indent string) string { - comments := MustFormatComments(opts, id.Comments, indent, "") - if comments != "" { - comments = comments - if lineDistance(id.Comments[len(id.Comments)-1], id.Name) >= 1 { - comments = comments + "\n" - } else { - comments = comments + " " - } - } - return fmt.Sprintf("%s%s", comments, indent+id.Name.Text) -} diff --git a/format/include.go b/format/include.go deleted file mode 100644 index df01ced..0000000 --- a/format/include.go +++ /dev/null @@ -1,46 +0,0 @@ -package format - -import ( - "github.com/joyme123/thrift-ls/parser" -) - -const includeTpl = "{{.Comments}}{{.Include}} {{.Path}}{{.EndLineComments}}\n" - -type IncludeFormatter struct { - Comments string - Include string - Path string - EndLineComments string -} - -func MustFormatInclude(inc *parser.Include, opts Options) string { - comments, _ := formatCommentsAndAnnos(opts, inc.Comments, nil, "") - if len(inc.Comments) > 0 && lineDistance(inc.Comments[len(inc.Comments)-1], inc.IncludeKeyword) > 1 { - comments = comments + "\n" - } - - f := &IncludeFormatter{ - Comments: comments, - Include: MustFormatKeyword(opts, inc.IncludeKeyword.Keyword), - Path: MustFormatLiteral(opts, inc.Path, ""), - EndLineComments: MustFormatComments(opts, inc.EndLineComments, "", ""), - } - - return MustFormat(includeTpl, f) -} - -func MustFormatCPPInclude(inc *parser.CPPInclude, opts Options) string { - comments, _ := formatCommentsAndAnnos(opts, inc.Comments, nil, "") - if len(inc.Comments) > 0 && lineDistance(inc.Comments[len(inc.Comments)-1], inc.CPPIncludeKeyword) > 1 { - comments = comments + "\n" - } - - f := &IncludeFormatter{ - Comments: comments, - Include: MustFormatKeyword(opts, inc.CPPIncludeKeyword.Keyword), - Path: MustFormatLiteral(opts, inc.Path, ""), - EndLineComments: MustFormatComments(opts, inc.EndLineComments, "", ""), - } - - return MustFormat(includeTpl, f) -} diff --git a/format/keyword.go b/format/keyword.go deleted file mode 100644 index d4053d0..0000000 --- a/format/keyword.go +++ /dev/null @@ -1,26 +0,0 @@ -package format - -import ( - "bytes" - - "github.com/joyme123/thrift-ls/parser" -) - -func MustFormatKeyword(opts Options, kw parser.Keyword) string { - if len(kw.Comments) > 0 { - buf := bytes.NewBuffer(nil) - buf.WriteString(MustFormatComments(opts, kw.Comments, "", "")) - - if lineDistance(kw.Comments[len(kw.Comments)-1], kw.Literal) >= 1 { - buf.WriteString("\n") - } else { - buf.WriteString(" ") - } - - buf.WriteString(kw.Literal.Text) - - return buf.String() - } - - return kw.Literal.Text -} diff --git a/format/literal.go b/format/literal.go deleted file mode 100644 index d22d099..0000000 --- a/format/literal.go +++ /dev/null @@ -1,27 +0,0 @@ -package format - -import ( - "bytes" - "fmt" - - "github.com/joyme123/thrift-ls/parser" -) - -func MustFormatLiteral(opts Options, l *parser.Literal, indent string) string { - if len(l.Comments) > 0 { - buf := bytes.NewBuffer(nil) - buf.WriteString(MustFormatComments(opts, l.Comments, indent, "")) - - if lineDistance(l.Comments[len(l.Comments)-1], l.Value) >= 1 { - buf.WriteString("\n") - buf.WriteString(indent) - } else { - buf.WriteString(" ") - } - - buf.WriteString(fmt.Sprintf("%s%s%s", l.Quote, l.Value.Text, l.Quote)) - - return buf.String() - } - return indent + fmt.Sprintf("%s%s%s", l.Quote, l.Value.Text, l.Quote) -} diff --git a/format/namespace.go b/format/namespace.go deleted file mode 100644 index 92f8159..0000000 --- a/format/namespace.go +++ /dev/null @@ -1,34 +0,0 @@ -package format - -import ( - "github.com/joyme123/thrift-ls/parser" -) - -const namespaceOneLineTpl = "{{.Comments}}{{.Namespace}} {{.Language}} {{.Name}}{{.Annotations}}{{.EndLineComments}}\n" - -type NamespaceFormatter struct { - Comments string - Namespace string - Language string - Name string - Annotations string - EndLineComments string -} - -func MustFormatNamespace(ns *parser.Namespace, opts Options) string { - comments, annos := formatCommentsAndAnnos(opts, ns.Comments, ns.Annotations, "") - if len(ns.Comments) > 0 && lineDistance(ns.Comments[len(ns.Comments)-1], ns.NamespaceKeyword) > 1 { - comments = comments + "\n" - } - - f := &NamespaceFormatter{ - Comments: comments, - Namespace: MustFormatKeyword(opts, ns.NamespaceKeyword.Keyword), - Language: MustFormatIdentifier(opts, &ns.Language.Identifier, ""), - Name: MustFormatIdentifier(opts, ns.Name, ""), - Annotations: annos, - EndLineComments: MustFormatEndLineComments(opts, ns.EndLineComments, "", ""), - } - - return MustFormat(namespaceOneLineTpl, f) -} diff --git a/format/options.go b/format/options.go deleted file mode 100644 index e187a16..0000000 --- a/format/options.go +++ /dev/null @@ -1,115 +0,0 @@ -package format - -import ( - "flag" - "strconv" - "strings" -) - -type Options struct { - // Do not print reformatted sources to standard output. - // If a file's formatting is different from thriftls's, overwrite it - // with thrfitls's version. - Write bool `yaml:"rewrite"` - - // Indent to use. Support: nspace(s), ntab(s). example: 4spaces, 1tab, tab - // if indent format is invalid or not specified, default is 4spaces - Indent string `yaml:"indent"` - - // Do not print reformatted sources to standard output. - // If a file's formatting is different than gofmt's, print diffs - // to standard output. - Diff bool `yaml:"diff"` - - // Align enables align option for struct/enum/exception/union fields - // Options: "field", "assign", "disable" - // Default is "field" if not set - Align string `yaml:"alignByAssign"` - - // FieldLineComma represents whether to add or remove comma at the end of field line. - // Options: "add", "remove", "disable" - // if choose disable, user input will be retained without modification - // Default is "disable" if not set - FieldLineComma string `yaml:"fieldLineComma"` - - // TrailingNewline controls whether to add a trailing newline at end of file. - // Default is false (no trailing newline added) - TrailingNewline bool `yaml:"trailingNewline"` -} - -func (o *Options) SetFlags() { - flag.BoolVar(&o.Write, "w", false, "Do not print reformatted sources to standard output. If a file's formatting is different from thriftls's, overwrite it with thrfitls's version.") - flag.BoolVar(&o.Diff, "d", false, "Do not print reformatted sources to standard output. If a file's formatting is different than gofmt's, print diffs to standard output.") - flag.StringVar(&o.Indent, "indent", "4spaces", "Indent to use. Support: num*space, num*tab. example: 4spaces, 1tab, tab") - flag.StringVar(&o.Align, "align", "field", `Align enables align option for struct/enum/exception/union fields, Options: "field", "assign", "disable", Default is "field" if not set.`) - flag.StringVar(&o.FieldLineComma, "fieldLineComma", "disable", `FieldLineComma enables whether to add or remove comma at end of field line. Options: "add", "remove", "disable". If choose disable, user input will be retained without modification. Default is "disable" if not set`) - flag.BoolVar(&o.TrailingNewline, "trailingNewline", false, "Add trailing newline at end of file") -} - -// InitDefault validates and sets default values for Options fields. -// It returns the Options with defaults applied for method chaining. -func (o *Options) InitDefault() Options { - if o.Align == "" || (o.Align != AlignTypeField && o.Align != AlignTypeAssign && o.Align != AlignTypeDisable) { - o.Align = "field" - } - - if o.FieldLineComma == "" || - (o.FieldLineComma != FieldLineCommaAdd && - o.FieldLineComma != FieldLineCommaRemove && - o.FieldLineComma != FieldLineCommaDisable) { - o.FieldLineComma = "disable" - } - - return *o -} - -// getAlign returns the align option, using default if empty. -func (o Options) getAlign() string { - if o.Align == "" { - return AlignTypeField - } - return o.Align -} - -// getFieldLineComma returns the field line comma option, using default if empty. -func (o Options) getFieldLineComma() string { - if o.FieldLineComma == "" { - return FieldLineCommaDisable - } - return o.FieldLineComma -} - -func (o *Options) GetIndent() string { - if o.Indent == "" { - o.Indent = "4spaces" - } - - indent := o.Indent - suffixes := []string{"spaces", "space", "tabs", "tab"} - for _, suffix := range suffixes { - if strings.HasSuffix(indent, suffix) { - char := "" - if strings.HasPrefix(suffix, "tab") { - char = " " - } else { - char = " " - } - num := 1 - numStr := strings.TrimSuffix(indent, suffix) - if len(numStr) == 0 { - num = 1 - } else { - num, _ = strconv.Atoi(numStr) - if num == 0 { - num = 4 - char = " " - } - } - - return strings.Repeat(char, num) - } - } - - return " " - -} diff --git a/format/options_test.go b/format/options_test.go deleted file mode 100644 index 16393b8..0000000 --- a/format/options_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package format - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOptions_GetIndent(t *testing.T) { - type fields struct { - Write bool - Indent string - } - tests := []struct { - name string - fields fields - want string - }{ - { - name: "4spaces", - fields: fields{ - Indent: "4spaces", - }, - want: " ", - }, - { - name: "1space", - fields: fields{ - Indent: "1spaces", - }, - want: " ", - }, - { - name: "space", - fields: fields{ - Indent: "space", - }, - want: " ", - }, - { - name: "2tabs", - fields: fields{ - Indent: "2tabs", - }, - want: "\t\t", - }, - { - name: "1tab", - fields: fields{ - Indent: "1tab", - }, - want: "\t", - }, - { - name: "tab", - fields: fields{ - Indent: "tab", - }, - want: "\t", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - o := &Options{ - Write: tt.fields.Write, - Indent: tt.fields.Indent, - } - assert.Equal(t, tt.want, o.GetIndent()) - }) - } -} diff --git a/format/service.go b/format/service.go deleted file mode 100644 index 9c7286a..0000000 --- a/format/service.go +++ /dev/null @@ -1,58 +0,0 @@ -package format - -import ( - "github.com/joyme123/thrift-ls/parser" -) - -const ( - serviceOneLineTpl = `{{.Comments}}{{.Service}} {{.Identifier}}{{.Extends}}{{.ExtendServiceName}} {{.LCUR}}{{.RCUR}}{{.Annotations}}{{.EndLineComments}}` - - serviceMultiLineTpl = `{{.Comments}}{{.Service}} {{.Identifier}}{{.Extends}}{{.ExtendServiceName}} {{.LCUR}} -{{.Functions}} -{{.RCUR}}{{.Annotations}}{{.EndLineComments}} -` -) - -type ServiceFormatter struct { - Comments string - Service string - Identifier string - LCUR string - Functions string - RCUR string - Annotations string - EndLineComments string - Extends string - ExtendServiceName string -} - -func MustFormatService(svc *parser.Service, opts Options) string { - comments, annos := formatCommentsAndAnnos(opts, svc.Comments, svc.Annotations, "") - if len(svc.Comments) > 0 && lineDistance(svc.Comments[len(svc.Comments)-1], svc.ServiceKeyword) > 1 { - comments = comments + "\n" - } - - f := ServiceFormatter{ - Comments: comments, - Service: MustFormatKeyword(opts, svc.ServiceKeyword.Keyword), - Identifier: MustFormatIdentifier(opts, svc.Name, ""), - LCUR: MustFormatKeyword(opts, svc.LCurKeyword.Keyword), - Functions: MustFormatFunctions(svc.Functions, opts, opts.GetIndent()), - RCUR: MustFormatKeyword(opts, svc.RCurKeyword.Keyword), - Annotations: annos, - EndLineComments: MustFormatEndLineComments(opts, svc.EndLineComments, "", ""), - } - - if svc.ExtendsKeyword != nil { - f.Extends = " " + MustFormatKeyword(opts, svc.ExtendsKeyword.Keyword) - } - if svc.Extends != nil { - f.ExtendServiceName = " " + MustFormatIdentifier(opts, svc.Extends, "") - } - - if len(svc.Functions) > 0 { - return MustFormat(serviceMultiLineTpl, f) - } - - return MustFormat(serviceOneLineTpl, f) -} diff --git a/format/struct.go b/format/struct.go deleted file mode 100644 index 30e35f8..0000000 --- a/format/struct.go +++ /dev/null @@ -1,49 +0,0 @@ -package format - -import ( - "github.com/joyme123/thrift-ls/parser" -) - -const ( - structOneLineTpl = `{{.Comments}}{{.Struct}} {{.Identifier}} {{.LCUR}}{{.RCUR}}{{.Annotations}}{{.EndLineComments}}` - - structMultiLineTpl = `{{.Comments}}{{.Struct}} {{.Identifier}} {{.LCUR}} -{{.Fields}}{{.RCUR}}{{.Annotations}}{{.EndLineComments}} -` -) - -type StructFormatter struct { - Comments string - Struct string - Identifier string - LCUR string - Fields string - RCUR string - Annotations string - EndLineComments string -} - -func MustFormatStruct(st *parser.Struct, opts Options) string { - comments, annos := formatCommentsAndAnnos(opts, st.Comments, st.Annotations, "") - - if len(st.Comments) > 0 && lineDistance(st.Comments[len(st.Comments)-1], st.StructKeyword) > 1 { - comments = comments + "\n" - } - - f := StructFormatter{ - Comments: comments, - Struct: MustFormatKeyword(opts, st.StructKeyword.Keyword), - Identifier: MustFormatIdentifier(opts, st.Identifier, ""), - LCUR: MustFormatKeyword(opts, st.LCurKeyword.Keyword), - Fields: MustFormatFields(st.Fields, opts, opts.GetIndent()), - RCUR: MustFormatKeyword(opts, st.RCurKeyword.Keyword), - Annotations: annos, - EndLineComments: MustFormatEndLineComments(opts, st.EndLineComments, "", ""), - } - - if len(st.Fields) > 0 { - return MustFormat(structMultiLineTpl, f) - } - - return MustFormat(structOneLineTpl, f) -} diff --git a/format/struct_test.go b/format/struct_test.go deleted file mode 100644 index 6150679..0000000 --- a/format/struct_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package format - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func TestMustFormatStruct(t *testing.T) { - doc := ` -// comments - - -/* - * comments - */ -struct test { - /* - * field 1 - */ - 1: required string test, -} (a.b = "c") // endline comments -` - - type args struct { - st *parser.Struct - } - tests := []struct { - name string - args args - want string - }{ - { - name: "test", - args: args{ - st: func() *parser.Struct { - ast, err := parser.Parse("test.thrift", []byte(doc)) - assert.NoError(t, err) - return ast.(*parser.Document).Structs[0] - }(), - }, - want: `// comments - -/* - * comments - */ -struct test { - /* - * field 1 - */ - 1: required string test, -} (a.b = "c") // endline comments -`, - }, - { - name: "test with CRLF", - args: args{ - st: func() *parser.Struct { - ast, err := parser.Parse("test.thrift", []byte("struct test {\r\n 1: required string test,\r\n 2: required string test2\r\n}")) - assert.NoError(t, err) - return ast.(*parser.Document).Structs[0] - }(), - }, - want: "struct test {\n 1: required string test,\n 2: required string test2\n}\n", - }, - { - name: "test with LF", - args: args{ - st: func() *parser.Struct { - ast, err := parser.Parse("test.thrift", []byte("struct test {\n 1: required string test,\n 2: required string test2\n}")) - assert.NoError(t, err) - return ast.(*parser.Document).Structs[0] - }(), - }, - want: "struct test {\n 1: required string test,\n 2: required string test2\n}\n", - }, - { - name: "test with additional LF", - args: args{ - st: func() *parser.Struct { - ast, err := parser.Parse("test.thrift", []byte("struct test {\n 1: required string test,\n\n 2: required string test2\n}")) - assert.NoError(t, err) - return ast.(*parser.Document).Structs[0] - }(), - }, - want: "struct test {\n 1: required string test,\n\n 2: required string test2\n}\n", - }, - { - name: "test struct annotation case 1", - args: args{ - st: func() *parser.Struct { - ast, err := parser.Parse("test.thrift", []byte(` -struct Foo { - // 用户名列表 - 2: list strings ( - custom_tag_1 = "1" - custom_tag_2 = "2" - ) -} -`)) - assert.NoError(t, err) - return ast.(*parser.Document).Structs[0] - }(), - }, - want: `struct Foo { - // 用户名列表 - 2: list strings ( - custom_tag_1 = "1" - custom_tag_2 = "2" - ) -} -`, - }, - { - name: "test struct annotation case 2", - args: args{ - st: func() *parser.Struct { - ast, err := parser.Parse("test.thrift", []byte(` -struct Foo { - // 用户名列表 - 2: list strings ( - custom_tag_1 = "1", - // 自定义标签2 - custom_tag_2 = "2" - ) -} -`)) - assert.NoError(t, err) - return ast.(*parser.Document).Structs[0] - }(), - }, - want: `struct Foo { - // 用户名列表 - 2: list strings ( - custom_tag_1 = "1", - // 自定义标签2 - custom_tag_2 = "2" - ) -} -`, - }, - { - name: "test struct annotation case 3", - args: args{ - st: func() *parser.Struct { - ast, err := parser.Parse("test.thrift", []byte(` -struct Foo { - // 用户名列表 - 2: list strings (custom_tag_1 = "1", /* 自定义标签2 */ custom_tag_2 = "2") -} -`)) - assert.NoError(t, err) - return ast.(*parser.Document).Structs[0] - }(), - }, - want: `struct Foo { - // 用户名列表 - 2: list strings (custom_tag_1 = "1", /* 自定义标签2 */ custom_tag_2 = "2") -} -`, - }, - { - name: "test struct annotation case 4", - args: args{ - st: func() *parser.Struct { - ast, err := parser.Parse("test.thrift", []byte(` -struct Foo { - // 用户名列表 - 2: list strings ( - custom_tag_1 = "1", /* 自定义标签2 */ custom_tag_2 = "2" - ) -} -`)) - assert.NoError(t, err) - return ast.(*parser.Document).Structs[0] - }(), - }, - want: `struct Foo { - // 用户名列表 - 2: list strings ( - custom_tag_1 = "1", /* 自定义标签2 */ custom_tag_2 = "2" - ) -} -`, - }, - { - name: "test struct annotation case 5", - args: args{ - st: func() *parser.Struct { - ast, err := parser.Parse("test.thrift", []byte(` -struct Foo { - // 用户名列表 - 2: list strings ( - custom_tag_1 = "1", custom_tag_2 = "2" - ) -} -`)) - assert.NoError(t, err) - return ast.(*parser.Document).Structs[0] - }(), - }, - want: `struct Foo { - // 用户名列表 - 2: list strings ( - custom_tag_1 = "1", custom_tag_2 = "2" - ) -} -`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := Options{} - assert.Equal(t, tt.want, MustFormatStruct(tt.args.st, opts)) - }) - } -} diff --git a/format/typedef.go b/format/typedef.go deleted file mode 100644 index b95650d..0000000 --- a/format/typedef.go +++ /dev/null @@ -1,36 +0,0 @@ -package format - -import ( - "github.com/joyme123/thrift-ls/parser" -) - -const typedefOneLineTpl = `{{.Comments}}{{.Typedef}} {{.Type}} {{.Name}}{{.Annotations}}{{.EndLineComments}} -` - -type TypedefFormatter struct { - Comments string - Typedef string - Type string - Name string - Annotations string - EndLineComments string -} - -func MustFormatTypedef(td *parser.Typedef, opts Options) string { - comments, annos := formatCommentsAndAnnos(opts, td.Comments, td.Annotations, "") - - if len(td.Comments) > 0 && lineDistance(td.Comments[len(td.Comments)-1], td.TypedefKeyword) > 1 { - comments = comments + "\n" - } - - f := &TypedefFormatter{ - Comments: comments, - Typedef: MustFormatKeyword(opts, td.TypedefKeyword.Keyword), - Type: MustFormatFieldType(td.T, opts), - Name: MustFormatIdentifier(opts, td.Alias, ""), - Annotations: annos, - EndLineComments: MustFormatEndLineComments(opts, td.EndLineComments, "", ""), - } - - return MustFormat(typedefOneLineTpl, f) -} diff --git a/format/union.go b/format/union.go deleted file mode 100644 index 4bf8304..0000000 --- a/format/union.go +++ /dev/null @@ -1,49 +0,0 @@ -package format - -import ( - "github.com/joyme123/thrift-ls/parser" -) - -const ( - unionOneLineTpl = `{{.Comments}}{{.Union}} {{.Identifier}} {{.LCUR}}{{.RCUR}}{{.Annotations}}{{.EndLineComments}}` - - unionMultiLineTpl = `{{.Comments}}{{.Union}} {{.Identifier}} {{.LCUR}} -{{.Fields}}{{.RCUR}}{{.Annotations}}{{.EndLineComments}} -` -) - -type UnionFormatter struct { - Comments string - Union string - Identifier string - LCUR string - Fields string - RCUR string - Annotations string - EndLineComments string -} - -func MustFormatUnion(union *parser.Union, opts Options) string { - comments, annos := formatCommentsAndAnnos(opts, union.Comments, union.Annotations, "") - - if len(union.Comments) > 0 && lineDistance(union.Comments[len(union.Comments)-1], union.UnionKeyword) > 1 { - comments = comments + "\n" - } - - f := UnionFormatter{ - Comments: comments, - Union: MustFormatKeyword(opts, union.UnionKeyword.Keyword), - Identifier: MustFormatIdentifier(opts, union.Name, ""), - LCUR: MustFormatKeyword(opts, union.LCurKeyword.Keyword), - Fields: MustFormatFields(union.Fields, opts, opts.GetIndent()), - RCUR: MustFormatKeyword(opts, union.RCurKeyword.Keyword), - Annotations: annos, - EndLineComments: MustFormatEndLineComments(opts, union.EndLineComments, "", ""), - } - - if len(union.Fields) > 0 { - return MustFormat(unionMultiLineTpl, f) - } - - return MustFormat(unionOneLineTpl, f) -} diff --git a/format/utils.go b/format/utils.go deleted file mode 100644 index ade3bf7..0000000 --- a/format/utils.go +++ /dev/null @@ -1,120 +0,0 @@ -package format - -import ( - "bytes" - "fmt" - "text/template" - "unicode" - - "github.com/joyme123/thrift-ls/parser" -) - -const ( - AlignTypeAssign = "assign" - AlignTypeField = "field" - AlignTypeDisable = "disable" -) - -const ( - FieldLineCommaAdd = "add" - FieldLineCommaRemove = "remove" - FieldLineCommaDisable = "disable" -) - -func MustFormat(tplText string, formatter any) string { - tpl, err := template.New("default").Parse(tplText) - if err != nil { - panic(err) - } - - buf := bytes.NewBuffer(nil) - err = tpl.Execute(buf, formatter) - if err != nil { - panic(err) - } - - return buf.String() -} - -func formatCommentsAndAnnos(opts Options, comments []*parser.Comment, annotations *parser.Annotations, indent string) (string, string) { - commentsStr := "" - if len(comments) > 0 { - commentsStr = MustFormatComments(opts, comments, indent, "") + "\n" - } - annos := "" - if annotations != nil && len(annotations.Annotations) > 0 { - annos = " " + MustFormatAnnotations(annotations, opts) - } - - return commentsStr, annos -} - -func formatListSeparator(opts Options, sep *parser.ListSeparatorKeyword) string { - if sep == nil { - return "" - } - - return MustFormatKeyword(opts, sep.Keyword) -} - -func lineDistance(preNode parser.Node, currentNode parser.Node) int { - return currentNode.Pos().Line - preNode.End().Line -} - -// FormatedEquals is used to judge if documents has been changed after formated -// implementation: ignore all space charactor to compare two strings -func EqualsAfterFormat(doc1, doc2 string) error { - cur1, cur2 := 0, 0 - runes1 := []rune(doc1) - runes2 := []rune(doc2) - - for cur1 < len(runes1) && cur2 < len(runes2) { - for cur1 < len(runes1) && unicode.IsSpace(runes1[cur1]) { - cur1++ - } - for cur2 < len(runes2) && unicode.IsSpace(runes2[cur2]) { - cur2++ - } - - if cur1 >= len(runes1) || cur2 >= len(runes2) { - break - } - - if runes1[cur1] != runes2[cur2] { - return fmt.Errorf("different at doc1: %s as %d, doc2: %s at %d, str1: %s, str2: %s", string(runes1[cur1]), cur1, string(runes2[cur2]), cur2, showStringContext(runes1, cur1, 40), showStringContext(runes2, cur2, 40)) - } - - cur1++ - cur2++ - } - - for cur1 < len(runes1) { - for !unicode.IsSpace(runes1[cur1]) { - return fmt.Errorf("") - } - cur1++ - } - - for cur2 < len(runes2) { - for !unicode.IsSpace(runes2[cur2]) { - return fmt.Errorf("") - } - cur2++ - } - - return nil -} - -func showStringContext(text []rune, offset int, n int) string { - start := offset - n - if start < 0 { - start = 0 - } - - end := offset + n - if end >= len(text) { - end = len(text) - 1 - } - - return string(text[start : end+1]) -} diff --git a/format/utils_test.go b/format/utils_test.go deleted file mode 100644 index 37c76f9..0000000 --- a/format/utils_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package format - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func TestEqualsAfterFormat(t *testing.T) { - - ast, err := parser.Parse("test.thrift", []byte(ThriftTestContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - opts := Options{} - formated, err := FormatDocument(ast.(*parser.Document), opts) - assert.NoError(t, err) - type args struct { - doc1 string - doc2 string - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "equals", - args: args{ - - doc1: ThriftTestContent, - doc2: formated, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := EqualsAfterFormat(tt.args.doc1, tt.args.doc2) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} diff --git a/formatter/body.go b/formatter/body.go new file mode 100644 index 0000000..714530d --- /dev/null +++ b/formatter/body.go @@ -0,0 +1,236 @@ +package formatter + +import ( + "github.com/karitham/thrift-ls/doc" + "github.com/karitham/thrift-ls/syntax" +) + +// structLike formats struct, union, and exception declarations. +func (f *formatter) structLike(v *syntax.Struct) doc.Doc { + parts := []doc.Doc{ + doc.Text(v.Kind.String()), + doc.Text(" "), + doc.Text(v.Name.Text), + f.bracedBody(v.Fields), + } + parts = append(parts, f.annotationsDoc(v.Annotations)) + return doc.Concat(parts) +} + +// enum formats an enum declaration. +func (f *formatter) enum(v *syntax.Enum) doc.Doc { + parts := []doc.Doc{ + doc.Text("enum "), + doc.Text(v.Name.Text), + f.bracedEnumBody(v.Values), + } + parts = append(parts, f.annotationsDoc(v.Annotations)) + return doc.Concat(parts) +} + +// bracedBody formats "{ fields }": flat as "S { 1: i32 a }" when it fits, +// otherwise one field per line. Field alignment and trailing separators +// switch on the body group breaking. +func (f *formatter) bracedBody(fields []*syntax.Field) doc.Doc { + if len(fields) == 0 { + return doc.Text(" {}") + } + bodyID := f.id() + return doc.GroupID(bodyID, doc.Concat{ + doc.Text(" {"), + doc.Indent(doc.Concat{doc.Line, f.fieldList(fields, bodyID)}), + doc.IfBreak(doc.SoftLine, doc.Text(" ")), + doc.Text("}"), + }) +} + +// bracedEnumBody is bracedBody for enum values. +func (f *formatter) bracedEnumBody(values []*syntax.EnumValue) doc.Doc { + if len(values) == 0 { + return doc.Text(" {}") + } + bodyID := f.id() + return doc.GroupID(bodyID, doc.Concat{ + doc.Text(" {"), + doc.Indent(doc.Concat{doc.Line, f.enumValueList(values, bodyID)}), + doc.IfBreak(doc.SoftLine, doc.Text(" ")), + doc.Text("}"), + }) +} + +// service formats a service declaration. The body is always multiline: +// functions are too complex to flatten. +func (f *formatter) service(v *syntax.Service) doc.Doc { + var parts []doc.Doc + for i, fn := range v.Functions { + if i > 0 { + parts = append(parts, doc.HardLineNoBreak) + if f.blankBefore(fn) >= 1 { + parts = append(parts, doc.HardLineNoBreak) + } + } + parts = append(parts, f.function(fn)) + } + + body := doc.Concat{ + doc.Text(" {"), + doc.Indent(doc.Concat{doc.HardLineNoBreak, doc.Concat(parts)}), + doc.HardLineNoBreak, + doc.Text("}"), + } + + out := []doc.Doc{ + doc.Text("service "), + doc.Text(v.Name.Text), + } + if v.Extends != nil { + out = append(out, doc.Text(" extends "), doc.Text(v.Extends.Text)) + } + out = append(out, body) + out = append(out, f.annotationsDoc(v.Annotations)) + return doc.Concat(out) +} + +// function formats a service method with the signature escalation: +// +// 1. the whole signature on one line, when it fits; +// 2. arguments on one line, the throws clause broken; +// 3. arguments and throws clause both broken. +// +// Each state is tried in order by the conditional group, so the escalation +// is decided by the remaining width at the function's position. +func (f *formatter) function(v *syntax.Function) doc.Doc { + parts := append(f.leadingComments(v), f.functionBody(v)) + parts = append(parts, f.trailingComments(v)...) + return doc.Concat(parts) +} + +func (f *formatter) functionBody(v *syntax.Function) doc.Doc { + // Comments or blank lines inside the argument or throws lists force the + // multiline layout: the flat states would drop them. + if f.fieldsForcedBroken(v.Args) || v.Throws != nil && f.fieldsForcedBroken(v.Throws.Fields) { + return f.functionBrokenArgs(v) + } + + if v.Throws == nil { + return doc.ConditionalGroup(0, + f.functionFlat(v, false), + f.functionBrokenArgs(v), + ) + } + return doc.ConditionalGroup(0, + f.functionFlat(v, false), + f.functionFlat(v, true), + f.functionBrokenArgs(v), + ) +} + +// functionHeader renders "[oneway] ". +func (f *formatter) functionHeader(v *syntax.Function) string { + out := "" + if v.Oneway != nil { + out += "oneway " + } + if v.Void != nil { + out += "void " + } else { + out += typeText(v.Type) + " " + } + return out + v.Name.Text +} + +// functionFlat renders the signature with args flat and throws either flat +// or broken. The states are printed flat (or measured flat), so line docs +// render as spaces; throwsBroken inserts hard lines to break the clause. +func (f *formatter) functionFlat(v *syntax.Function, throwsBroken bool) doc.Doc { + parts := []doc.Doc{ + doc.Text(f.functionHeader(v)), + doc.Text("("), + doc.Join(doc.Text(", "), f.flatFields(v.Args)), + doc.Text(")"), + } + if v.Throws != nil { + if throwsBroken { + parts = append(parts, doc.Text(" throws "), f.brokenParens("(", ")", v.Throws.Fields)) + } else { + parts = append(parts, + doc.Text(" throws ("), + doc.Join(doc.Text(", "), f.flatFields(v.Throws.Fields)), + doc.Text(")"), + ) + } + } + parts = append(parts, f.annotationsDoc(v.Annotations)) + return doc.Concat(parts) +} + +// functionBrokenArgs renders the signature with arguments and throws both +// broken, one per line. +func (f *formatter) functionBrokenArgs(v *syntax.Function) doc.Doc { + parts := []doc.Doc{ + doc.Text(f.functionHeader(v)), + f.brokenParens("(", ")", v.Args), + } + if v.Throws != nil { + parts = append(parts, doc.Text(" throws "), f.brokenParens("(", ")", v.Throws.Fields)) + } + parts = append(parts, f.annotationsDoc(v.Annotations)) + return doc.Concat(parts) +} + +// flatFields renders fields joined by ", " on one line, without comments. +func (f *formatter) flatFields(fields []*syntax.Field) []doc.Doc { + out := make([]doc.Doc, 0, len(fields)) + for _, field := range fields { + out = append(out, f.fieldContent(field, nil, false)) + } + return out +} + +// brokenFields renders fields one per line, each with its trailing +// separator per the FieldLineComma option. Comments and blank lines inside +// the list are preserved. +func (f *formatter) brokenFields(fields []*syntax.Field) doc.Doc { + var parts []doc.Doc + for i, field := range fields { + if i > 0 { + parts = append(parts, doc.HardLineNoBreak) + if f.blankBefore(field) >= 1 { + parts = append(parts, doc.HardLineNoBreak) + } + } + content := doc.Concat{f.fieldContent(field, nil, false), f.trailingSep(field.Sep)} + fieldDoc := append(f.leadingComments(field), content) + fieldDoc = append(fieldDoc, f.trailingComments(field)...) + parts = append(parts, doc.Concat(fieldDoc)) + } + return doc.Concat(parts) +} + +// brokenParens renders "open, fields, close" one field per line, or just +// "openclose" when there are no fields. +func (f *formatter) brokenParens(open, close string, fields []*syntax.Field) doc.Doc { + if len(fields) == 0 { + return doc.Text(open + close) + } + return doc.Concat{ + doc.Text(open), + doc.Indent(doc.Concat{doc.HardLineNoBreak, f.brokenFields(fields)}), + doc.HardLineNoBreak, + doc.Text(close), + } +} + +// fieldsForcedBroken reports whether any field has comments or blank lines +// that require the multiline function layout. +func (f *formatter) fieldsForcedBroken(fields []*syntax.Field) bool { + for _, field := range fields { + if f.blankBefore(field) >= 1 { + return true + } + if len(f.token(field.TokStart()).Leading) > 0 || len(f.token(field.TokEnd()).Trailing) > 0 { + return true + } + } + return false +} diff --git a/formatter/field.go b/formatter/field.go new file mode 100644 index 0000000..b4b86af --- /dev/null +++ b/formatter/field.go @@ -0,0 +1,254 @@ +package formatter + +import ( + "strings" + + "github.com/karitham/thrift-ls/doc" + "github.com/karitham/thrift-ls/syntax" +) + +// fieldList formats struct-like body fields. Fields are joined with ", " +// when the body stays on one line and with newlines otherwise; the comma in +// break mode comes from each field's own trailing separator, driven by the +// FieldLineComma option. Blank lines between fields are preserved and force +// the body to break. Column alignment applies per blank-line group, +// matching the previous formatter. +func (f *formatter) fieldList(fields []*syntax.Field, bodyID int) doc.Doc { + sep := doc.IfBreak(doc.Line, doc.Concat{doc.Text(","), doc.Line}) + var parts []doc.Doc + for i, field := range fields { + if i > 0 { + parts = append(parts, sep) + if f.blankBefore(field) >= 1 { + parts = append(parts, doc.HardLine) + } + } + parts = append(parts, f.field(field, f.alignmentFor(fields, i), bodyID)) + } + return doc.Concat(parts) +} + +// enumValueList formats enum bodies with the same layout as fieldList. +func (f *formatter) enumValueList(values []*syntax.EnumValue, bodyID int) doc.Doc { + sep := doc.IfBreak(doc.Line, doc.Concat{doc.Text(","), doc.Line}) + var parts []doc.Doc + for i, value := range values { + if i > 0 { + parts = append(parts, sep) + if f.blankBefore(value) >= 1 { + parts = append(parts, doc.HardLine) + } + } + parts = append(parts, f.enumValue(value, f.alignmentForEnum(values, i), bodyID)) + } + return doc.Concat(parts) +} + +// alignmentFor returns the column alignment for field i, or nil when +// alignment is disabled. Alignment is scoped to the blank-line group the +// field belongs to. +func (f *formatter) alignmentFor(fields []*syntax.Field, i int) *columnAlign { + if f.opts.Align == AlignDisable { + return nil + } + start := i + for start > 0 && f.blankBefore(fields[start]) < 1 { + start-- + } + end := i + for end+1 < len(fields) && f.blankBefore(fields[end+1]) < 1 { + end++ + } + return computeFieldAlign(fields[start : end+1]) +} + +func (f *formatter) alignmentForEnum(values []*syntax.EnumValue, i int) *columnAlign { + if f.opts.Align == AlignDisable { + return nil + } + start := i + for start > 0 && f.blankBefore(values[start]) < 1 { + start-- + } + end := i + for end+1 < len(values) && f.blankBefore(values[end+1]) < 1 { + end++ + } + return computeEnumAlign(values[start : end+1]) +} + +// columnAlign holds the computed column widths for one aligned group. +type columnAlign struct { + hasReq bool + idWidth int // width of the "N:" prefix column + reqWidth int // width of the required/optional column + typeWidth int // width of the type column + nameWidth int // for AlignAssign: max name width among fields with values + enumAssign bool +} + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + +// computeFieldAlign computes column widths for a group of fields. +func computeFieldAlign(fields []*syntax.Field) *columnAlign { + a := &columnAlign{} + for _, field := range fields { + if field.FieldID != nil { + a.idWidth = maxInt(a.idWidth, len(field.FieldID.Text)+1) // "N:" + } + if field.Req != 0 { + a.hasReq = true + a.reqWidth = maxInt(a.reqWidth, len(field.Req.String())) + } + a.typeWidth = maxInt(a.typeWidth, len(typeText(field.Type))) + if field.Value != nil { + a.nameWidth = maxInt(a.nameWidth, len(field.Name.Text)) + } + } + return a +} + +// computeEnumAlign computes the name width for aligning '=' signs. +func computeEnumAlign(values []*syntax.EnumValue) *columnAlign { + a := &columnAlign{enumAssign: true} + for _, value := range values { + if value.Value != nil { + a.nameWidth = maxInt(a.nameWidth, len(value.Name.Text)) + } + } + return a +} + +// field assembles a struct-like body field: leading comments, content, and +// trailing comments. When the body breaks (referenced by bodyID), the +// content switches to its column-aligned form with the trailing separator. +func (f *formatter) field(v *syntax.Field, align *columnAlign, bodyID int) doc.Doc { + content := f.fieldContent(v, align, false) + if bodyID != 0 { + content = doc.IfBreakFor( + doc.Concat{f.fieldContent(v, align, true), f.trailingSep(v.Sep)}, + content, + bodyID, + ) + } + parts := append(f.leadingComments(v), content) + parts = append(parts, f.trailingComments(v)...) + return doc.Concat(parts) +} + +// fieldContent renders the field line: id, requiredness, type, reference, +// name, default value, and annotations. padded selects the column-aligned +// form used when the enclosing body breaks; it has no effect when align is +// nil. +func (f *formatter) fieldContent(v *syntax.Field, align *columnAlign, padded bool) doc.Doc { + padded = padded && align != nil + var parts []doc.Doc + + if v.FieldID != nil { + id := v.FieldID.Text + ":" + if padded { + id = padRight(id, align.idWidth) + } + parts = append(parts, doc.Text(id), doc.Text(" ")) + } + + if v.Req != 0 || padded && align.hasReq { + req := "" + if v.Req != 0 { + req = v.Req.String() + } + if padded { + req = padRight(req, align.reqWidth) + } + parts = append(parts, doc.Text(req), doc.Text(" ")) + } + + typ := doc.Text(typeText(v.Type)) + if padded && f.opts.Align == AlignField { + typ = doc.Text(padRight(typeText(v.Type), align.typeWidth)) + } + parts = append(parts, typ, f.annotationsDoc(v.Type.Annotations), doc.Text(" ")) + + if v.Reference { + parts = append(parts, doc.Text("&")) + } + + name := doc.Text(v.Name.Text) + if padded && v.Value != nil && align.nameWidth > 0 { + // Fields with values have their name padded so the "=" signs + // align, in both align modes. + name = doc.Text(padRight(v.Name.Text, align.nameWidth)) + } + parts = append(parts, name) + + if v.Value != nil { + parts = append(parts, doc.Text(" = "), f.constValue(v.Value)) + } + + parts = append(parts, f.annotationsDoc(v.Annotations)) + return doc.Concat(parts) +} + +// enumValue assembles an enum value with comments, aligning '=' signs when +// the body breaks. +func (f *formatter) enumValue(v *syntax.EnumValue, align *columnAlign, bodyID int) doc.Doc { + content := f.enumValueContent(v, align, false) + if bodyID != 0 { + content = doc.IfBreakFor( + doc.Concat{f.enumValueContent(v, align, true), f.trailingSep(v.Sep)}, + content, + bodyID, + ) + } + parts := append(f.leadingComments(v), content) + parts = append(parts, f.trailingComments(v)...) + return doc.Concat(parts) +} + +func (f *formatter) enumValueContent(v *syntax.EnumValue, align *columnAlign, padded bool) doc.Doc { + padded = padded && align != nil + + name := doc.Text(v.Name.Text) + if padded && align.enumAssign { + name = doc.Text(padRight(v.Name.Text, align.nameWidth)) + } + parts := []doc.Doc{name} + + if v.Value != nil { + parts = append(parts, doc.Text(" = "), doc.Text(v.Value.Text)) + } + + parts = append(parts, f.annotationsDoc(v.Annotations)) + return doc.Concat(parts) +} + +// trailingSep returns the trailing separator for the given original +// separator, per the FieldLineComma option: always a comma when adding, +// nothing when removing, the original separator when preserving. +func (f *formatter) trailingSep(sep syntax.TokenKind) doc.Doc { + switch f.opts.FieldLineComma { + case CommaAdd: + return doc.Text(",") + case CommaRemove: + return doc.Concat{} + default: + switch sep { + case syntax.TokenComma, syntax.TokenSemicolon: + return doc.Text(",") + } + return doc.Concat{} + } +} + +// padRight pads s with trailing spaces to width w. +func padRight(s string, w int) string { + if n := w - len(s); n > 0 { + return s + strings.Repeat(" ", n) + } + return s +} diff --git a/formatter/format.go b/formatter/format.go new file mode 100644 index 0000000..f26d26c --- /dev/null +++ b/formatter/format.go @@ -0,0 +1,313 @@ +// Package formatter turns a parsed thrift document into a doc IR document +// and renders it. It is the pure core of the formatting pipeline: parsing +// and file I/O happen in the caller (CLI, LSP), and the formatter never +// touches the filesystem. +// +// Layout decisions are width-driven: every construct is a group that stays +// on one line when it fits and breaks otherwise, with nested groups +// deciding independently based on the remaining width at their position. +package formatter + +import ( + "errors" + "fmt" + + "github.com/karitham/thrift-ls/doc" + "github.com/karitham/thrift-ls/syntax" +) + +// AlignMode controls column alignment of struct-like bodies and enum values. +type AlignMode uint8 + +const ( + // AlignField aligns the id, requiredness, and type columns of fields. + AlignField AlignMode = iota + // AlignAssign aligns the '=' sign of fields and enum values that have + // default values. + AlignAssign + // AlignDisable disables alignment. + AlignDisable +) + +// CommaMode controls trailing separators after fields, enum values, and +// function signatures. +type CommaMode uint8 + +const ( + // CommaPreserve keeps the original separators (',', ';', or none). + CommaPreserve CommaMode = iota + // CommaAdd adds a trailing comma everywhere. + CommaAdd + // CommaRemove removes all trailing separators. + CommaRemove +) + +// Options controls formatting behavior. Zero values mean defaults. +type Options struct { + // PrintWidth is the target line width. Must be positive. + PrintWidth int + // Indent is the string emitted for one indentation level. + Indent string + // TabWidth is the display width of one indentation level. Must be + // positive. + TabWidth int + // Align controls column alignment (default AlignField). + Align AlignMode + // FieldLineComma controls trailing separators (default CommaPreserve). + FieldLineComma CommaMode + // NoTrailingNewline suppresses the final newline that is otherwise + // appended to the formatted output. + NoTrailingNewline bool +} + +// DefaultOptions returns the default formatting options. +func DefaultOptions() Options { + return Options{ + PrintWidth: 80, + Indent: " ", + TabWidth: 4, + Align: AlignField, + FieldLineComma: CommaPreserve, + } +} + +// normalize fills zero values with defaults. +func (o Options) normalize() Options { + d := DefaultOptions() + if o.PrintWidth <= 0 { + o.PrintWidth = d.PrintWidth + } + if o.Indent == "" { + o.Indent = d.Indent + } + if o.TabWidth <= 0 { + o.TabWidth = d.TabWidth + } + return o +} + +// Format renders a parsed document. The document must have been parsed +// without errors; callers check the parse errors before formatting. Format +// is deterministic and pure: it reads nothing but the document and options. +func Format(d *syntax.Document, o Options) (string, error) { + if d == nil { + return "", errors.New("formatter: nil document") + } + o = o.normalize() + + f := &formatter{ + doc: d, + toks: d.Tokens, + opts: o, + } + ir := f.document() + return doc.Print(ir, doc.Options{ + PrintWidth: o.PrintWidth, + Indent: o.Indent, + TabWidth: o.TabWidth, + NewLine: "\n", + }) +} + +// FormatNode renders a single node with its comments, for hover previews +// and similar snippets. +func FormatNode(d *syntax.Document, n syntax.Node, o Options) (string, error) { + if d == nil || n == nil { + return "", errors.New("formatter: nil document or node") + } + o = o.normalize() + + f := &formatter{ + doc: d, + toks: d.Tokens, + opts: o, + } + ir := f.node(n) + return doc.Print(ir, doc.Options{ + PrintWidth: o.PrintWidth, + Indent: o.Indent, + TabWidth: o.TabWidth, + NewLine: "\n", + }) +} + +type formatter struct { + doc *syntax.Document + toks []syntax.Token + opts Options + nextID int +} + +// id returns a fresh non-zero group id for IfBreak references. +func (f *formatter) id() int { + f.nextID++ + return f.nextID +} + +func (f *formatter) token(i int) syntax.Token { + return f.toks[i] +} + +// blankBefore returns the number of blank lines before the node's first +// token. +func (f *formatter) blankBefore(n syntax.Node) int { + return f.token(n.TokStart()).BlankLinesBefore +} + +// leadingComments returns the comments attached before the node's first +// token, as docs that each end with a hard line. +func (f *formatter) leadingComments(n syntax.Node) []doc.Doc { + var parts []doc.Doc + for _, c := range f.token(n.TokStart()).Leading { + parts = append(parts, doc.Text(c.Text), doc.HardLine) + } + return parts +} + +// trailingComments returns the comments attached after the node's last token +// on the same line, as line-suffix docs. The break parent forces the +// enclosing group to break so the comment stays on the node's own line. +func (f *formatter) trailingComments(n syntax.Node) []doc.Doc { + var parts []doc.Doc + for _, c := range f.token(n.TokEnd()).Trailing { + parts = append(parts, doc.LineSuffix(doc.Text(" "+c.Text)), doc.BreakParent) + } + return parts +} + +// node assembles a top-level node: its leading comments, its formatted +// body, and its trailing comments. +func (f *formatter) node(n syntax.Node) doc.Doc { + parts := append(f.leadingComments(n), f.nodeBody(n)) + parts = append(parts, f.trailingComments(n)...) + return doc.Concat(parts) +} + +// nodeBody formats a node without its comments. +func (f *formatter) nodeBody(n syntax.Node) doc.Doc { + switch v := n.(type) { + case *syntax.Include: + return f.include(v) + case *syntax.CPPInclude: + return f.cppInclude(v) + case *syntax.Namespace: + return f.namespace(v) + case *syntax.Const: + return f.constant(v) + case *syntax.Typedef: + return f.typedef(v) + case *syntax.Enum: + return f.enum(v) + case *syntax.Struct: + return f.structLike(v) + case *syntax.Service: + return f.service(v) + default: + panic(fmt.Sprintf("formatter: unknown node type %T", n)) + } +} + +// document assembles the whole file: top-level nodes separated by hard +// lines, blank lines preserved, and trailing comments. +func (f *formatter) document() doc.Doc { + var parts []doc.Doc + for i, n := range f.doc.Nodes { + if i > 0 { + parts = append(parts, doc.HardLine) + if f.blankBefore(n) >= 1 { + parts = append(parts, doc.HardLine) + } + } + parts = append(parts, f.node(n)) + } + + // Comments at the end of the file attach to the EOF token. + eof := f.toks[len(f.toks)-1] + if len(eof.Leading) > 0 { + if len(f.doc.Nodes) > 0 { + parts = append(parts, doc.HardLine) + if eof.BlankLinesBefore >= 1 { + parts = append(parts, doc.HardLine) + } + } + for i, c := range eof.Leading { + parts = append(parts, doc.Text(c.Text)) + if i < len(eof.Leading)-1 { + parts = append(parts, doc.HardLine) + } + } + } + + if !f.opts.NoTrailingNewline { + parts = append(parts, doc.HardLine) + } + return doc.Concat(parts) +} + +// --- headers --------------------------------------------------------------- + +func (f *formatter) include(v *syntax.Include) doc.Doc { + return doc.Concat{doc.Text("include "), doc.Text(v.Path.Text)} +} + +func (f *formatter) cppInclude(v *syntax.CPPInclude) doc.Doc { + return doc.Concat{doc.Text("cpp_include "), doc.Text(v.Path.Text)} +} + +func (f *formatter) namespace(v *syntax.Namespace) doc.Doc { + parts := []doc.Doc{ + doc.Text("namespace "), + doc.Text(v.Scope.Text), + doc.Text(" "), + doc.Text(v.Name.Text), + } + parts = append(parts, f.annotationsDoc(v.Annotations)) + return doc.Concat(parts) +} + +func (f *formatter) typedef(v *syntax.Typedef) doc.Doc { + parts := []doc.Doc{ + doc.Text("typedef "), + f.fieldType(v.Type), + doc.Text(" "), + doc.Text(v.Name.Text), + } + parts = append(parts, f.annotationsDoc(v.Annotations)) + return doc.Concat(parts) +} + +func (f *formatter) constant(v *syntax.Const) doc.Doc { + return doc.Concat{ + doc.Text("const "), + f.fieldType(v.Type), + doc.Text(" "), + doc.Text(v.Name.Text), + doc.Text(" = "), + f.constValue(v.Value), + } +} + +// --- annotations ----------------------------------------------------------- + +// annotationsDoc returns an annotation group, or an empty doc when absent. +func (f *formatter) annotationsDoc(a *syntax.Annotations) doc.Doc { + if a == nil { + return doc.Concat{} + } + items := make([]doc.Doc, 0, len(a.Items)) + for _, item := range a.Items { + var parts []doc.Doc + parts = append(parts, doc.Text(item.Name.Text)) + if item.Value != nil { + parts = append(parts, doc.Text(" = "), doc.Text(item.Value.Text)) + } + items = append(items, doc.Concat(parts)) + } + group := doc.Group(doc.Concat{ + doc.Text("("), + doc.Indent(doc.Concat{doc.SoftLine, doc.Join(doc.Concat{doc.Text(","), doc.Line}, items)}), + doc.SoftLine, + doc.Text(")"), + }) + return doc.Concat{doc.Text(" "), group} +} diff --git a/formatter/format_fuzz_test.go b/formatter/format_fuzz_test.go new file mode 100644 index 0000000..b582f16 --- /dev/null +++ b/formatter/format_fuzz_test.go @@ -0,0 +1,75 @@ +package formatter + +import ( + "testing" + + "github.com/karitham/thrift-ls/syntax" +) + +// FuzzFormat checks the full formatting pipeline over arbitrary source: +// +// - formatting never panics or loops forever +// - formatted output of a clean document parses without errors +// - formatting is idempotent: format(format(x)) == format(x) +// - formatted output is deterministic +func FuzzFormat(f *testing.F) { + for _, seed := range []string{ + "", + "struct S {\n 1: i32 a\n}", + "service S {\n i32 f(1: i64 a, 2: string b) throws (E e)\n}", + "const map> m = {\"a\": [1, 2]}", + "enum E {\n A = 1,\n B\n} (tag = \"x\")", + "// c\nstruct S {\n 1: i32 a // eol\n}", + "typedef i32 T (bare, a = \"1\"; b = '2')", + "include \"a.thrift\"\nnamespace go x", + "struct S {", + "service S {\n void f(1: i32 a // c\n )\n}", + } { + f.Add(seed) + } + + f.Fuzz(func(t *testing.T, src string) { + doc, errs := syntax.Parse([]byte(src)) + for _, err := range errs { + if err.Severity == syntax.SeverityError { + return // only format clean documents + } + } + + opts := testOpts(1 + len(src)%100) + out1, err := Format(doc, opts) + if err != nil { + t.Fatalf("Format: %v", err) + } + + // The output must parse cleanly. + _, errs2 := syntax.Parse([]byte(out1)) + for _, err := range errs2 { + if err.Severity == syntax.SeverityError { + t.Fatalf("formatted output does not parse: %v\ninput: %q\noutput: %q", errs2, src, out1) + } + } + + // Idempotency and determinism. + out2, err := Format(doc, opts) + if err != nil { + t.Fatalf("Format (second): %v", err) + } + if out1 != out2 { + t.Fatalf("Format is not deterministic:\nfirst: %q\nsecond: %q", out1, out2) + } + doc3, errs3 := syntax.Parse([]byte(out1)) + for _, err := range errs3 { + if err.Severity == syntax.SeverityError { + t.Fatalf("re-parse failed: %v", errs3) + } + } + out3, err := Format(doc3, opts) + if err != nil { + t.Fatalf("Format (third): %v", err) + } + if out1 != out3 { + t.Fatalf("not idempotent:\nfirst: %q\nsecond: %q", out1, out3) + } + }) +} diff --git a/formatter/format_test.go b/formatter/format_test.go new file mode 100644 index 0000000..0b9e4c9 --- /dev/null +++ b/formatter/format_test.go @@ -0,0 +1,639 @@ +package formatter + +import ( + "strconv" + "testing" + + "github.com/karitham/thrift-ls/syntax" +) + +// fmtSrc formats src at the given width with the given options. It fails the +// test when parsing fails. +func fmtSrc(t *testing.T, src string, opts Options) string { + t.Helper() + doc, errs := syntax.Parse([]byte(src)) + for _, err := range errs { + if err.Severity == syntax.SeverityError { + t.Fatalf("parse errors: %v", errs) + } + } + got, err := Format(doc, opts) + if err != nil { + t.Fatalf("Format: %v", err) + } + return got +} + +// parseDoc parses src and fails the test on parse errors. +func parseDoc(t *testing.T, src string) *syntax.Document { + t.Helper() + doc, errs := syntax.Parse([]byte(src)) + for _, err := range errs { + if err.Severity == syntax.SeverityError { + t.Fatalf("parse errors: %v", errs) + } + } + return doc +} + +func testOpts(width int) Options { + o := DefaultOptions() + o.PrintWidth = width + o.Indent = " " + o.TabWidth = 2 + return o +} + +// runCase formats, checks idempotency, and re-parses the output. +func runCase(t *testing.T, src string, opts Options, want string) { + t.Helper() + got := fmtSrc(t, src, opts) + if got != want { + t.Errorf("format mismatch\n got: %q\nwant: %q", got, want) + return + } + // Idempotency: formatting the output again must not change it. + again := fmtSrc(t, got, opts) + if again != got { + t.Errorf("not idempotent:\n first: %q\nsecond: %q", got, again) + } + // Self-validation: the output must parse cleanly. + _, errs := syntax.Parse([]byte(got)) + for _, err := range errs { + if err.Severity == syntax.SeverityError { + t.Errorf("formatted output does not parse: %v", errs) + break + } + } +} + +func TestFormatHeaders(t *testing.T) { + tests := []struct { + name string + src string + want string + }{ + { + name: "includes and namespaces", + src: "include \"a.thrift\"\nnamespace go thrift\ncpp_include \"\"", + want: "include \"a.thrift\"\nnamespace go thrift\ncpp_include \"\"\n", + }, + { + name: "namespace with star scope and annotations", + src: "namespace * all (tag = \"x\")", + want: "namespace * all (tag = \"x\")\n", + }, + { + name: "blank lines between headers are preserved", + src: "include \"a.thrift\"\n\ninclude \"b.thrift\"\n\nnamespace go x", + want: "include \"a.thrift\"\n\ninclude \"b.thrift\"\n\nnamespace go x\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runCase(t, tt.src, testOpts(80), tt.want) + }) + } +} + +func TestFormatTypedefs(t *testing.T) { + tests := []struct { + name string + src string + width int + want string + }{ + { + name: "simple", + src: "typedef\ti64\tTimestamp", + want: "typedef i64 Timestamp\n", + }, + { + name: "container with annotations", + src: "typedef map> Index (tag = \"x\")", + want: "typedef map> Index (tag = \"x\")\n", + }, + { + name: "cpp_type", + src: "typedef list cpp_type \"std::vector\" < i32 > Vec", + want: "typedef list cpp_type \"std::vector\" Vec\n", + }, + { + name: "long annotations break", + src: "typedef string Id (id_type = \"uuid\", long_annotation = \"some value\")", + width: 60, + want: "typedef string Id (\n id_type = \"uuid\",\n long_annotation = \"some value\"\n)\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + width := tt.width + if width == 0 { + width = 80 + } + runCase(t, tt.src, testOpts(width), tt.want) + }) + } +} + +func TestFormatConsts(t *testing.T) { + tests := []struct { + name string + src string + width int + want string + }{ + { + name: "scalars", + src: "const i32 a = 0xa1\nconst double b = -1.5e-3\nconst string c = \"x\"", + want: "const i32 a = 0xa1\nconst double b = -1.5e-3\nconst string c = \"x\"\n", + }, + { + name: "list fits", + src: "const list l = [1, 2, 3]", + want: "const list l = [1, 2, 3]\n", + }, + { + name: "list breaks", + src: "const list l = [1, 2, 3, 4, 5]", + width: 30, + want: "const list l = [\n 1,\n 2,\n 3,\n 4,\n 5\n]\n", + }, + { + name: "map breaks", + src: "const map m = {\"aaaa\": 1, \"bbbb\": 2}", + width: 40, + want: "const map m = {\n \"aaaa\": 1,\n \"bbbb\": 2\n}\n", + }, + { + name: "nested values", + src: "const list> n = [[1, 2], [3]]", + want: "const list> n = [[1, 2], [3]]\n", + }, + { + name: "empty list and map", + src: "const list a = []\nconst map b = {}", + want: "const list a = []\nconst map b = {}\n", + }, + { + name: "value list breaks inside const", + src: "const list l = [1, 2, 3, 4, 5, 6, 7, 8]", + width: 40, + want: "const list l = [\n 1,\n 2,\n 3,\n 4,\n 5,\n 6,\n 7,\n 8\n]\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + width := tt.width + if width == 0 { + width = 40 + } + runCase(t, tt.src, testOpts(width), tt.want) + }) + } +} + +func TestFormatStructs(t *testing.T) { + tests := []struct { + name string + src string + width int + want string + }{ + { + name: "empty struct", + src: "struct Empty {}", + want: "struct Empty {}\n", + }, + { + name: "small struct stays flat", + src: "struct S {\n 1: i32 a\n}", + want: "struct S { 1: i32 a }\n", + }, + { + name: "fields align", + src: "struct S {\n 1: i64 id\n 2: string name\n 10: i32 x\n}", + width: 40, + want: "struct S {\n 1: i64 id\n 2: string name\n 10: i32 x\n}\n", + }, + { + name: "requiredness column aligns", + src: "struct S {\n 1: required i64 id\n 2: string name\n}", + width: 40, + want: "struct S {\n 1: required i64 id\n 2: string name\n}\n", + }, + { + name: "union and exception keywords", + src: "union U {\n 1: i32 a\n}\nexception E {\n 1: string msg\n}", + width: 15, + want: "union U {\n 1: i32 a\n}\nexception E {\n 1: string msg\n}\n", + }, + { + name: "implicit ids and defaults", + src: "struct S {\n i32 a\n string b = \"x\"\n}", + width: 30, + want: "struct S {\n i32 a\n string b = \"x\"\n}\n", + }, + { + name: "annotations on struct", + src: "struct S {\n 1: i32 a\n} (tag = \"x\")", + want: "struct S { 1: i32 a } (tag = \"x\")\n", + }, + { + name: "annotations break when too long", + src: "struct S {\n 1: i32 a\n} (tag = \"x\")", + width: 10, + want: "struct S {\n 1: i32 a\n} (\n tag = \"x\"\n)\n", + }, + { + name: "annotations stay flat on the closing line", + src: "struct S {\n 1: i32 a\n} (tag = \"x\")", + width: 15, + want: "struct S {\n 1: i32 a\n} (tag = \"x\")\n", + }, + { + name: "field annotations", + src: "struct S {\n 1: i32 a (tag = \"y\")\n}", + width: 30, + want: "struct S {\n 1: i32 a (tag = \"y\")\n}\n", + }, + { + name: "type annotations before name", + src: "struct S {\n 1: map (tag = \"t\") m\n}", + width: 40, + want: "struct S {\n 1: map (tag = \"t\") m\n}\n", + }, + { + name: "field reference", + src: "struct S {\n 1: i32 &parent\n}", + width: 20, + want: "struct S {\n 1: i32 &parent\n}\n", + }, + { + name: "semicolon separators normalized", + src: "struct S {\n 1: i32 a;\n 2: string b;\n}", + want: "struct S { 1: i32 a, 2: string b }\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + width := tt.width + if width == 0 { + width = 80 + } + runCase(t, tt.src, testOpts(width), tt.want) + }) + } +} + +func TestFormatEnums(t *testing.T) { + tests := []struct { + name string + src string + width int + want string + }{ + { + name: "empty enum", + src: "enum E {}", + want: "enum E {}\n", + }, + { + name: "values align equals", + src: "enum E {\n A = 1\n LONG = 2\n}", + width: 20, + want: "enum E {\n A = 1\n LONG = 2\n}\n", + }, + { + name: "auto-increment values", + src: "enum E {\n A = 1,\n B,\n C = 0x10\n}", + want: "enum E { A = 1, B, C = 0x10 }\n", + }, + { + name: "negative value", + src: "enum E {\n A = -1\n}", + want: "enum E { A = -1 }\n", + }, + { + name: "annotations", + src: "enum E {\n A (a_anno = \"y\")\n} (e_anno = \"x\")", + want: "enum E { A (a_anno = \"y\") } (e_anno = \"x\")\n", + }, + { + name: "annotations break when too long", + src: "enum E {\n A (a_anno = \"y\")\n} (e_anno = \"x\")", + width: 12, + want: "enum E {\n A (\n a_anno = \"y\"\n )\n} (\n e_anno = \"x\"\n)\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + width := tt.width + if width == 0 { + width = 80 + } + runCase(t, tt.src, testOpts(width), tt.want) + }) + } +} + +func TestFormatFunctions(t *testing.T) { + tests := []struct { + name string + src string + width int + want string + }{ + { + name: "short signature stays flat", + src: "service S {\n void ping()\n}", + width: 80, + want: "service S {\n void ping()\n}\n", + }, + { + name: "oneway void", + src: "service S {\n oneway void fire()\n}", + width: 80, + want: "service S {\n oneway void fire()\n}\n", + }, + { + name: "async normalizes to oneway", + src: "service S {\n async void fire()\n}", + width: 80, + want: "service S {\n oneway void fire()\n}\n", + }, + { + name: "signature with args fits", + src: "service S {\n User getUser(1: i64 id, 2: string name)\n}", + width: 80, + want: "service S {\n User getUser(1: i64 id, 2: string name)\n}\n", + }, + { + name: "throws breaks before args", + src: "service S {\n i32 getUser(1: i64 id, 2: string name) throws (NotFound e)\n}", + width: 50, + want: "service S {\n i32 getUser(1: i64 id, 2: string name) throws (\n NotFound e\n )\n}\n", + }, + { + name: "everything breaks when signature is long", + src: "service S {\n i32 getUser(1: i64 id, 2: string name) throws (NotFound e)\n}", + width: 45, + want: "service S {\n i32 getUser(\n 1: i64 id,\n 2: string name\n ) throws (\n NotFound e\n )\n}\n", + }, + { + name: "args break without throws", + src: "service S {\n i32 getUser(1: i64 id, 2: string name)\n}", + width: 35, + want: "service S {\n i32 getUser(\n 1: i64 id,\n 2: string name\n )\n}\n", + }, + { + name: "one arg stays flat", + src: "service S {\n i32 getUser(1: i64 id) throws (NotFound e)\n}", + width: 60, + want: "service S {\n i32 getUser(1: i64 id) throws (NotFound e)\n}\n", + }, + { + name: "function annotations", + src: "service S {\n void f() (f_anno = \"x\")\n}", + width: 80, + want: "service S {\n void f() (f_anno = \"x\")\n}\n", + }, + { + name: "empty throws stays inline", + src: "service S {\n void f() throws ()\n}", + width: 80, + want: "service S {\n void f() throws ()\n}\n", + }, + { + name: "extends", + src: "service Child extends Parent {\n void f()\n}", + width: 80, + want: "service Child extends Parent {\n void f()\n}\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runCase(t, tt.src, testOpts(tt.width), tt.want) + }) + } +} + +func TestFormatComments(t *testing.T) { + tests := []struct { + name string + src string + width int + want string + }{ + { + name: "leading comments", + src: "// before\nstruct S {\n 1: i32 a\n}", + width: 15, + want: "// before\nstruct S {\n 1: i32 a\n}\n", + }, + { + name: "doc comment", + src: "/** doc */\nstruct S {\n 1: i32 a\n}", + width: 15, + want: "/** doc */\nstruct S {\n 1: i32 a\n}\n", + }, + { + name: "end-of-line comment forces break", + src: "struct S {\n 1: i32 a // eol\n}", + width: 80, + want: "struct S {\n 1: i32 a // eol\n}\n", + }, + { + name: "field leading comment", + src: "struct S {\n // c\n 1: i32 a\n}", + width: 80, + want: "struct S {\n // c\n 1: i32 a\n}\n", + }, + { + name: "blank lines between fields preserved", + src: "struct S {\n 1: i32 a\n\n 2: string b\n}", + width: 80, + want: "struct S {\n 1: i32 a\n\n 2: string b\n}\n", + }, + { + name: "comment at end of file", + src: "struct S {\n 1: i32 a\n}\n// tail", + want: "struct S { 1: i32 a }\n// tail\n", + }, + { + name: "hash comment", + src: "# hash\nstruct S {\n 1: i32 a\n}", + width: 15, + want: "# hash\nstruct S {\n 1: i32 a\n}\n", + }, + { + name: "block comment", + src: "/* block */\nstruct S {\n 1: i32 a\n}", + width: 15, + want: "/* block */\nstruct S {\n 1: i32 a\n}\n", + }, + { + name: "comments between definitions", + src: "struct A {\n 1: i32 a\n}\n\n// b comment\nstruct B {\n 1: i32 b\n}", + want: "struct A { 1: i32 a }\n\n// b comment\nstruct B { 1: i32 b }\n", + }, + { + name: "comment inside function args forces broken", + src: "service S {\n void f(1: i32 a // arg comment\n )\n}", + width: 80, + want: "service S {\n void f(\n 1: i32 a // arg comment\n )\n}\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + width := tt.width + if width == 0 { + width = 80 + } + runCase(t, tt.src, testOpts(width), tt.want) + }) + } +} + +func TestFormatOptions(t *testing.T) { + tests := []struct { + name string + opts Options + src string + want string + }{ + { + name: "comma add", + opts: func() Options { + o := testOpts(30) + o.FieldLineComma = CommaAdd + return o + }(), + src: "struct S {\n 1: i32 a\n 2: string b\n}", + want: "struct S {\n 1: i32 a,\n 2: string b,\n}\n", + }, + { + name: "comma remove", + opts: func() Options { + o := testOpts(30) + o.FieldLineComma = CommaRemove + return o + }(), + src: "struct S {\n 1: i32 a,\n 2: string b,\n}", + want: "struct S {\n 1: i32 a\n 2: string b\n}\n", + }, + { + name: "align disable", + opts: func() Options { + o := testOpts(40) + o.Align = AlignDisable + return o + }(), + src: "struct S {\n 1: required i64 id\n 2: string name\n}", + want: "struct S {\n 1: required i64 id\n 2: string name\n}\n", + }, + { + name: "align assign", + opts: func() Options { + o := testOpts(40) + o.Align = AlignAssign + return o + }(), + src: "struct S {\n 1: i32 a = 5\n 2: string longer = \"x\"\n}", + want: "struct S {\n 1: i32 a = 5\n 2: string longer = \"x\"\n}\n", + }, + { + name: "trailing newline by default", + src: "struct S {\n 1: i32 a\n}", + want: "struct S { 1: i32 a }\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runCase(t, tt.src, tt.opts, tt.want) + }) + } +} + +func TestFormatRejectsNilDocument(t *testing.T) { + if _, err := Format(nil, testOpts(80)); err == nil { + t.Error("expected error formatting nil document") + } +} + +func TestFormatWholeDocument(t *testing.T) { + src := `include "shared.thrift" + +namespace go example + +// A user. +struct User { + 1: required i64 id + 2: optional string name = "bob" +} + +enum Role { + ADMIN = 1, + USER +} + +service UserService { + User getUser(1: i64 id) throws (NotFound e) + oneway void notify(1: User user) +} +` + want := `include "shared.thrift" + +namespace go example + +// A user. +struct User { + 1: required i64 id + 2: optional string name = "bob" +} + +enum Role { ADMIN = 1, USER } + +service UserService { + User getUser(1: i64 id) throws (NotFound e) + oneway void notify(1: User user) +} +` + runCase(t, src, testOpts(60), want) +} + +func TestFormatIsIdempotent(t *testing.T) { + sources := []string{ + "struct S {\n 1: required i64 id\n 2: optional string name\n}", + "service S {\n i32 f(1: i64 a, 2: string b) throws (E e)\n}", + "const map> m = {\"a\": [1, 2], \"b\": []}", + "enum E {\n A = 1,\n B\n} (tag = \"x\")", + "// c\nstruct S {\n 1: i32 a // eol\n\n 2: string b\n}", + } + for i, src := range sources { + t.Run("case-"+strconv.Itoa(i), func(t *testing.T) { + first := fmtSrc(t, src, testOpts(40)) + second := fmtSrc(t, first, testOpts(40)) + if first != second { + t.Errorf("not idempotent:\nfirst: %q\nsecond: %q", first, second) + } + }) + } +} + +func TestFormatNode(t *testing.T) { + src := `// leading +struct User { + 1: required i64 id +} (tag = "x")` + doc := parseDoc(t, src) + want := `// leading +struct User { 1: required i64 id } (tag = "x")` + got, err := FormatNode(doc, doc.Structs()[0], testOpts(80)) + if err != nil { + t.Fatalf("FormatNode: %v", err) + } + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} diff --git a/formatter/testdata/fuzz/FuzzFormat/fd21a395a5f4cef9 b/formatter/testdata/fuzz/FuzzFormat/fd21a395a5f4cef9 new file mode 100644 index 0000000..e3cb81b --- /dev/null +++ b/formatter/testdata/fuzz/FuzzFormat/fd21a395a5f4cef9 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("const mapA={}") diff --git a/formatter/type.go b/formatter/type.go new file mode 100644 index 0000000..d9c345a --- /dev/null +++ b/formatter/type.go @@ -0,0 +1,47 @@ +package formatter + +import ( + "strings" + + "github.com/karitham/thrift-ls/doc" + "github.com/karitham/thrift-ls/syntax" +) + +// fieldType formats a type reference: base keyword, identifier, or +// container. Containers and base types carry optional annotations. +func (f *formatter) fieldType(t *syntax.FieldType) doc.Doc { + parts := []doc.Doc{doc.Text(typeText(t))} + parts = append(parts, f.annotationsDoc(t.Annotations)) + return doc.Concat(parts) +} + +// typeText renders a type reference as plain text, matching the compiler's +// spelling: containers use "map", optional cpp_type comes after the +// container keyword ("list cpp_type \"x\" "), matching the grammar. +func typeText(t *syntax.FieldType) string { + if t == nil { + return "" + } + switch t.Kind { + case syntax.TypeBase: + return t.Base.String() + case syntax.TypeIdent: + return t.Ident.Text + case syntax.TypeMap: + return containerText("map", t.CPPType, typeText(t.KeyType), typeText(t.ValueType)) + case syntax.TypeList: + return containerText("list", t.CPPType, typeText(t.ValueType)) + case syntax.TypeSet: + return containerText("set", t.CPPType, typeText(t.ValueType)) + } + return "" +} + +func containerText(kind string, cppType *syntax.Token, inner ...string) string { + prefix := kind + if cppType != nil { + // The grammar reads cpp_type before '<': "list cpp_type \"x\" ". + return prefix + " cpp_type " + cppType.Text + " <" + strings.Join(inner, ", ") + ">" + } + return prefix + "<" + strings.Join(inner, ", ") + ">" +} diff --git a/formatter/value.go b/formatter/value.go new file mode 100644 index 0000000..a4eba24 --- /dev/null +++ b/formatter/value.go @@ -0,0 +1,54 @@ +package formatter + +import ( + "github.com/karitham/thrift-ls/doc" + "github.com/karitham/thrift-ls/syntax" +) + +// constValue formats a constant value. Scalars are emitted verbatim from +// their source text; lists and maps are groups that stay on one line when +// they fit and break with one entry per line otherwise. +func (f *formatter) constValue(v *syntax.ConstValue) doc.Doc { + if v == nil { + return doc.Concat{} + } + switch v.Kind { + case syntax.ValueList: + items := make([]doc.Doc, 0, len(v.List)) + for _, item := range v.List { + items = append(items, f.constValue(item)) + } + return f.bracketList("[", "]", items) + + case syntax.ValueMap: + entries := make([]doc.Doc, 0, len(v.Map)) + for _, entry := range v.Map { + entries = append(entries, doc.Concat{ + f.constValue(entry.Key), + doc.Text(": "), + f.constValue(entry.Value), + }) + } + return f.bracketList("{", "}", entries) + + default: + return doc.Text(v.Text) + } +} + +// bracketList formats [open, items, close] as a group: flat when it fits, +// otherwise one item per line, indented. Empty lists stay on one line. +func (f *formatter) bracketList(open, close string, items []doc.Doc) doc.Doc { + if len(items) == 0 { + return doc.Text(open + close) + } + return doc.Group(doc.Concat{ + doc.Text(open), + doc.Indent(doc.Concat{ + doc.SoftLine, + doc.Join(doc.Concat{doc.Text(","), doc.Line}, items), + }), + doc.SoftLine, + doc.Text(close), + }) +} diff --git a/go.mod b/go.mod index 21c7000..49615b8 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,21 @@ -module github.com/joyme123/thrift-ls +module github.com/karitham/thrift-ls -go 1.19 +go 1.26 require ( - github.com/cloudwego/thriftgo v0.2.11 - github.com/joyme123/protocol v0.12.0-patch20250429 - github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.7.0 - go.lsp.dev/jsonrpc2 v0.10.0 + github.com/stretchr/testify v1.11.1 + go.lsp.dev/jsonrpc2 v1.0.1 go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 - go.lsp.dev/uri v0.3.0 - go.uber.org/zap v1.21.0 - gopkg.in/yaml.v2 v2.4.0 + go.lsp.dev/uri v1.0.1 + golang.org/x/text v0.40.0 ) +require github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/segmentio/asm v1.1.3 // indirect - github.com/segmentio/encoding v0.3.4 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + github.com/urfave/cli/v3 v3.10.1 + go.lsp.dev/protocol v1.0.1 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 356b33d..79c018d 100644 --- a/go.sum +++ b/go.sum @@ -1,99 +1,26 @@ -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/cloudwego/thriftgo v0.2.11 h1:uwFyTMBwmBJKpwxRdBvn46aHEVJJSgxkHo93RN0r3fw= -github.com/cloudwego/thriftgo v0.2.11/go.mod h1:dAyXHEmKXo0LfMCrblVEY3mUZsdeuA5+i0vF5f09j7E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/joyme123/protocol v0.12.0-patch20250429 h1:xpdpCrPh3iiEIQvdg+qe5jvRLOZuGIB6sszHdXrypYk= -github.com/joyme123/protocol v0.12.0-patch20250429/go.mod h1:DtA/00dR9o4lqtyqUzD6mGDfHKoCwHtazn62H2gnerA= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg= +github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= -github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= -github.com/segmentio/encoding v0.3.4 h1:WM4IBnxH8B9TakiM2QD5LyNl9JSndh88QbHqVC+Pauc= -github.com/segmentio/encoding v0.3.4/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI= -go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY= +github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +go.lsp.dev/jsonrpc2 v1.0.1 h1:JZAchpm6RcOuwEOnZ7kF3kA1EpEjdJZ/6rMSfOF3+F4= +go.lsp.dev/jsonrpc2 v1.0.1/go.mod h1:WN+TesqrPumBmUdsuWyn9DHq3zl4i85L/qG0NLdj8qg= go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE= go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw= -go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo= -go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= -go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +go.lsp.dev/protocol v1.0.1 h1:kLWUIk1v/deXxo1D0VO4avjZnfeuZ82mouJgJd/NbdU= +go.lsp.dev/protocol v1.0.1/go.mod h1:e//i41aEqc9ml32xiNuneUwbjOjuvTA4+uv+AkssJgk= +go.lsp.dev/uri v1.0.1 h1:ywxJN1UXoQ+v0I9lkoQNMmsm7lub5G9ZG7KXT8YTx30= +go.lsp.dev/uri v1.0.1/go.mod h1:06a0ghafs4PuqkLsJGUmlgPHw6+kH79edRXjOT/rA8s= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/log/log.go b/log/log.go index c052ed9..0b150da 100644 --- a/log/log.go +++ b/log/log.go @@ -1,20 +1,39 @@ +// Package log configures the process-wide slog logger for thrift-ls. +// +// thrift-ls logs to a file in the temp directory (thriftls.log) so that LSP +// traffic on stdio is never polluted with log output. package log import ( + "log/slog" "os" - - log "github.com/sirupsen/logrus" ) -func Init(logLevel int) { +// Init configures the default slog logger with the given level and redirects +// it to the temp log file. +// +// level uses the historical thrift-ls scale (1 fatal .. 6 trace), matching +// the old logrus levels so CLI flags keep their meaning. +func Init(level int) { file := os.TempDir() + "/thriftls.log" - logFile, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0766) + logFile, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o766) if err != nil { panic(err) } - log.SetLevel(log.Level(logLevel)) - log.SetOutput(logFile) // 将文件设置为log输出的文件 + slog.SetDefault(slog.New(slog.NewTextHandler(logFile, &slog.HandlerOptions{ + Level: slogLevel(level), + }))) } -type Logger struct { +func slogLevel(level int) slog.Level { + switch { + case level >= 5: // logrus Debug / Trace + return slog.LevelDebug + case level == 4: // logrus Info + return slog.LevelInfo + case level == 3: // logrus Warn + return slog.LevelWarn + default: // logrus Fatal / Error / Panic + return slog.LevelError + } } diff --git a/lsp/cache/cache.go b/lsp/cache/cache.go index 05f3e26..1f2ab91 100644 --- a/lsp/cache/cache.go +++ b/lsp/cache/cache.go @@ -5,7 +5,7 @@ import ( "strconv" "sync/atomic" - "github.com/joyme123/thrift-ls/lsp/memoize" + "github.com/karitham/thrift-ls/lsp/memoize" ) type Cache struct { diff --git a/lsp/cache/file.go b/lsp/cache/file.go index c623146..5515d31 100644 --- a/lsp/cache/file.go +++ b/lsp/cache/file.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/joyme123/protocol" + "go.lsp.dev/protocol" "go.lsp.dev/uri" ) @@ -34,10 +34,10 @@ func HashOf(data []byte) Hash { } // Hashf returns the hash of a printf-formatted string. -func Hashf(format string, args ...interface{}) Hash { +func Hashf(format string, args ...any) Hash { // Although this looks alloc-heavy, it is faster than using // Fprintf on sha256.New() because the allocations don't escape. - return HashOf([]byte(fmt.Sprintf(format, args...))) + return HashOf(fmt.Appendf(nil, format, args...)) } // String returns the digest as a string of hex digits. @@ -181,10 +181,16 @@ func (f *FileChange) FullContent(base []byte) []byte { func FileChangeFromLSPDidChange(params *protocol.DidChangeTextDocumentParams) []*FileChange { changes := make([]*FileChange, 0, len(params.ContentChanges)) for i := range params.ContentChanges { + event, ok := params.ContentChanges[i].(*protocol.TextDocumentContentChangeWholeDocument) + if !ok { + // Incremental changes are not supported; fall back to full reload + // semantics using the current full content. + continue + } changes = append(changes, &FileChange{ - URI: params.TextDocument.TextDocumentIdentifier.URI, + URI: params.TextDocument.URI, Version: int(params.TextDocument.Version), - Content: []byte(params.ContentChanges[i].Text), + Content: []byte(event.Text), From: FileChangeTypeDidChange, }) } diff --git a/lsp/cache/file_posix.go b/lsp/cache/file_posix.go index a88971c..1203e6f 100644 --- a/lsp/cache/file_posix.go +++ b/lsp/cache/file_posix.go @@ -1,5 +1,4 @@ //go:build !windows && !plan9 -// +build !windows,!plan9 package cache diff --git a/lsp/cache/file_windows.go b/lsp/cache/file_windows.go index 5e5b965..412b16e 100644 --- a/lsp/cache/file_windows.go +++ b/lsp/cache/file_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package cache diff --git a/lsp/cache/fs_memoized.go b/lsp/cache/fs_memoized.go index bdd611b..50377e4 100644 --- a/lsp/cache/fs_memoized.go +++ b/lsp/cache/fs_memoized.go @@ -19,10 +19,6 @@ type memoizedFS struct { filesByID map[FileID][]*DiskFile } -func newMemoizedFS() *memoizedFS { - return &memoizedFS{filesByID: make(map[FileID][]*DiskFile)} -} - // A DiskFile is a file on the filesystem, or a failure to read one. // It implements the source.FileHandle interface. type DiskFile struct { @@ -48,7 +44,7 @@ func (h *DiskFile) Content() ([]byte, error) { return h.content, h.err } // ReadFile stats and (maybe) reads the file, updates the cache, and returns it. func (fs *memoizedFS) ReadFile(ctx context.Context, uri uri.URI) (FileHandle, error) { - id, mtime, err := GetFileID(uri.Filename()) + id, mtime, err := GetFileID(uri.Path()) if err != nil { // file does not exist return &DiskFile{ @@ -123,7 +119,7 @@ func readFile(ctx context.Context, uri uri.URI, mtime time.Time) (*DiskFile, err // ID, or whose mtime differs from the given mtime. However, in these cases // we expect the client to notify of a subsequent file change, and the file // content should be eventually consistent. - content, err := os.ReadFile(uri.Filename()) // ~20us + content, err := os.ReadFile(uri.Path()) // ~20us if err != nil { content = nil // just in case } diff --git a/lsp/cache/fs_overlay.go b/lsp/cache/fs_overlay.go index 662014a..3518321 100644 --- a/lsp/cache/fs_overlay.go +++ b/lsp/cache/fs_overlay.go @@ -2,9 +2,9 @@ package cache import ( "context" + "log/slog" "sync" - log "github.com/sirupsen/logrus" "go.lsp.dev/uri" ) @@ -36,7 +36,7 @@ func (fs *overlayFS) Overlays() []*Overlay { } func (fs *overlayFS) ReadFile(ctx context.Context, uri uri.URI) (FileHandle, error) { - log.Debug("read uri: ", uri) + slog.Debug("reading uri", "uri", uri) fs.mu.Lock() overlay, ok := fs.overlays[uri] fs.mu.Unlock() @@ -62,7 +62,7 @@ func (fs *overlayFS) Update(ctx context.Context, changes []*FileChange) error { } overlay := NewOverlay(change.URI, change.FullContent(base), int32(change.Version)) - log.Debug("new overlay content: ", string(overlay.content), "uri", change.URI) + slog.Debug("new overlay content", "content", string(overlay.content), "uri", change.URI) fs.mu.Lock() fs.overlays[change.URI] = overlay diff --git a/lsp/cache/graph.go b/lsp/cache/graph.go index 267d22f..b64b4d1 100644 --- a/lsp/cache/graph.go +++ b/lsp/cache/graph.go @@ -1,13 +1,14 @@ package cache import ( + "log/slog" "sort" + "strings" "sync" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" - log "github.com/sirupsen/logrus" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/syntax" ) type IncludeNode struct { @@ -54,16 +55,18 @@ func (g *IncludeGraph) Get(file uri.URI) *IncludeNode { return g.mapper[file] } -func (g *IncludeGraph) Set(file uri.URI, includes []*parser.Include, includePaths []string) { +// Set records the include edges of file. resolve maps an include path text +// to the URI of the included file (e.g. the snapshot's include resolver). +func (g *IncludeGraph) Set(file uri.URI, includes []*syntax.Include, resolve func(cur uri.URI, includePath string) uri.URI) { g.mu.Lock() defer g.mu.Unlock() includeURIs := make([]uri.URI, 0, len(includes)) for _, inc := range includes { - if inc.BadNode || inc.Path == nil || inc.Path.ChildrenBadNode() || inc.Path.Value == nil { + if inc.Path == nil { continue } - includeURI := lsputils.IncludeURIWithPaths(file, inc.Path.Value.Text, includePaths) + includeURI := resolve(file, strings.Trim(inc.Path.Text, "\"'")) includeURIs = append(includeURIs, includeURI) } sort.SliceStable(includeURIs, func(i, j int) bool { @@ -104,8 +107,6 @@ func (g *IncludeGraph) Set(file uri.URI, includes []*parser.Include, includePath } g.mapper[file] = node - return - } func (g *IncludeGraph) Remove(file uri.URI) { @@ -162,6 +163,6 @@ func (g *IncludeGraph) removeWithoutLock(file uri.URI) { func (g *IncludeGraph) Debug() { for file, node := range g.mapper { - log.Debugln("file: ", file, "node: ", node) + slog.Debug("graph file node", "file", file, "node", node) } } diff --git a/lsp/cache/graph_test.go b/lsp/cache/graph_test.go index ee3d884..1dafb6e 100644 --- a/lsp/cache/graph_test.go +++ b/lsp/cache/graph_test.go @@ -5,9 +5,11 @@ import ( "path/filepath" "testing" - "github.com/joyme123/thrift-ls/parser" "github.com/stretchr/testify/assert" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/resolver" + "github.com/karitham/thrift-ls/syntax" ) func Test_IncludeGraph_Set(t *testing.T) { @@ -22,23 +24,23 @@ func Test_IncludeGraph_Set(t *testing.T) { tmpDir, err := os.MkdirTemp("", "thrift-test") assert.NoError(t, err) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() baseDir := filepath.Join(tmpDir, "base") serviceDir := filepath.Join(tmpDir, "service") - err = os.MkdirAll(baseDir, 0755) + err = os.MkdirAll(baseDir, 0o755) assert.NoError(t, err) - err = os.MkdirAll(serviceDir, 0755) + err = os.MkdirAll(serviceDir, 0o755) assert.NoError(t, err) // Create shared.thrift in base/ sharedThrift := filepath.Join(baseDir, "shared.thrift") - err = os.WriteFile(sharedThrift, []byte(""), 0644) + err = os.WriteFile(sharedThrift, []byte(""), 0o644) assert.NoError(t, err) // Create order.thrift in service/ orderThrift := filepath.Join(serviceDir, "order.thrift") - err = os.WriteFile(orderThrift, []byte(""), 0644) + err = os.WriteFile(orderThrift, []byte(""), 0o644) assert.NoError(t, err) orderURI := uri.File(orderThrift) @@ -46,9 +48,9 @@ func Test_IncludeGraph_Set(t *testing.T) { // Test: include "shared.thrift" from order.thrift with includePaths pointing to base/ // Should resolve to base/shared.thrift - graph.Set(orderURI, []*parser.Include{ - {Path: &parser.Literal{Value: &parser.LiteralValue{Text: "shared.thrift"}}}, - }, []string{baseDir}) + graph.Set(orderURI, []*syntax.Include{ + {Path: &syntax.Token{Text: "shared.thrift"}}, + }, resolveWithPaths([]string{baseDir})) node := graph.Get(orderURI) assert.NotNil(t, node) @@ -57,9 +59,9 @@ func Test_IncludeGraph_Set(t *testing.T) { // Test: include "shared.thrift" from order.thrift without includePaths // Should fall back to relative path (service/shared.thrift which doesn't exist) graph2 := NewIncludeGraph() - graph2.Set(orderURI, []*parser.Include{ - {Path: &parser.Literal{Value: &parser.LiteralValue{Text: "shared.thrift"}}}, - }, []string{}) + graph2.Set(orderURI, []*syntax.Include{ + {Path: &syntax.Token{Text: "shared.thrift"}}, + }, resolveWithPaths(nil)) node2 := graph2.Get(orderURI) assert.NotNil(t, node2) @@ -80,17 +82,17 @@ func Test_Graph(t *testing.T) { // node3: // file:///tmp/addr.thrift // include "./base.thrift" - file1 := uri.New("file:///tmp/model/user.thrift") - file2 := uri.New("file:///tmp/base.thrift") - file3 := uri.New("file:///tmp/addr.thrift") - graph.Set(file1, []*parser.Include{ - {Path: &parser.Literal{Value: &parser.LiteralValue{Text: "../base.thrift"}}}, - {Path: &parser.Literal{Value: &parser.LiteralValue{Text: "../addr.thrift"}}}, - }, nil) + file1 := uri.MustParse("file:///tmp/model/user.thrift") + file2 := uri.MustParse("file:///tmp/base.thrift") + file3 := uri.MustParse("file:///tmp/addr.thrift") + graph.Set(file1, []*syntax.Include{ + {Path: &syntax.Token{Text: "../base.thrift"}}, + {Path: &syntax.Token{Text: "../addr.thrift"}}, + }, resolveWithPaths(nil)) graph.Set(file2, nil, nil) - graph.Set(file3, []*parser.Include{ - {Path: &parser.Literal{Value: &parser.LiteralValue{Text: "./base.thrift"}}}, - }, nil) + graph.Set(file3, []*syntax.Include{ + {Path: &syntax.Token{Text: "./base.thrift"}}, + }, resolveWithPaths(nil)) expectNode1 := &IncludeNode{ outdegree: []uri.URI{file3, file2}, @@ -133,3 +135,12 @@ func Test_Graph(t *testing.T) { assert.Nil(t, graph.Get("file:///tmp/base.thrift"), "base.thrift") assert.Nil(t, graph.Get("file:///tmp/addr.thrift"), "addr.thrift") } + +// resolveWithPaths builds a graph resolve func from include paths, mirroring +// the snapshot's resolver-based resolution. +func resolveWithPaths(includePaths []string) func(uri.URI, string) uri.URI { + r := resolver.NewWithFS(includePaths, resolver.FS()) + return func(cur uri.URI, includePath string) uri.URI { + return uri.File(r.Resolve(cur.Path(), includePath)) + } +} diff --git a/lsp/cache/parse.go b/lsp/cache/parse.go index 8cf09aa..6e1c601 100644 --- a/lsp/cache/parse.go +++ b/lsp/cache/parse.go @@ -3,12 +3,13 @@ package cache import ( "encoding/json" "fmt" + "log/slog" "sync" - "github.com/joyme123/thrift-ls/lsp/mapper" - "github.com/joyme123/thrift-ls/parser" - log "github.com/sirupsen/logrus" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/mapper" + "github.com/karitham/thrift-ls/syntax" ) type ParseCaches struct { @@ -52,10 +53,7 @@ func (c *ParseCaches) Clone() *ParseCaches { for i := range c.caches { clone[i] = c.caches[i] } - newCaches := &ParseCaches{ - caches: clone, - } - return newCaches + return &ParseCaches{caches: clone} } func (c *ParseCaches) Tokens() map[string]struct{} { @@ -75,7 +73,8 @@ func (c *ParseCaches) Tokens() map[string]struct{} { return tokens } -// TokensForFile returns tokens for the given file and its transitively included files +// TokensForFile returns tokens for the given file and its transitively +// included files. func (c *ParseCaches) TokensForFile(file uri.URI, getIncludes func(uri.URI) []uri.URI) map[string]struct{} { tokens := make(map[string]struct{}) visited := make(map[uri.URI]bool) @@ -101,104 +100,106 @@ func (c *ParseCaches) TokensForFile(file uri.URI, getIncludes func(uri.URI) []ur return tokens } -func collectTokens(ast *parser.Document, tokens map[string]struct{}) { - for _, item := range ast.Includes { - if item.Path == nil || item.Path.BadNode { - continue - } - tokens[item.Name()] = struct{}{} - } - for _, item := range ast.Enums { - if item.Name == nil || item.Name.BadNode || item.Name.Name == nil || item.Name.Name.BadNode { - continue - } - tokens[item.Name.Name.Text] = struct{}{} - for i := range item.Values { - if item.Values[i].BadNode || item.Values[i].Name == nil || item.Values[i].Name.BadNode || - item.Values[i].Name.Name == nil || item.Values[i].Name.Name.BadNode { - continue +// collectTokens collects every identifier name in the document: definition +// names, field names, and identifier references. +func collectTokens(ast *syntax.Document, tokens map[string]struct{}) { + var walk func(n syntax.Node) + walk = func(n syntax.Node) { + switch v := n.(type) { + case *syntax.Identifier: + tokens[v.Text] = struct{}{} + case *syntax.ConstValue: + if v.Kind == syntax.ValueIdent { + tokens[v.Text] = struct{}{} } - tokens[item.Name.Name.Text] = struct{}{} - } - } - for _, item := range ast.Consts { - if item.Name == nil || item.Name.BadNode || item.Name.Name == nil || item.Name.Name.BadNode { - continue - } - tokens[item.Name.Name.Text] = struct{}{} - } - for _, item := range ast.Typedefs { - if item.Alias == nil || item.Alias.BadNode || item.Alias.Name == nil || item.Alias.Name.BadNode { - continue - } - tokens[item.Alias.Name.Text] = struct{}{} - } - for _, item := range ast.Services { - if item.Name == nil || item.Name.BadNode { - continue - } - tokens[item.Name.Name.Text] = struct{}{} - } - for _, item := range ast.Unions { - if item.Name == nil || item.Name.BadNode { - continue - } - tokens[item.Name.Name.Text] = struct{}{} - for i := range item.Fields { - if item.Fields[i].BadNode || item.Fields[i].Identifier == nil || item.Fields[i].Identifier.Name == nil || item.Fields[i].Identifier.Name.BadNode { - continue + for _, item := range v.List { + walk(item) } - tokens[item.Fields[i].Identifier.Name.Text] = struct{}{} - } - } - for _, item := range ast.Structs { - if item.Identifier == nil || item.Identifier.BadNode || item.Identifier.Name == nil || item.Identifier.Name.BadNode { - continue - } - tokens[item.Identifier.Name.Text] = struct{}{} - - for _, field := range item.Fields { - if field.BadNode || field.Identifier == nil || field.Identifier.BadNode || field.Identifier.Name == nil || field.Identifier.Name.BadNode { - continue + for _, entry := range v.Map { + walk(entry.Key) + walk(entry.Value) } - tokens[field.Identifier.Name.Text] = struct{}{} - } - } - for _, item := range ast.Exceptions { - if item.Name == nil || item.Name.BadNode || item.Name.Name == nil || item.Name.Name.BadNode { - continue - } - tokens[item.Name.Name.Text] = struct{}{} - for i := range item.Fields { - if item.Fields[i].BadNode || item.Fields[i].Identifier == nil || item.Fields[i].Identifier.Name == nil || item.Fields[i].Identifier.Name.BadNode { - continue + case *syntax.Struct: + walk(v.Name) + for _, f := range v.Fields { + walk(f) + } + case *syntax.Service: + walk(v.Name) + for _, fn := range v.Functions { + walk(fn) + } + case *syntax.Enum: + walk(v.Name) + for _, ev := range v.Values { + walk(ev) + } + case *syntax.Field: + walk(v.Type) + walk(v.Name) + if v.Value != nil { + walk(v.Value) + } + case *syntax.Function: + if v.Type != nil { + walk(v.Type) } - tokens[item.Fields[i].Identifier.Name.Text] = struct{}{} + walk(v.Name) + for _, a := range v.Args { + walk(a) + } + if v.Throws != nil { + for _, f := range v.Throws.Fields { + walk(f) + } + } + case *syntax.FieldType: + if v.Ident != nil { + walk(v.Ident) + } + if v.KeyType != nil { + walk(v.KeyType) + } + if v.ValueType != nil { + walk(v.ValueType) + } + case *syntax.Const: + walk(v.Type) + walk(v.Name) + walk(v.Value) + case *syntax.Typedef: + walk(v.Type) + walk(v.Name) + case *syntax.EnumValue: + walk(v.Name) } } + for _, n := range ast.Nodes { + walk(n) + } } type ParsedFile struct { fh FileHandle - // ast is latest available ast. current fh content may not to be parsed. - // so it may be nil when fh content is invalid - ast *parser.Document + // ast is the latest available ast. The current fh content may not be + // parsed, so ast may be nil when fh content is invalid. + ast *syntax.Document mapper *mapper.Mapper // errs hold all ast parsing errors - errs []parser.ParserError + errs []syntax.Error } func (p *ParsedFile) Mapper() *mapper.Mapper { return p.mapper } -func (p *ParsedFile) AST() *parser.Document { +func (p *ParsedFile) AST() *syntax.Document { return p.ast } -func (p *ParsedFile) Errors() []parser.ParserError { +func (p *ParsedFile) Errors() []syntax.Error { return p.errs } @@ -209,7 +210,7 @@ func (p *ParsedFile) AggregatedError() error { return fmt.Errorf("aggregated error: %v", p.errs) } -// DumpAST is for debug +// DumpAST is for debug. func (p *ParsedFile) DumpAST() { if p.ast == nil { return @@ -219,7 +220,7 @@ func (p *ParsedFile) DumpAST() { fmt.Println(string(data)) } -// TODO(jpf): use promise +// Parse lexes and parses the file content into a ParsedFile. func Parse(fh FileHandle) (*ParsedFile, error) { content, err := fh.Content() if err != nil { @@ -230,65 +231,15 @@ func Parse(fh FileHandle) (*ParsedFile, error) { fh: fh, } - psr := &parser.PEGParser{} - - ast, errs := psr.Parse(fh.URI().Filename(), content) - for i := range errs { - parserErr, ok := errs[i].(parser.ParserError) - if ok { - pf.errs = append(pf.errs, parserErr) - } - } + ast, errs := syntax.Parse(content) + pf.errs = errs pf.ast = ast if len(errs) > 0 { - log.Debugf("peg parsed err: %v", errs) + slog.Debug("parse failed", "errs", errs) } - mp := mapper.NewMapper(fh.URI(), content) - pf.mapper = mp + pf.mapper = mapper.NewMapper(fh.URI(), content) return pf, nil } - -// type ParseError struct { -// Pos Position -// Msg string -// } -// -// func (e *ParseError) Error() string { -// if e.Pos.Filename != "" || e.Pos.IsValid() { -// // don't print "" -// // TODO(gri) reconsider the semantics of Position.IsValid -// return e.Pos.String() + ": " + e.Msg -// } -// return e.Msg -// } - -type Position struct { - Filename string // filename, if any - Offset int // offset, starting at 0 - Line int // line number, starting at 1 - Column int // column number, starting at 1 (byte count) -} - -func (p Position) IsValid() bool { - return p.Line > 0 -} - -func (p Position) String() string { - s := p.Filename - if p.IsValid() { - if s != "" { - s += ":" - } - s += fmt.Sprintf("%d", p.Line) - if p.Column != 0 { - s += fmt.Sprintf(":%d", p.Column) - } - } - if s == "" { - s = "-" - } - return s -} diff --git a/lsp/cache/parse_ast.go b/lsp/cache/parse_ast.go deleted file mode 100644 index c2d8930..0000000 --- a/lsp/cache/parse_ast.go +++ /dev/null @@ -1,7 +0,0 @@ -package cache - -import "github.com/cloudwego/thriftgo/parser" - -type AST struct { - origin *parser.Thrift -} diff --git a/lsp/cache/resolver_test.go b/lsp/cache/resolver_test.go index 6b54263..09d38dd 100644 --- a/lsp/cache/resolver_test.go +++ b/lsp/cache/resolver_test.go @@ -5,26 +5,27 @@ import ( "path/filepath" "testing" - "github.com/joyme123/thrift-ls/lsp/memoize" - "github.com/joyme123/thrift-ls/parser" "github.com/stretchr/testify/assert" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/memoize" + "github.com/karitham/thrift-ls/syntax" ) func TestResolver(t *testing.T) { tmpDir, err := os.MkdirTemp("", "resolver-test") assert.NoError(t, err) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() baseDir := filepath.Join(tmpDir, "base") sharedDir := filepath.Join(tmpDir, "shared") - err = os.MkdirAll(baseDir, 0755) + err = os.MkdirAll(baseDir, 0o755) assert.NoError(t, err) - err = os.MkdirAll(sharedDir, 0755) + err = os.MkdirAll(sharedDir, 0o755) assert.NoError(t, err) sharedThrift := filepath.Join(sharedDir, "shared.thrift") - err = os.WriteFile(sharedThrift, []byte(""), 0644) + err = os.WriteFile(sharedThrift, []byte(""), 0o644) assert.NoError(t, err) store := &memoize.Store{} @@ -45,7 +46,7 @@ func TestResolver(t *testing.T) { name: "ResolveInclude/relative_to_current_file", fn: func(t *testing.T) { currentFile := filepath.Join(baseDir, "current.thrift") - err := os.WriteFile(currentFile, []byte(""), 0644) + err := os.WriteFile(currentFile, []byte(""), 0o644) assert.NoError(t, err) currentURI := uri.File(currentFile) @@ -59,7 +60,7 @@ func TestResolver(t *testing.T) { name: "ResolveInclude/using_include_paths", fn: func(t *testing.T) { currentFile := filepath.Join(baseDir, "current.thrift") - err := os.WriteFile(currentFile, []byte(""), 0644) + err := os.WriteFile(currentFile, []byte(""), 0o644) assert.NoError(t, err) currentURI := uri.File(currentFile) @@ -73,7 +74,7 @@ func TestResolver(t *testing.T) { name: "ResolveInclude/fallback_uri_when_not_found", fn: func(t *testing.T) { currentFile := filepath.Join(baseDir, "current.thrift") - err := os.WriteFile(currentFile, []byte(""), 0644) + err := os.WriteFile(currentFile, []byte(""), 0o644) assert.NoError(t, err) currentURI := uri.File(currentFile) @@ -86,13 +87,9 @@ func TestResolver(t *testing.T) { { name: "GetIncludePath/matching_name", fn: func(t *testing.T) { - doc := &parser.Document{ - Includes: []*parser.Include{ - { - Path: &parser.Literal{ - Value: &parser.LiteralValue{Text: "shared.thrift"}, - }, - }, + doc := &syntax.Document{ + Nodes: []syntax.Node{ + &syntax.Include{Path: &syntax.Token{Text: "shared.thrift"}}, }, } @@ -103,13 +100,9 @@ func TestResolver(t *testing.T) { { name: "GetIncludePath/non_matching_name", fn: func(t *testing.T) { - doc := &parser.Document{ - Includes: []*parser.Include{ - { - Path: &parser.Literal{ - Value: &parser.LiteralValue{Text: "other.thrift"}, - }, - }, + doc := &syntax.Document{ + Nodes: []syntax.Node{ + &syntax.Include{Path: &syntax.Token{Text: "other.thrift"}}, }, } @@ -120,14 +113,9 @@ func TestResolver(t *testing.T) { { name: "GetIncludePath/bad_nodes_skipped", fn: func(t *testing.T) { - doc := &parser.Document{ - Includes: []*parser.Include{ - {BadNode: true}, - { - Path: &parser.Literal{ - Value: &parser.LiteralValue{Text: "shared.thrift"}, - }, - }, + doc := &syntax.Document{ + Nodes: []syntax.Node{ + &syntax.Include{Path: &syntax.Token{Text: "shared.thrift"}}, }, } @@ -139,17 +127,13 @@ func TestResolver(t *testing.T) { name: "GetIncludeURI/returns_correct_uri", fn: func(t *testing.T) { currentFile := filepath.Join(baseDir, "current.thrift") - err := os.WriteFile(currentFile, []byte(""), 0644) + err := os.WriteFile(currentFile, []byte(""), 0o644) assert.NoError(t, err) currentURI := uri.File(currentFile) - doc := &parser.Document{ - Includes: []*parser.Include{ - { - Path: &parser.Literal{ - Value: &parser.LiteralValue{Text: "shared.thrift"}, - }, - }, + doc := &syntax.Document{ + Nodes: []syntax.Node{ + &syntax.Include{Path: &syntax.Token{Text: "shared.thrift"}}, }, } @@ -163,17 +147,13 @@ func TestResolver(t *testing.T) { name: "GetIncludeURI/not_found_returns_empty", fn: func(t *testing.T) { currentFile := filepath.Join(baseDir, "current.thrift") - err := os.WriteFile(currentFile, []byte(""), 0644) + err := os.WriteFile(currentFile, []byte(""), 0o644) assert.NoError(t, err) currentURI := uri.File(currentFile) - doc := &parser.Document{ - Includes: []*parser.Include{ - { - Path: &parser.Literal{ - Value: &parser.LiteralValue{Text: "other.thrift"}, - }, - }, + doc := &syntax.Document{ + Nodes: []syntax.Node{ + &syntax.Include{Path: &syntax.Token{Text: "other.thrift"}}, }, } diff --git a/lsp/cache/session.go b/lsp/cache/session.go index dbf3692..c3e8de0 100644 --- a/lsp/cache/session.go +++ b/lsp/cache/session.go @@ -52,7 +52,7 @@ func (s *Session) Initialize(fn func()) { } func (s *Session) CreateView(folder uri.URI) { - view := NewView(folder.Filename(), folder, s.overlayFS, s.cache.store, s.cache.IncludePaths) + view := NewView(folder.Path(), folder, s.overlayFS, s.cache.store, s.cache.IncludePaths) s.views = append(s.views, view) } @@ -86,5 +86,5 @@ func (s *Session) ViewOf(fileURI uri.URI) (*View, error) { } func (s *Session) UpdateOverlayFS(ctx context.Context, changes []*FileChange) error { - return s.overlayFS.Update(ctx, changes) + return s.Update(ctx, changes) } diff --git a/lsp/cache/snapshot.go b/lsp/cache/snapshot.go index e8c728b..e157dc1 100644 --- a/lsp/cache/snapshot.go +++ b/lsp/cache/snapshot.go @@ -1,16 +1,21 @@ package cache import ( + "bytes" "context" + "io/fs" + "log/slog" "math/rand" "strings" "sync" + "time" - "github.com/joyme123/thrift-ls/lsp/memoize" - "github.com/joyme123/thrift-ls/parser" - "github.com/joyme123/thrift-ls/resolver" - log "github.com/sirupsen/logrus" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/lsputils" + "github.com/karitham/thrift-ls/lsp/memoize" + "github.com/karitham/thrift-ls/resolver" + "github.com/karitham/thrift-ls/syntax" ) // Resolver provides centralized include path resolution. @@ -25,10 +30,59 @@ type Resolver struct { func NewResolver(ss *Snapshot) *Resolver { return &Resolver{ ss: ss, - central: resolver.New(ss.includePaths), + central: resolver.NewWithFS(ss.includePaths, &snapshotFS{ss: ss}), + } +} + +// snapshotFS is an fs.FS for include resolution: files open in the snapshot +// (editor overlays) exist first, everything else falls through to the disk. +// This lets includes resolve for files that are open but not yet saved. +type snapshotFS struct { + ss *Snapshot + disk fs.FS +} + +func (s *snapshotFS) Stat(name string) (fs.FileInfo, error) { + if _, ok := s.ss.files.Get(uri.File(name)); ok { + return snapshotFileInfo{}, nil + } + if s.disk == nil { + s.disk = resolver.FS() + } + return fs.Stat(s.disk, name) +} + +func (s *snapshotFS) Open(name string) (fs.File, error) { + if fh, ok := s.ss.files.Get(uri.File(name)); ok { + content, err := fh.Content() + if err != nil { + return nil, err + } + return &snapshotFile{Reader: bytes.NewReader(content), info: snapshotFileInfo{}}, nil + } + if s.disk == nil { + s.disk = resolver.FS() } + return s.disk.Open(name) +} + +type snapshotFileInfo struct{} + +func (snapshotFileInfo) Name() string { return "" } +func (snapshotFileInfo) Size() int64 { return 0 } +func (snapshotFileInfo) Mode() fs.FileMode { return 0 } +func (snapshotFileInfo) ModTime() time.Time { return time.Time{} } +func (snapshotFileInfo) IsDir() bool { return false } +func (snapshotFileInfo) Sys() any { return nil } + +type snapshotFile struct { + *bytes.Reader + info fs.FileInfo } +func (f *snapshotFile) Stat() (fs.FileInfo, error) { return f.info, nil } +func (f *snapshotFile) Close() error { return nil } + // IncludePaths returns the include paths configured for this snapshot func (r *Resolver) IncludePaths() []string { return r.ss.includePaths @@ -37,7 +91,7 @@ func (r *Resolver) IncludePaths() []string { // ResolveInclude resolves an include path to a file URI. // It first tries relative to the current file, then tries each include path. func (r *Resolver) ResolveInclude(cur uri.URI, includePath string) uri.URI { - filePath := cur.Filename() + filePath := cur.Path() resolvedPath := r.central.Resolve(filePath, includePath) return uri.File(resolvedPath) } @@ -50,12 +104,12 @@ func (r *Resolver) ResolveIncludeWithText(cur uri.URI, includeText string) uri.U // GetIncludePath returns the include path text for a given include name. // Returns empty string if not found. -func (r *Resolver) GetIncludePath(ast *parser.Document, includeName string) string { - for _, include := range ast.Includes { - if include.BadNode || include.Path == nil || include.Path.BadNode || include.Path.Value == nil { +func (r *Resolver) GetIncludePath(ast *syntax.Document, includeName string) string { + for _, include := range ast.Includes() { + if include.Path == nil { continue } - path := include.Path.Value.Text + path := lsputils.IncludePathText(include) name := getIncludeNameFromPath(path) if name == includeName { return path @@ -66,7 +120,7 @@ func (r *Resolver) GetIncludePath(ast *parser.Document, includeName string) stri // GetIncludeURI returns the URI for an included file by include name. // Returns empty URI if not found. -func (r *Resolver) GetIncludeURI(cur uri.URI, ast *parser.Document, includeName string) uri.URI { +func (r *Resolver) GetIncludeURI(cur uri.URI, ast *syntax.Document, includeName string) uri.URI { path := r.GetIncludePath(ast, includeName) if path == "" { return "" @@ -126,7 +180,6 @@ func (s *Snapshot) Acquire() func() { } func (s *Snapshot) Initialize(ctx context.Context) { - } func (s *Snapshot) Graph() *IncludeGraph { @@ -140,14 +193,14 @@ func (s *Snapshot) Resolver() *Resolver { } func (s *Snapshot) ReadFile(ctx context.Context, uri uri.URI) (FileHandle, error) { - log.Debugln("snapshot read file", uri) + slog.Debug("snapshot read file", "uri", uri) s.view.MarkFileKnown(uri) if fh, ok := s.files.Get(uri); ok { return fh, nil } - log.Debugln("snapshot read from fs") + slog.Debug("snapshot read from fs") fh, err := s.view.fs.ReadFile(ctx, uri) if err != nil { return nil, err @@ -177,16 +230,16 @@ func (s *Snapshot) Parse(ctx context.Context, uri uri.URI) (*ParsedFile, error) // DEBUG // content, _ := fh.Content() - // log.Debugln("parse content:", string(content)) + // slog.Debug("parse content", "content", string(content)) pf, err := Parse(fh) if err != nil { - log.Debugf("snapshot parse err: %v", err) + slog.Debug("snapshot parse failed", "err", err) return nil, err } if pf.AST() != nil { - s.graph.Set(uri, pf.AST().Includes, s.includePaths) + s.graph.Set(uri, pf.AST().Includes(), s.Resolver().ResolveInclude) } s.parsedCache.Set(uri, pf) @@ -227,16 +280,22 @@ func (s *Snapshot) clone() (*Snapshot, func()) { } func BuildSnapshotForTest(files []*FileChange) *Snapshot { + return BuildSnapshotForTestWithPaths(nil, files) +} + +// BuildSnapshotForTestWithPaths is BuildSnapshotForTest with configured +// include paths, for cross-project include resolution tests. +func BuildSnapshotForTestWithPaths(includePaths []string, files []*FileChange) *Snapshot { store := &memoize.Store{} - c := New(store, nil) + c := New(store, includePaths) fs := NewOverlayFS(c) - fs.Update(context.TODO(), files) + _ = fs.Update(context.TODO(), files) - view := NewView("test", "file:///tmp", fs, store, nil) - ss := NewSnapshot(view, store, nil) + view := NewView("test", "file:///tmp", fs, store, includePaths) + ss := NewSnapshot(view, store, includePaths) for _, f := range files { - ss.Parse(context.TODO(), f.URI) + _, _ = ss.Parse(context.TODO(), f.URI) } return ss diff --git a/lsp/cache/view.go b/lsp/cache/view.go index 39dddfd..82ad1c2 100644 --- a/lsp/cache/view.go +++ b/lsp/cache/view.go @@ -2,13 +2,14 @@ package cache import ( "context" + "log/slog" "math/rand" "strings" "sync" - "github.com/joyme123/thrift-ls/lsp/memoize" - log "github.com/sirupsen/logrus" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/memoize" ) type View struct { @@ -62,7 +63,6 @@ func NewView(name string, folder uri.URI, fs FileSource, store *memoize.Store, i view.snapshotMu.Lock() view.snapshot.Initialize(context.Background()) view.snapshotMu.Unlock() - return }() return view @@ -72,8 +72,8 @@ func (v *View) ContainsFile(uri uri.URI) bool { // folder: file:///workdir/ // file: file:///workdir/file.idl - folder := v.folder.Filename() - file := uri.Filename() + folder := v.folder.Path() + file := uri.Path() if !strings.HasPrefix(file, folder) { return false @@ -82,11 +82,7 @@ func (v *View) ContainsFile(uri uri.URI) bool { folder = strings.TrimSuffix(folder, "/") file = strings.TrimPrefix(file, folder) - if strings.HasPrefix(file, "/") { - return true - } - - return false + return strings.HasPrefix(file, "/") } func (v *View) MarkFileKnown(fileURI uri.URI) { @@ -139,24 +135,19 @@ func (v *View) FileChange(ctx context.Context, changes []*FileChange, postFns .. _, err := v.snapshot.Parse(ctx, uri) v.snapshotMu.Unlock() if err != nil { - log.Errorf("parse error: %v", err) + slog.Error("parse error", "err", err) } } for i := range postFns { postFns[i]() } - // }() - return } func (v *View) Snapshot() (*Snapshot, func()) { v.snapshotMu.Lock() defer v.snapshotMu.Unlock() - if v.snapshot == nil { - - } - + // The snapshot is created in NewView and only set to nil on shutdown. return v.snapshot, v.snapshot.Acquire() } diff --git a/lsp/codejump.go b/lsp/codejump.go index 83652bb..958670f 100644 --- a/lsp/codejump.go +++ b/lsp/codejump.go @@ -3,8 +3,9 @@ package lsp import ( "context" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/codejump" + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/lsp/codejump" ) func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) (result []protocol.Location, err error) { diff --git a/lsp/codejump/cross_project_test.go b/lsp/codejump/cross_project_test.go new file mode 100644 index 0000000..90c83ee --- /dev/null +++ b/lsp/codejump/cross_project_test.go @@ -0,0 +1,87 @@ +package codejump + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" + "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" +) + +// TestDefinitionCrossProjectInclude verifies go-to-definition across project +// boundaries: the current file includes "gundam.thrift" which only exists +// under the configured include path (like the compiler's -I). No files are +// written to disk: both files live in the snapshot's overlay, and include +// resolution consults open files before the disk. +func TestDefinitionCrossProjectInclude(t *testing.T) { + dir := t.TempDir() + includeDir := filepath.Join(dir, "whitebase") + appDir := filepath.Join(dir, "zeon", "apis", "v1") + + gundamFile := uri.File(filepath.Join(includeDir, "gundam.thrift")) + appFile := uri.File(filepath.Join(appDir, "char.thrift")) + appContent := "include \"gundam.thrift\"\n\nstruct Hangar {\n 1: optional list suits,\n}\n\nstruct Garrison {\n 1: optional MobileSuit ace,\n}\n" + + ss := cache.BuildSnapshotForTestWithPaths([]string{includeDir}, []*cache.FileChange{ + { + URI: gundamFile, + Version: 0, + Content: []byte("namespace * gundam\n\nstruct MobileSuit {\n 1: string model\n}"), + From: cache.FileChangeTypeDidOpen, + }, + { + URI: appFile, + Version: 0, + Content: []byte(appContent), + From: cache.FileChangeTypeDidOpen, + }, + }) + + // Marker-based positions: the cursor on "MobileSuit" in both usages. + posOf := func(marker string, offset ...int) protocol.Position { + idx := strings.Index(appContent, marker) + if len(offset) > 0 { + idx += offset[0] + } + lineStart := idx + for lineStart > 0 && appContent[lineStart-1] != '\n' { + lineStart-- + } + line := 0 + for i := 0; i < lineStart; i++ { + if appContent[i] == '\n' { + line++ + } + } + return protocol.Position{Line: uint32(line), Character: uint32(idx - lineStart)} + } + + tests := []struct { + name string + marker string + offset int + }{ + { + name: "qualified cross-project type", + marker: "gundam.MobileSuit", + offset: len("gundam."), + }, + { + name: "unqualified cross-project type", + marker: " 1: optional MobileSuit ace", + offset: len(" 1: optional "), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + locs, err := Definition(t.Context(), ss, appFile, posOf(tt.marker, tt.offset)) + assert.NoError(t, err) + assert.Len(t, locs, 1, "should find the definition in the included file") + assert.Equal(t, gundamFile, locs[0].URI) + }) + } +} diff --git a/lsp/codejump/definition.go b/lsp/codejump/definition.go index 137bf56..ef77d11 100644 --- a/lsp/codejump/definition.go +++ b/lsp/codejump/definition.go @@ -2,215 +2,178 @@ package codejump import ( "context" - "errors" - - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/lsp/types" - "github.com/joyme123/thrift-ls/parser" - log "github.com/sirupsen/logrus" + "log/slog" + + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/lsputils" + "github.com/karitham/thrift-ls/syntax" ) +// Definition returns the locations of the definition under the cursor: +// a type reference, a constant value identifier, or a service reference. func Definition(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { res = make([]protocol.Location, 0) - pf, err := ss.Parse(ctx, file) + pf, target, err := resolveTarget(ctx, ss, file, pos) if err != nil { - return + return res, err } - if pf.AST() == nil { - err = errors.New("parse ast failed") - return + switch target.kind { + case TargetTypeName: + return typeNameDefinition(ctx, ss, file, pf, target) + case TargetConstValue: + return constValueDefinition(ctx, ss, file, pf, target) + case TargetService: + return serviceDefinition(ctx, ss, file, pf, target) } + return res, err +} - astPos, err := pf.Mapper().LSPPosToParserPosition(types.Position{Line: pos.Line, Character: pos.Character}) - if err != nil { - return +// FindTypeDefinition resolves a type reference to its definition: an +// exception, struct, enum, union, or typedef, possibly in an included file. +func FindTypeDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *syntax.Document, ft *syntax.FieldType) (uri.URI, *syntax.Identifier, DefinitionKind, error) { + name := typeReferenceName(ft) + if name == "" || IsBasicType(name) { + return "", nil, DefinitionNone, nil } - nodePath := parser.SearchNodePathByPosition(pf.AST(), astPos) - targetNode := nodePath[len(nodePath)-1] - switch targetNode.Type() { - case "TypeName": - return typeNameDefinition(ctx, ss, file, pf.AST(), targetNode) - case "ConstValue": - return constValueTypeDefinition(ctx, ss, file, pf.AST(), targetNode) - case "IdentifierName": // service extends - return serviceDefinition(ctx, ss, file, pf.AST(), targetNode) - } + _, identifier := lsputils.ParseIdent(file, ast.Includes(), name) + for _, astFile := range definitionFiles(ctx, ss, file, ast, name) { + dstAst, err := parseDefinitionFile(ctx, ss, astFile) + if err != nil { + return astFile, nil, DefinitionNone, err + } - return + if dstException := GetExceptionNode(dstAst, identifier); dstException != nil { + return astFile, dstException.Name, DefinitionException, nil + } + if dstStruct := GetStructNode(dstAst, identifier); dstStruct != nil { + return astFile, dstStruct.Name, DefinitionStruct, nil + } + if dstEnum := GetEnumNode(dstAst, identifier); dstEnum != nil { + return astFile, dstEnum.Name, DefinitionEnum, nil + } + if dstUnion := GetUnionNode(dstAst, identifier); dstUnion != nil { + return astFile, dstUnion.Name, DefinitionUnion, nil + } + if dstTypedef := GetTypedefNode(dstAst, identifier); dstTypedef != nil { + return astFile, dstTypedef.Name, DefinitionTypedef, nil + } + } + return file, nil, DefinitionNone, nil } -func serviceDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, targetNode parser.Node) ([]protocol.Location, error) { - res := make([]protocol.Location, 0) - astFile, id, _, err := ServiceDefinitionIdentifier(ctx, ss, file, ast, targetNode) - if err != nil { - return res, err +// FindConstValueDefinition resolves a constant value identifier to its +// definition: an enum value or a const, possibly in an included file. +func FindConstValueDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *syntax.Document, value *syntax.ConstValue) (uri.URI, *syntax.Identifier, error) { + if value == nil || value.Kind != syntax.ValueIdent { + return "", nil, nil } - if id != nil { - res = append(res, jump(astFile, id.Name)) + name := value.Text + if name == "true" || name == "false" { + return "", nil, nil } - return res, nil -} + _, identifier := lsputils.ParseIdent(file, ast.Includes(), name) + for _, astFile := range definitionFiles(ctx, ss, file, ast, name) { + dstAst, err := parseDefinitionFile(ctx, ss, astFile) + if err != nil { + return astFile, nil, err + } -func ServiceDefinitionIdentifier(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, targetNode parser.Node) (uri.URI, *parser.Identifier, string, error) { - identifierName := targetNode.(*parser.IdentifierName) - - include, identifier := lsputils.ParseIdent(file, ast.Includes, identifierName.Text) - var astFile uri.URI - if include == "" { - astFile = file - } else { - resolver := ss.Resolver() - path := resolver.GetIncludePath(ast, include) - if path == "" { // doesn't match any include path - return "", nil, "", nil + if dstEnumValue := GetEnumValueIdentifierNode(dstAst, identifier); dstEnumValue != nil { + return astFile, dstEnumValue, nil + } + if constIdentifier := GetConstIdentifierNode(dstAst, identifier); constIdentifier != nil { + return astFile, constIdentifier, nil } - astFile = resolver.ResolveInclude(file, path) } + return file, nil, nil +} - // now we can find destinate definition in `dstAst` by `identifier` - dstAst, err := ss.Parse(ctx, astFile) - if err != nil { - return astFile, nil, "", err +// FindServiceDefinition resolves a service name or extends reference to the +// service definition. +func FindServiceDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *syntax.Document, ident *syntax.Identifier) (uri.URI, *syntax.Identifier, error) { + if ident == nil { + return "", nil, nil } - if len(dstAst.Errors()) > 0 { - log.Errorf("parse error: %v", dstAst.Errors()) - } + _, identifier := lsputils.ParseIdent(file, ast.Includes(), ident.Text) + for _, astFile := range definitionFiles(ctx, ss, file, ast, ident.Text) { + dstAst, err := parseDefinitionFile(ctx, ss, astFile) + if err != nil { + return astFile, nil, err + } - dstService := GetServiceNode(dstAst.AST(), identifier) - if dstService != nil { - return astFile, dstService.Name, "Service", nil + if dstService := GetServiceNode(dstAst, identifier); dstService != nil { + return astFile, dstService.Name, nil + } } - - return astFile, nil, "", nil + return file, nil, nil } -func typeNameDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, targetNode parser.Node) ([]protocol.Location, error) { - res := make([]protocol.Location, 0) - astFile, id, _, err := TypeNameDefinitionIdentifier(ctx, ss, file, ast, targetNode) +// parseDefinitionFile parses the definition file, tolerating parse errors +// in the target file (the definitions may still be found in the partial +// AST). +func parseDefinitionFile(ctx context.Context, ss *cache.Snapshot, file uri.URI) (*syntax.Document, error) { + pf, err := ss.Parse(ctx, file) if err != nil { - return res, err + return nil, err } - if id != nil { - res = append(res, jump(astFile, id.Name)) + if len(pf.Errors()) > 0 { + slog.Error("parse error", "errs", pf.Errors()) } - - return res, nil -} - -func TypeNameDefinitionIdentifier(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, targetNode parser.Node) (uri.URI, *parser.Identifier, string, error) { - typeName := targetNode.(*parser.TypeName) - typeV := typeName.Name - if IsBasicType(typeV) { - return "", nil, "", nil - } - - include, identifier := lsputils.ParseIdent(file, ast.Includes, typeV) - var astFile uri.URI - if include == "" { - astFile = file - } else { - resolver := ss.Resolver() - path := resolver.GetIncludePath(ast, include) - if path == "" { // doesn't match any include path - return "", nil, "", nil - } - astFile = resolver.ResolveInclude(file, path) + if pf.AST() == nil { + return nil, errNoAST } + return pf.AST(), nil +} - // now we can find destinate definition in `dstAst` by `identifier` - dstAst, err := ss.Parse(ctx, astFile) +func typeNameDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) ([]protocol.Location, error) { + ft := target.parent.(*syntax.FieldType) + astFile, id, _, err := FindTypeDefinition(ctx, ss, file, pf.AST(), ft) if err != nil { - return astFile, nil, "", err + return nil, err } - - if len(dstAst.Errors()) > 0 { - log.Errorf("parse error: %v", dstAst.Errors()) + if id == nil { + return nil, nil } - - // struct, exception, enum or union - dstException := GetExceptionNode(dstAst.AST(), identifier) - if dstException != nil { - return astFile, dstException.Name, "Exception", nil - } - dstStruct := GetStructNode(dstAst.AST(), identifier) - if dstStruct != nil { - return astFile, dstStruct.Identifier, "Struct", nil - } - dstEnum := GetEnumNode(dstAst.AST(), identifier) - if dstEnum != nil { - return astFile, dstEnum.Name, "Enum", nil - } - dstUnion := GetUnionNode(dstAst.AST(), identifier) - if dstUnion != nil { - return astFile, dstUnion.Name, "Union", nil - } - dstTypedef := GetTypedefNode(dstAst.AST(), identifier) - if dstTypedef != nil { - return astFile, dstTypedef.Alias, "Typedef", nil + loc, err := jumpInFile(ctx, ss, astFile, id) + if err != nil { + return nil, err } - - return astFile, nil, "", nil + return []protocol.Location{loc}, nil } -// search enum -func constValueTypeDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, targetNode parser.Node) ([]protocol.Location, error) { - res := make([]protocol.Location, 0) - astFile, id, err := ConstValueTypeDefinitionIdentifier(ctx, ss, file, ast, targetNode) +func constValueDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) ([]protocol.Location, error) { + astFile, id, err := FindConstValueDefinition(ctx, ss, file, pf.AST(), target.node.(*syntax.ConstValue)) if err != nil { - return res, err - } - - if id != nil { - res = append(res, jump(astFile, id)) + return nil, err } - - return res, nil -} - -func ConstValueTypeDefinitionIdentifier(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, targetNode parser.Node) (uri.URI, *parser.Identifier, error) { - constValue := targetNode.(*parser.ConstValue) - if constValue.TypeName != "identifier" { - return "", nil, nil + if id == nil { + return nil, nil } - - include, identifier := lsputils.ParseIdent(file, ast.Includes, constValue.Value.(string)) - var astFile uri.URI - if include == "" { - astFile = file - } else { - resolver := ss.Resolver() - path := resolver.GetIncludePath(ast, include) - if path == "" { // doesn't match any include path, maybe enum value - include = "" - identifier = constValue.Value.(string) - astFile = file - } else { - astFile = resolver.ResolveInclude(file, path) - } + loc, err := jumpInFile(ctx, ss, astFile, id) + if err != nil { + return nil, err } + return []protocol.Location{loc}, nil +} - // now we can find destinate definition in `dstAst` by `identifier` - dstAst, err := ss.Parse(ctx, astFile) +func serviceDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) ([]protocol.Location, error) { + astFile, id, err := FindServiceDefinition(ctx, ss, file, pf.AST(), target.identifier()) if err != nil { - return astFile, nil, err + return nil, err } - - dstEnumValueIdentifier := GetEnumValueIdentifierNode(dstAst.AST(), identifier) - if dstEnumValueIdentifier != nil { - return astFile, dstEnumValueIdentifier, nil + if id == nil { + return nil, nil } - - constIdentifier := GetConstIdentifierNode(dstAst.AST(), identifier) - if constIdentifier != nil { - return astFile, constIdentifier, nil + loc, err := jumpInFile(ctx, ss, astFile, id) + if err != nil { + return nil, err } - - return astFile, nil, nil + return []protocol.Location{loc}, nil } diff --git a/lsp/codejump/definition_list_test.go b/lsp/codejump/definition_list_test.go index 4e5fff0..8078da3 100644 --- a/lsp/codejump/definition_list_test.go +++ b/lsp/codejump/definition_list_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" ) func TestDefinition_EnumValueInConstList(t *testing.T) { @@ -46,7 +47,7 @@ const list my_list = [MyEnum.Value1, MyEnum.Value2]` { name: "enum value in const list - first element", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/test.thrift", pos: protocol.Position{ @@ -64,7 +65,7 @@ const list my_list = [MyEnum.Value1, MyEnum.Value2]` }, End: protocol.Position{ Line: 1, - Character: 9, // End of "Value1" + Character: 8, // End of "Value1" }, }, }, @@ -74,7 +75,7 @@ const list my_list = [MyEnum.Value1, MyEnum.Value2]` { name: "enum value in const list - second element", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/test.thrift", pos: protocol.Position{ @@ -92,7 +93,7 @@ const list my_list = [MyEnum.Value1, MyEnum.Value2]` }, End: protocol.Position{ Line: 2, - Character: 9, // End of "Value2" + Character: 8, // End of "Value2" }, }, }, diff --git a/lsp/codejump/definition_test.go b/lsp/codejump/definition_test.go index ad4c249..14334df 100644 --- a/lsp/codejump/definition_test.go +++ b/lsp/codejump/definition_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" ) func TestDefinition(t *testing.T) { @@ -99,7 +100,7 @@ struct Person { { name: "case struct", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -127,7 +128,7 @@ struct Person { { name: "case union", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -155,7 +156,7 @@ struct Person { { name: "case enum", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -183,7 +184,7 @@ struct Person { { name: "case exceptions", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -211,7 +212,7 @@ struct Person { { name: "case typedef", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -239,7 +240,7 @@ struct Person { { name: "case enumvalue", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -267,7 +268,7 @@ struct Person { { name: "case const", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -295,7 +296,7 @@ struct Person { { name: "case include 1", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/app.thrift", pos: protocol.Position{ @@ -323,7 +324,7 @@ struct Person { { name: "case include 2", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/app.thrift", pos: protocol.Position{ diff --git a/lsp/codejump/hits.go b/lsp/codejump/hits.go new file mode 100644 index 0000000..6017c4d --- /dev/null +++ b/lsp/codejump/hits.go @@ -0,0 +1,20 @@ +package codejump + +import ( + "go.lsp.dev/protocol" +) + +// referenceHit is a matched reference with the text of the matched +// identifier, so rename can preserve include qualifiers. +type referenceHit struct { + loc protocol.Location + text string +} + +func hits(hits []referenceHit) []protocol.Location { + out := make([]protocol.Location, 0, len(hits)) + for _, h := range hits { + out = append(out, h.loc) + } + return out +} diff --git a/lsp/codejump/hover.go b/lsp/codejump/hover.go index 24003dd..88134fe 100644 --- a/lsp/codejump/hover.go +++ b/lsp/codejump/hover.go @@ -2,184 +2,100 @@ package codejump import ( "context" - "errors" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/format" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/lsp/types" - "github.com/joyme123/thrift-ls/parser" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/formatter" + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/syntax" ) +// Hover returns the formatted definition under the cursor: a type +// reference, a constant value identifier, or a service reference. func Hover(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (res string, err error) { - pf, err := ss.Parse(ctx, file) - if err != nil { - return - } - - if pf.AST() == nil { - err = errors.New("parse ast failed") - return - } - - astPos, err := pf.Mapper().LSPPosToParserPosition(types.Position{Line: pos.Line, Character: pos.Character}) + pf, target, err := resolveTarget(ctx, ss, file, pos) if err != nil { - return + return res, err } - nodePath := parser.SearchNodePathByPosition(pf.AST(), astPos) - targetNode := nodePath[len(nodePath)-1] - - log.Info("node type:", targetNode.Type()) - switch targetNode.Type() { - case "TypeName": - return hoverDefinition(ctx, ss, file, pf.AST(), targetNode) - case "ConstValue": - return hoverConstValue(ctx, ss, file, pf.AST(), targetNode) - case "IdentifierName": // service extends - return hoverService(ctx, ss, file, pf.AST(), targetNode) + switch target.kind { + case TargetTypeName: + return hoverDefinition(ctx, ss, file, pf, target) + case TargetConstValue: + return hoverConstValue(ctx, ss, file, pf, target) + case TargetService: + return hoverService(ctx, ss, file, pf, target) } + return res, err +} - return +// formatNode renders a definition node as hover text with default options. +func formatNode(doc *syntax.Document, node syntax.Node) (string, error) { + return formatter.FormatNode(doc, node, formatter.DefaultOptions()) } -func hoverService(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, targetNode parser.Node) (string, error) { - identifierName := targetNode.(*parser.IdentifierName) - name := identifierName.Text - include, identifier := lsputils.ParseIdent(file, ast.Includes, name) - var astFile uri.URI - if include == "" { - astFile = file - } else { - resolver := ss.Resolver() - path := resolver.GetIncludePath(ast, include) - if path == "" { // doesn't match any include path - return "", nil - } - astFile = resolver.ResolveInclude(file, path) +func hoverService(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) (string, error) { + astFile, id, err := FindServiceDefinition(ctx, ss, file, pf.AST(), target.identifier()) + if err != nil || id == nil { + return "", err } - - // now we can find destinate definition in `dstAst` by `identifier` - dstAst, err := ss.Parse(ctx, astFile) + dstAst, err := parseDefinitionFile(ctx, ss, astFile) if err != nil { return "", err } - - if len(dstAst.Errors()) > 0 { - log.Errorf("parse error: %v", dstAst.Errors()) - } - - dstService := GetServiceNode(dstAst.AST(), identifier) - if dstService != nil { - // Use default options for hover display - opts := format.Options{} - return format.MustFormatService(dstService, opts), nil - } - - return "", nil -} - -func hoverDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, targetNode parser.Node) (string, error) { - typeName := targetNode.(*parser.TypeName) - typeV := typeName.Name - if IsBasicType(typeV) { + svc := GetServiceNode(dstAst, id.Text) + if svc == nil { return "", nil } + return formatNode(dstAst, svc) +} - include, identifier := lsputils.ParseIdent(file, ast.Includes, typeV) - var astFile uri.URI - if include == "" { - astFile = file - } else { - resolver := ss.Resolver() - path := resolver.GetIncludePath(ast, include) - if path == "" { // doesn't match any include path - return "", nil - } - astFile = resolver.ResolveInclude(file, path) +func hoverDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) (string, error) { + ft := target.parent.(*syntax.FieldType) + astFile, id, kind, err := FindTypeDefinition(ctx, ss, file, pf.AST(), ft) + if err != nil || id == nil { + return "", err } - - // now we can find destinate definition in `dstAst` by `identifier` - dstAst, err := ss.Parse(ctx, astFile) + dstAst, err := parseDefinitionFile(ctx, ss, astFile) if err != nil { return "", err } - if len(dstAst.Errors()) > 0 { - log.Errorf("parse error: %v", dstAst.Errors()) - } - - // Use default options for hover display - opts := format.Options{} - - // struct, exception, enum or union - dstException := GetExceptionNode(dstAst.AST(), identifier) - if dstException != nil { - return format.MustFormatException(dstException, opts), nil - } - dstStruct := GetStructNode(dstAst.AST(), identifier) - if dstStruct != nil { - return format.MustFormatStruct(dstStruct, opts), nil - } - dstEnum := GetEnumNode(dstAst.AST(), identifier) - if dstEnum != nil { - return format.MustFormatEnum(dstEnum, opts), nil - } - dstUnion := GetUnionNode(dstAst.AST(), identifier) - if dstUnion != nil { - return format.MustFormatUnion(dstUnion, opts), nil - } - dstTypedef := GetTypedefNode(dstAst.AST(), identifier) - if dstTypedef != nil { - return format.MustFormatTypedef(dstTypedef, opts), nil - } - - return "", nil -} - -func hoverConstValue(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, targetNode parser.Node) (string, error) { - constValue := targetNode.(*parser.ConstValue) - if constValue.TypeName != "identifier" { + var node syntax.Node + switch kind { + case DefinitionException: + node = GetExceptionNode(dstAst, id.Text) + case DefinitionStruct: + node = GetStructNode(dstAst, id.Text) + case DefinitionEnum: + node = GetEnumNode(dstAst, id.Text) + case DefinitionUnion: + node = GetUnionNode(dstAst, id.Text) + case DefinitionTypedef: + node = GetTypedefNode(dstAst, id.Text) + } + if node == nil { return "", nil } + return formatNode(dstAst, node) +} - include, identifier := lsputils.ParseIdent(file, ast.Includes, constValue.Value.(string)) - var astFile uri.URI - if include == "" { - astFile = file - } else { - resolver := ss.Resolver() - path := resolver.GetIncludePath(ast, include) - if path == "" { // doesn't match any include path, maybe enum value - include = "" - identifier = constValue.Value.(string) - astFile = file - } else { - astFile = resolver.ResolveInclude(file, path) - } +func hoverConstValue(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) (string, error) { + astFile, id, err := FindConstValueDefinition(ctx, ss, file, pf.AST(), target.node.(*syntax.ConstValue)) + if err != nil || id == nil { + return "", err } - - // now we can find destinate definition in `dstAst` by `identifier` - dstAst, err := ss.Parse(ctx, astFile) + dstAst, err := parseDefinitionFile(ctx, ss, astFile) if err != nil { return "", err } - // Use default options for hover display - opts := format.Options{} - - dstEnum := GetEnumNodeByEnumValue(dstAst.AST(), identifier) - if dstEnum != nil { - return format.MustFormatEnum(dstEnum, opts), nil + if dstEnum := GetEnumNodeByEnumValue(dstAst, id.Text); dstEnum != nil { + return formatNode(dstAst, dstEnum) } - - dstConst := GetConstNode(dstAst.AST(), identifier) - if dstConst != nil { - return format.MustFormatConst(dstConst, opts), nil + if dstConst := GetConstNode(dstAst, id.Text); dstConst != nil { + return formatNode(dstAst, dstConst) } - return "", nil } diff --git a/lsp/codejump/hover_test.go b/lsp/codejump/hover_test.go new file mode 100644 index 0000000..baf481f --- /dev/null +++ b/lsp/codejump/hover_test.go @@ -0,0 +1,97 @@ +package codejump + +import ( + "strings" + "testing" + + "go.lsp.dev/protocol" + + "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" +) + +func TestHover(t *testing.T) { + file1 := `struct User { + 1: required i64 id +} + +enum Color { + RED = 1, + GREEN +} + +const i32 DEFAULT = 5 + +service Base { void ping() }` + file2 := `include "user.thrift" + +service Svc extends Base { + User getUser(1: i64 id, 2: string name) throws (Color c) +} + +const Color defaultColor = GREEN` + ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + {URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), From: cache.FileChangeTypeDidOpen}, + {URI: "file:///tmp/api.thrift", Version: 0, Content: []byte(file2), From: cache.FileChangeTypeDidOpen}, + }) + + posOf := func(file, marker string, offset ...int) protocol.Position { + idx := indexOf(file, marker) + if len(offset) > 0 { + idx += offset[0] + } + if idx < 0 { + t.Fatalf("marker %q not found", marker) + } + line, col := 0, 0 + for i := 0; i < idx; i++ { + if file[i] == '\n' { + line++ + col = 0 + } else { + col++ + } + } + return protocol.Position{Line: uint32(line), Character: uint32(col)} + } + + tests := []struct { + name string + content string + file string + marker string + want string // substring of the hover text + offset int + }{ + {"struct type reference", file2, "file:///tmp/api.thrift", "User getUser", "struct User", 0}, + {"enum type reference", file2, "file:///tmp/api.thrift", "Color c", "enum Color", 0}, + {"service extends", file2, "file:///tmp/api.thrift", "extends Base", "service Base", 8}, + {"service name", file2, "file:///tmp/api.thrift", "service Svc", "service Svc", 8}, + {"const value", file2, "file:///tmp/api.thrift", "= GREEN", "enum Color", 2}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Hover(t.Context(), ss, uri.URI(tt.file), posOf(tt.content, tt.marker, tt.offset)) + if err != nil { + t.Fatalf("Hover: %v", err) + } + if !strings.Contains(got, tt.want) { + t.Errorf("hover text %q does not contain %q", got, tt.want) + } + }) + } +} + +func TestHoverUnresolvable(t *testing.T) { + ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + {URI: "file:///tmp/test.thrift", Version: 0, Content: []byte("struct S {\n 1: Missing x\n}"), From: cache.FileChangeTypeDidOpen}, + }) + got, err := Hover(t.Context(), ss, "file:///tmp/test.thrift", protocol.Position{Line: 1, Character: 8}) + if err != nil { + t.Fatalf("Hover: %v", err) + } + if got != "" { + t.Errorf("expected empty hover for undefined type, got %q", got) + } +} diff --git a/lsp/codejump/reference.go b/lsp/codejump/reference.go index 2c12281..867b460 100644 --- a/lsp/codejump/reference.go +++ b/lsp/codejump/reference.go @@ -2,471 +2,396 @@ package codejump import ( "context" - "errors" "fmt" + "log/slog" "strings" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/lsp/types" - "github.com/joyme123/thrift-ls/parser" - utilerrors "github.com/joyme123/thrift-ls/utils/errors" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/lsputils" + "github.com/karitham/thrift-ls/syntax" ) -var validReferenceDefinitionType = map[string]struct{}{ - "Struct": {}, - "Union": {}, - "Enum": {}, - "Exception": {}, - "Typedef": {}, +var validReferenceDefinitionType = map[DefinitionKind]struct{}{ + DefinitionStruct: {}, + DefinitionUnion: {}, + DefinitionEnum: {}, + DefinitionException: {}, + DefinitionTypedef: {}, } +// Reference returns the locations of all references to the definition under +// the cursor: type definitions, constant values, enum values, and services. func Reference(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { res = make([]protocol.Location, 0) - pf, err := ss.Parse(ctx, file) + pf, target, err := resolveTarget(ctx, ss, file, pos) if err != nil { - return - } - - if pf.AST() == nil { - err = errors.New("parse ast failed") - return + return res, err } - astPos, err := pf.Mapper().LSPPosToParserPosition(types.Position{Line: pos.Line, Character: pos.Character}) - if err != nil { - return + switch target.kind { + case TargetTypeName: + refs, err := searchTypeNameReferences(ctx, ss, file, pf, target) + if err != nil { + return nil, err + } + return hits(refs), nil + case TargetConstValue: + refs, err := searchConstValueReferences(ctx, ss, file, pf, target) + if err != nil { + return nil, err + } + return hits(refs), nil + case TargetService: + refs, err := searchServiceReferences(ctx, ss, file, target.identifier().Text) + if err != nil { + return nil, err + } + return hits(refs), nil + case TargetDefinition: + refs, err := searchDefinitionReferences(ctx, ss, file, pf, target) + if err != nil { + return nil, err + } + return hits(refs), nil } - nodePath := parser.SearchNodePathByPosition(pf.AST(), astPos) - targetNode := nodePath[len(nodePath)-1] + return res, err +} - switch targetNode.Type() { - case "TypeName": - return searchTypeNameReferences(ctx, ss, file, pf.AST(), nodePath, targetNode) - case "IdentifierName": - if len(nodePath) <= 2 { - return - } - // identifierName -> identifier -> definition - parentDefinitionNode := nodePath[len(nodePath)-3] - definitionType := parentDefinitionNode.Type() - if definitionType == "EnumValue" || definitionType == "Const" { - var typeName string - if definitionType == "Const" { - typeName = fmt.Sprintf("%s.%s", lsputils.GetIncludeName(file), targetNode.(*parser.IdentifierName).Text) - } else { - enumNode := nodePath[len(nodePath)-4] - typeName = fmt.Sprintf("%s.%s.%s", lsputils.GetIncludeName(file), enumNode.(*parser.Enum).Name.Name.Text, targetNode.(*parser.IdentifierName).Text) - } - // search in const value - return searchConstValueIdentifierReferences(ctx, ss, file, typeName) - } else if definitionType == "Service" { - svcName := targetNode.(*parser.IdentifierName).Text - if !strings.Contains(svcName, ".") { - svcName = fmt.Sprintf("%s.%s", lsputils.GetIncludeName(file), svcName) - } else { - include, _ := lsputils.ParseIdent(file, pf.AST().Includes, svcName) +// searchDefinitionReferences handles references from a definition name: +// const and enum value names reference constant usages; struct, union, +// enum, exception, and typedef names reference type usages. +func searchDefinitionReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) (res []referenceHit, err error) { + id := target.identifier() + if id == nil { + return res, err + } + + parent := target.parent + switch parent.(type) { + case *syntax.Const: + typeName := fmt.Sprintf("%s.%s", lsputils.GetIncludeName(file), id.Text) + return searchConstValueIdentifierReferences(ctx, ss, file, typeName) + case *syntax.EnumValue: + enum, ok := grandparent(target.path).(*syntax.Enum) + if !ok { + return res, err + } + typeName := fmt.Sprintf("%s.%s.%s", lsputils.GetIncludeName(file), enum.Name.Text, id.Text) + return searchConstValueIdentifierReferences(ctx, ss, file, typeName) + case *syntax.Service: + svcName := id.Text + if strings.Contains(svcName, ".") { + include, _ := lsputils.ParseIdent(file, pf.AST().Includes(), svcName) resolver := ss.Resolver() - path := resolver.GetIncludePath(pf.AST(), include) - if path != "" { // doesn't match any include path + if path := resolver.GetIncludePath(pf.AST(), include); path != "" { file = resolver.ResolveInclude(file, path) } + } else { + svcName = fmt.Sprintf("%s.%s", lsputils.GetIncludeName(file), svcName) } - return searchServiceReferences(ctx, ss, file, svcName) - } + return searchServiceReferences(ctx, ss, file, svcName) + } - if _, ok := validReferenceDefinitionType[definitionType]; !ok { - return - } + kind, ok := definitionKindOf(parent) + if !ok { + return res, err + } + if _, ok := validReferenceDefinitionType[kind]; !ok { + return res, err + } + typeName := fmt.Sprintf("%s.%s", lsputils.GetIncludeName(file), id.Text) + return searchIdentifierReferences(ctx, ss, file, typeName, kind) +} - // typeName is base.User - typeName := fmt.Sprintf("%s.%s", lsputils.GetIncludeName(file), targetNode.(*parser.IdentifierName).Text) - return searchIdentifierReferences(ctx, ss, file, typeName, definitionType) - case "ConstValue": - return searchConstValueReferences(ctx, ss, file, pf.AST(), nodePath, targetNode) - default: - log.Warningln("unsupport type for reference:", targetNode.Type()) +func grandparent(path []syntax.Node) syntax.Node { + if len(path) < 3 { + return nil } + return path[len(path)-3] +} - return +// definitionKindOf maps a definition node to its kind. +func definitionKindOf(n syntax.Node) (DefinitionKind, bool) { + switch v := n.(type) { + case *syntax.Struct: + switch v.Kind { + case syntax.StructDecl: + return DefinitionStruct, true + case syntax.UnionDecl: + return DefinitionUnion, true + case syntax.ExceptionDecl: + return DefinitionException, true + } + case *syntax.Enum: + return DefinitionEnum, true + case *syntax.Typedef: + return DefinitionTypedef, true + case *syntax.Const: + return DefinitionConst, true + case *syntax.Service: + return DefinitionService, true + } + return DefinitionNone, false } -func searchTypeNameReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, nodePath []parser.Node, targetNode parser.Node) (res []protocol.Location, err error) { - res = make([]protocol.Location, 0) - typeNameNode := targetNode.(*parser.TypeName) - typeName := typeNameNode.Name - if IsBasicType(typeName) { - return +func searchTypeNameReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) (res []referenceHit, err error) { + res = make([]referenceHit, 0) + ft := target.parent.(*syntax.FieldType) + typeName := typeReferenceName(ft) + if typeName == "" || IsBasicType(typeName) { + return res, err } - var errs []error - // search type definition - definitionFile, identifierNode, definitionType, err := TypeNameDefinitionIdentifier(ctx, ss, file, ast, targetNode) + // Search the type definition. + definitionFile, identifierNode, definitionType, err := FindTypeDefinition(ctx, ss, file, pf.AST(), ft) if err != nil { - errs = append(errs, err) + return res, err } - if identifierNode == nil { - return + return res, err } - res = append(res, jump(definitionFile, identifierNode.Name)) + loc, err := jumpInFile(ctx, ss, definitionFile, identifierNode) + if err != nil { + return res, err + } + res = append(res, referenceHit{loc: loc, text: identifierNode.Text}) + // Search usages of the type name. locations, err := searchIdentifierReferences(ctx, ss, definitionFile, typeName, definitionType) if err != nil { - errs = append(errs, err) + return res, err } res = append(res, locations...) - - if len(errs) > 0 { - err = utilerrors.NewAggregate(errs) - } - - return + return res, err } -func searchServiceReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, svcName string) (res []protocol.Location, err error) { - log.Debugln("searchServiceReferences for file:", file, "svcName:", svcName) - var errs []error +func searchServiceReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, svcName string) (res []referenceHit, err error) { + slog.Debug("searching service references", "file", file, "svcName", svcName) - // search in it self locations, err := searchServiceDefinitionReferences(ctx, ss, file, strings.TrimPrefix(svcName, fmt.Sprintf("%s.", lsputils.GetIncludeName(file)))) if err != nil { - errs = append(errs, err) + return nil, err } res = append(res, locations...) - // search service references in other file - includeNode := ss.Graph().Get(file) - if includeNode != nil { - if len(includeNode.InDegree()) == 0 && len(includeNode.OutDegree()) == 0 { - ss.Graph().Debug() - } - referenceFiles := includeNode.InDegree() - for _, referenceFile := range referenceFiles { - log.Debugln("reference file: ", referenceFile) - locations, err := searchServiceDefinitionReferences(ctx, ss, referenceFile, svcName) - if err != nil { - errs = append(errs, err) - } - res = append(res, locations...) + for _, referenceFile := range referenceFiles(ss, file) { + locations, err := searchServiceDefinitionReferences(ctx, ss, referenceFile, svcName) + if err != nil { + return nil, err } + res = append(res, locations...) } + return res, err +} - if len(errs) > 0 { - err = utilerrors.NewAggregate(errs) +// referenceFiles returns the files that include the given file, per the +// include graph. +func referenceFiles(ss *cache.Snapshot, file uri.URI) []uri.URI { + includeNode := ss.Graph().Get(file) + if includeNode == nil { + return nil } - - return + if len(includeNode.InDegree()) == 0 && len(includeNode.OutDegree()) == 0 { + ss.Graph().Debug() + } + return includeNode.InDegree() } -func searchServiceDefinitionReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, svcName string) (res []protocol.Location, err error) { - ast, err := ss.Parse(ctx, file) +func searchServiceDefinitionReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, svcName string) (res []referenceHit, err error) { + pf, err := ss.Parse(ctx, file) if err != nil { - return + return res, err } - - if ast.AST() == nil { - return + if pf.AST() == nil { + return res, err } - for _, svc := range ast.AST().Services { - if svc.BadNode || svc.ChildrenBadNode() || svc.Extends == nil || svc.Extends.Name == nil { + for _, svc := range pf.AST().Services() { + if svc.Extends == nil || svc.Extends.Text != svcName { continue } - - if svcName == svc.Extends.Name.Text { - res = append(res, jump(file, svc.Extends.Name)) - } + res = append(res, referenceHit{loc: jump(file, pf.AST(), svc.Extends), text: svc.Extends.Text}) } - - return + return res, err } -func searchIdentifierReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, typeName string, definitionType string) (res []protocol.Location, err error) { - log.Debugln("searchIdentifierReferences for file:", file, "typeName:", typeName) - var errs []error +func searchIdentifierReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, typeName string, definitionType DefinitionKind) (res []referenceHit, err error) { + slog.Debug("searching identifier references", "file", file, "typeName", typeName) - // search in it self locations, err := searchDefinitionIdentifierReferences(ctx, ss, file, strings.TrimPrefix(typeName, fmt.Sprintf("%s.", lsputils.GetIncludeName(file))), definitionType) if err != nil { - errs = append(errs, err) + return nil, err } res = append(res, locations...) - // search type references in other file - includeNode := ss.Graph().Get(file) - log.Debugln("includeNode: ", includeNode) - if includeNode != nil { - if len(includeNode.InDegree()) == 0 && len(includeNode.OutDegree()) == 0 { - ss.Graph().Debug() - } - referenceFiles := includeNode.InDegree() - for _, referenceFile := range referenceFiles { - log.Debugln("reference file: ", referenceFile) - locations, err := searchDefinitionIdentifierReferences(ctx, ss, referenceFile, typeName, definitionType) - if err != nil { - errs = append(errs, err) - } - res = append(res, locations...) + for _, referenceFile := range referenceFiles(ss, file) { + locations, err := searchDefinitionIdentifierReferences(ctx, ss, referenceFile, typeName, definitionType) + if err != nil { + return nil, err } + res = append(res, locations...) } - - if len(errs) > 0 { - err = utilerrors.NewAggregate(errs) - } - return - + return res, err } -func searchDefinitionIdentifierReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, typeName string, definitionType string) (res []protocol.Location, err error) { - ast, err := ss.Parse(ctx, file) +// searchDefinitionIdentifierReferences finds type references matching +// typeName in one file: field types, function return types, arguments, +// throws, typedef types, and const types. +func searchDefinitionIdentifierReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, typeName string, definitionType DefinitionKind) (res []referenceHit, err error) { + pf, err := ss.Parse(ctx, file) if err != nil { - return + return res, err } - - if ast.AST() == nil { - return + if pf.AST() == nil { + return res, err } - var searchFieldType func(fieldType *parser.FieldType) - searchFieldType = func(fieldType *parser.FieldType) { - if fieldType.KeyType != nil && !fieldType.KeyType.BadNode { - searchFieldType(fieldType.KeyType) + jumpFieldType := func(ft *syntax.FieldType) { + if ft == nil || typeReferenceName(ft) != typeName { + return } - if fieldType.ValueType != nil && !fieldType.ValueType.BadNode { - searchFieldType(fieldType.ValueType) + res = append(res, referenceHit{loc: jump(file, pf.AST(), ft.Ident), text: ft.Ident.Text}) + } + var searchFieldType func(ft *syntax.FieldType) + searchFieldType = func(ft *syntax.FieldType) { + if ft == nil { + return } - - if fieldType.TypeName != nil && fieldType.TypeName.Name == typeName { - res = append(res, jump(file, fieldType.TypeName)) + if ft.KeyType != nil { + searchFieldType(ft.KeyType) + } + if ft.ValueType != nil { + searchFieldType(ft.ValueType) } + jumpFieldType(ft) } - - jumpField := func(field *parser.Field) { - if field.BadNode || field.FieldType == nil || field.FieldType.BadNode || field.FieldType.TypeName == nil { - return + jumpField := func(field *syntax.Field) { + searchFieldType(field.Type) + } + processStructLike := func(fields []*syntax.Field) { + for _, field := range fields { + jumpField(field) } - searchFieldType(field.FieldType) } - for _, svc := range ast.AST().Services { + for _, svc := range pf.AST().Services() { for _, fn := range svc.Functions { - if fn.BadNode { - continue - } - - if fn.FunctionType != nil && !fn.FunctionType.BadNode { - searchFieldType(fn.FunctionType) - } - - for i := range fn.Arguments { - log.Debugln("search function args", "definitionType", definitionType, "typeName", typeName) - jumpField(fn.Arguments[i]) - } - + searchFieldType(fn.Type) + processStructLike(fn.Args) if fn.Throws != nil { - for i := range fn.Throws.Fields { - jumpField(fn.Throws.Fields[i]) - } + processStructLike(fn.Throws.Fields) } } } - if definitionType == "Exception" { - return + if definitionType == DefinitionException { + return res, err } - for _, st := range ast.AST().Structs { - if st.BadNode { - continue - } - - for _, field := range st.Fields { - jumpField(field) - } + for _, st := range pf.AST().Structs() { + processStructLike(st.Fields) } - - for _, st := range ast.AST().Unions { - if st.BadNode { - continue - } - - for _, field := range st.Fields { - jumpField(field) - } + for _, st := range pf.AST().Unions() { + processStructLike(st.Fields) } - - for _, st := range ast.AST().Exceptions { - if st.BadNode { - continue - } - - for _, field := range st.Fields { - jumpField(field) - } + for _, st := range pf.AST().Exceptions() { + processStructLike(st.Fields) } - - for _, typedef := range ast.AST().Typedefs { - if typedef.BadNode || typedef.T == nil || typedef.T.BadNode { - continue - } - searchFieldType(typedef.T) + for _, typedef := range pf.AST().Typedefs() { + searchFieldType(typedef.Type) } - - for _, cst := range ast.AST().Consts { - if cst.BadNode || cst.ConstType == nil || cst.ConstType.BadNode { - continue - } - searchFieldType(cst.ConstType) + for _, cst := range pf.AST().Consts() { + searchFieldType(cst.Type) } - - return res, nil + return res, err } -func searchConstValueReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *parser.Document, nodePath []parser.Node, targetNode parser.Node) (res []protocol.Location, err error) { - res = make([]protocol.Location, 0) +func searchConstValueReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) (res []referenceHit, err error) { + res = make([]referenceHit, 0) + value := target.node.(*syntax.ConstValue) - var errs []error - // search type definition - definitionFile, identifierNode, err := ConstValueTypeDefinitionIdentifier(ctx, ss, file, ast, targetNode) + definitionFile, identifierNode, err := FindConstValueDefinition(ctx, ss, file, pf.AST(), value) if err != nil { - errs = append(errs, err) + return res, err } - if identifierNode == nil { - return + return res, err } - res = append(res, jump(definitionFile, identifierNode.Name)) - - valueName := targetNode.(*parser.ConstValue).Value.(string) - locations, err := searchConstValueIdentifierReferences(ctx, ss, definitionFile, valueName) + loc, err := jumpInFile(ctx, ss, definitionFile, identifierNode) if err != nil { - errs = append(errs, err) + return res, err } - res = append(res, locations...) + res = append(res, referenceHit{loc: loc, text: identifierNode.Text}) - if len(errs) > 0 { - err = utilerrors.NewAggregate(errs) + locations, err := searchConstValueIdentifierReferences(ctx, ss, definitionFile, value.Text) + if err != nil { + return res, err } - - return + res = append(res, locations...) + return res, err } -// a const value maybe a const defintion or enum value definition -func searchConstValueIdentifierReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, valueName string) (res []protocol.Location, err error) { - var errs []error - // search in it self +// searchConstValueIdentifierReferences finds usages of a const or enum +// value name: field default values and const values. +func searchConstValueIdentifierReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, valueName string) (res []referenceHit, err error) { locations, err := searchConstValueIdentifierReference(ctx, ss, file, strings.TrimPrefix(valueName, fmt.Sprintf("%s.", lsputils.GetIncludeName(file)))) if err != nil { - errs = append(errs, err) + return nil, err } res = append(res, locations...) - // search type references in other file - includeNode := ss.Graph().Get(file) - if includeNode != nil { - referenceFiles := includeNode.InDegree() - for _, referenceFile := range referenceFiles { - locations, err := searchConstValueIdentifierReference(ctx, ss, referenceFile, valueName) - if err != nil { - errs = append(errs, err) - } - res = append(res, locations...) + for _, referenceFile := range referenceFiles(ss, file) { + locations, err := searchConstValueIdentifierReference(ctx, ss, referenceFile, valueName) + if err != nil { + return nil, err } + res = append(res, locations...) } - - if len(errs) > 0 { - err = utilerrors.NewAggregate(errs) - } - - return + return res, err } -func searchConstValueIdentifierReference(ctx context.Context, ss *cache.Snapshot, file uri.URI, valueName string) (res []protocol.Location, err error) { - ast, err := ss.Parse(ctx, file) +func searchConstValueIdentifierReference(ctx context.Context, ss *cache.Snapshot, file uri.URI, valueName string) (res []referenceHit, err error) { + pf, err := ss.Parse(ctx, file) if err != nil { - return + return res, err } - - if ast.AST() == nil { - return + if pf.AST() == nil { + return res, err } - jumpField := func(field *parser.Field) { - if field.BadNode || field.ConstValue == nil || field.ConstValue.TypeName != "identifier" { - return - } - if field.ConstValue.Value == valueName { - res = append(res, jump(file, field.ConstValue)) + jumpValue := func(v *syntax.ConstValue) { + if v != nil && v.Kind == syntax.ValueIdent && v.Text == valueName { + res = append(res, referenceHit{loc: jump(file, pf.AST(), v), text: v.Text}) } } - - for _, st := range ast.AST().Structs { - if st.BadNode { - continue - } - for _, field := range st.Fields { - jumpField(field) + processStructLike := func(fields []*syntax.Field) { + for _, field := range fields { + jumpValue(field.Value) } } - for _, union := range ast.AST().Unions { - if union.BadNode { - continue - } - for _, field := range union.Fields { - jumpField(field) - } + for _, st := range pf.AST().Structs() { + processStructLike(st.Fields) } - - for _, excep := range ast.AST().Exceptions { - if excep.BadNode { - continue - } - for _, field := range excep.Fields { - jumpField(field) - } + for _, st := range pf.AST().Unions() { + processStructLike(st.Fields) } - - for _, cst := range ast.AST().Consts { - if cst.BadNode || cst.Value == nil || cst.Value.TypeName != "identifier" { - continue - } - if cst.Value.Value == valueName { - res = append(res, jump(file, cst.Value)) - } + for _, st := range pf.AST().Exceptions() { + processStructLike(st.Fields) } - - for _, enum := range ast.AST().Enums { - if enum.BadNode { - continue - } - for _, enumValue := range enum.Values { - if enumValue.ValueNode == nil || enumValue.ValueNode.TypeName != "identifier" { - continue - } - - if enumValue.ValueNode.Value == valueName { - res = append(res, jump(file, enumValue.ValueNode)) - } - } + for _, cst := range pf.AST().Consts() { + jumpValue(cst.Value) } - - for _, svc := range ast.AST().Services { + for _, svc := range pf.AST().Services() { for _, fn := range svc.Functions { - for _, field := range fn.Arguments { - jumpField(field) - } + processStructLike(fn.Args) if fn.Throws != nil { - for _, field := range fn.Throws.Fields { - jumpField(field) - } + processStructLike(fn.Throws.Fields) } } } - - return + return res, err } diff --git a/lsp/codejump/reference_test.go b/lsp/codejump/reference_test.go index bc30098..01a4a22 100644 --- a/lsp/codejump/reference_test.go +++ b/lsp/codejump/reference_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" ) func TestReference(t *testing.T) { @@ -81,7 +82,7 @@ const UserKind kind = "1" { name: "case struct", // user.Test args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -122,7 +123,7 @@ const UserKind kind = "1" { name: "case struct 2", // Test args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -150,7 +151,7 @@ const UserKind kind = "1" { name: "case union", // user.Test2 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -191,7 +192,7 @@ const UserKind kind = "1" { name: "case union 2", // Test2 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -219,7 +220,7 @@ const UserKind kind = "1" { name: "case enum", // user.Test3 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -273,7 +274,7 @@ const UserKind kind = "1" { name: "case enum 2", // Test3 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -314,7 +315,7 @@ const UserKind kind = "1" { name: "case exceptions", // user.Error1 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -355,7 +356,7 @@ const UserKind kind = "1" { name: "case exceptions 2", // Error1 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -383,7 +384,7 @@ const UserKind kind = "1" { name: "case typedef", // user.UserType args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -450,7 +451,7 @@ const UserKind kind = "1" { name: "case typedef 2", // UserType args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -504,7 +505,7 @@ const UserKind kind = "1" { name: "case enumvalue", // user.Test3.TWO args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -545,7 +546,7 @@ const UserKind kind = "1" { name: "case enumvalue 2", // TWO args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -573,7 +574,7 @@ const UserKind kind = "1" { name: "case const", // user.DefaultName args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -614,7 +615,7 @@ const UserKind kind = "1" { name: "case const 2", // DefaultName args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -642,7 +643,7 @@ const UserKind kind = "1" { name: "type in same file", // UserKind args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ diff --git a/lsp/codejump/rename.go b/lsp/codejump/rename.go index 4401fdf..7d89737 100644 --- a/lsp/codejump/rename.go +++ b/lsp/codejump/rename.go @@ -2,167 +2,121 @@ package codejump import ( "context" - "errors" "fmt" "strings" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/lsp/types" - "github.com/joyme123/thrift-ls/parser" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/lsputils" + "github.com/karitham/thrift-ls/syntax" ) +// PrepareRename returns the range of the identifier under the cursor when +// renaming is supported: definition names, const values, and services. func PrepareRename(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (res *protocol.Range, err error) { - pf, err := ss.Parse(ctx, file) - if err != nil { - return - } - - if pf.AST() == nil { - err = errors.New("parse ast failed") - return - } - - astPos, err := pf.Mapper().LSPPosToParserPosition(types.Position{Line: pos.Line, Character: pos.Character}) + pf, target, err := resolveTarget(ctx, ss, file, pos) if err != nil { - return + return res, err } - nodePath := parser.SearchNodePathByPosition(pf.AST(), astPos) - targetNode := nodePath[len(nodePath)-1] - switch targetNode.Type() { - case "IdentifierName", "ConstValue": - rg := lsputils.ASTNodeToRange(targetNode) + switch target.kind { + case TargetDefinition, TargetConstValue, TargetService: + rg := nodeRange(pf.AST(), target.node) return &rg, nil - default: - err = fmt.Errorf("%s doesn't support rename", targetNode.Type()) - return } + return nil, fmt.Errorf("rename not supported at this position") } +// Rename renames the definition under the cursor and all its references, +// preserving include qualifiers on qualified references (user.Test becomes +// user.newtext, not newtext). func Rename(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position, newName string) (res *protocol.WorkspaceEdit, err error) { - pf, err := ss.Parse(ctx, file) - if err != nil { - return - } - - if pf.AST() == nil { - err = errors.New("parse ast failed") - return - } - - astPos, err := pf.Mapper().LSPPosToParserPosition(types.Position{Line: pos.Line, Character: pos.Character}) + pf, target, err := resolveTarget(ctx, ss, file, pos) if err != nil { - return + return res, err } - nodePath := parser.SearchNodePathByPosition(pf.AST(), astPos) - targetNode := nodePath[len(nodePath)-1] - - self := lsputils.ASTNodeToRange(targetNode) - switch targetNode.Type() { - case "IdentifierName": - if len(nodePath) <= 2 { - return + var refs []referenceHit + switch target.kind { + case TargetTypeName: + ft := target.parent.(*syntax.FieldType) + if typeReferenceName(ft) == "" || IsBasicType(typeReferenceName(ft)) { + return nil, fmt.Errorf("rename not supported for basic types") } - // identifierName -> identifier -> definition - parentDefinitionNode := nodePath[len(nodePath)-3] - definitionType := parentDefinitionNode.Type() - if definitionType == "EnumValue" || definitionType == "Const" { - var typeName string - if definitionType == "Const" { - typeName = fmt.Sprintf("%s.%s", lsputils.GetIncludeName(file), targetNode.(*parser.IdentifierName).Text) - } else { - enumNode := nodePath[len(nodePath)-4] - typeName = fmt.Sprintf("%s.%s.%s", lsputils.GetIncludeName(file), enumNode.(*parser.Enum).Name.Name.Text, targetNode.(*parser.IdentifierName).Text) - } - // search in const value - locations, err := searchConstValueIdentifierReferences(ctx, ss, file, typeName) - if err != nil { - return nil, err - } - - locations = append(locations, protocol.Location{ - URI: file, - Range: self, - }) - - return convertLocationToWorkspaceEdit(locations, file, newName), nil - } else if definitionType == "Service" { - svcName := targetNode.(*parser.IdentifierName).Text - if !strings.Contains(svcName, ".") { - svcName = fmt.Sprintf("%s.%s", lsputils.GetIncludeName(file), svcName) - } else { - include, _ := lsputils.ParseIdent(file, pf.AST().Includes, svcName) - resolver := ss.Resolver() - path := resolver.GetIncludePath(pf.AST(), include) - if path != "" { // doesn't match any include path - file = resolver.ResolveInclude(file, path) - } - } - locations, err := searchServiceReferences(ctx, ss, file, svcName) - if err != nil { - return nil, err - } - - locations = append(locations, protocol.Location{ - URI: file, - Range: self, - }) - - return convertLocationToWorkspaceEdit(locations, file, newName), nil + refs, err = searchTypeNameReferences(ctx, ss, file, pf, target) + if err != nil { + return nil, err } - if _, ok := validReferenceDefinitionType[definitionType]; !ok { - return + case TargetConstValue: + value := target.node.(*syntax.ConstValue) + if _, id, err := FindConstValueDefinition(ctx, ss, file, pf.AST(), value); err != nil { + return nil, err + } else if id == nil { + return nil, fmt.Errorf("definition not found") + } + refs, err = searchConstValueReferences(ctx, ss, file, pf, target) + if err != nil { + return nil, err } - // typeName is base.User - typeName := fmt.Sprintf("%s.%s", lsputils.GetIncludeName(file), targetNode.(*parser.IdentifierName).Text) - locations, err := searchIdentifierReferences(ctx, ss, file, typeName, definitionType) + case TargetService: + svcName := target.identifier().Text + if !strings.Contains(svcName, ".") { + svcName = fmt.Sprintf("%s.%s", lsputils.GetIncludeName(file), svcName) + } else { + include, _ := lsputils.ParseIdent(file, pf.AST().Includes(), svcName) + resolver := ss.Resolver() + if path := resolver.GetIncludePath(pf.AST(), include); path != "" { + file = resolver.ResolveInclude(file, path) + } + } + refs, err = searchServiceReferences(ctx, ss, file, svcName) if err != nil { return nil, err } - locations = append(locations, protocol.Location{ - URI: file, - Range: self, - }) - return convertLocationToWorkspaceEdit(locations, file, newName), nil - case "ConstValue": - locations, err := searchConstValueReferences(ctx, ss, file, pf.AST(), nodePath, targetNode) + case TargetDefinition: + refs, err = searchDefinitionReferences(ctx, ss, file, pf, target) if err != nil { return nil, err } - locations = append(locations, protocol.Location{ - URI: file, - Range: self, - }) - return convertLocationToWorkspaceEdit(locations, file, newName), nil default: - err = fmt.Errorf("%s doesn't support rename", targetNode.Type()) - return + return nil, fmt.Errorf("rename not supported at this position") } -} -func convertLocationToWorkspaceEdit(locations []protocol.Location, fileURI uri.URI, newName string) *protocol.WorkspaceEdit { - res := &protocol.WorkspaceEdit{ - Changes: make(map[protocol.DocumentURI][]protocol.TextEdit), - } + // The definition under the cursor itself. + refs = append(refs, referenceHit{ + loc: protocol.Location{ + URI: file, + Range: nodeRange(pf.AST(), target.node), + }, + text: "", + }) + + return convertHitsToWorkspaceEdit(refs, newName), nil +} - for _, loc := range locations { - newText := newName - if loc.URI != fileURI { - newText = lsputils.GetIncludeName(fileURI) + "." + newName +// convertHitsToWorkspaceEdit groups the edits by file. A reference whose +// text has an include qualifier (user.Test) keeps the qualifier: the new +// text becomes user.newtext. +func convertHitsToWorkspaceEdit(refs []referenceHit, newName string) *protocol.WorkspaceEdit { + changes := make(map[uri.URI][]protocol.TextEdit) + for i := range refs { + text := newName + if dot := strings.LastIndexByte(refs[i].text, '.'); dot >= 0 { + text = refs[i].text[:dot+1] + newName } - res.Changes[loc.URI] = append(res.Changes[loc.URI], protocol.TextEdit{ - Range: loc.Range, - NewText: newText, + changes[refs[i].loc.URI] = append(changes[refs[i].loc.URI], protocol.TextEdit{ + Range: refs[i].loc.Range, + NewText: text, }) } - return res + return &protocol.WorkspaceEdit{ + Changes: changes, + } } diff --git a/lsp/codejump/rename_test.go b/lsp/codejump/rename_test.go index fd543ff..7fb4b5c 100644 --- a/lsp/codejump/rename_test.go +++ b/lsp/codejump/rename_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" ) func TestPrepareRename(t *testing.T) { - file1 := `struct Test { 1: required string name, 2: required string email, @@ -82,7 +82,7 @@ const UserKind kind = "1" { name: "case struct", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -105,7 +105,7 @@ const UserKind kind = "1" { name: "case union", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -128,7 +128,7 @@ const UserKind kind = "1" { name: "case enum", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -151,7 +151,7 @@ const UserKind kind = "1" { name: "case exception", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -174,7 +174,7 @@ const UserKind kind = "1" { name: "typedef", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -197,7 +197,7 @@ const UserKind kind = "1" { name: "const", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -228,7 +228,6 @@ const UserKind kind = "1" } func TestRename(t *testing.T) { - file1 := `struct Test { 1: required string name, 2: required string email, @@ -300,7 +299,7 @@ const UserKind kind = "1" { name: "case struct", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -311,7 +310,7 @@ const UserKind kind = "1" }, wantRes: &protocol.WorkspaceEdit{ Changes: map[uri.URI][]protocol.TextEdit{ - "file:///tmp/user.thrift": []protocol.TextEdit{ + "file:///tmp/user.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -326,7 +325,7 @@ const UserKind kind = "1" NewText: "newtext", }, }, - "file:///tmp/api.thrift": []protocol.TextEdit{ + "file:///tmp/api.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -348,7 +347,7 @@ const UserKind kind = "1" { name: "case union", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -359,7 +358,7 @@ const UserKind kind = "1" }, wantRes: &protocol.WorkspaceEdit{ Changes: map[uri.URI][]protocol.TextEdit{ - "file:///tmp/user.thrift": []protocol.TextEdit{ + "file:///tmp/user.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -374,7 +373,7 @@ const UserKind kind = "1" NewText: "newtext", }, }, - "file:///tmp/api.thrift": []protocol.TextEdit{ + "file:///tmp/api.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -396,7 +395,7 @@ const UserKind kind = "1" { name: "case enum", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -407,7 +406,7 @@ const UserKind kind = "1" }, wantRes: &protocol.WorkspaceEdit{ Changes: map[uri.URI][]protocol.TextEdit{ - "file:///tmp/user.thrift": []protocol.TextEdit{ + "file:///tmp/user.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -422,7 +421,7 @@ const UserKind kind = "1" NewText: "newtext", }, }, - "file:///tmp/api.thrift": []protocol.TextEdit{ + "file:///tmp/api.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -457,7 +456,7 @@ const UserKind kind = "1" { name: "case exception", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -468,7 +467,7 @@ const UserKind kind = "1" }, wantRes: &protocol.WorkspaceEdit{ Changes: map[uri.URI][]protocol.TextEdit{ - "file:///tmp/user.thrift": []protocol.TextEdit{ + "file:///tmp/user.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -483,7 +482,7 @@ const UserKind kind = "1" NewText: "newtext", }, }, - "file:///tmp/api.thrift": []protocol.TextEdit{ + "file:///tmp/api.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -505,7 +504,7 @@ const UserKind kind = "1" { name: "typedef", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -516,7 +515,7 @@ const UserKind kind = "1" }, wantRes: &protocol.WorkspaceEdit{ Changes: map[uri.URI][]protocol.TextEdit{ - "file:///tmp/user.thrift": []protocol.TextEdit{ + "file:///tmp/user.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -531,7 +530,7 @@ const UserKind kind = "1" NewText: "newtext", }, }, - "file:///tmp/api.thrift": []protocol.TextEdit{ + "file:///tmp/api.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -579,7 +578,7 @@ const UserKind kind = "1" { name: "const", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", pos: protocol.Position{ @@ -590,7 +589,7 @@ const UserKind kind = "1" }, wantRes: &protocol.WorkspaceEdit{ Changes: map[uri.URI][]protocol.TextEdit{ - "file:///tmp/user.thrift": []protocol.TextEdit{ + "file:///tmp/user.thrift": { { Range: protocol.Range{ Start: protocol.Position{ @@ -605,7 +604,7 @@ const UserKind kind = "1" NewText: "newtext", }, }, - "file:///tmp/api.thrift": []protocol.TextEdit{ + "file:///tmp/api.thrift": { { Range: protocol.Range{ Start: protocol.Position{ diff --git a/lsp/codejump/target.go b/lsp/codejump/target.go new file mode 100644 index 0000000..21ac510 --- /dev/null +++ b/lsp/codejump/target.go @@ -0,0 +1,141 @@ +package codejump + +import ( + "context" + "errors" + + "go.lsp.dev/protocol" + "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/types" + "github.com/karitham/thrift-ls/syntax" +) + +// TargetKind classifies what the cursor is on. +type TargetKind uint8 + +const ( + // TargetNone is a position with no relevant target. + TargetNone TargetKind = iota + // TargetTypeName is a type reference (a field/return/typedef type). + TargetTypeName + // TargetConstValue is an identifier in a constant value position. + TargetConstValue + // TargetService is a service name or an extends reference. + TargetService + // TargetDefinition is a definition name: struct, enum, const, typedef, + // field, function, enum value, and so on. + TargetDefinition +) + +// target is the resolved cursor position: the node under the cursor, its +// kind, and the full node path from the document root. +type target struct { + path []syntax.Node + node syntax.Node + parent syntax.Node + kind TargetKind +} + +var errNoAST = errors.New("parse ast failed") + +// resolveTarget parses the file and finds what the cursor is on. +func resolveTarget(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (*cache.ParsedFile, *target, error) { + pf, err := ss.Parse(ctx, file) + if err != nil { + return nil, nil, err + } + if pf.AST() == nil { + return nil, nil, errNoAST + } + + astPos, err := pf.Mapper().LSPPosToParserPosition(types.Position{Line: pos.Line, Character: pos.Character}) + if err != nil { + return nil, nil, err + } + + path := pf.AST().SearchNodePathByPosition(astPos) + if len(path) == 0 { + return nil, nil, errors.New("no node at position") + } + + t := &target{ + path: path, + node: path[len(path)-1], + } + if len(path) > 1 { + t.parent = path[len(path)-2] + } + t.kind = classify(t) + return pf, t, nil +} + +// classify determines what the cursor is on from the deepest node and its +// parent. Identifiers carry the role of their parent: an identifier inside +// a FieldType is a type reference, an identifier inside a Service is a +// service name or extends, and any other identifier is a definition name. +func classify(t *target) TargetKind { + switch n := t.node.(type) { + case *syntax.Identifier: + switch t.parent.(type) { + case *syntax.FieldType: + return TargetTypeName + case *syntax.Service: + return TargetService + } + return TargetDefinition + case *syntax.ConstValue: + if n.Kind == syntax.ValueIdent { + return TargetConstValue + } + } + return TargetNone +} + +// targetIdentifier returns the identifier node for definition, service, and +// type-name targets. +func (t *target) identifier() *syntax.Identifier { + if id, ok := t.node.(*syntax.Identifier); ok { + return id + } + return nil +} + +// jump builds an LSP location for a node. +func jump(file uri.URI, doc *syntax.Document, node syntax.Node) protocol.Location { + return protocol.Location{ + Range: nodeRange(doc, node), + URI: file, + } +} + +// jumpInFile parses file and builds a location for a node belonging to that +// file's AST. Use this for nodes resolved from a different file than the +// one under the cursor: the node's token indices are only meaningful in its +// own document's token stream. +func jumpInFile(ctx context.Context, ss *cache.Snapshot, file uri.URI, node syntax.Node) (protocol.Location, error) { + pf, err := ss.Parse(ctx, file) + if err != nil { + return protocol.Location{}, err + } + if pf.AST() == nil { + return protocol.Location{}, errNoAST + } + return jump(file, pf.AST(), node), nil +} + +// nodeRange converts a node span to an LSP range. +func nodeRange(doc *syntax.Document, node syntax.Node) protocol.Range { + start, end := doc.Range(node) + return protocol.Range{ + Start: protocol.Position{ + Line: uint32(start.Line - 1), + Character: uint32(start.Col - 1), + }, + End: protocol.Position{ + Line: uint32(end.Line - 1), + Character: uint32(end.Col - 1), + }, + } +} diff --git a/lsp/codejump/target_test.go b/lsp/codejump/target_test.go new file mode 100644 index 0000000..6645f65 --- /dev/null +++ b/lsp/codejump/target_test.go @@ -0,0 +1,117 @@ +package codejump + +import ( + "testing" + + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/lsp/cache" +) + +func TestResolveTargetClassification(t *testing.T) { + src := `include "base.thrift" + +struct User { + 1: required i64 id + 2: optional string name = "bob" + 3: i32 d = DEFAULT +} + +enum Color { RED = 1, GREEN } + +const i32 DEFAULT = 5 +const Color defaultColor = GREEN + +service Svc extends Base { + User getUser(1: i64 id, 2: string name) throws (NotFound e) +} +` + ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + { + URI: "file:///tmp/test.thrift", + Version: 0, + Content: []byte(src), + From: cache.FileChangeTypeDidOpen, + }, + }) + + // Find the position of the first occurrence of a marker text. + posOf := func(marker string, offset ...int) protocol.Position { + idx := indexOf(src, marker) + if len(offset) > 0 { + idx += offset[0] + } + if idx < 0 { + t.Fatalf("marker %q not found", marker) + } + // Convert byte offset to line/character. + line, col := 0, 0 + for i := 0; i < idx; i++ { + if src[i] == '\n' { + line++ + col = 0 + } else { + col++ + } + } + return protocol.Position{Line: uint32(line), Character: uint32(col)} + } + + tests := []struct { + name string + marker string + want TargetKind + offset int + }{ + {"type reference", "NotFound", TargetTypeName, 0}, + {"base type has no target", "i64 id", TargetNone, 0}, + {"const value reference", "= DEFAULT", TargetConstValue, 2}, + {"enum value reference", "= GREEN", TargetConstValue, 2}, + {"service name", "Svc", TargetService, 0}, + {"extends", "Base", TargetService, 0}, + {"struct name", "User", TargetDefinition, 0}, + {"field name", "string name", TargetDefinition, 7}, + {"const name", "DEFAULT = 5", TargetDefinition, 0}, + {"enum value declaration name", "GREEN }", TargetDefinition, 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, target, err := resolveTarget(t.Context(), ss, "file:///tmp/test.thrift", posOf(tt.marker, tt.offset)) + if err != nil { + t.Fatalf("resolveTarget: %v", err) + } + if target.kind != tt.want { + t.Errorf("kind = %v, want %v (node %T)", target.kind, tt.want, target.node) + } + }) + } +} + +func TestResolveTargetNoNode(t *testing.T) { + ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + { + URI: "file:///tmp/test.thrift", + Version: 0, + Content: []byte("struct S {\n 1: i32 a\n}\n\nconst i32 X = 1\n"), + From: cache.FileChangeTypeDidOpen, + }, + }) + // Position in the blank line between definitions: resolves to the + // document itself with no target kind. + _, target, err := resolveTarget(t.Context(), ss, "file:///tmp/test.thrift", protocol.Position{Line: 3, Character: 0}) + if err != nil { + t.Fatalf("resolveTarget: %v", err) + } + if target.kind != TargetNone { + t.Errorf("kind = %v, want TargetNone", target.kind) + } +} + +func indexOf(s, sub string) int { + for i := 0; i+len(sub) <= len(s); i++ { + if s[i:i+len(sub)] == sub { + return i + } + } + return -1 +} diff --git a/lsp/codejump/type_definition.go b/lsp/codejump/type_definition.go index 5a2825b..e54d5d2 100644 --- a/lsp/codejump/type_definition.go +++ b/lsp/codejump/type_definition.go @@ -2,67 +2,77 @@ package codejump import ( "context" - "errors" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/types" - "github.com/joyme123/thrift-ls/parser" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/syntax" ) +// TypeDefinition returns the definition of the type of the declaration +// under the cursor: for a type reference, the type's definition; for a +// field, function, typedef, or const name, the definition of its declared +// type. func TypeDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { res = make([]protocol.Location, 0) - pf, err := ss.Parse(ctx, file) - if err != nil { - return - } - - if pf.AST() == nil { - err = errors.New("parse ast failed") - return - } - - astPos, err := pf.Mapper().LSPPosToParserPosition(types.Position{Line: pos.Line, Character: pos.Character}) + pf, target, err := resolveTarget(ctx, ss, file, pos) if err != nil { - return + return res, err } - nodePath := parser.SearchNodePathByPosition(pf.AST(), astPos) - targetNode := nodePath[len(nodePath)-1] - switch targetNode.Type() { - case "TypeName": - return typeNameDefinition(ctx, ss, file, pf.AST(), targetNode) - case "IdentifierName": - // no parent - if len(nodePath) <= 2 { - return res, nil + switch target.kind { + case TargetTypeName: + return typeNameDefinition(ctx, ss, file, pf, target) + case TargetConstValue: + // The type definition of a constant value is the value's own + // definition: the enum value or const it references. + astFile, id, err := FindConstValueDefinition(ctx, ss, file, pf.AST(), target.node.(*syntax.ConstValue)) + if err != nil { + return nil, err } - parent := nodePath[len(nodePath)-3] - var fieldType *parser.FieldType - switch parent.Type() { - case "Field": - field := parent.(*parser.Field) - fieldType = field.FieldType - case "Typedef": - typedef := parent.(*parser.Typedef) - fieldType = typedef.T - case "Function": - fn := parent.(*parser.Function) - fieldType = fn.FunctionType - case "Const": - cst := parent.(*parser.Const) - fieldType = cst.ConstType + if id == nil { + return nil, nil } - if fieldType != nil && !fieldType.BadNode && fieldType.TypeName != nil { - return typeNameDefinition(ctx, ss, file, pf.AST(), fieldType.TypeName) + loc, err := jumpInFile(ctx, ss, astFile, id) + if err != nil { + return nil, err } - case "ConstValue": - return constValueTypeDefinition(ctx, ss, file, pf.AST(), targetNode) - default: - log.Warningln("unsupport type for type definition:", targetNode.Type()) + return []protocol.Location{loc}, nil + case TargetDefinition: + return declarationTypeDefinition(ctx, ss, file, pf, target) } + return res, err +} - return +// declarationTypeDefinition jumps to the definition of the declared type of +// a field, typedef, function, or const under the cursor. +func declarationTypeDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) ([]protocol.Location, error) { + var ft *syntax.FieldType + switch parent := target.parent.(type) { + case *syntax.Field: + ft = parent.Type + case *syntax.Typedef: + ft = parent.Type + case *syntax.Function: + ft = parent.Type + case *syntax.Const: + ft = parent.Type + } + if ft == nil { + return nil, nil + } + + astFile, id, _, err := FindTypeDefinition(ctx, ss, file, pf.AST(), ft) + if err != nil { + return nil, err + } + if id == nil { + return nil, nil + } + loc, err := jumpInFile(ctx, ss, astFile, id) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil } diff --git a/lsp/codejump/type_definition_test.go b/lsp/codejump/type_definition_test.go index 8893328..d8b5498 100644 --- a/lsp/codejump/type_definition_test.go +++ b/lsp/codejump/type_definition_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" ) func TestTypeDefinition(t *testing.T) { @@ -80,7 +81,7 @@ const user.UserType usermale = "male" { name: "case struct", // user.Test args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -108,7 +109,7 @@ const user.UserType usermale = "male" { name: "case struct 2", // Api args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -136,7 +137,7 @@ const user.UserType usermale = "male" { name: "case union", // user.Test2 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -164,7 +165,7 @@ const user.UserType usermale = "male" { name: "case union 2", // arg1 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -192,7 +193,7 @@ const user.UserType usermale = "male" { name: "case enum", // user.Test3 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -220,7 +221,7 @@ const user.UserType usermale = "male" { name: "case enum 2", // arg2 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -248,7 +249,7 @@ const user.UserType usermale = "male" { name: "case exceptions", // user.Error1 args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -276,7 +277,7 @@ const user.UserType usermale = "male" { name: "case exceptions 2", // err args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -304,7 +305,7 @@ const user.UserType usermale = "male" { name: "case typedef", // user.UserType args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -332,7 +333,7 @@ const user.UserType usermale = "male" { name: "case enumvalue", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -360,7 +361,7 @@ const user.UserType usermale = "male" { name: "case const", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -388,7 +389,7 @@ const user.UserType usermale = "male" { name: "case typedef 2", // UserKind args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ @@ -416,7 +417,7 @@ const user.UserType usermale = "male" { name: "case const 2", // usermale args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/api.thrift", pos: protocol.Position{ diff --git a/lsp/codejump/utils.go b/lsp/codejump/utils.go index 9cf6a4f..cd27cf4 100644 --- a/lsp/codejump/utils.go +++ b/lsp/codejump/utils.go @@ -1,206 +1,213 @@ package codejump import ( + "context" "strings" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/lsputils" + "github.com/karitham/thrift-ls/syntax" ) -func GetExceptionNode(ast *parser.Document, name string) *parser.Exception { - if ast == nil { - return nil - } - for _, excep := range ast.Exceptions { - if excep.BadNode || excep.Name == nil { - continue +// DefinitionKind identifies the kind of a resolved definition. +type DefinitionKind uint8 + +const ( + DefinitionNone DefinitionKind = iota + DefinitionStruct + DefinitionUnion + DefinitionException + DefinitionEnum + DefinitionTypedef + DefinitionConst + DefinitionEnumValue + DefinitionService +) + +// definitionFiles returns the files to search for a definition, in order. +// A qualified name ("base.User") resolves to the include file; an +// unqualified name searches the current file first, then each included +// file. +func definitionFiles(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *syntax.Document, name string) []uri.URI { + include, _ := lsputils.ParseIdent(file, ast.Includes(), name) + if include != "" { + resolver := ss.Resolver() + path := resolver.GetIncludePath(ast, include) + if path == "" { + // Doesn't match any include path; treat as local. + return []uri.URI{file} } + return []uri.URI{resolver.ResolveInclude(file, path)} + } - if excep.Name.BadNode || excep.Name.Name == nil { - continue + files := []uri.URI{file} + resolver := ss.Resolver() + for _, inc := range ast.Includes() { + if path := lsputils.IncludePathText(inc); path != "" { + files = append(files, resolver.ResolveInclude(file, path)) } + } + return files +} - if excep.Name.Name.Text == name { +// GetExceptionNode finds an exception declaration by name. +func GetExceptionNode(ast *syntax.Document, name string) *syntax.Struct { + if ast == nil { + return nil + } + for _, excep := range ast.Exceptions() { + if excep.Name != nil && excep.Name.Text == name { return excep } } - return nil } -func GetStructNode(ast *parser.Document, name string) *parser.Struct { +// GetStructNode finds a struct declaration by name. +func GetStructNode(ast *syntax.Document, name string) *syntax.Struct { if ast == nil { return nil } - for _, st := range ast.Structs { - if st.BadNode || st.Identifier == nil { - continue - } - - if st.Identifier.BadNode || st.Identifier.Name == nil { - continue - } - - if st.Identifier.Name.Text == name { + for _, st := range ast.Structs() { + if st.Name != nil && st.Name.Text == name { return st } } - return nil } -func GetUnionNode(ast *parser.Document, name string) *parser.Union { +// GetUnionNode finds a union declaration by name. +func GetUnionNode(ast *syntax.Document, name string) *syntax.Struct { if ast == nil { return nil } - for _, st := range ast.Unions { - if st.BadNode || st.Name == nil { - continue - } - - if st.Name.BadNode || st.Name.Name == nil { - continue - } - - if st.Name.Name.Text == name { + for _, st := range ast.Unions() { + if st.Name != nil && st.Name.Text == name { return st } } - return nil } -func GetEnumNode(ast *parser.Document, name string) *parser.Enum { +// GetEnumNode finds an enum declaration by name. +func GetEnumNode(ast *syntax.Document, name string) *syntax.Enum { if ast == nil { return nil } - for _, st := range ast.Enums { - if st.BadNode || st.Name == nil { - continue - } - - if st.Name.BadNode || st.Name.Name == nil { - continue - } - - if st.Name.Name.Text == name { + for _, st := range ast.Enums() { + if st.Name != nil && st.Name.Text == name { return st } } - return nil } -func GetEnumNodeByEnumValue(ast *parser.Document, enumValueName string) *parser.Enum { +// GetEnumNodeByEnumValue finds the enum declaring an enum value reference +// like "EnumName.VALUE" or a bare value name. +func GetEnumNodeByEnumValue(ast *syntax.Document, enumValueName string) *syntax.Enum { if ast == nil { return nil } - enumName, _, found := strings.Cut(enumValueName, ".") - if !found { - return nil + if found { + return GetEnumNode(ast, enumName) } - - return GetEnumNode(ast, enumName) + for _, enum := range ast.Enums() { + for _, value := range enum.Values { + if value.Name != nil && value.Name.Text == enumValueName { + return enum + } + } + } + return nil } -// GetEnumValueIdentifierNode enum A { ONE }, ONE is the target node -func GetEnumValueIdentifierNode(ast *parser.Document, name string) *parser.Identifier { +// GetEnumValueIdentifierNode returns the identifier of an enum value +// referenced as "EnumName.VALUE", or as a bare name. +func GetEnumValueIdentifierNode(ast *syntax.Document, name string) *syntax.Identifier { if ast == nil { return nil } - enumName, identifier, found := strings.Cut(name, ".") if !found { + // Bare name: search all enum values. + for _, enum := range ast.Enums() { + for _, enumValue := range enum.Values { + if enumValue.Name != nil && enumValue.Name.Text == name { + return enumValue.Name + } + } + } return nil } - - for _, enum := range ast.Enums { - if enum.BadNode || enum.Name == nil || enum.Name.Name == nil || enum.Name.Name.Text != enumName { + for _, enum := range ast.Enums() { + if enum.Name == nil || enum.Name.Text != enumName { continue } for _, enumValue := range enum.Values { - if enumValue.Name == nil || enumValue.Name.BadNode || enumValue.Name.Name == nil || enumValue.Name.Name.Text != identifier { - continue + if enumValue.Name != nil && enumValue.Name.Text == identifier { + return enumValue.Name } - return enumValue.Name } } - return nil } -func GetConstNode(ast *parser.Document, name string) *parser.Const { +// GetConstNode finds a const declaration by name. +func GetConstNode(ast *syntax.Document, name string) *syntax.Const { if ast == nil { return nil } - - for _, cst := range ast.Consts { - if cst.BadNode || cst.Name == nil || cst.Name.Name == nil || cst.Name.Name.Text != name { - continue + for _, cst := range ast.Consts() { + if cst.Name != nil && cst.Name.Text == name { + return cst } - return cst } - return nil } -func GetConstIdentifierNode(ast *parser.Document, name string) *parser.Identifier { +// GetConstIdentifierNode returns the name identifier of a const +// declaration. +func GetConstIdentifierNode(ast *syntax.Document, name string) *syntax.Identifier { if ast == nil { return nil } - - for _, cst := range ast.Consts { - if cst.BadNode || cst.Name == nil || cst.Name.Name == nil || cst.Name.Name.Text != name { - continue + for _, cst := range ast.Consts() { + if cst.Name != nil && cst.Name.Text == name { + return cst.Name } - return cst.Name } - return nil } -func GetTypedefNode(ast *parser.Document, name string) *parser.Typedef { +// GetTypedefNode finds a typedef declaration by name. +func GetTypedefNode(ast *syntax.Document, name string) *syntax.Typedef { if ast == nil { return nil } - for _, td := range ast.Typedefs { - if td.BadNode || td.Alias == nil || td.Alias.Name == nil { - continue - } - if td.Alias.Name.Text == name { + for _, td := range ast.Typedefs() { + if td.Name != nil && td.Name.Text == name { return td } } - return nil } -func GetServiceNode(ast *parser.Document, name string) *parser.Service { +// GetServiceNode finds a service declaration by name. +func GetServiceNode(ast *syntax.Document, name string) *syntax.Service { if ast == nil { return nil } - - for _, svc := range ast.Services { - if svc.BadNode || svc.Name == nil || svc.Name.Name == nil || svc.Name.Name.Text != name { - continue + for _, svc := range ast.Services() { + if svc.Name != nil && svc.Name.Text == name { + return svc } - return svc } - return nil } -func jump(file uri.URI, node parser.Node) protocol.Location { - rng := lsputils.ASTNodeToRange(node) - return protocol.Location{ - Range: rng, - URI: file, - } -} - var basicType = map[string]struct{}{ "map": {}, "set": {}, @@ -215,6 +222,7 @@ var basicType = map[string]struct{}{ "byte": {}, "binary": {}, "uuid": {}, + "slist": {}, } var containerType = map[string]struct{}{ @@ -223,12 +231,26 @@ var containerType = map[string]struct{}{ "list": {}, } +// IsBasicType reports whether t is a built-in base type. func IsBasicType(t string) bool { _, ok := basicType[t] return ok } +// IsContainerType reports whether t is a container keyword. func IsContainerType(t string) bool { _, ok := containerType[t] return ok } + +// typeReferenceName returns the referenced type name of a FieldType, or "" +// for base types and containers. +func typeReferenceName(ft *syntax.FieldType) string { + if ft == nil { + return "" + } + if ft.Kind == syntax.TypeIdent && ft.Ident != nil { + return ft.Ident.Text + } + return "" +} diff --git a/lsp/completion/completion_test.go b/lsp/completion/completion_test.go new file mode 100644 index 0000000..555c4f3 --- /dev/null +++ b/lsp/completion/completion_test.go @@ -0,0 +1,119 @@ +package completion + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/memoize" + "github.com/karitham/thrift-ls/lsp/types" +) + +// buildSnapshot builds a snapshot from file contents with optional include +// paths. +func buildSnapshot(t *testing.T, includePaths []string, files ...*cache.FileChange) *cache.Snapshot { + t.Helper() + store := &memoize.Store{} + c := cache.New(store, nil) + fs := cache.NewOverlayFS(c) + _ = fs.Update(t.Context(), files) + view := cache.NewView("test", uri.File("/tmp"), fs, store, includePaths) + return cache.NewSnapshot(view, store, includePaths) +} + +func TestSemanticCompletion(t *testing.T) { + userFile := `struct User { + 1: required i64 id +} + +enum Color { + RED = 1, + GREEN +} + +const i32 DEFAULT = 5` + + apiFile := `include "user.thrift" + +struct Profile { + 1: required User user + 2: optional string bio +} + +const i32 LIMIT = 10` + + ss := buildSnapshot(t, nil, + &cache.FileChange{URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(userFile), From: cache.FileChangeTypeDidOpen}, + &cache.FileChange{URI: "file:///tmp/api.thrift", Version: 0, Content: []byte(apiFile), From: cache.FileChangeTypeDidOpen}, + ) + + tests := []struct { + name string + file string + pos types.Position + want []string + notWant []string + }{ + { + name: "type position completes type names from includes", + file: "file:///tmp/api.thrift", + pos: types.Position{Line: 3, Character: 15}, // on "User" in "1: required User user" + want: []string{"User", "Profile", "Color"}, + }, + { + name: "value position completes consts and enum values", + file: "file:///tmp/api.thrift", + pos: types.Position{Line: 7, Character: 19}, // on "10" in "const i32 LIMIT = 10" + want: []string{"DEFAULT", "RED", "Color.GREEN", "LIMIT"}, + notWant: []string{"User", "Profile"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pf, err := ss.Parse(t.Context(), uri.URI(tt.file)) + assert.NoError(t, err) + + pos, err := pf.Mapper().LSPPosToParserPosition(tt.pos) + assert.NoError(t, err) + + cands := semanticCandidates(t.Context(), ss, uri.URI(tt.file), pf, pos) + got := make(map[string]bool) + for _, c := range cands { + got[c.showText] = true + } + for _, w := range tt.want { + assert.True(t, got[w], "missing candidate %q in %v", w, got) + } + for _, nw := range tt.notWant { + assert.False(t, got[nw], "unexpected candidate %q in %v", nw, got) + } + }) + } +} + +func TestCompletionEndToEnd(t *testing.T) { + content := "struct User {\n 1: required i64 id\n}\n\nstruct Profile {\n 1: required Us\n}" + ss := buildSnapshot(t, nil, + &cache.FileChange{URI: "file:///tmp/test.thrift", Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen}, + ) + + fh, err := ss.ReadFile(t.Context(), "file:///tmp/test.thrift") + assert.NoError(t, err) + + cmp := &CompletionRequest{ + Fh: fh, + Pos: types.Position{Line: 5, Character: 16}, // after "Us" in "1: required Us" + } + items, _, err := DefaultTokenCompletion.Completion(t.Context(), ss, cmp) + assert.NoError(t, err) + + labels := make([]string, 0, len(items)) + for _, item := range items { + labels = append(labels, item.Label) + } + // The type position completes the struct name from the same file. + assert.Contains(t, labels, "User") +} diff --git a/lsp/completion/semantic_based_completion.go b/lsp/completion/semantic_based_completion.go index 8c607ec..66c6ac2 100644 --- a/lsp/completion/semantic_based_completion.go +++ b/lsp/completion/semantic_based_completion.go @@ -3,8 +3,9 @@ package completion import ( "context" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/lsp/cache" ) type Interface interface { @@ -13,8 +14,7 @@ type Interface interface { // SemanticBasedCompletion generates completion list based on semantic. It is more precisely than token based completion // TODO(jpf) -type SemanticBasedCompletion struct { -} +type SemanticBasedCompletion struct{} func BuildCompletionItem(candidate Candidate) *CompletionItem { return &CompletionItem{ diff --git a/lsp/completion/semantic_completion.go b/lsp/completion/semantic_completion.go new file mode 100644 index 0000000..3aa8c27 --- /dev/null +++ b/lsp/completion/semantic_completion.go @@ -0,0 +1,182 @@ +package completion + +import ( + "context" + "sort" + + "go.lsp.dev/protocol" + "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/syntax" +) + +// semanticCandidates returns context-aware completion candidates for the +// cursor position, or nil when the context is not a known completion +// position. Type positions complete with type names; constant value +// positions complete with const and enum value names. +func semanticCandidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, parsedFile *cache.ParsedFile, pos syntax.Position) []Candidate { + path := parsedFile.AST().SearchNodePathByPosition(pos) + if len(path) == 0 { + return nil + } + target := path[len(path)-1] + + switch n := target.(type) { + case *syntax.FieldType: + // Cursor on a type reference. + return typeCandidates(ctx, ss, file, parsedFile.AST()) + + case *syntax.Identifier: + // The role of an identifier is carried by its parent: inside a + // FieldType it is a type reference; as a field name the cursor is + // in the value position. + if len(path) < 2 { + return nil + } + switch parent := path[len(path)-2].(type) { + case *syntax.FieldType: + return typeCandidates(ctx, ss, file, parsedFile.AST()) + case *syntax.Field: + if parent.Value == nil { + return valueCandidates(ctx, ss, file, parsedFile.AST()) + } + } + + case *syntax.ConstValue: + // Cursor on a constant value. + return valueCandidates(ctx, ss, file, parsedFile.AST()) + + case *syntax.Field: + // Cursor before the field name is a type position; after the name + // is a value position. + if n.Name != nil && pos.Offset < tokenOffset(parsedFile.AST(), n.Name) { + return typeCandidates(ctx, ss, file, parsedFile.AST()) + } + if n.Value == nil { + return valueCandidates(ctx, ss, file, parsedFile.AST()) + } + + case *syntax.Const: + // Cursor on the const value. + if n.Value != nil && pos.Offset > tokenOffset(parsedFile.AST(), n.Name) { + return valueCandidates(ctx, ss, file, parsedFile.AST()) + } + + case *syntax.Typedef: + // Cursor on the typedef type. + if pos.Offset < tokenOffset(parsedFile.AST(), n.Name) { + return typeCandidates(ctx, ss, file, parsedFile.AST()) + } + } + return nil +} + +func tokenOffset(doc *syntax.Document, n syntax.Node) int { + return doc.TokenPosition(n.TokStart()).Offset +} + +// typeCandidates collects the names of all type definitions (structs, +// unions, exceptions, enums, typedefs, services) from the file and its +// transitively included files, plus the base type keywords. +func typeCandidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, doc *syntax.Document) []Candidate { + names := make(map[string]struct{}) + collectTypeNames := func(ast *syntax.Document) { + for _, st := range ast.Structs() { + names[st.Name.Text] = struct{}{} + } + for _, st := range ast.Unions() { + names[st.Name.Text] = struct{}{} + } + for _, st := range ast.Exceptions() { + names[st.Name.Text] = struct{}{} + } + for _, enum := range ast.Enums() { + names[enum.Name.Text] = struct{}{} + } + for _, td := range ast.Typedefs() { + names[td.Name.Text] = struct{}{} + } + for _, svc := range ast.Services() { + names[svc.Name.Text] = struct{}{} + } + } + + collectTypeNames(doc) + for _, inc := range includedFiles(ss, file) { + if pf, err := ss.Parse(ctx, inc); err == nil && pf.AST() != nil { + collectTypeNames(pf.AST()) + } + } + + var res []Candidate + for name := range names { + res = append(res, Candidate{ + showText: name, + insertText: name, + format: protocol.InsertTextFormatPlainText, + }) + } + sort.Slice(res, func(i, j int) bool { return res[i].showText < res[j].showText }) + return res +} + +// valueCandidates collects const names and enum names and values from the +// file and its transitively included files, both bare and enum-qualified. +func valueCandidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, doc *syntax.Document) []Candidate { + names := make(map[string]struct{}) + collectValueNames := func(ast *syntax.Document) { + for _, cst := range ast.Consts() { + names[cst.Name.Text] = struct{}{} + } + for _, enum := range ast.Enums() { + names[enum.Name.Text] = struct{}{} + for _, value := range enum.Values { + names[value.Name.Text] = struct{}{} + names[enum.Name.Text+"."+value.Name.Text] = struct{}{} + } + } + } + + collectValueNames(doc) + for _, inc := range includedFiles(ss, file) { + if pf, err := ss.Parse(ctx, inc); err == nil && pf.AST() != nil { + collectValueNames(pf.AST()) + } + } + + var res []Candidate + for name := range names { + res = append(res, Candidate{ + showText: name, + insertText: name, + format: protocol.InsertTextFormatPlainText, + }) + } + sort.Slice(res, func(i, j int) bool { return res[i].showText < res[j].showText }) + return res +} + +// includedFiles returns the files transitively included by file, per the +// include graph. +func includedFiles(ss *cache.Snapshot, file uri.URI) []uri.URI { + var out []uri.URI + visited := make(map[uri.URI]bool) + var visit func(f uri.URI) + visit = func(f uri.URI) { + if visited[f] { + return + } + visited[f] = true + node := ss.Graph().Get(f) + if node == nil { + return + } + for _, inc := range node.OutDegree() { + out = append(out, inc) + visit(inc) + } + } + visit(file) + return out +} diff --git a/lsp/completion/token_completion.go b/lsp/completion/token_completion.go index 31ca22d..46e61cc 100644 --- a/lsp/completion/token_completion.go +++ b/lsp/completion/token_completion.go @@ -3,24 +3,24 @@ package completion import ( "context" "fmt" + "log/slog" "path/filepath" "sort" "strings" + "unicode" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" - "github.com/joyme123/thrift-ls/utils" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/syntax" ) var DefaultTokenCompletion Interface = &TokenCompletion{} -// TokenCompletion is token based completion. It generates completion list based on identifier in ast -type TokenCompletion struct { -} +// TokenCompletion is token based completion. It generates completion list +// based on identifiers in the AST. +type TokenCompletion struct{} var keywords = map[string]protocol.InsertTextFormat{ "bool": protocol.InsertTextFormatPlainText, @@ -55,8 +55,6 @@ type Candidate struct { } func (c *TokenCompletion) Completion(ctx context.Context, ss *cache.Snapshot, cmp *CompletionRequest) ([]*CompletionItem, protocol.Range, error) { - tokens := ss.TokensForFile(cmp.Fh.URI()) - rng := protocol.Range{ Start: protocol.Position{ Line: cmp.Pos.Line, @@ -68,13 +66,10 @@ func (c *TokenCompletion) Completion(ctx context.Context, ss *cache.Snapshot, cm }, } - log.Debugln("all tokens:", tokens) - parsedFile, err := ss.Parse(ctx, cmp.Fh.URI()) if err != nil { return nil, rng, err } - if parsedFile.AST() == nil { return nil, rng, fmt.Errorf("parser ast failed") } @@ -84,24 +79,27 @@ func (c *TokenCompletion) Completion(ctx context.Context, ss *cache.Snapshot, cm return nil, rng, err } + tokens := ss.TokensForFile(cmp.Fh.URI()) + + slog.Debug("all tokens", "tokens", tokens) + candidates := make([]Candidate, 0) - log.Debugln("parser pos: ", pos) - includeLiteralPos := pos - includeLiteralPos.Col = includeLiteralPos.Col - 1 // remove quote - nodePath := parser.SearchNodePathByPosition(parsedFile.AST(), includeLiteralPos) - if items, includeRng, err := c.includeCompletion(ss, cmp.Fh.URI(), nodePath); err == nil { - for i := range items { - candidates = append(candidates, items[i]) - } + slog.Debug("parser pos", "pos", pos) + + // Include completion: the cursor is inside an include path literal. + includePos := pos + includePos.Col-- + includePath := parsedFile.AST().SearchNodePathByPosition(includePos) + if items, includeRng, err := c.includeCompletion(ss, cmp.Fh.URI(), parsedFile.AST(), includePath); err == nil { + candidates = append(candidates, items...) if len(items) > 0 { rng = includeRng - log.Debugln("include completion candidates: ", candidates) + slog.Debug("include completion candidates", "candidates", candidates) } } if len(candidates) == 0 { - nodePath = parser.SearchNodePathByPosition(parsedFile.AST(), pos) content, err := cmp.Fh.Content() if err != nil { return nil, rng, err @@ -109,7 +107,7 @@ func (c *TokenCompletion) Completion(ctx context.Context, ss *cache.Snapshot, cm var prefix []byte // get prefix by pos for i := pos.Offset - 1; i >= 0; i-- { - if utils.Space(content[i]) || content[i] == '.' || content[i] == '\'' || content[i] == '"' { + if unicode.IsSpace(rune(content[i])) || content[i] == '.' || content[i] == '\'' || content[i] == '"' { prefix = content[i+1 : pos.Offset] rng.Start.Character = rng.Start.Character - uint32(len(prefix)) break @@ -131,11 +129,22 @@ func (c *TokenCompletion) Completion(ctx context.Context, ss *cache.Snapshot, cm }) } } - for i := range keywords { - searchCandidate(i, keywords[i]) - } - for i := range tokens { - searchCandidate(i, protocol.InsertTextFormatPlainText) + + // Semantic completion: context-aware candidates for type and + // constant value positions; fall back to keywords and all + // identifiers otherwise. + semantic := semanticCandidates(ctx, ss, cmp.Fh.URI(), parsedFile, pos) + if len(semantic) > 0 { + for _, cand := range semantic { + searchCandidate(cand.showText, cand.format) + } + } else { + for i := range keywords { + searchCandidate(i, keywords[i]) + } + for i := range tokens { + searchCandidate(i, protocol.InsertTextFormatPlainText) + } } // Sort candidates: prefix matches first (by length, shorter first), then alphabetically @@ -156,7 +165,7 @@ func (c *TokenCompletion) Completion(ctx context.Context, ss *cache.Snapshot, cm candidates = candidates[:10] } - log.Debugln("token prefix:", string(prefix), "candidates: ", candidates) + slog.Debug("token prefix", "prefix", string(prefix), "candidates", candidates) } res := make([]*CompletionItem, 0, len(candidates)) @@ -167,25 +176,36 @@ func (c *TokenCompletion) Completion(ctx context.Context, ss *cache.Snapshot, cm return res, rng, nil } -func (c *TokenCompletion) includeCompletion(ss *cache.Snapshot, file uri.URI, nodePath []parser.Node) (res []Candidate, rng protocol.Range, err error) { - if len(nodePath) < 3 { - return +// includeCompletion completes include path literals by listing the +// directory of the current file. +func (c *TokenCompletion) includeCompletion(ss *cache.Snapshot, file uri.URI, doc *syntax.Document, path []syntax.Node) (res []Candidate, rng protocol.Range, err error) { + if len(path) == 0 { + return res, rng, err } - - if nodePath[len(nodePath)-1].Type() != "LiteralValue" || nodePath[len(nodePath)-2].Type() != "Literal" || nodePath[len(nodePath)-3].Type() != "Include" { - return + include, ok := path[len(path)-1].(*syntax.Include) + if !ok || include.Path == nil { + return res, rng, err } - targetNode := nodePath[len(nodePath)-1].(*parser.LiteralValue) - pathPrefix := targetNode.Text - rng = lsputils.ASTNodeToRange(targetNode) + pathPrefix := include.Path.Text + start, end := doc.TokenRange(include.Path) + rng = protocol.Range{ + Start: protocol.Position{ + Line: uint32(start.Line - 1), + Character: uint32(start.Col - 1), + }, + End: protocol.Position{ + Line: uint32(end.Line - 1), + Character: uint32(end.Col - 1), + }, + } - currentDir := filepath.Dir(file.Filename()) + currentDir := filepath.Dir(file.Path()) - log.Debugf("search prefix %s in path %s", pathPrefix, currentDir) + slog.Debug("searching prefix in path", "prefix", pathPrefix, "dir", currentDir) res, err = ListDirAndFiles(currentDir, pathPrefix) - log.Debugln("include completion: ", res, "err", err) - return + slog.Debug("include completion", "res", res, "err", err) + return res, rng, err } diff --git a/lsp/completion/types.go b/lsp/completion/types.go index 91612b4..47a8cd0 100644 --- a/lsp/completion/types.go +++ b/lsp/completion/types.go @@ -1,9 +1,10 @@ package completion import ( - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/types" + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/types" ) type CompletionRequest struct { diff --git a/lsp/completion/utils.go b/lsp/completion/utils.go index 28d1c90..94228bc 100644 --- a/lsp/completion/utils.go +++ b/lsp/completion/utils.go @@ -2,12 +2,13 @@ package completion import ( "io/fs" + "log/slog" "path/filepath" "strings" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/constants" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/lsp/constants" ) func ListDirAndFiles(dir, prefix string) (res []Candidate, err error) { @@ -25,7 +26,7 @@ func ListDirAndFiles(dir, prefix string) (res []Candidate, err error) { pathItems := strings.Split(dir, "/") if len(pathItems) < up { - return + return res, err } pathItems = pathItems[0 : len(pathItems)-up] @@ -35,12 +36,12 @@ func ListDirAndFiles(dir, prefix string) (res []Candidate, err error) { baseDir := strings.Join(pathItems, "/") + "/" + dir prefix = strings.TrimSuffix(prefix, filePrefix) - log.Debugf("include completion: walk dir %s with prefix %s, filePrefix %s", baseDir, prefix, filePrefix) - filepath.WalkDir(baseDir, func(path string, d fs.DirEntry, err error) error { + slog.Debug("include completion walk dir", "dir", baseDir, "prefix", prefix, "filePrefix", filePrefix) + _ = filepath.WalkDir(baseDir, func(path string, d fs.DirEntry, err error) error { if err != nil || baseDir == path { return nil } - log.Debugf("include completion: name: %s, prefix: %s", d.Name(), filePrefix) + slog.Debug("include completion name", "name", d.Name(), "prefix", filePrefix) if strings.HasPrefix(d.Name(), filePrefix) { if d.IsDir() { res = append(res, Candidate{ @@ -63,5 +64,5 @@ func ListDirAndFiles(dir, prefix string) (res []Candidate, err error) { return nil }) - return + return res, err } diff --git a/lsp/debug.go b/lsp/debug.go index 86a7da3..8045763 100644 --- a/lsp/debug.go +++ b/lsp/debug.go @@ -2,30 +2,25 @@ package lsp import ( "context" - "encoding/json" + "log/slog" - log "github.com/sirupsen/logrus" "go.lsp.dev/jsonrpc2" ) -func DebugReplier(reply jsonrpc2.Replier) jsonrpc2.Replier { - return func(ctx context.Context, result interface{}, err error) error { - res, _ := json.Marshal(result) - log.Debug("jsonrpc reply debug: ", "result", string(res), "err", err) - return reply(ctx, result, err) - } -} - +// DebugHandler wraps a jsonrpc2.Handler with request/response debug logging +// and panic recovery. func DebugHandler(handler jsonrpc2.Handler) jsonrpc2.Handler { - return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { - log.Debug("jsonrpc request debug: ", "req", req.Method(), "params", string(req.Params())) + return func(ctx context.Context, req *jsonrpc2.Request) (result any, err error) { + if req != nil { + slog.Debug("jsonrpc request", "method", req.Method(), "params", string(req.Params())) + } defer func() { if r := recover(); r != nil { - log.Errorln("Recovered: ", r) + slog.Error("recovered from panic", "panic", r) } }() - return handler(ctx, DebugReplier(reply), req) + return handler(ctx, req) } } diff --git a/lsp/diagnostic.go b/lsp/diagnostic.go index 800c8a4..6f8f18d 100644 --- a/lsp/diagnostic.go +++ b/lsp/diagnostic.go @@ -2,13 +2,14 @@ package lsp import ( "context" + "errors" + "log/slog" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/diagnostic" - "github.com/joyme123/thrift-ls/utils/errors" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/diagnostic" ) func (s *Server) diagnostic(ctx context.Context, ss *cache.Snapshot, changeFile *cache.FileChange) error { @@ -16,16 +17,16 @@ func (s *Server) diagnostic(ctx context.Context, ss *cache.Snapshot, changeFile return nil } - log.Debugln("-----------diagnostic called-----------") - defer log.Debugln("-----------diagnostic finish-----------") + slog.Debug("diagnostic called") + defer slog.Debug("diagnostic finished") diag := diagnostic.NewDiagnostic() diagRes, err := diag.Diagnostic(ctx, ss, []uri.URI{changeFile.URI}) if err != nil { - log.Errorf("diagnostic failed: %v", err) + slog.Error("diagnostic failed", "err", err) } - log.Debugln("publish diagnostric result: ", len(diagRes)) + slog.Debug("publish diagnostic result", "count", len(diagRes)) var errs []error for file, res := range diagRes { @@ -36,15 +37,12 @@ func (s *Server) diagnostic(ctx context.Context, ss *cache.Snapshot, changeFile URI: file, Diagnostics: res, } - log.Debugln("file:", file, "diagnostics", res) + slog.Debug("file diagnostics", "file", file, "diagnostics", res) err = s.client.PublishDiagnostics(ctx, params) if err != nil { errs = append(errs, err) } } - if len(errs) > 0 { - return errors.NewAggregate(errs) - } - return nil + return errors.Join(errs...) } diff --git a/lsp/diagnostic/cycle_detect.go b/lsp/diagnostic/cycle_detect.go index ab78e7c..f3b57b0 100644 --- a/lsp/diagnostic/cycle_detect.go +++ b/lsp/diagnostic/cycle_detect.go @@ -3,22 +3,22 @@ package diagnostic import ( "context" "fmt" + "log/slog" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/lsputils" + "github.com/karitham/thrift-ls/syntax" ) -type CycleCheck struct { -} +type CycleCheck struct{} func (c *CycleCheck) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { includesMap := make(map[uri.URI][]Include) for _, file := range changeFiles { - getIncludes(ctx, ss, file, &includesMap) + _ = getIncludes(ctx, ss, file, &includesMap) } cyclePairs := cycleDetect(&includesMap) @@ -40,17 +40,18 @@ func cycleToDiagnosticItems(pairs []CyclePair) DiagnosticResult { func cyclePairToDiagnostic(pair CyclePair) protocol.Diagnostic { res := protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(pair.include.include), + Range: nodeRange(pair.include.doc, pair.include.include), Severity: protocol.DiagnosticSeverityWarning, - Source: "thrift-ls", - Message: fmt.Sprintf("cycle dependency in %s", pair.include.file), + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String(fmt.Sprintf("cycle dependency in %s", pair.include.file)), } return res } type Include struct { file uri.URI - include *parser.Include + include *syntax.Include + doc *syntax.Document } type CyclePair struct { @@ -80,30 +81,31 @@ func cycleDetect(includesMap *map[uri.URI][]Include) []CyclePair { func getIncludes(ctx context.Context, ss *cache.Snapshot, file uri.URI, includesMap *map[uri.URI][]Include) error { pf, err := ss.Parse(ctx, file) if err != nil { - log.Errorf("parse %s failed: %v", file, err) + slog.Error("parse failed", "file", file, "err", err) return err } if pf.AST() == nil { - log.Errorf("parse ast failed: %v", pf.AggregatedError()) + slog.Error("parse ast failed", "errs", pf.AggregatedError()) return pf.AggregatedError() } - includes := pf.AST().Includes + includes := pf.AST().Includes() resolver := ss.Resolver() for i := range includes { - if includes[i].Path == nil || includes[i].Path.BadNode { + if includes[i].Path == nil { continue } - includeURI := resolver.ResolveIncludeWithText(file, includes[i].Path.Value.Text) + includeURI := resolver.ResolveIncludeWithText(file, lsputils.IncludePathText(includes[i])) (*includesMap)[file] = append((*includesMap)[file], Include{ file: includeURI, include: includes[i], + doc: pf.AST(), }) if _, ok := (*includesMap)[includeURI]; ok { continue } - getIncludes(ctx, ss, includeURI, includesMap) + _ = getIncludes(ctx, ss, includeURI, includesMap) } return nil diff --git a/lsp/diagnostic/cycle_detect_test.go b/lsp/diagnostic/cycle_detect_test.go index fcbd3b7..3bf5279 100644 --- a/lsp/diagnostic/cycle_detect_test.go +++ b/lsp/diagnostic/cycle_detect_test.go @@ -5,15 +5,15 @@ import ( "sort" "testing" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/memoize" - "github.com/joyme123/thrift-ls/parser" "github.com/stretchr/testify/assert" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/memoize" + "github.com/karitham/thrift-ls/syntax" ) func Test_cycleDetect(t *testing.T) { - includesMap := map[uri.URI][]Include{ "/user.thrift": { Include{file: "/goods.thrift"}, @@ -86,7 +86,7 @@ include "./test/address.thrift"` file2 := `include "../user.thrift"` file3 := `include "../user.thrift"` - ss := buildSnapshotForTest([]*cache.FileChange{ + ss := buildSnapshotForTest(t, []*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -107,338 +107,33 @@ include "./test/address.thrift"` }, }) + // Expected includes, built by parsing the same sources. + parseFor := func(uriStr, src string) *syntax.Document { + doc, errs := syntax.Parse([]byte(src)) + for _, e := range errs { + if e.Severity == syntax.SeverityError { + t.Fatal(e) + } + } + return doc + } + userDoc := parseFor("file:///tmp/user.thrift", file1) + goodsDoc := parseFor("file:///tmp/test/goods.thrift", file2) + addressDoc := parseFor("file:///tmp/test/address.thrift", file3) + expectIncludeMap := map[uri.URI][]Include{ "file:///tmp/user.thrift": { - Include{ - file: "file:///tmp/test/goods.thrift", - include: &parser.Include{ - IncludeKeyword: &parser.IncludeKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "include", - BadNode: false, - Location: parser.Location{ - StartPos: parser.Position{ - Line: 1, - Col: 1, - Offset: 0, - }, - EndPos: parser.Position{ - Line: 1, - Col: 8, - Offset: 7, - }, - }, - }, - BadNode: false, - Location: parser.Location{ - StartPos: parser.Position{ - Line: 1, - Col: 1, - Offset: 0, - }, - EndPos: parser.Position{ - Line: 1, - Col: 9, - Offset: 8, - }, - }, - }, - }, - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "./test/goods.thrift", - Location: parser.Location{ - StartPos: parser.Position{ - Line: 1, - Col: 10, - Offset: 9, - }, - EndPos: parser.Position{ - Line: 1, - Col: 29, - Offset: 28, - }, - }, - }, - BadNode: false, - Quote: "\"", - Location: parser.NewLocationFromPos( - parser.Position{ - Line: 1, - Col: 9, - Offset: 8, - }, - parser.Position{ - Line: 1, - Col: 30, - Offset: 29, - }, - ), - }, - Location: parser.NewLocationFromPos( - parser.Position{ - Line: 1, - Col: 1, - Offset: 0, - }, - parser.Position{ - Line: 1, - Col: 30, - Offset: 29, - }, - ), - }, - }, - Include{ - file: "file:///tmp/test/address.thrift", - include: &parser.Include{ - IncludeKeyword: &parser.IncludeKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "include", - BadNode: false, - Location: parser.Location{ - StartPos: parser.Position{ - Line: 2, - Col: 1, - Offset: 30, - }, - EndPos: parser.Position{ - Line: 2, - Col: 8, - Offset: 37, - }, - }, - }, - BadNode: false, - Location: parser.Location{ - StartPos: parser.Position{ - Line: 2, - Col: 1, - Offset: 30, - }, - EndPos: parser.Position{ - Line: 2, - Col: 9, - Offset: 38, - }, - }, - }, - }, - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "./test/address.thrift", - Location: parser.Location{ - StartPos: parser.Position{ - Line: 2, - Col: 10, - Offset: 39, - }, - EndPos: parser.Position{ - Line: 2, - Col: 31, - Offset: 60, - }, - }, - }, - Quote: "\"", - BadNode: false, - Location: parser.NewLocationFromPos( - parser.Position{ - Line: 2, - Col: 9, - Offset: 38, - }, - parser.Position{ - Line: 2, - Col: 32, - Offset: 61, - }, - ), - }, - Location: parser.NewLocationFromPos( - parser.Position{ - Line: 2, - Col: 1, - Offset: 30, - }, - parser.Position{ - Line: 2, - Col: 32, - Offset: 61, - }, - ), - }, - }, + Include{file: "file:///tmp/test/goods.thrift", include: userDoc.Includes()[0], doc: userDoc}, + Include{file: "file:///tmp/test/address.thrift", include: userDoc.Includes()[1], doc: userDoc}, }, "file:///tmp/test/goods.thrift": { - Include{ - file: "file:///tmp/user.thrift", - include: &parser.Include{ - IncludeKeyword: &parser.IncludeKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "include", - BadNode: false, - Location: parser.Location{ - StartPos: parser.Position{ - Line: 1, - Col: 1, - Offset: 0, - }, - EndPos: parser.Position{ - Line: 1, - Col: 8, - Offset: 7, - }, - }, - }, - BadNode: false, - Location: parser.Location{ - StartPos: parser.Position{ - Line: 1, - Col: 1, - Offset: 0, - }, - EndPos: parser.Position{ - Line: 1, - Col: 9, - Offset: 8, - }, - }, - }, - }, - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "../user.thrift", - Location: parser.Location{ - StartPos: parser.Position{ - Line: 1, - Col: 10, - Offset: 9, - }, - EndPos: parser.Position{ - Line: 1, - Col: 24, - Offset: 23, - }, - }, - }, - Quote: "\"", - BadNode: false, - Location: parser.NewLocationFromPos( - parser.Position{ - Line: 1, - Col: 9, - Offset: 8, - }, - parser.Position{ - Line: 1, - Col: 25, - Offset: 24, - }, - ), - }, - Location: parser.NewLocationFromPos( - parser.Position{ - Line: 1, - Col: 1, - Offset: 0, - }, - parser.Position{ - Line: 1, - Col: 25, - Offset: 24, - }, - ), - }, - }, + Include{file: "file:///tmp/user.thrift", include: goodsDoc.Includes()[0], doc: goodsDoc}, }, "file:///tmp/test/address.thrift": { - Include{ - file: "file:///tmp/user.thrift", - include: &parser.Include{ - IncludeKeyword: &parser.IncludeKeyword{ - Keyword: parser.Keyword{ - Literal: &parser.KeywordLiteral{ - Text: "include", - BadNode: false, - Location: parser.Location{ - StartPos: parser.Position{ - Line: 1, - Col: 1, - Offset: 0, - }, - EndPos: parser.Position{ - Line: 1, - Col: 8, - Offset: 7, - }, - }, - }, - BadNode: false, - Location: parser.Location{ - StartPos: parser.Position{ - Line: 1, - Col: 1, - Offset: 0, - }, - EndPos: parser.Position{ - Line: 1, - Col: 9, - Offset: 8, - }, - }, - }, - }, - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "../user.thrift", - Location: parser.Location{ - StartPos: parser.Position{ - Line: 1, - Col: 10, - Offset: 9, - }, - EndPos: parser.Position{ - Line: 1, - Col: 24, - Offset: 23, - }, - }, - }, - Quote: "\"", - BadNode: false, - Location: parser.NewLocationFromPos( - parser.Position{ - Line: 1, - Col: 9, - Offset: 8, - }, - parser.Position{ - Line: 1, - Col: 25, - Offset: 24, - }, - ), - }, - Location: parser.NewLocationFromPos( - parser.Position{ - Line: 1, - Col: 1, - Offset: 0, - }, - parser.Position{ - Line: 1, - Col: 25, - Offset: 24, - }, - ), - }, - }, + Include{file: "file:///tmp/user.thrift", include: addressDoc.Includes()[0], doc: addressDoc}, }, } + includeMap := make(map[uri.URI][]Include) type args struct { @@ -456,7 +151,7 @@ include "./test/address.thrift"` { name: "normal", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, file: "file:///tmp/user.thrift", includesMap: &includeMap, @@ -474,11 +169,11 @@ include "./test/address.thrift"` } } -func buildSnapshotForTest(files []*cache.FileChange) *cache.Snapshot { +func buildSnapshotForTest(t *testing.T, files []*cache.FileChange) *cache.Snapshot { store := &memoize.Store{} c := cache.New(store, nil) fs := cache.NewOverlayFS(c) - fs.Update(context.TODO(), files) + _ = fs.Update(t.Context(), files) view := cache.NewView("test", "file:///tmp", fs, store, nil) ss := cache.NewSnapshot(view, store, nil) diff --git a/lsp/diagnostic/diagnostic.go b/lsp/diagnostic/diagnostic.go index 387865d..a0b107d 100644 --- a/lsp/diagnostic/diagnostic.go +++ b/lsp/diagnostic/diagnostic.go @@ -2,12 +2,14 @@ package diagnostic import ( "context" + "errors" + "log/slog" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/utils/errors" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/syntax" ) var registry []Interface @@ -26,8 +28,7 @@ type Interface interface { Name() string } -type Diagnostic struct { -} +type Diagnostic struct{} func NewDiagnostic() Interface { return &Diagnostic{} @@ -37,7 +38,7 @@ func (d *Diagnostic) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeF res := make(DiagnosticResult) var errs []error for _, impl := range registry { - log.Debugln("diagnostic called: ", impl.Name()) + slog.Debug("diagnostic called", "impl", impl.Name()) diagRes, err := impl.Diagnostic(ctx, ss, changeFiles) if err != nil { errs = append(errs, err) @@ -47,8 +48,7 @@ func (d *Diagnostic) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeF } } if len(errs) > 0 { - return res, errors.NewAggregate(errs) - + return res, errors.Join(errs...) } return res, nil } @@ -58,3 +58,53 @@ func (d *Diagnostic) Name() string { } type DiagnosticResult map[uri.URI][]protocol.Diagnostic + +// tokenRange converts a token's span to an LSP range. +func tokenRange(doc *syntax.Document, tok *syntax.Token) protocol.Range { + if tok == nil { + return protocol.Range{} + } + start := doc.TokenPosition(tokIndex(doc, tok)) + end := doc.TokenEndPosition(tokIndex(doc, tok)) + return protocol.Range{ + Start: protocol.Position{ + Line: uint32(start.Line - 1), + Character: uint32(start.Col - 1), + }, + End: protocol.Position{ + Line: uint32(end.Line - 1), + Character: uint32(end.Col - 1), + }, + } +} + +// nodeRange converts a node's span to an LSP range. +func nodeRange(doc *syntax.Document, node syntax.Node) protocol.Range { + start, end := doc.Range(node) + return protocol.Range{ + Start: protocol.Position{ + Line: uint32(start.Line - 1), + Character: uint32(start.Col - 1), + }, + End: protocol.Position{ + Line: uint32(end.Line - 1), + Character: uint32(end.Col - 1), + }, + } +} + +// tokIndex finds the index of a token pointer in the document's token +// stream by its offset. +func tokIndex(doc *syntax.Document, tok *syntax.Token) int { + if tok == nil { + return 0 + } + // Token pointers are stable: the first token with a matching offset is + // the one. + for i, t := range doc.Tokens { + if t.Offset == tok.Offset { + return i + } + } + return 0 +} diff --git a/lsp/diagnostic/fieldid_check.go b/lsp/diagnostic/fieldid_check.go index 6f33ae2..bf11061 100644 --- a/lsp/diagnostic/fieldid_check.go +++ b/lsp/diagnostic/fieldid_check.go @@ -3,22 +3,20 @@ package diagnostic import ( "context" "errors" - "fmt" + "log/slog" + "strconv" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/syntax" ) -type FieldIDCheck struct { -} +type FieldIDCheck struct{} -// FieldIDCheck checks struct, union, exception, function pramas, function throws field id -// field id format: have a unique, positive integer identifier. -// ref doc: http://diwakergupta.github.io/thrift-missing-guide/#_defining_structs +// FieldIDCheck checks struct, union, exception, function parameter, and +// throws field ids: they must be unique positive integers in [1, 32767]. func (c *FieldIDCheck) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { res := make(DiagnosticResult) for _, file := range changeFiles { @@ -41,36 +39,38 @@ func (c *FieldIDCheck) diagnostic(ctx context.Context, ss *cache.Snapshot, file if err != nil { return nil, err } - if pf.AST() == nil { return nil, errors.New("parse ast failed") } for _, err := range pf.Errors() { - log.Debugln("parse err", err) + slog.Debug("parse failed", "err", err) } var ret []protocol.Diagnostic - processStructLike := func(fields []*parser.Field) { - fieldIDSet := make(map[int][]*parser.Field) + processStructLike := func(fields []*syntax.Field) { + fieldIDSet := make(map[int][]*syntax.Field) for i := range fields { field := fields[i] - if field.Index == nil || field.Index.BadNode { + if field.FieldID == nil { + continue + } + value, err := strconv.ParseInt(field.FieldID.Text, 0, 32) + if err != nil { continue } - fieldIDSet[field.Index.Value] = append(fieldIDSet[field.Index.Value], field) + fieldIDSet[int(value)] = append(fieldIDSet[int(value)], field) } for fieldID, set := range fieldIDSet { if fieldID < 1 || fieldID > 32767 { for _, field := range set { - // field ID exceeded ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(field.Index), + Range: tokenRange(pf.AST(), field.FieldID), Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("field id should be a positive integer in [1, 32767]"), + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }) } } @@ -79,34 +79,28 @@ func (c *FieldIDCheck) diagnostic(ctx context.Context, ss *cache.Snapshot, file continue } for _, field := range set { - // field id conflict ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(field.Index), + Range: tokenRange(pf.AST(), field.FieldID), Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("field id conflict"), + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }) } } } - for _, st := range pf.AST().Structs { + for _, st := range pf.AST().Structs() { processStructLike(st.Fields) } - - for _, union := range pf.AST().Unions { + for _, union := range pf.AST().Unions() { processStructLike(union.Fields) } - - for _, excep := range pf.AST().Exceptions { - + for _, excep := range pf.AST().Exceptions() { processStructLike(excep.Fields) - } - - for _, svc := range pf.AST().Services { + for _, svc := range pf.AST().Services() { for _, fn := range svc.Functions { - processStructLike(fn.Arguments) + processStructLike(fn.Args) if fn.Throws != nil { processStructLike(fn.Throws.Fields) } diff --git a/lsp/diagnostic/fieldid_check_test.go b/lsp/diagnostic/fieldid_check_test.go index bc09ffa..bec69e3 100644 --- a/lsp/diagnostic/fieldid_check_test.go +++ b/lsp/diagnostic/fieldid_check_test.go @@ -5,10 +5,11 @@ import ( "sort" "testing" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" ) func Test_FieldIDCheck_Diagnostic(t *testing.T) { @@ -39,7 +40,7 @@ service Demo { } ` - ss := buildSnapshotForTest([]*cache.FileChange{ + ss := buildSnapshotForTest(t, []*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -63,7 +64,7 @@ service Demo { name: "case1", c: &FieldIDCheck{}, args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, changeFiles: []uri.URI{ "file:///tmp/user.thrift", @@ -84,8 +85,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id conflict", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }, { Range: protocol.Range{ @@ -99,8 +100,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id conflict", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }, { Range: protocol.Range{ @@ -114,8 +115,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id should be a positive integer in [1, 32767]", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }, { Range: protocol.Range{ @@ -129,8 +130,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id should be a positive integer in [1, 32767]", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }, // union @@ -146,8 +147,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id conflict", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }, { Range: protocol.Range{ @@ -161,8 +162,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id conflict", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }, { Range: protocol.Range{ @@ -176,8 +177,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id should be a positive integer in [1, 32767]", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }, { Range: protocol.Range{ @@ -191,8 +192,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id should be a positive integer in [1, 32767]", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }, // exception @@ -208,8 +209,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id conflict", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }, { Range: protocol.Range{ @@ -223,8 +224,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id conflict", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }, { Range: protocol.Range{ @@ -238,8 +239,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id should be a positive integer in [1, 32767]", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }, { Range: protocol.Range{ @@ -253,8 +254,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id should be a positive integer in [1, 32767]", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }, // function params @@ -270,8 +271,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id should be a positive integer in [1, 32767]", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }, { Range: protocol.Range{ @@ -285,8 +286,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id conflict", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }, { Range: protocol.Range{ @@ -300,8 +301,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id conflict", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }, { Range: protocol.Range{ @@ -315,8 +316,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id should be a positive integer in [1, 32767]", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }, // function throws @@ -332,8 +333,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id should be a positive integer in [1, 32767]", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }, { Range: protocol.Range{ @@ -347,8 +348,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id conflict", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }, { Range: protocol.Range{ @@ -362,8 +363,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id conflict", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id conflict"), }, { Range: protocol.Range{ @@ -377,8 +378,8 @@ service Demo { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field id should be a positive integer in [1, 32767]", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field id should be a positive integer in [1, 32767]"), }, }, }, diff --git a/lsp/diagnostic/parse.go b/lsp/diagnostic/parse.go index d85c218..60299e3 100644 --- a/lsp/diagnostic/parse.go +++ b/lsp/diagnostic/parse.go @@ -2,17 +2,17 @@ package diagnostic import ( "context" + "errors" + "log/slog" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/parser" - "github.com/joyme123/thrift-ls/utils/errors" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/syntax" ) -type Parse struct { -} +type Parse struct{} func (p *Parse) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { var errs []error @@ -25,20 +25,14 @@ func (p *Parse) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles continue } - // TODO(jpf): 递归解析 for _, err := range parseRes.Errors() { - parseErr, ok := err.(parser.ParserError) - if !ok { - continue - } - log.Debugf("diagnostic parse err: %v", parseErr) - diag := parseErrToDiagnostic(parseErr) - res[uri] = append(res[uri], diag) + slog.Debug("diagnostic parse failed", "err", err) + res[uri] = append(res[uri], syntaxErrorToDiagnostic(err)) } } if len(errs) > 0 { - return res, errors.NewAggregate(errs) + return res, errors.Join(errs...) } return res, nil @@ -48,23 +42,26 @@ func (p *Parse) Name() string { return "Parse" } -func parseErrToDiagnostic(err parser.ParserError) protocol.Diagnostic { - line, col, _ := err.Pos() - diag := protocol.Diagnostic{ +// syntaxErrorToDiagnostic converts a syntax error or warning to an LSP +// diagnostic. +func syntaxErrorToDiagnostic(err syntax.Error) protocol.Diagnostic { + severity := protocol.DiagnosticSeverityError + if err.Severity == syntax.SeverityWarning { + severity = protocol.DiagnosticSeverityWarning + } + return protocol.Diagnostic{ Range: protocol.Range{ Start: protocol.Position{ - Line: uint32(line - 1), - Character: uint32(col - 1), + Line: uint32(err.Line - 1), + Character: uint32(err.Col - 1), }, End: protocol.Position{ - Line: uint32(line - 1), - Character: uint32(col - 1), + Line: uint32(err.Line - 1), + Character: uint32(err.Col - 1), }, }, - Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: err.InnerError().Error(), + Severity: severity, + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String(err.Message), } - - return diag } diff --git a/lsp/diagnostic/semantic_analysis.go b/lsp/diagnostic/semantic_analysis.go index 071a307..4b161b6 100644 --- a/lsp/diagnostic/semantic_analysis.go +++ b/lsp/diagnostic/semantic_analysis.go @@ -4,18 +4,17 @@ import ( "context" "errors" "fmt" + "log/slog" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/codejump" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/codejump" + "github.com/karitham/thrift-ls/syntax" ) -type SemanticAnalysis struct { -} +type SemanticAnalysis struct{} func (s *SemanticAnalysis) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { res := make(DiagnosticResult) @@ -44,183 +43,88 @@ func (s *SemanticAnalysis) diagnostic(ctx context.Context, ss *cache.Snapshot, c } for _, err := range pf.Errors() { - log.Debugln("parse err", err) + slog.Debug("parse failed", "err", err) } res := s.checkDefineConflict(ctx, pf) - items := s.checkDefinitionExist(ctx, ss, changeFile, pf) res = append(res, items...) return res, nil } +// checkDefineConflict reports duplicate field names, function names, and +// top-level definition names. func (s *SemanticAnalysis) checkDefineConflict(ctx context.Context, pf *cache.ParsedFile) []protocol.Diagnostic { var ret []protocol.Diagnostic - processStructLike := func(fields []*parser.Field) { + processStructLike := func(fields []*syntax.Field) { fieldMap := make(map[string]struct{}) for i := range fields { field := fields[i] - if field.IsBadNode() || field.ChildrenBadNode() { - continue - } - if _, exist := fieldMap[field.Identifier.Name.Text]; exist { - // struct conflict + if _, exist := fieldMap[field.Name.Text]; exist { ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(field.Identifier.Name), + Range: nodeRange(pf.AST(), field.Name), Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("field name conflict with other field"), + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field name conflict with other field"), }) } - fieldMap[field.Identifier.Name.Text] = struct{}{} + fieldMap[field.Name.Text] = struct{}{} } } definitionNameMap := make(map[string]string) - structMap := make(map[string]struct{}) - for _, st := range pf.AST().Structs { - if st.IsBadNode() || st.ChildrenBadNode() { - continue - } - - if _, exist := structMap[st.Identifier.Name.Text]; exist { - // struct conflict + processDefinition := func(name string, node syntax.Node, kind string) { + if previous, exist := definitionNameMap[name]; exist { ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(st.Identifier.Name), + Range: nodeRange(pf.AST(), node), Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("struct name conflict with other struct"), + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String(fmt.Sprintf("%s name conflict with other %s", kind, previous)), }) } + definitionNameMap[name] = kind + } - if t, exist := definitionNameMap[st.Identifier.Name.Text]; exist && t != st.Type() { - // struct conflict - ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(st.Identifier.Name), - Severity: protocol.DiagnosticSeverityHint, - Source: "thrift-ls", - Message: fmt.Sprintf("struct name conflict with other type"), - }) - } - - structMap[st.Identifier.Name.Text] = struct{}{} - definitionNameMap[st.Identifier.Name.Text] = st.Type() - + for _, st := range pf.AST().Structs() { + processDefinition(st.Name.Text, st.Name, "struct") processStructLike(st.Fields) } - - unionMap := make(map[string]struct{}) - for _, union := range pf.AST().Unions { - if union.IsBadNode() || union.ChildrenBadNode() { - continue - } - - if _, exist := unionMap[union.Name.Name.Text]; exist { - // union conflict - ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(union.Name.Name), - Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("union name conflict with other union"), - }) - } - - if t, exist := definitionNameMap[union.Name.Name.Text]; exist && t != union.Type() { - // union conflict with others - ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(union.Name.Name), - Severity: protocol.DiagnosticSeverityHint, - Source: "thrift-ls", - Message: fmt.Sprintf("union name conflict with other type"), - }) - } - - unionMap[union.Name.Name.Text] = struct{}{} - definitionNameMap[union.Name.Name.Text] = union.Type() - + for _, union := range pf.AST().Unions() { + processDefinition(union.Name.Text, union.Name, "union") processStructLike(union.Fields) } - - excepMap := make(map[string]struct{}) - for _, excep := range pf.AST().Exceptions { - if excep.IsBadNode() || excep.ChildrenBadNode() { - continue - } - - if _, exist := excepMap[excep.Name.Name.Text]; exist { - // exception conflict - ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(excep.Name.Name), - Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("exception name conflict with other exception"), - }) - } - - if t, exist := definitionNameMap[excep.Name.Name.Text]; exist && t != excep.Type() { - // union conflict with others - ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(excep.Name.Name), - Severity: protocol.DiagnosticSeverityHint, - Source: "thrift-ls", - Message: fmt.Sprintf("exception name conflict with other type"), - }) - } - - excepMap[excep.Name.Name.Text] = struct{}{} - definitionNameMap[excep.Name.Name.Text] = excep.Type() - + for _, excep := range pf.AST().Exceptions() { + processDefinition(excep.Name.Text, excep.Name, "exception") processStructLike(excep.Fields) - + } + for _, enum := range pf.AST().Enums() { + processDefinition(enum.Name.Text, enum.Name, "enum") + } + for _, cst := range pf.AST().Consts() { + processDefinition(cst.Name.Text, cst.Name, "const") + } + for _, td := range pf.AST().Typedefs() { + processDefinition(td.Name.Text, td.Name, "typedef") } - svcMap := make(map[string]struct{}) - for _, svc := range pf.AST().Services { - if svc.IsBadNode() || svc.ChildrenBadNode() { - continue - } - if _, exist := svcMap[svc.Name.Name.Text]; exist { - // service conflict - ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(svc.Name.Name), - Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("service name conflict with other service"), - }) - } - - if t, exist := definitionNameMap[svc.Name.Name.Text]; exist && t != svc.Type() { - // service conflict with others - ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(svc.Name.Name), - Severity: protocol.DiagnosticSeverityHint, - Source: "thrift-ls", - Message: fmt.Sprintf("service name conflict with other type"), - }) - } - - svcMap[svc.Name.Name.Text] = struct{}{} - definitionNameMap[svc.Name.Name.Text] = svc.Type() + for _, svc := range pf.AST().Services() { + processDefinition(svc.Name.Text, svc.Name, "service") fnMap := make(map[string]struct{}) for _, fn := range svc.Functions { - if fn.IsBadNode() || svc.ChildrenBadNode() { - continue - } - if _, exist := fnMap[fn.Name.Name.Text]; exist { - // function conflict + if _, exist := fnMap[fn.Name.Text]; exist { ret = append(ret, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(fn.Name.Name), + Range: nodeRange(pf.AST(), fn.Name), Severity: protocol.DiagnosticSeverityWarning, - Source: "thrift-ls", - Message: fmt.Sprintf("function name conflict with other function"), + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("function name conflict with other function"), }) } - fnMap[fn.Name.Name.Text] = struct{}{} - processStructLike(fn.Arguments) + fnMap[fn.Name.Text] = struct{}{} + processStructLike(fn.Args) if fn.Throws != nil { processStructLike(fn.Throws.Fields) } @@ -230,27 +134,22 @@ func (s *SemanticAnalysis) checkDefineConflict(ctx context.Context, pf *cache.Pa return ret } -// same as goto definition -// struct/union/exception field type +// checkDefinitionExist reports field types, const values, and return types +// that reference undefined definitions. func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile) []protocol.Diagnostic { ret := make([]protocol.Diagnostic, 0) - // struct/union/exception/function arguments/throw fields field type - processStructLike := func(fields []*parser.Field) { + processStructLike := func(fields []*syntax.Field) { for i := range fields { field := fields[i] - if field.IsBadNode() || field.ChildrenBadNode() { - continue - } - items := s.checkTypeExist(ctx, ss, file, pf, field.FieldType) + items := s.checkTypeExist(ctx, ss, file, pf, field.Type) ret = append(ret, items...) - // default value check - if field.ConstValue != nil { - items := s.checkConstValueExist(ctx, ss, file, pf, field.ConstValue) + if field.Value != nil { + items := s.checkConstValueExist(ctx, ss, file, pf, field.Value) ret = append(ret, items...) - dig := s.checkConstValueMatchType(ctx, field) + dig := s.checkConstValueMatchType(pf, field) if dig != nil { ret = append(ret, *dig) } @@ -258,31 +157,25 @@ func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, ss *cache.S } } - for _, st := range pf.AST().Structs { + for _, st := range pf.AST().Structs() { processStructLike(st.Fields) } - - for _, union := range pf.AST().Unions { + for _, union := range pf.AST().Unions() { processStructLike(union.Fields) } - - for _, excep := range pf.AST().Exceptions { + for _, excep := range pf.AST().Exceptions() { processStructLike(excep.Fields) } - - for _, cst := range pf.AST().Consts { + for _, cst := range pf.AST().Consts() { items := s.checkConstValueExist(ctx, ss, file, pf, cst.Value) ret = append(ret, items...) } - - for _, svc := range pf.AST().Services { + for _, svc := range pf.AST().Services() { for _, fn := range svc.Functions { - if fn.FunctionType != nil { - items := s.checkTypeExist(ctx, ss, file, pf, fn.FunctionType) - ret = append(ret, items...) - } + items := s.checkTypeExist(ctx, ss, file, pf, fn.Type) + ret = append(ret, items...) - processStructLike(fn.Arguments) + processStructLike(fn.Args) if fn.Throws != nil { processStructLike(fn.Throws.Fields) } @@ -293,123 +186,159 @@ func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, ss *cache.S } func (s *SemanticAnalysis) checkConstValueExist(ctx context.Context, ss *cache.Snapshot, - file uri.URI, pf *cache.ParsedFile, cst *parser.ConstValue) (res []protocol.Diagnostic) { - if cst.TypeName != "identifier" { - return + file uri.URI, pf *cache.ParsedFile, cst *syntax.ConstValue, +) (res []protocol.Diagnostic) { + if cst == nil || cst.Kind != syntax.ValueIdent { + return res } - if cst.Value == "true" || cst.Value == "false" { - return + if cst.Text == "true" || cst.Text == "false" { + return res } - _, id, err := codejump.ConstValueTypeDefinitionIdentifier(ctx, ss, file, pf.AST(), cst) + _, id, err := codejump.FindConstValueDefinition(ctx, ss, file, pf.AST(), cst) if err != nil || id == nil { res = append(res, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(cst), + Range: nodeRange(pf.AST(), cst), Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("default value doesn't exist"), + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("default value doesn't exist"), }) } - return + return res } -func (s *SemanticAnalysis) checkConstValueMatchType(ctx context.Context, field *parser.Field) (res *protocol.Diagnostic) { - if field.BadNode || field.ChildrenBadNode() { +func (s *SemanticAnalysis) checkConstValueMatchType(pf *cache.ParsedFile, field *syntax.Field) (res *protocol.Diagnostic) { + if field.Value == nil { return nil } - - expectTypeName := field.FieldType.TypeName - - if field.ConstValue != nil { - valueType := field.ConstValue.TypeName - // TypeName can be: list, map, pair, string, identifier, i64, double - switch valueType { - case "list", "map", "string", "double": - if expectTypeName.Name != valueType { - return &protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(field.ConstValue), - Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("expect %s but got %s", expectTypeName.Name, valueType), - } - } - case "identifier": - if valueType == "identifier" && - (field.ConstValue.Value == "true" || field.ConstValue.Value == "false") { - valueType = "bool" - } - if expectTypeName.Name == "bool" { - if field.ConstValue.Value != "true" && field.ConstValue.Value != "false" { - return &protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(field.ConstValue), - Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("expect %s but got %s", expectTypeName.Name, valueType), - } - } - } else if codejump.IsBasicType(valueType) { - return &protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(field.ConstValue), - Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("expect %s but got %s", expectTypeName.Name, valueType), - } - } - case "i64": - if expectTypeName.Name != "i8" && - expectTypeName.Name != "i16" && - expectTypeName.Name != "i32" && - expectTypeName.Name != "i64" { - return &protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(field.ConstValue), - Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("expect %s but got %s", expectTypeName.Name, valueType), - } + expect := typeName(field.Type) + value := field.Value + valueKind := value.Kind + + switch valueKind { + case syntax.ValueList, syntax.ValueMap, syntax.ValueString, syntax.ValueDouble: + if !sameKind(expect, valueKind) { + return mismatchDiagnostic(pf.AST(), field, expect, kindName(valueKind)) + } + case syntax.ValueInt: + // true/false lex as int constants but are bools. + if value.Text == "true" || value.Text == "false" { + if expect != "bool" { + return mismatchDiagnostic(pf.AST(), field, expect, "bool") } + return nil + } + switch expect { + case "i8", "i16", "i32", "i64": + default: + return mismatchDiagnostic(pf.AST(), field, expect, "i64") + } + case syntax.ValueIdent: + if expect == "bool" { + return mismatchDiagnostic(pf.AST(), field, expect, "identifier") } } return nil } +func sameKind(expect string, kind syntax.ConstValueKind) bool { + switch kind { + case syntax.ValueList: + return expect == "list" + case syntax.ValueMap: + return expect == "map" + case syntax.ValueString: + return expect == "string" + case syntax.ValueDouble: + return expect == "double" + } + return false +} + +func kindName(kind syntax.ConstValueKind) string { + switch kind { + case syntax.ValueList: + return "list" + case syntax.ValueMap: + return "map" + case syntax.ValueString: + return "string" + case syntax.ValueDouble: + return "double" + case syntax.ValueInt: + return "int" + case syntax.ValueIdent: + return "identifier" + } + return "unknown" +} + +func mismatchDiagnostic(doc *syntax.Document, field *syntax.Field, expect, got string) *protocol.Diagnostic { + return &protocol.Diagnostic{ + Range: nodeRange(doc, field.Value), + Severity: protocol.DiagnosticSeverityError, + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String(fmt.Sprintf("expect %s but got %s", expect, got)), + } +} + +// typeName returns the referenced type name of a field type, or the +// container/base keyword. +func typeName(ft *syntax.FieldType) string { + if ft == nil { + return "" + } + switch ft.Kind { + case syntax.TypeIdent: + return ft.Ident.Text + case syntax.TypeMap: + return "map" + case syntax.TypeList: + return "list" + case syntax.TypeSet: + return "set" + case syntax.TypeBase: + return ft.Base.String() + } + return "" +} + func (s *SemanticAnalysis) checkTypeExist(ctx context.Context, ss *cache.Snapshot, - file uri.URI, pf *cache.ParsedFile, ft *parser.FieldType) (res []protocol.Diagnostic) { - if codejump.IsContainerType(ft.TypeName.Name) { + file uri.URI, pf *cache.ParsedFile, ft *syntax.FieldType, +) (res []protocol.Diagnostic) { + if ft == nil { + return res + } + switch ft.Kind { + case syntax.TypeMap, syntax.TypeList, syntax.TypeSet: return s.checkContainerTypeExist(ctx, ss, file, pf, ft) - } else if codejump.IsBasicType(ft.TypeName.Name) { + case syntax.TypeBase: return nil - } else { - _, id, _, err := codejump.TypeNameDefinitionIdentifier(ctx, ss, file, pf.AST(), ft.TypeName) + case syntax.TypeIdent: + _, id, _, err := codejump.FindTypeDefinition(ctx, ss, file, pf.AST(), ft) if err != nil || id == nil { res = append(res, protocol.Diagnostic{ - Range: lsputils.ASTNodeToRange(ft), + Range: nodeRange(pf.AST(), ft.Ident), Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: fmt.Sprintf("field type doesn't exist"), + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field type doesn't exist"), }) } } - return res } func (s *SemanticAnalysis) checkContainerTypeExist(ctx context.Context, - ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, ft *parser.FieldType) (res []protocol.Diagnostic) { - + ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, ft *syntax.FieldType, +) (res []protocol.Diagnostic) { if ft.KeyType != nil { - items := s.checkTypeExist(ctx, ss, file, pf, ft.KeyType) - res = append(res, items...) + res = append(res, s.checkTypeExist(ctx, ss, file, pf, ft.KeyType)...) } - if ft.ValueType != nil { - items := s.checkTypeExist(ctx, ss, file, pf, ft.ValueType) - res = append(res, items...) + res = append(res, s.checkTypeExist(ctx, ss, file, pf, ft.ValueType)...) } - return res } - -// TODO(jpf): 类型和默认值的类型要一致 diff --git a/lsp/diagnostic/semantic_analysis_test.go b/lsp/diagnostic/semantic_analysis_test.go index feda044..3ca5575 100644 --- a/lsp/diagnostic/semantic_analysis_test.go +++ b/lsp/diagnostic/semantic_analysis_test.go @@ -5,14 +5,14 @@ import ( "sort" "testing" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" ) func Test_SemanticAnalysis_Diagnostic(t *testing.T) { - file1 := `struct Student { 1: required string name, 2: required User user1, @@ -53,7 +53,7 @@ struct TestUUID { 1: required uuid id } ` - ss := buildSnapshotForTest([]*cache.FileChange{ + ss := buildSnapshotForTest(t, []*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -75,7 +75,7 @@ struct TestUUID { { name: "case 1", args: args{ - ctx: context.TODO(), + ctx: t.Context(), ss: ss, changeFiles: []uri.URI{ "file:///tmp/user.thrift", @@ -91,12 +91,12 @@ struct TestUUID { }, End: protocol.Position{ Line: 2, - Character: 18, + Character: 17, }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field type doesn't exist", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field type doesn't exist"), }, { Range: protocol.Range{ @@ -110,8 +110,8 @@ struct TestUUID { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "struct name conflict with other struct", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("struct name conflict with other struct"), }, { Range: protocol.Range{ @@ -121,12 +121,12 @@ struct TestUUID { }, End: protocol.Position{ Line: 10, - Character: 18, + Character: 17, }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field type doesn't exist", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field type doesn't exist"), }, { Range: protocol.Range{ @@ -140,8 +140,8 @@ struct TestUUID { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "default value doesn't exist", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("default value doesn't exist"), }, { Range: protocol.Range{ @@ -151,12 +151,12 @@ struct TestUUID { }, End: protocol.Position{ Line: 16, - Character: 18, + Character: 17, }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field type doesn't exist", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field type doesn't exist"), }, { Range: protocol.Range{ @@ -166,12 +166,12 @@ struct TestUUID { }, End: protocol.Position{ Line: 21, - Character: 21, + Character: 20, }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field type doesn't exist", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field type doesn't exist"), }, { Range: protocol.Range{ @@ -181,12 +181,12 @@ struct TestUUID { }, End: protocol.Position{ Line: 21, - Character: 75, + Character: 74, }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "field type doesn't exist", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("field type doesn't exist"), }, { Range: protocol.Range{ @@ -200,8 +200,8 @@ struct TestUUID { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "expect i32 but got bool", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("expect i32 but got bool"), }, { Range: protocol.Range{ @@ -215,8 +215,8 @@ struct TestUUID { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "expect i32 but got string", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("expect i32 but got string"), }, { Range: protocol.Range{ @@ -230,8 +230,8 @@ struct TestUUID { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "expect string but got bool", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("expect string but got bool"), }, { Range: protocol.Range{ @@ -245,8 +245,8 @@ struct TestUUID { }, }, Severity: protocol.DiagnosticSeverityError, - Source: "thrift-ls", - Message: "expect string but got i64", + Source: protocol.NewOptional("thrift-ls"), + Message: protocol.String("expect string but got i64"), }, }, }, diff --git a/lsp/format.go b/lsp/format.go index c168b14..3688144 100644 --- a/lsp/format.go +++ b/lsp/format.go @@ -1,11 +1,16 @@ package lsp import ( + "bytes" "context" + "strings" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/format" - "github.com/joyme123/thrift-ls/lsp/mapper" + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/formatter" + "github.com/karitham/thrift-ls/lsp/mapper" + "github.com/karitham/thrift-ls/lsp/types" + "github.com/karitham/thrift-ls/syntax" ) func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) (result []protocol.TextEdit, err error) { @@ -39,7 +44,7 @@ func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormat return nil, pf.AggregatedError() } - formatted, err := format.FormatDocument(pf.AST(), opts) + formatted, err := formatter.Format(pf.AST(), opts) if err != nil { return nil, err } @@ -62,6 +67,203 @@ func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormat result = append(result, textEdit) - return + return result, err +} + +// rangeFormatting implements textDocument/rangeFormatting. +// +// The formatter only knows how to print whole documents, so a range is +// formatted by extracting the selected slice, formatting it as a standalone +// document, and splicing the result back into the file. Following Prettier's +// range-formatting contract, the range must be bounded by blank lines (or +// file edges) after expansion to line boundaries; otherwise no edits are +// produced and the request is a no-op. +func (s *Server) rangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) (result []protocol.TextEdit, err error) { + fileURI := params.TextDocument.URI + view, err := s.session.ViewOf(fileURI) + if err != nil { + return nil, err + } + + ss, release := view.Snapshot() + defer release() + + fh, err := ss.ReadFile(ctx, fileURI) + if err != nil { + return nil, err + } + + content, err := fh.Content() + if err != nil { + return nil, err + } + + mp := mapper.NewMapper(fileURI, content) + start, err := mp.LSPPosToParserPosition(lspPosition(params.Range.Start)) + if err != nil { + return nil, nil + } + end, err := mp.LSPPosToParserPosition(lspPosition(params.Range.End)) + if err != nil { + return nil, nil + } + rs, re := start.Offset, end.Offset + if rs >= re { + return nil, nil + } + + // A selection covering the whole document delegates to full formatting. + if rs == 0 && re == len(content) { + return s.formatting(ctx, &protocol.DocumentFormattingParams{TextDocument: params.TextDocument}) + } + + newText, rs, re, ok := formatRangeText(content, rs, re, s.formatOpts) + if !ok { + return nil, nil + } + + startPos, err := mp.OffsetToLSPPosition(rs) + if err != nil { + return nil, nil + } + endPos, err := mp.OffsetToLSPPosition(re) + if err != nil { + return nil, nil + } + + return []protocol.TextEdit{{ + Range: protocol.Range{ + Start: protocolPosition(startPos), + End: protocolPosition(endPos), + }, + NewText: newText, + }}, nil +} + +// lspPosition converts a protocol position to the internal position type. +func lspPosition(p protocol.Position) types.Position { + return types.Position{ + Line: uint32(p.Line), + Character: uint32(p.Character), + } +} + +// protocolPosition converts an internal position to a protocol position. +func protocolPosition(p types.Position) protocol.Position { + return protocol.Position{ + Line: p.Line, + Character: p.Character, + } +} +// formatRangeText formats content[rs:re] (half-open byte offsets) and returns +// the replacement text. ok is false when the range is not safely bounded by +// blank lines or file edges, or the slice does not parse cleanly. +func formatRangeText(content []byte, rs, re int, opts formatter.Options) (newText string, outRS, outRE int, ok bool) { + // An empty or inverted selection produces no edits. + if rs >= re { + return "", rs, re, false + } + + // Expand to line boundaries, then trim leading and trailing blank lines + // from the selection. + rs = lineStart(content, rs) + re = lineEnd(content, re) + rs = skipBlankLinesForward(content, rs, re) + re = skipBlankLinesBackward(content, rs, re) + if rs >= re { + return "", rs, re, false + } + + // The lines immediately outside the range must be blank, or the range + // must touch a file edge. + if !blankLineBefore(content, rs) || !blankLineAfter(content, re) { + return "", rs, re, false + } + + slice := content[rs:re] + doc, errs := syntax.Parse(slice) + if len(errs) > 0 { + return "", rs, re, false + } + + formatted, err := formatter.Format(doc, opts) + if err != nil { + return "", rs, re, false + } + + // The splice must not add or drop newlines at the boundaries: the slice + // starts at a line start and ends just before its last line's newline. + formatted = strings.TrimLeft(formatted, "\n") + formatted = strings.TrimRight(formatted, "\r\n") + return formatted, rs, re, true +} + +// lineStart returns the byte offset of the start of the line containing offset. +func lineStart(content []byte, offset int) int { + if i := bytes.LastIndexByte(content[:offset], '\n'); i != -1 { + return i + 1 + } + return 0 +} + +// lineEnd returns the byte offset of the newline ending the line containing +// offset, or len(content) for the last line. +func lineEnd(content []byte, offset int) int { + if i := bytes.IndexByte(content[offset:], '\n'); i != -1 { + return offset + i + } + return len(content) +} + +// blankLineBefore reports whether the line before the line starting at offset +// is blank (whitespace only) or offset is at the start of the file. +func blankLineBefore(content []byte, offset int) bool { + if offset == 0 { + return true + } + start := lineStart(content, offset-1) + return len(bytes.TrimSpace(content[start:offset])) == 0 +} + +// blankLineAfter reports whether the line after the one ending at offset is +// blank (whitespace only) or offset is at the end of the file. +func blankLineAfter(content []byte, offset int) bool { + if offset == len(content) { + return true + } + end := lineEnd(content, offset+1) + return len(bytes.TrimSpace(content[offset+1:end])) == 0 +} + +// skipBlankLinesForward advances offset past blank lines, stopping at limit. +func skipBlankLinesForward(content []byte, offset, limit int) int { + for offset < limit { + end := lineEnd(content, offset) + if end >= limit { + break + } + if len(bytes.TrimSpace(content[offset:end])) > 0 { + break + } + offset = end + 1 + } + return offset +} + +// skipBlankLinesBackward retreats offset (a newline position or len(content)) +// past blank lines, stopping at start. +func skipBlankLinesBackward(content []byte, start, offset int) int { + for offset > start { + + lineStart := lineStart(content, offset-1) + if len(bytes.TrimSpace(content[lineStart:offset])) > 0 { + break + } + offset = lineStart - 1 + if offset < 0 { + offset = 0 + } + } + return offset } diff --git a/lsp/format_range_fuzz_test.go b/lsp/format_range_fuzz_test.go new file mode 100644 index 0000000..c88070a --- /dev/null +++ b/lsp/format_range_fuzz_test.go @@ -0,0 +1,91 @@ +package lsp + +import ( + "testing" + + "github.com/karitham/thrift-ls/formatter" + "github.com/karitham/thrift-ls/syntax" +) + +// FuzzFormatRangeText checks the invariants of range formatting: +// - a refused range must produce no output +// - an accepted range must be structurally sane (line-aligned, in bounds) +// - splicing the formatted slice back into the document must keep the +// document parseable +func FuzzFormatRangeText(f *testing.F) { + seeds := []string{ + "struct A {\n1: string a\n}\n\nstruct B { 1: i32 b }", + "\nstruct A {\n1: string a\n}\n\nstruct B {\n2: i32 b\n}\n\nstruct C {}", + "struct A {}\n\n\n\nstruct B {\n1: string b\n}", + "struct A {}\r\n\r\nstruct B {\r\n1: string b\r\n}\r\n\r\nstruct C {}", + "include \"base.thrift\"\n\nnamespace go test\n\nenum Color {\n RED = 1\n BLUE = 2\n}\n\nstruct S {\n 1: Color c\n}", + "// leading comment\nstruct A {}", + "struct A {}", + "", + "\n", + "\n\n\n", + } + for _, s := range seeds { + f.Add([]byte(s), 0, 0) + } + + f.Fuzz(func(t *testing.T, content []byte, rs, re int) { + opts := formatter.Options{} + // Clamp the fuzzed offsets into the content. + limit := len(content) + 1 + rs = rs % limit + if rs < 0 { + rs += limit + } + re = re % limit + if re < 0 { + re += limit + } + + newText, outRS, outRE, ok := formatRangeText(content, rs, re, opts) + if !ok { + return + } + + // The accepted range must be line-aligned and in bounds. + if outRS < 0 || outRE > len(content) || outRS >= outRE { + t.Fatalf("invalid accepted range [%d, %d) for content %q", outRS, outRE, content) + } + if outRS != 0 && content[outRS-1] != '\n' { + t.Fatalf("accepted range starts mid-line at %d in %q", outRS, content) + } + if outRE != len(content) && content[outRE] != '\n' { + t.Fatalf("accepted range ends mid-line at %d in %q", outRE, content) + } + + // Splicing the formatted slice back must not introduce new parse + // errors: errors outside the range (the original may not parse + // cleanly) must be preserved, and the formatted slice itself is + // known to parse. + spliced := make([]byte, 0, len(content)-outRE+outRS+len(newText)) + spliced = append(spliced, content[:outRS]...) + spliced = append(spliced, newText...) + spliced = append(spliced, content[outRE:]...) + + origErrs := errorMessages(syntax.Parse(content)) + splicedErrs := errorMessages(syntax.Parse(spliced)) + for msg, splicedCount := range splicedErrs { + if splicedCount > origErrs[msg] { + t.Fatalf("splice introduced new parse error %q\ncontent: %q\nrange: [%d, %d)\nnewText: %q", + msg, content, outRS, outRE, newText) + } + } + }) +} + +// errorMessages counts error-severity messages, ignoring warnings. +func errorMessages(doc *syntax.Document, errs []syntax.Error) map[string]int { + _ = doc + counts := make(map[string]int) + for _, err := range errs { + if err.Severity == syntax.SeverityError { + counts[err.Message]++ + } + } + return counts +} diff --git a/lsp/format_range_server_test.go b/lsp/format_range_server_test.go new file mode 100644 index 0000000..49fb0ea --- /dev/null +++ b/lsp/format_range_server_test.go @@ -0,0 +1,104 @@ +package lsp + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" + "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/formatter" + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/memoize" +) + +func TestServerRangeFormatting(t *testing.T) { + ctx := t.Context() + fileURI, err := uri.Parse("file:///tmp/range.thrift") + assert.NoError(t, err) + + // Only struct B needs reformatting; A and C are already formatted. + content := `struct A { 1: string a } + +struct B { +2: i32 b +} + +struct C { 3: i64 c } +` + + store := &memoize.Store{} + srv := NewServer(cache.New(store, nil), nil, formatter.Options{}) + + err = srv.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ + TextDocument: protocol.TextDocumentItem{ + URI: fileURI, + LanguageID: "thrift", + Version: 0, + Text: content, + }, + }) + assert.NoError(t, err) + + formatting := func() string { + edits, err := srv.Formatting(ctx, &protocol.DocumentFormattingParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: fileURI}, + }) + assert.NoError(t, err) + assert.Len(t, edits, 1) + return edits[0].NewText + } + + t.Run("formats struct bounded by blank lines", func(t *testing.T) { + // Select struct B entirely (lines 2-4). + edits, err := srv.RangeFormatting(ctx, &protocol.DocumentRangeFormattingParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: fileURI}, + Range: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 0}, + End: protocol.Position{Line: 4, Character: 1}, + }, + }) + assert.NoError(t, err) + assert.Len(t, edits, 1) + + // The edit covers exactly struct B's lines and collapses it. + assert.Equal(t, protocol.Range{ + Start: protocol.Position{Line: 2, Character: 0}, + End: protocol.Position{Line: 4, Character: 1}, + }, edits[0].Range) + assert.Equal(t, "struct B { 2: i32 b }", edits[0].NewText) + + // Applying the edit reproduces the whole-document formatting. + want := formatting() + spliced := content[:strings.Index(content, "struct B")] + edits[0].NewText + content[strings.Index(content, "}\n\nstruct C")+1:] + assert.Equal(t, want, spliced) + }) + + t.Run("whole document range delegates to full formatting", func(t *testing.T) { + edits, err := srv.RangeFormatting(ctx, &protocol.DocumentRangeFormattingParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: fileURI}, + Range: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 7, Character: 0}, + }, + }) + assert.NoError(t, err) + assert.Len(t, edits, 1) + assert.Equal(t, formatting(), edits[0].NewText) + }) + + t.Run("no edits for a selection that cuts a declaration", func(t *testing.T) { + // Start inside struct B's opening line, end inside its field line: + // the expanded range is not bounded by blank lines. + edits, err := srv.RangeFormatting(ctx, &protocol.DocumentRangeFormattingParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: fileURI}, + Range: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 5}, + End: protocol.Position{Line: 3, Character: 4}, + }, + }) + assert.NoError(t, err) + assert.Empty(t, edits) + }) +} diff --git a/lsp/format_range_test.go b/lsp/format_range_test.go new file mode 100644 index 0000000..eb62c6f --- /dev/null +++ b/lsp/format_range_test.go @@ -0,0 +1,163 @@ +package lsp + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/karitham/thrift-ls/formatter" + "github.com/karitham/thrift-ls/syntax" +) + +func TestFormatRangeText(t *testing.T) { + opts := formatter.Options{} + + tests := []struct { + name string + content string + sel func(content string) (rs, re int) + want string + wantOK bool + }{ + { + // selection inside the struct B line: expands to the whole line, + // bounded by the blank lines around struct B. + name: "formats struct bounded by blank lines", + content: "struct A {\n1: string a\n}\n\nstruct B { 1: i32 b }", + sel: func(c string) (int, int) { + return strings.Index(c, " 1:") + 1, strings.Index(c, "i32") + 1 + }, + want: "struct B { 1: i32 b }", + wantOK: true, + }, + { + // selection covering both declarations, bounded by blank lines. + name: "selection of two structs formats both", + content: "\nstruct A {\n1: string a\n}\n\nstruct B {\n2: i32 b\n}\n\nstruct C {}", + sel: func(c string) (int, int) { + return strings.Index(c, "struct A"), strings.Index(c, "struct C") - 2 + }, + want: "struct A { 1: string a }\n\nstruct B { 2: i32 b }", + wantOK: true, + }, + { + // selection starts on a blank line before struct B. + name: "trims leading blank lines from selection", + content: "struct A {}\n\n\n\nstruct B {\n1: string b\n}", + sel: func(c string) (int, int) { + return strings.Index(c, "struct B"), len(c) + }, + want: "struct B { 1: string b }", + wantOK: true, + }, + { + // selection ends on a blank line after struct B. + name: "trims trailing blank lines from selection", + content: "struct A {}\n\nstruct B {\n1: string b\n}\n\n\nstruct C {}", + sel: func(c string) (int, int) { + return strings.Index(c, "struct B"), strings.Index(c, "struct C") - 2 + }, + want: "struct B { 1: string b }", + wantOK: true, + }, + { + // selection cuts struct B in half: the expanded slice does not + // parse, so formatting is refused. + name: "refuses when selection cuts a declaration", + content: "struct A {}\n\nstruct B {\n1: string b\n}\n\nstruct C {}", + sel: func(c string) (int, int) { + return strings.Index(c, "struct B") + 6, strings.Index(c, "1: string b") + 4 + }, + wantOK: false, + }, + { + // the line right after the range is not blank. + name: "refuses when next line is not blank", + content: "struct A {\n1: string a\n}\nstruct B {}", + sel: func(c string) (int, int) { + return 0, strings.Index(c, "struct B") - 1 + }, + wantOK: false, + }, + { + // slice is "struct B {\n1: string b" — unclosed, does not parse. + name: "refuses when slice does not parse", + content: "\nstruct B {\n1: string b\n\nstruct C {}", + sel: func(c string) (int, int) { + return strings.Index(c, "struct B"), strings.Index(c, "1: string b") + 3 + }, + wantOK: false, + }, + { + name: "refuses whitespace-only selection", + content: "struct A {}\n\n\nstruct B {}", + sel: func(c string) (int, int) { + return strings.Index(c, "struct B") - 2, strings.Index(c, "struct B") - 1 + }, + wantOK: false, + }, + { + name: "refuses empty selection", + content: "struct A {}", + sel: func(c string) (int, int) { return 5, 5 }, + wantOK: false, + }, + { + name: "handles CRLF line endings", + content: "struct A {}\r\n\r\nstruct B {\r\n1: string b\r\n}\r\n\r\nstruct C {}", + sel: func(c string) (int, int) { + return strings.Index(c, "struct B") + 3, strings.Index(c, "struct C") - 4 + }, + want: "struct B { 1: string b }", + wantOK: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rs, re := tt.sel(tt.content) + got, _, _, ok := formatRangeText([]byte(tt.content), rs, re, opts) + assert.Equal(t, tt.wantOK, ok) + assert.Equal(t, tt.want, got) + }) + } +} + +// TestFormatRangeTextSplice verifies that applying the formatted slice to the +// original content reproduces a whole-document formatting. +func TestFormatRangeTextSplice(t *testing.T) { + opts := formatter.Options{} + content := "struct A { 1: string a }\n\nstruct B { 1: i32 b }\n\nstruct C { 3: i64 c }\n" + + rs := strings.Index(content, "struct B") + re := rs + len("struct B { ") // inside struct B's line + + newText, gotRS, gotRE, ok := formatRangeText([]byte(content), rs, re, opts) + if !ok { + t.Fatalf("formatRangeText refused a valid range") + } + + // The returned range must cover the full struct B line, not the raw + // mid-line selection: it starts at the line start and ends at the + // line's newline. + if gotRS != strings.Index(content, "struct B") { + t.Errorf("effective start = %d, want %d", gotRS, strings.Index(content, "struct B")) + } + if got := content[gotRE]; got != '\n' { + t.Errorf("effective end = %d, want a newline position (got %q)", gotRE, got) + } + + spliced := content[:gotRS] + newText + content[gotRE:] + + doc, errs := syntax.Parse([]byte(content)) + if len(errs) > 0 { + t.Fatalf("parse errors: %v", errs) + } + want, err := formatter.Format(doc, opts) + if err != nil { + t.Fatalf("whole-doc format: %v", err) + } + if spliced != want { + t.Errorf("splice mismatch\n got: %q\nwant: %q", spliced, want) + } +} diff --git a/lsp/hover.go b/lsp/hover.go index 97cc26a..1fc7888 100644 --- a/lsp/hover.go +++ b/lsp/hover.go @@ -4,8 +4,9 @@ import ( "context" "strings" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/codejump" + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/lsp/codejump" ) func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { @@ -36,8 +37,8 @@ func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*prot } return &protocol.Hover{ - Contents: protocol.MarkupContent{ - Kind: protocol.Markdown, + Contents: &protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, Value: markdown_prefix + content + markdown_suffix, }, }, nil diff --git a/lsp/impl.go b/lsp/impl.go index 74dddd2..b8eade0 100644 --- a/lsp/impl.go +++ b/lsp/impl.go @@ -3,15 +3,16 @@ package lsp import ( "context" "fmt" + "log/slog" "path" "strings" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/completion" - "github.com/joyme123/thrift-ls/lsp/types" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/completion" + "github.com/karitham/thrift-ls/lsp/types" ) func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { @@ -50,8 +51,8 @@ func (s *Server) openFile(ctx context.Context, change *cache.FileChange) error { if _, err := s.session.ViewOf(change.URI); err != nil { // create view for this folder - filename := change.URI.Filename() - dir := uri.New(path.Dir(filename)) + filename := change.URI.Path() + dir := uri.File(path.Dir(filename)) s.session.CreateView(dir) } @@ -61,7 +62,7 @@ func (s *Server) openFile(ctx context.Context, change *cache.FileChange) error { defer release() err := s.diagnostic(ctx, ss, change) if err != nil { - log.Errorf("diagnostic error: %v", err) + slog.Error("diagnostic error", "err", err) } }) @@ -87,7 +88,7 @@ func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDo for i := range changes { err := s.diagnostic(ctx, ss, changes[i]) if err != nil { - log.Error("diagnostic error", err) + slog.Error("diagnostic error", "err", err) } } }) @@ -124,18 +125,18 @@ func toLspCompletionList(items []*completion.CompletionItem, rng protocol.Range) for i := range items { item := protocol.CompletionItem{ Label: items[i].Label, - Detail: items[i].Detail, + Detail: protocol.NewOptional(items[i].Detail), Kind: items[i].Kind, TextEdit: &protocol.TextEdit{ NewText: items[i].InsertText, Range: rng, }, - FilterText: strings.TrimLeft(items[i].Label, "&*"), + FilterText: protocol.NewOptional(strings.TrimLeft(items[i].Label, "&*")), InsertTextFormat: items[i].InsertTextFormat, - SortText: fmt.Sprintf("%05d", i), - Preselect: i == 0, - Deprecated: items[i].Deprecated, - Documentation: items[i].Documentation, + SortText: protocol.NewOptional(fmt.Sprintf("%05d", i)), + Preselect: protocol.NewOptional(i == 0), + Deprecated: protocol.NewOptional(items[i].Deprecated), + Documentation: protocol.String(items[i].Documentation), } list.Items = append(list.Items, item) } @@ -146,7 +147,7 @@ func (s *Server) getFileContext(ctx context.Context, uri uri.URI) (ss *cache.Sna var view *cache.View view, err = s.session.ViewOf(uri) if err != nil { - return + return ss, release, fh, err } ss, release = view.Snapshot() @@ -154,8 +155,8 @@ func (s *Server) getFileContext(ctx context.Context, uri uri.URI) (ss *cache.Sna fh, err = ss.ReadFile(ctx, uri) if err != nil { release() - return + return ss, release, fh, err } - return + return ss, release, fh, err } diff --git a/lsp/impl_test.go b/lsp/impl_test.go index f031e92..5c38a63 100644 --- a/lsp/impl_test.go +++ b/lsp/impl_test.go @@ -1,19 +1,19 @@ package lsp import ( - "context" "testing" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/format" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/memoize" "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/formatter" + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/memoize" ) func Test_DidOpen(t *testing.T) { - ctx := context.TODO() + ctx := t.Context() fileURI, err := uri.Parse("file:///tmp/file.thrift") assert.NoError(t, err) fileContent := ` @@ -34,7 +34,7 @@ struct Test { store := &memoize.Store{} cache := cache.New(store, nil) - srv := NewServer(cache, nil, format.Options{}) + srv := NewServer(cache, nil, formatter.Options{}) err = srv.DidOpen(ctx, params) assert.NoError(t, err) @@ -49,7 +49,7 @@ struct Test { } func Test_DidChange(t *testing.T) { - ctx := context.TODO() + ctx := t.Context() fileURI, err := uri.Parse("file:///tmp/file.thrift") assert.NoError(t, err) fileContentInit := ` @@ -84,7 +84,7 @@ struct Test { Version: 1, }, ContentChanges: []protocol.TextDocumentContentChangeEvent{ - { + &protocol.TextDocumentContentChangeWholeDocument{ Text: fileContent, }, }, @@ -92,7 +92,7 @@ struct Test { store := &memoize.Store{} cache := cache.New(store, nil) - srv := NewServer(cache, nil, format.Options{}) + srv := NewServer(cache, nil, formatter.Options{}) err = srv.DidOpen(ctx, openParams) assert.NoError(t, err) @@ -109,7 +109,7 @@ struct Test { } func Test_Completion(t *testing.T) { - ctx := context.TODO() + ctx := t.Context() for _, tt := range []struct { name string @@ -155,7 +155,7 @@ struct Test { store := &memoize.Store{} cache := cache.New(store, nil) - srv := NewServer(cache, nil, format.Options{}) + srv := NewServer(cache, nil, formatter.Options{}) err = srv.DidOpen(ctx, openParams) assert.NoError(t, err) @@ -170,23 +170,26 @@ struct Test { }, }, WorkDoneProgressParams: protocol.WorkDoneProgressParams{ - WorkDoneToken: &protocol.ProgressToken{}, + WorkDoneToken: protocol.String(""), }, PartialResultParams: protocol.PartialResultParams{ - PartialResultToken: &protocol.ProgressToken{}, + PartialResultToken: protocol.String(""), }, - Context: &protocol.CompletionContext{ + Context: protocol.CompletionContext{ TriggerKind: protocol.CompletionTriggerKindInvoked, }, } - completionList, err := srv.Completion(ctx, completionParams) + completionResult, err := srv.Completion(ctx, completionParams) assert.NoError(t, err) + assert.IsType(t, &protocol.CompletionList{}, completionResult) + completionList := completionResult.(*protocol.CompletionList) assert.True(t, len(completionList.Items) > 0) assert.True(t, len(completionList.Items) <= 10) assert.Equal(t, tt.wantLabel, completionList.Items[0].Label) - assert.Equal(t, tt.wantPreselect, completionList.Items[0].Preselect) + preselect, _ := completionList.Items[0].Preselect.Get() + assert.Equal(t, tt.wantPreselect, preselect) textEdit, ok := completionList.Items[0].TextEdit.(*protocol.TextEdit) assert.True(t, ok) @@ -200,7 +203,7 @@ struct Test { } func Test_CompletionIncludeScope(t *testing.T) { - ctx := context.TODO() + ctx := t.Context() for _, tt := range []struct { name string @@ -250,7 +253,7 @@ struct Test { store := &memoize.Store{} cache := cache.New(store, []string{"/tmp"}) - srv := NewServer(cache, nil, format.Options{}) + srv := NewServer(cache, nil, formatter.Options{}) err = srv.DidOpen(ctx, baseParams) assert.NoError(t, err) @@ -268,19 +271,21 @@ struct Test { }, }, WorkDoneProgressParams: protocol.WorkDoneProgressParams{ - WorkDoneToken: &protocol.ProgressToken{}, + WorkDoneToken: protocol.String(""), }, PartialResultParams: protocol.PartialResultParams{ - PartialResultToken: &protocol.ProgressToken{}, + PartialResultToken: protocol.String(""), }, - Context: &protocol.CompletionContext{ + Context: protocol.CompletionContext{ TriggerKind: protocol.CompletionTriggerKindInvoked, }, } - completionList, err := srv.Completion(ctx, completionParams) + completionResult, err := srv.Completion(ctx, completionParams) assert.NoError(t, err) + assert.IsType(t, &protocol.CompletionList{}, completionResult) + completionList := completionResult.(*protocol.CompletionList) labels := make([]string, len(completionList.Items)) for i, item := range completionList.Items { labels[i] = item.Label @@ -294,7 +299,7 @@ struct Test { } func Test_CompletionNoGlobalPollution(t *testing.T) { - ctx := context.TODO() + ctx := t.Context() for _, tt := range []struct { name string @@ -354,7 +359,7 @@ struct Other { store := &memoize.Store{} cache := cache.New(store, nil) - srv := NewServer(cache, nil, format.Options{}) + srv := NewServer(cache, nil, formatter.Options{}) err = srv.DidOpen(ctx, file1Params) assert.NoError(t, err) @@ -375,19 +380,21 @@ struct Other { }, }, WorkDoneProgressParams: protocol.WorkDoneProgressParams{ - WorkDoneToken: &protocol.ProgressToken{}, + WorkDoneToken: protocol.String(""), }, PartialResultParams: protocol.PartialResultParams{ - PartialResultToken: &protocol.ProgressToken{}, + PartialResultToken: protocol.String(""), }, - Context: &protocol.CompletionContext{ + Context: protocol.CompletionContext{ TriggerKind: protocol.CompletionTriggerKindInvoked, }, } - completionList, err := srv.Completion(ctx, completionParams) + completionResult, err := srv.Completion(ctx, completionParams) assert.NoError(t, err) + assert.IsType(t, &protocol.CompletionList{}, completionResult) + completionList := completionResult.(*protocol.CompletionList) labels := make([]string, len(completionList.Items)) for i, item := range completionList.Items { labels[i] = item.Label diff --git a/lsp/include_paths_test.go b/lsp/include_paths_test.go new file mode 100644 index 0000000..128bab1 --- /dev/null +++ b/lsp/include_paths_test.go @@ -0,0 +1,43 @@ +package lsp + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/formatter" + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/memoize" +) + +// TestServerIncludePathsFlow verifies that include paths configured on the +// server's cache flow through the session, view, and snapshot into the +// resolver. +func TestServerIncludePathsFlow(t *testing.T) { + dir := t.TempDir() + includeDir := filepath.Join(dir, "base") + assert.NoError(t, os.MkdirAll(includeDir, 0o755)) + shared := filepath.Join(includeDir, "shared.thrift") + assert.NoError(t, os.WriteFile(shared, []byte("struct Shared {}"), 0o644)) + + c := cache.New(&memoize.Store{}, []string{includeDir}) + srv := NewServer(c, nil, formatter.DefaultOptions()) + + // Views are created per workspace folder at initialization. + srv.session.CreateView(uri.File(dir)) + view, err := srv.session.ViewOf(uri.File(filepath.Join(dir, "app.thrift"))) + assert.NoError(t, err) + snapshot, release := view.Snapshot() + defer release() + + // Resolving an include that only exists in the configured include path + // finds it there. + resolved := snapshot.Resolver().ResolveInclude(uri.File(filepath.Join(dir, "app.thrift")), "shared.thrift") + assert.Equal(t, uri.File(shared), resolved) + + // The snapshot exposes the configured paths. + assert.Equal(t, []string{includeDir}, snapshot.Resolver().IncludePaths()) +} diff --git a/lsp/initialize.go b/lsp/initialize.go index 502b9a3..300e070 100644 --- a/lsp/initialize.go +++ b/lsp/initialize.go @@ -3,31 +3,42 @@ package lsp import ( "context" "io/fs" + "log/slog" "path/filepath" "strings" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" ) func (s *Server) initialize(ctx context.Context, params *protocol.InitializeParams) (result *protocol.InitializeResult, err error) { - rootURI := params.RootURI - if rootURI == "" { - rootURI = uri.URI(params.RootPath) - } - - folders := make([]uri.URI, 0) - if rootURI != "" { - folders = append(folders, rootURI) + // Prefer WorkspaceFolders; fall back to the deprecated RootURI/RootPath + // fields for older clients. + folders := make([]uri.URI, 0, 1) + if wsf, ok := params.WorkspaceFolders.Get(); ok { + folders = make([]uri.URI, 0, len(wsf)) + for _, ws := range wsf { + folders = append(folders, ws.URI) + } } - - for _, ws := range params.WorkspaceFolders { - folders = append(folders, uri.URI(ws.URI)) + if len(folders) == 0 { + //nolint:staticcheck // intentional handling of legacy client params + rootURI := params.RootURI + if rootURI == nil { + //nolint:staticcheck // intentional handling of legacy client params + if rootPath, ok := params.RootPath.Get(); ok { + r := uri.URI(rootPath) + rootURI = &r + } + } + if rootURI != nil { + folders = append(folders, *rootURI) + } } - log.Debugln("initialized folders: ", folders) + slog.Debug("initialized folders", "folders", folders) if len(folders) > 0 { s.session.Initialize(func() { for i := range folders { @@ -40,10 +51,10 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara } func (s *Server) walkFoldersThriftFile(folder uri.URI) { - log.Debugln("walk dir 2: ", folder.Filename()) + slog.Debug("walk dir", "folder", folder.Path()) // WalkDir walk files with lexical order - filepath.WalkDir(folder.Filename(), func(path string, d fs.DirEntry, err error) error { - log.Debugln("walk:", path) + _ = filepath.WalkDir(folder.Path(), func(path string, d fs.DirEntry, err error) error { + slog.Debug("walking", "path", path) if err != nil { return nil } @@ -57,16 +68,14 @@ func (s *Server) walkFoldersThriftFile(folder uri.URI) { } fileURI := uri.File(path) - log.Debugln("file path:", fileURI) - err = s.openFile(context.TODO(), &cache.FileChange{ + slog.Debug("file path", "uri", fileURI) + if err := s.openFile(context.TODO(), &cache.FileChange{ URI: fileURI, Version: 0, Content: []byte{}, From: cache.FileChangeTypeInitialize, - }) - if err != nil { - log.Error("openFile err:", err) - err = nil + }); err != nil { + slog.Error("openFile failed", "err", err) } // always return nil to continue parse @@ -75,20 +84,23 @@ func (s *Server) walkFoldersThriftFile(folder uri.URI) { } func initializeResult() *protocol.InitializeResult { + thriftSelector := &protocol.DocumentSelector{ + &protocol.TextDocumentFilterLanguage{Language: "thrift"}, + } res := &protocol.InitializeResult{ Capabilities: protocol.ServerCapabilities{ TextDocumentSync: &protocol.TextDocumentSyncOptions{ - OpenClose: true, + OpenClose: new(true), // full is easy to implement. consider to use incremental for performance - Change: protocol.TextDocumentSyncKindFull, - WillSave: true, - WillSaveWaitUntil: true, + Change: new(protocol.TextDocumentSyncKindFull), + WillSave: new(true), + WillSaveWaitUntil: new(true), Save: &protocol.SaveOptions{ - IncludeText: true, + IncludeText: new(true), }, }, CompletionProvider: &protocol.CompletionOptions{ - ResolveProvider: false, + ResolveProvider: new(false), /** * The additional characters, beyond the defaults provided by the client (typically * [a-zA-Z]), that should automatically trigger a completion request. For example @@ -106,7 +118,7 @@ func initializeResult() *protocol.InitializeResult { }, HoverProvider: &protocol.HoverOptions{ WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{ - WorkDoneProgress: true, + WorkDoneProgress: new(true), }, }, SignatureHelpProvider: &protocol.SignatureHelpOptions{ @@ -116,136 +128,124 @@ func initializeResult() *protocol.InitializeResult { DeclarationProvider: &protocol.DeclarationRegistrationOptions{ DeclarationOptions: protocol.DeclarationOptions{ WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{ - WorkDoneProgress: true, + WorkDoneProgress: new(true), }, }, TextDocumentRegistrationOptions: protocol.TextDocumentRegistrationOptions{ - DocumentSelector: []*protocol.DocumentFilter{ - { - Language: "thrift", - }, - }, + DocumentSelector: thriftSelector, }, StaticRegistrationOptions: protocol.StaticRegistrationOptions{ - ID: "thriftls", + ID: new("thriftls"), }, }, DefinitionProvider: &protocol.DefinitionOptions{ WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{ - WorkDoneProgress: true, + WorkDoneProgress: new(true), }, }, TypeDefinitionProvider: &protocol.TypeDefinitionRegistrationOptions{ TextDocumentRegistrationOptions: protocol.TextDocumentRegistrationOptions{ - DocumentSelector: []*protocol.DocumentFilter{ - { - Language: "thrift", - }, - }, + DocumentSelector: thriftSelector, }, TypeDefinitionOptions: protocol.TypeDefinitionOptions{ WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{ - WorkDoneProgress: true, + WorkDoneProgress: new(true), }, }, StaticRegistrationOptions: protocol.StaticRegistrationOptions{ - ID: "thriftls", + ID: new("thriftls"), }, }, ReferencesProvider: &protocol.ReferenceOptions{ WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{ - WorkDoneProgress: true, + WorkDoneProgress: new(true), }, }, - DocumentHighlightProvider: false, + DocumentHighlightProvider: protocol.Boolean(false), DocumentSymbolProvider: &protocol.DocumentSymbolOptions{ WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{ - WorkDoneProgress: true, + WorkDoneProgress: new(true), }, - Label: "thriftls", + Label: new("thriftls"), }, CodeActionProvider: &protocol.CodeActionOptions{ // TODO(jpf): should support code actions CodeActionKinds: []protocol.CodeActionKind{}, - ResolveProvider: false, + ResolveProvider: new(false), }, CodeLensProvider: &protocol.CodeLensOptions{ - ResolveProvider: false, + ResolveProvider: new(false), }, DocumentLinkProvider: &protocol.DocumentLinkOptions{ - ResolveProvider: false, + ResolveProvider: new(false), }, - ColorProvider: false, + ColorProvider: protocol.Boolean(false), WorkspaceSymbolProvider: &protocol.WorkspaceSymbolOptions{ WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{ - WorkDoneProgress: true, + WorkDoneProgress: new(true), }, }, DocumentFormattingProvider: &protocol.DocumentFormattingOptions{ WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{ - WorkDoneProgress: true, + WorkDoneProgress: new(true), }, }, DocumentRangeFormattingProvider: &protocol.DocumentRangeFormattingOptions{ WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{ - WorkDoneProgress: true, + WorkDoneProgress: new(true), }, }, - DocumentOnTypeFormattingProvider: &protocol.DocumentOnTypeFormattingOptions{ + DocumentOnTypeFormattingProvider: protocol.DocumentOnTypeFormattingOptions{ FirstTriggerCharacter: "}", MoreTriggerCharacter: []string{}, }, RenameProvider: &protocol.RenameOptions{ - PrepareProvider: false, + PrepareProvider: new(false), }, - ExecuteCommandProvider: &protocol.ExecuteCommandOptions{ + ExecuteCommandProvider: protocol.ExecuteCommandOptions{ Commands: []string{}, }, - CallHierarchyProvider: false, - LinkedEditingRangeProvider: false, + CallHierarchyProvider: protocol.Boolean(false), + LinkedEditingRangeProvider: protocol.Boolean(false), SemanticTokensProvider: &protocol.SemanticTokensRegistrationOptions{ TextDocumentRegistrationOptions: protocol.TextDocumentRegistrationOptions{ - DocumentSelector: []*protocol.DocumentFilter{ - { - Language: "thrift", - }, - }, + DocumentSelector: thriftSelector, }, SemanticTokensOptions: protocol.SemanticTokensOptions{ WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{ - WorkDoneProgress: true, + WorkDoneProgress: new(true), }, Legend: protocol.SemanticTokensLegend{ - TokenTypes: []protocol.SemanticTokenTypes{}, - TokenModifiers: []protocol.SemanticTokenModifiers{}, + TokenTypes: []string{}, + TokenModifiers: []string{}, }, }, StaticRegistrationOptions: protocol.StaticRegistrationOptions{ - ID: "thriftls", + ID: new("thriftls"), }, }, - Workspace: &protocol.ServerCapabilitiesWorkspace{ - WorkspaceFolders: &protocol.ServerCapabilitiesWorkspaceFolders{ - Supported: true, - ChangeNotifications: true, + Workspace: &protocol.WorkspaceOptions{ + WorkspaceFolders: &protocol.WorkspaceFoldersServerCapabilities{ + Supported: new(true), + ChangeNotifications: protocol.Boolean(true), }, - FileOperations: &protocol.ServerCapabilitiesWorkspaceFileOperations{ - DidCreate: &protocol.FileOperationRegistrationOptions{ + FileOperations: &protocol.FileOperationOptions{ + DidCreate: protocol.FileOperationRegistrationOptions{ Filters: []protocol.FileOperationFilter{}, }, - WillCreate: &protocol.FileOperationRegistrationOptions{ + WillCreate: protocol.FileOperationRegistrationOptions{ Filters: []protocol.FileOperationFilter{}, }, - DidRename: &protocol.FileOperationRegistrationOptions{ + DidRename: protocol.FileOperationRegistrationOptions{ Filters: []protocol.FileOperationFilter{}, }, - WillRename: &protocol.FileOperationRegistrationOptions{ + WillRename: protocol.FileOperationRegistrationOptions{ Filters: []protocol.FileOperationFilter{}, }, - DidDelete: &protocol.FileOperationRegistrationOptions{ + DidDelete: protocol.FileOperationRegistrationOptions{ Filters: []protocol.FileOperationFilter{}, }, - WillDelete: &protocol.FileOperationRegistrationOptions{ + WillDelete: protocol.FileOperationRegistrationOptions{ Filters: []protocol.FileOperationFilter{}, }, }, @@ -253,9 +253,9 @@ func initializeResult() *protocol.InitializeResult { MonikerProvider: nil, Experimental: nil, }, - ServerInfo: &protocol.ServerInfo{ + ServerInfo: protocol.ServerInfo{ Name: ServerName, - Version: ServerVersion, + Version: protocol.NewOptional(ServerVersion), }, } diff --git a/lsp/lsputils/utils.go b/lsp/lsputils/utils.go index 6a5e0c6..e48b4f7 100644 --- a/lsp/lsputils/utils.go +++ b/lsp/lsputils/utils.go @@ -6,28 +6,15 @@ import ( "sort" "strings" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/parser" "go.lsp.dev/uri" -) -func ASTNodeToRange(node parser.Node) protocol.Range { - return protocol.Range{ - Start: protocol.Position{ - Line: uint32(node.Pos().Line - 1), - Character: uint32(node.Pos().Col - 1), - }, - End: protocol.Position{ - Line: uint32(node.End().Line - 1), - Character: uint32(node.End().Col - 1), - }, - } -} + "github.com/karitham/thrift-ls/syntax" +) // GetIncludeName return include name by file uri // for example: file uri is file:///base.thrift, then `base` is include name func GetIncludeName(file uri.URI) string { - fileName := file.Filename() + fileName := file.Path() index := strings.LastIndexByte(fileName, filepath.Separator) if index == -1 { return fileName @@ -41,21 +28,31 @@ func GetIncludeName(file uri.URI) string { return string(fileName[0:index]) } +// IncludePathText returns the include path of an include node without its +// quotes. The syntax token keeps the raw literal text, including quotes. +func IncludePathText(inc *syntax.Include) string { + if inc == nil || inc.Path == nil { + return "" + } + return strings.Trim(inc.Path.Text, "\"'") +} + // includeName: base.User. `base` is the includeName. returns ../../base.thrift // if doesn't match, return empty string -func GetIncludePath(ast *parser.Document, includeName string) string { - for _, include := range ast.Includes { - if include.BadNode || include.Path == nil || include.Path.BadNode || include.Path.Value == nil { +func GetIncludePath(ast *syntax.Document, includeName string) string { + for _, include := range ast.Includes() { + path := IncludePathText(include) + if path == "" { continue } - items := strings.Split(include.Path.Value.Text, "/") - path := items[len(items)-1] + items := strings.Split(path, "/") + path = items[len(items)-1] if !strings.HasSuffix(path, ".thrift") { continue } name := strings.TrimSuffix(path, ".thrift") if name == includeName { - return include.Path.Value.Text + return IncludePathText(include) } } @@ -65,7 +62,7 @@ func GetIncludePath(ast *parser.Document, includeName string) string { // cur is current file uri. for example file:///tmp/user.thrift // includePath is include name used in code. for example: base.thrift func IncludeURI(cur uri.URI, includePath string) uri.URI { - filePath := cur.Filename() + filePath := cur.Path() items := strings.Split(filePath, string(filepath.Separator)) basePath := strings.TrimSuffix(filePath, items[len(items)-1]) @@ -77,7 +74,7 @@ func IncludeURI(cur uri.URI, includePath string) uri.URI { // IncludeURIWithPaths resolves include path, first trying relative to current file, // then trying each include path func IncludeURIWithPaths(cur uri.URI, includePath string, includePaths []string) uri.URI { - filePath := cur.Filename() + filePath := cur.Path() items := strings.Split(filePath, string(filepath.Separator)) basePath := strings.TrimSuffix(filePath, items[len(items)-1]) path := filepath.Join(basePath, includePath) @@ -105,7 +102,7 @@ func IncludeURIWithPaths(cur uri.URI, includePath string, includePaths []string) // 2. include.identifier // // it returns include, ident -func ParseIdent(cur uri.URI, includes []*parser.Include, identifier string) (include, ident string) { +func ParseIdent(cur uri.URI, includes []*syntax.Include, identifier string) (include, ident string) { includeNames := IncludeNames(cur, includes) // parse include from includeNames @@ -119,19 +116,19 @@ func ParseIdent(cur uri.URI, includes []*parser.Include, identifier string) (inc for _, incName := range includeNames { prefix := incName + "." - if strings.HasPrefix(identifier, prefix) { - return incName, strings.TrimPrefix(identifier, prefix) + if after, ok := strings.CutPrefix(identifier, prefix); ok { + return incName, after } } return "", identifier } -// IncludeNames returns include names from include ast nodes -func IncludeNames(cur uri.URI, includes []*parser.Include) (includeNames []string) { +// includeNames returns include names from include ast nodes +func IncludeNames(cur uri.URI, includes []*syntax.Include) (includeNames []string) { for _, inc := range includes { - if inc.Path != nil && inc.Path.Value != nil { - path := inc.Path.Value.Text + path := IncludePathText(inc) + if path != "" { u := IncludeURI(cur, path) includeName := GetIncludeName(u) includeNames = append(includeNames, includeName) diff --git a/lsp/lsputils/utils_test.go b/lsp/lsputils/utils_test.go index 1184a7d..0342e2b 100644 --- a/lsp/lsputils/utils_test.go +++ b/lsp/lsputils/utils_test.go @@ -5,9 +5,10 @@ import ( "path/filepath" "testing" - "github.com/joyme123/thrift-ls/parser" "github.com/stretchr/testify/assert" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/syntax" ) func Test_IncludeURI(t *testing.T) { @@ -39,10 +40,10 @@ func Test_IncludeURI(t *testing.T) { { name: "case3", args: args{ - cur: uri.URI("file:///c:/Users/Administrator/Downloads/galaxy-thrift-api-master/galaxy-thrift-api-master/sds/Common.thrift"), + cur: uri.URI("file:///c:/Users/Administrator/Downloads/whitebase-master/whitebase-master/zeon/Common.thrift"), includePath: "Errors.thrift", }, - want: uri.URI("file:///c:/Users/Administrator/Downloads/galaxy-thrift-api-master/galaxy-thrift-api-master/sds/Errors.thrift"), + want: uri.MustParse("file:///c%3A/Users/Administrator/Downloads/whitebase-master/whitebase-master/zeon/Errors.thrift"), }, { name: "case4", @@ -66,11 +67,15 @@ include "../../user.extra.thrift" service Demo { user.Test Api(1:user.Test2 arg1, 2:user.Test3 arg2) throws (1:user.Error1 err) }` - ast, err := parser.Parse("file:///test.thrift", []byte(file)) - assert.NoError(t, err) + ast, errs := syntax.Parse([]byte(file)) + for _, e := range errs { + if e.Severity == syntax.SeverityError { + t.Fatal(e) + } + } type args struct { - ast *parser.Document + ast *syntax.Document includeName string } tests := []struct { @@ -81,7 +86,7 @@ service Demo { { name: "case", args: args{ - ast: ast.(*parser.Document), + ast: ast, includeName: "user", }, want: "../../user.thrift", @@ -89,7 +94,7 @@ service Demo { { name: "case", args: args{ - ast: ast.(*parser.Document), + ast: ast, includeName: "user.extra", }, want: "../../user.extra.thrift", @@ -114,21 +119,21 @@ func TestGetIncludeName(t *testing.T) { { name: "file name", args: args{ - file: uri.New("base.thrift"), + file: uri.MustParse("base.thrift"), }, want: "base", }, { name: "file name with dir", args: args{ - file: uri.New("/tmp/base.thrift"), + file: uri.MustParse("/tmp/base.thrift"), }, want: "base", }, { name: "file name with .", args: args{ - file: uri.New("/tmp/base.subpath.thrift"), + file: uri.MustParse("/tmp/base.subpath.thrift"), }, want: "base.subpath", }, @@ -143,7 +148,7 @@ func TestGetIncludeName(t *testing.T) { func TestIncludeNames(t *testing.T) { type args struct { cur uri.URI - includes []*parser.Include + includes []*syntax.Include } tests := []struct { name string @@ -153,28 +158,16 @@ func TestIncludeNames(t *testing.T) { { name: "case 1", args: args{ - cur: uri.New("/tmp/app.thrift"), - includes: []*parser.Include{ + cur: uri.MustParse("/tmp/app.thrift"), + includes: []*syntax.Include{ { - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "../../base.sub.thrift", - }, - }, + Path: &syntax.Token{Text: "../../base.sub.thrift"}, }, { - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "user.sub.thrift", - }, - }, + Path: &syntax.Token{Text: "user.sub.thrift"}, }, { - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "app.thrift", - }, - }, + Path: &syntax.Token{Text: "app.thrift"}, }, }, }, @@ -202,23 +195,23 @@ func TestIncludeURIWithPaths(t *testing.T) { tmpDir, err := os.MkdirTemp("", "thrift-test") assert.NoError(t, err) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() baseDir := filepath.Join(tmpDir, "base") serviceDir := filepath.Join(tmpDir, "service") - err = os.MkdirAll(baseDir, 0755) + err = os.MkdirAll(baseDir, 0o755) assert.NoError(t, err) - err = os.MkdirAll(serviceDir, 0755) + err = os.MkdirAll(serviceDir, 0o755) assert.NoError(t, err) // Create shared.thrift in base/ sharedThrift := filepath.Join(baseDir, "shared.thrift") - err = os.WriteFile(sharedThrift, []byte(""), 0644) + err = os.WriteFile(sharedThrift, []byte(""), 0o644) assert.NoError(t, err) // Create order.thrift in service/ orderThrift := filepath.Join(serviceDir, "order.thrift") - err = os.WriteFile(orderThrift, []byte(""), 0644) + err = os.WriteFile(orderThrift, []byte(""), 0o644) assert.NoError(t, err) orderURI := uri.File(orderThrift) @@ -271,7 +264,7 @@ func TestIncludeURIWithPaths(t *testing.T) { func TestParseIdent(t *testing.T) { type args struct { cur uri.URI - includes []*parser.Include + includes []*syntax.Include identifier string } tests := []struct { @@ -283,21 +276,13 @@ func TestParseIdent(t *testing.T) { { name: "case 1", args: args{ - cur: uri.New("/tmp/app.thrift"), - includes: []*parser.Include{ + cur: uri.MustParse("/tmp/app.thrift"), + includes: []*syntax.Include{ { - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "user.sub.thrift", - }, - }, + Path: &syntax.Token{Text: "user.sub.thrift"}, }, { - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "user.thrift", - }, - }, + Path: &syntax.Token{Text: "user.thrift"}, }, }, identifier: "user.Name", @@ -308,21 +293,13 @@ func TestParseIdent(t *testing.T) { { name: "case 2", args: args{ - cur: uri.New("/tmp/app.thrift"), - includes: []*parser.Include{ + cur: uri.MustParse("/tmp/app.thrift"), + includes: []*syntax.Include{ { - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "user.sub.thrift", - }, - }, + Path: &syntax.Token{Text: "user.sub.thrift"}, }, { - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "user.thrift", - }, - }, + Path: &syntax.Token{Text: "user.thrift"}, }, }, identifier: "user.sub.Name", @@ -333,14 +310,10 @@ func TestParseIdent(t *testing.T) { { name: "case 3", args: args{ - cur: uri.New("/tmp/app.thrift"), - includes: []*parser.Include{ + cur: uri.MustParse("/tmp/app.thrift"), + includes: []*syntax.Include{ { - Path: &parser.Literal{ - Value: &parser.LiteralValue{ - Text: "user.thrift", - }, - }, + Path: &syntax.Token{Text: "user.thrift"}, }, }, identifier: "user.sub.Name", diff --git a/lsp/mapper/mapper.go b/lsp/mapper/mapper.go index c53ca3c..ea658fd 100644 --- a/lsp/mapper/mapper.go +++ b/lsp/mapper/mapper.go @@ -4,12 +4,14 @@ import ( "bytes" "errors" "fmt" + "sort" "sync" "unicode/utf8" - "github.com/joyme123/thrift-ls/lsp/types" - "github.com/joyme123/thrift-ls/parser" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/types" + "github.com/karitham/thrift-ls/syntax" ) type Mapper struct { @@ -57,19 +59,38 @@ func (m *Mapper) GetLSPEndPosition() types.Position { } } +// OffsetToLSPPosition converts a byte offset in the mapped content to an LSP +// position (0-based line, UTF-16 code-unit column). +func (m *Mapper) OffsetToLSPPosition(offset int) (types.Position, error) { + m.initLineStart() + if offset < 0 || offset > len(m.content) { + return types.Position{}, fmt.Errorf("invalid offset: %d, total content: %d", offset, len(m.content)) + } + + line := sort.Search(len(m.lineStart), func(i int) bool { return m.lineStart[i] > offset }) - 1 + if line < 0 { + line = 0 + } + + return types.Position{ + Line: uint32(line), + Character: uint32(utf16Count(m.content[m.lineStart[line]:offset])), + }, nil +} + // convert from utf16-based to rune-based position -func (m *Mapper) LSPPosToParserPosition(pos types.Position) (parser.Position, error) { +func (m *Mapper) LSPPosToParserPosition(pos types.Position) (syntax.Position, error) { m.initLineStart() line := int(pos.Line) + 1 if line > len(m.lineStart) { - return parser.InvalidPosition, fmt.Errorf("invalid position line, request line: %d, total line: %d", line, len(m.lineStart)) + return syntax.InvalidPosition, fmt.Errorf("invalid position line, request line: %d, total line: %d", line, len(m.lineStart)) } if !m.nonASCII { col := int(pos.Character) + 1 offset := m.lineStart[pos.Line] + int(pos.Character) if offset > len(m.content) { - return parser.InvalidPosition, fmt.Errorf("invalid position offset: %d, total content: %d, %s", offset, len(m.content), string(m.content)) + return syntax.InvalidPosition, fmt.Errorf("invalid position offset: %d, total content: %d, %s", offset, len(m.content), string(m.content)) } var lineLength int if int(pos.Line+1) >= len(m.lineStart) { @@ -79,10 +100,10 @@ func (m *Mapper) LSPPosToParserPosition(pos types.Position) (parser.Position, er } if col > lineLength+1 { // if line length is 0, col is 1 means col is at end of line - return parser.InvalidPosition, fmt.Errorf("invalid position column: %d, line length: %d, %s", col, lineLength, string(m.content)) + return syntax.InvalidPosition, fmt.Errorf("invalid position column: %d, line length: %d, %s", col, lineLength, string(m.content)) } - return parser.Position{ + return syntax.Position{ Line: line, Col: col, Offset: offset, @@ -98,20 +119,21 @@ func (m *Mapper) LSPPosToParserPosition(pos types.Position) (parser.Position, er } lineBytes := m.content[lineStart:lineEnd] - utf16Col := -1 - bytesCol := -1 + utf16Col := 0 + bytesCol := 0 for len(lineBytes) > 0 { if utf16Col >= int(pos.Character) { break } - utf16Col++ if lineBytes[0] < utf8.RuneSelf { + utf16Col++ lineBytes = lineBytes[1:] bytesCol++ continue } r, size := utf8.DecodeRune(lineBytes) + utf16Col++ if r >= 0x10000 { utf16Col++ } @@ -119,21 +141,21 @@ func (m *Mapper) LSPPosToParserPosition(pos types.Position) (parser.Position, er bytesCol += size } - runeLen := utf8.RuneCount(m.content[lineStart : lineStart+bytesCol+1]) + runeLen := utf8.RuneCount(m.content[lineStart : lineStart+bytesCol]) offset := lineStart + bytesCol - if offset >= len(m.content) { - return parser.InvalidPosition, errors.New("invalid position character") + if offset > len(m.content) { + return syntax.InvalidPosition, errors.New("invalid position character") } /* if offset >= m.lineStart[pos.Line+1] { - return parser.InvalidPosition, errors.New("invalid position character") + return syntax.InvalidPosition, errors.New("invalid position character") } */ - return parser.Position{ + return syntax.Position{ Line: line, - Col: runeLen, + Col: runeLen + 1, Offset: lineStart + bytesCol, }, nil } diff --git a/lsp/mapper/mapper_fuzz_test.go b/lsp/mapper/mapper_fuzz_test.go new file mode 100644 index 0000000..deb2bab --- /dev/null +++ b/lsp/mapper/mapper_fuzz_test.go @@ -0,0 +1,52 @@ +package mapper + +import ( + "testing" + "unicode/utf8" + + "go.lsp.dev/uri" +) + +// FuzzOffsetRoundTrip checks that byte offsets round-trip through +// OffsetToLSPPosition and back to the same offset, for offsets aligned to +// rune boundaries (LSP positions are UTF-16 code-unit aligned, so byte +// offsets inside a multi-byte rune have no representable position). +func FuzzOffsetRoundTrip(f *testing.F) { + seeds := []string{ + "struct A {\n1: string a\n}", + "struct A { 1: string s } // café ☕", + "中文注释\nstruct A { 1: string x }", + "emoji 🎉 in comment\nstruct B {}", + "", + "\n", + "a\r\nb\r\nc", + } + for _, s := range seeds { + f.Add([]byte(s), 0) + } + + f.Fuzz(func(t *testing.T, content []byte, offset int) { + limit := len(content) + 1 + offset = offset % limit + if offset < 0 { + offset += limit + } + if offset < len(content) && !utf8.RuneStart(content[offset]) { + // Byte offset inside a multi-byte rune: not representable. + return + } + + m := NewMapper(uri.File("/tmp/fuzz.thrift"), content) + pos, err := m.OffsetToLSPPosition(offset) + if err != nil { + t.Fatalf("OffsetToLSPPosition(%d): %v", offset, err) + } + back, err := m.LSPPosToParserPosition(pos) + if err != nil { + t.Fatalf("LSPPosToParserPosition(%+v): %v (content %q)", pos, err, content) + } + if back.Offset != offset { + t.Fatalf("round trip mismatch: %d -> %+v -> %d (content %q)", offset, pos, back.Offset, content) + } + }) +} diff --git a/lsp/mapper/mapper_test.go b/lsp/mapper/mapper_test.go index 32cb5b9..2a22070 100644 --- a/lsp/mapper/mapper_test.go +++ b/lsp/mapper/mapper_test.go @@ -3,10 +3,11 @@ package mapper import ( "testing" - "github.com/joyme123/thrift-ls/lsp/types" - "github.com/joyme123/thrift-ls/parser" "github.com/stretchr/testify/assert" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/types" + "github.com/karitham/thrift-ls/syntax" ) func TestMapper_LSPPosToParserPosition(t *testing.T) { @@ -30,7 +31,7 @@ func TestMapper_LSPPosToParserPosition(t *testing.T) { name string fields fields args args - want parser.Position + want syntax.Position assertion assert.ErrorAssertionFunc }{ { @@ -45,7 +46,7 @@ func TestMapper_LSPPosToParserPosition(t *testing.T) { Character: 5, }, }, - want: parser.Position{ + want: syntax.Position{ Line: 2, Col: 6, Offset: 19, @@ -64,7 +65,7 @@ func TestMapper_LSPPosToParserPosition(t *testing.T) { Character: 5, }, }, - want: parser.InvalidPosition, + want: syntax.InvalidPosition, assertion: assert.Error, }, { @@ -79,7 +80,7 @@ func TestMapper_LSPPosToParserPosition(t *testing.T) { Character: 28, }, }, - want: parser.InvalidPosition, + want: syntax.InvalidPosition, assertion: assert.Error, }, { @@ -94,7 +95,7 @@ func TestMapper_LSPPosToParserPosition(t *testing.T) { Character: 1, }, }, - want: parser.Position{ + want: syntax.Position{ Line: 3, Col: 2, Offset: 42, @@ -113,7 +114,7 @@ func TestMapper_LSPPosToParserPosition(t *testing.T) { Character: 2, }, }, - want: parser.InvalidPosition, + want: syntax.InvalidPosition, assertion: assert.Error, }, { @@ -128,7 +129,7 @@ func TestMapper_LSPPosToParserPosition(t *testing.T) { Character: 12, }, }, - want: parser.Position{ + want: syntax.Position{ Line: 1, Col: 11, Offset: 16, @@ -147,7 +148,7 @@ func TestMapper_LSPPosToParserPosition(t *testing.T) { Character: 12, }, }, - want: parser.InvalidPosition, + want: syntax.InvalidPosition, assertion: assert.Error, }, { @@ -162,7 +163,7 @@ func TestMapper_LSPPosToParserPosition(t *testing.T) { Character: 15, }, }, - want: parser.InvalidPosition, + want: syntax.InvalidPosition, assertion: assert.Error, }, } diff --git a/lsp/memoize/promise.go b/lsp/memoize/promise.go index db11bfa..52378b3 100644 --- a/lsp/memoize/promise.go +++ b/lsp/memoize/promise.go @@ -22,7 +22,7 @@ import ( // The main purpose of the argument is to avoid the Function closure // needing to retain large objects (in practice: the snapshot) in // memory that can be supplied at call time by any caller. -type Function func(ctx context.Context, arg interface{}) interface{} +type Function func(ctx context.Context, arg any) any // A RefCounted is a value whose functional lifetime is determined by // reference counting. @@ -74,7 +74,7 @@ type Promise struct { // the function that will be used to populate the value function Function // value is set in completed state. - value interface{} + value any } // NewPromise returns a promise for the future result of calling the @@ -104,7 +104,7 @@ const ( // // It will never cause the value to be generated. // It will return the cached value, if present. -func (p *Promise) Cached() interface{} { +func (p *Promise) Cached() any { p.mu.Lock() defer p.mu.Unlock() if p.state == stateCompleted { @@ -124,7 +124,7 @@ func (p *Promise) Cached() interface{} { // If all concurrent calls to Get are cancelled, the context provided // to the function is cancelled. A later call to Get may attempt to // call the function again. -func (p *Promise) Get(ctx context.Context, arg interface{}) (interface{}, error) { +func (p *Promise) Get(ctx context.Context, arg any) (any, error) { if ctx.Err() != nil { return nil, ctx.Err() } @@ -143,7 +143,7 @@ func (p *Promise) Get(ctx context.Context, arg interface{}) (interface{}, error) } // run starts p.function and returns the result. p.mu must be locked. -func (p *Promise) run(ctx context.Context, arg interface{}) (interface{}, error) { +func (p *Promise) run(ctx context.Context, arg any) (any, error) { childCtx, cancel := context.WithCancel(xcontext.Detach(ctx)) p.cancel = cancel p.state = stateRunning @@ -190,7 +190,7 @@ func (p *Promise) run(ctx context.Context, arg interface{}) (interface{}, error) } // wait waits for the value to be computed, or ctx to be cancelled. p.mu must be locked. -func (p *Promise) wait(ctx context.Context) (interface{}, error) { +func (p *Promise) wait(ctx context.Context) (any, error) { p.waiters++ done := p.done p.mu.Unlock() diff --git a/lsp/memoize/store.go b/lsp/memoize/store.go index 57061c1..f56860b 100644 --- a/lsp/memoize/store.go +++ b/lsp/memoize/store.go @@ -22,7 +22,7 @@ type Store struct { evictionPolicy EvictionPolicy promisesMu sync.Mutex - promises map[interface{}]*Promise + promises map[any]*Promise } // Promise returns a reference-counted promise for the future result of @@ -35,13 +35,13 @@ type Store struct { // // Once the last reference has been released, the promise is removed from the // store. -func (store *Store) Promise(key interface{}, function Function) (*Promise, func()) { +func (store *Store) Promise(key any, function Function) (*Promise, func()) { store.promisesMu.Lock() p, ok := store.promises[key] if !ok { p = NewPromise(reflect.TypeOf(key).String(), function) if store.promises == nil { - store.promises = map[interface{}]*Promise{} + store.promises = map[any]*Promise{} } store.promises[key] = p } @@ -82,7 +82,7 @@ func (s *Store) Stats() map[reflect.Type]int { // DebugOnlyIterate iterates through the store and, for each completed // promise, calls f(k, v) for the map key k and function result v. It // should only be used for debugging purposes. -func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { +func (s *Store) DebugOnlyIterate(f func(k, v any)) { s.promisesMu.Lock() defer s.promisesMu.Unlock() diff --git a/lsp/rename.go b/lsp/rename.go index b55611d..01150de 100644 --- a/lsp/rename.go +++ b/lsp/rename.go @@ -3,8 +3,9 @@ package lsp import ( "context" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/codejump" + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/lsp/codejump" ) func (s *Server) prepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.Range, error) { diff --git a/lsp/server.go b/lsp/server.go index a199e2f..7a1ae11 100644 --- a/lsp/server.go +++ b/lsp/server.go @@ -2,11 +2,12 @@ package lsp import ( "context" + "log/slog" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/format" - "github.com/joyme123/thrift-ls/lsp/cache" - log "github.com/sirupsen/logrus" + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/formatter" + "github.com/karitham/thrift-ls/lsp/cache" ) type Server struct { @@ -14,10 +15,10 @@ type Server struct { session *cache.Session client protocol.Client - formatOpts format.Options + formatOpts formatter.Options } -func NewServer(c *cache.Cache, client protocol.Client, formatOpts format.Options) *Server { +func NewServer(c *cache.Cache, client protocol.Client, formatOpts formatter.Options) *Server { return &Server{ cache: c, session: cache.NewSession(c), @@ -27,8 +28,8 @@ func NewServer(c *cache.Cache, client protocol.Client, formatOpts format.Options } func (s *Server) Initialize(ctx context.Context, params *protocol.InitializeParams) (result *protocol.InitializeResult, err error) { - log.Debugln("------------Initialize called--------------") - defer log.Debugln("-----------Initialize finish--------------") + slog.Debug("Initialize called") + defer slog.Debug("Initialize finished") return s.initialize(ctx, params) } @@ -56,8 +57,8 @@ func (s *Server) SetTrace(ctx context.Context, params *protocol.SetTraceParams) return nil } -func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) (result []protocol.CodeAction, err error) { - return []protocol.CodeAction{}, nil +func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) (result []protocol.CommandOrCodeAction, err error) { + return []protocol.CommandOrCodeAction{}, nil } func (s *Server) CodeLens(ctx context.Context, params *protocol.CodeLensParams) (result []protocol.CodeLens, err error) { @@ -72,9 +73,9 @@ func (s *Server) ColorPresentation(ctx context.Context, params *protocol.ColorPr return []protocol.ColorPresentation{}, nil } -func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (result *protocol.CompletionList, err error) { - log.Debugln("------------Completion called--------------") - defer log.Debugln("-----------Completion finish--------------") +func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (result protocol.CompletionResult, err error) { + slog.Debug("Completion called") + defer slog.Debug("Completion finished") return s.completion(ctx, params) } @@ -82,19 +83,20 @@ func (s *Server) CompletionResolve(ctx context.Context, params *protocol.Complet return params, nil } -func (s *Server) Declaration(ctx context.Context, params *protocol.DeclarationParams) (result []protocol.Location, err error) { - return []protocol.Location{}, nil +func (s *Server) Declaration(ctx context.Context, params *protocol.DeclarationParams) (result protocol.DeclarationResult, err error) { + return protocol.LocationSlice{}, nil } -func (s *Server) Definition(ctx context.Context, params *protocol.DefinitionParams) (result []protocol.Location, err error) { - log.Debugln("-------------------Definition called-----------------") - defer log.Debugln("-------------------Definition finish-----------------") - return s.definition(ctx, params) +func (s *Server) Definition(ctx context.Context, params *protocol.DefinitionParams) (result protocol.DefinitionResult, err error) { + slog.Debug("Definition called") + defer slog.Debug("Definition finished") + res, err := s.definition(ctx, params) + return protocol.LocationSlice(res), err } func (s *Server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) (err error) { - log.Debugln("-----------DidChange called-----------") - defer log.Debugln("-----------DidChange finish-----------") + slog.Debug("DidChange called") + defer slog.Debug("DidChange finished") return s.didChange(ctx, params) } @@ -115,8 +117,8 @@ func (s *Server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocu } func (s *Server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) (err error) { - log.Debugln("-----------DidOpen called-----------") - defer log.Debugln("-----------DidOpen finish-----------") + slog.Debug("DidOpen called") + defer slog.Debug("DidOpen finished") return s.didOpen(ctx, params) } @@ -140,13 +142,13 @@ func (s *Server) DocumentLinkResolve(ctx context.Context, params *protocol.Docum return nil, nil } -func (s *Server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) (result []interface{}, err error) { - log.Debugln("-----------DocumentSymbol called-----------") - defer log.Debugln("-----------DocumentSymbol finish-----------") +func (s *Server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) (result protocol.DocumentSymbolResult, err error) { + slog.Debug("DocumentSymbol called") + defer slog.Debug("DocumentSymbol finished") return s.documentSymbol(ctx, params) } -func (s *Server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (result interface{}, err error) { +func (s *Server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (result protocol.LSPAny, err error) { return nil, nil } @@ -155,44 +157,44 @@ func (s *Server) FoldingRanges(ctx context.Context, params *protocol.FoldingRang } func (s *Server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) (result []protocol.TextEdit, err error) { - log.Debugln("-----------Formatting called-----------") - defer log.Debugln("-----------Formatting finish-----------") + slog.Debug("Formatting called") + defer slog.Debug("Formatting finished") return s.formatting(ctx, params) } func (s *Server) Hover(ctx context.Context, params *protocol.HoverParams) (result *protocol.Hover, err error) { - log.Debugln("------------hover called----------------") - defer log.Debugln("------------hover finish------------") + slog.Debug("hover called") + defer slog.Debug("hover finished") return s.hover(ctx, params) } -func (s *Server) Implementation(ctx context.Context, params *protocol.ImplementationParams) (result []protocol.Location, err error) { - return []protocol.Location{}, nil +func (s *Server) Implementation(ctx context.Context, params *protocol.ImplementationParams) (result protocol.DefinitionResult, err error) { + return protocol.LocationSlice{}, nil } func (s *Server) OnTypeFormatting(ctx context.Context, params *protocol.DocumentOnTypeFormattingParams) (result []protocol.TextEdit, err error) { return []protocol.TextEdit{}, nil } -func (s *Server) PrepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (result *protocol.Range, err error) { - log.Debugln("--------------------PrepareRename called----------------------") - defer log.Debugln("--------------------PrepareRename finish----------------------") +func (s *Server) PrepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (result protocol.PrepareRenameResult, err error) { + slog.Debug("PrepareRename called") + defer slog.Debug("PrepareRename finished") return s.prepareRename(ctx, params) } func (s *Server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) (result []protocol.TextEdit, err error) { - return []protocol.TextEdit{}, nil + return s.rangeFormatting(ctx, params) } func (s *Server) References(ctx context.Context, params *protocol.ReferenceParams) (result []protocol.Location, err error) { - log.Debugln("--------------------References called----------------------") - defer log.Debugln("--------------------References finish----------------------") + slog.Debug("References called") + defer slog.Debug("References finished") return s.references(ctx, params) } func (s *Server) Rename(ctx context.Context, params *protocol.RenameParams) (result *protocol.WorkspaceEdit, err error) { - log.Debugln("--------------------Rename called----------------------") - defer log.Debugln("--------------------Rename finish----------------------") + slog.Debug("Rename called") + defer slog.Debug("Rename finished") return s.rename(ctx, params) } @@ -200,14 +202,15 @@ func (s *Server) SignatureHelp(ctx context.Context, params *protocol.SignatureHe return nil, nil } -func (s *Server) Symbols(ctx context.Context, params *protocol.WorkspaceSymbolParams) (result []protocol.SymbolInformation, err error) { - return []protocol.SymbolInformation{}, nil +func (s *Server) Symbols(ctx context.Context, params *protocol.WorkspaceSymbolParams) (result protocol.WorkspaceSymbolResult, err error) { + return protocol.SymbolInformationSlice{}, nil } -func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) (result []protocol.Location, err error) { - log.Debugln("--------------------TypeDefinition called----------------------") - defer log.Debugln("--------------------TypeDefinition finish----------------------") - return s.typeDefinition(ctx, params) +func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) (result protocol.DefinitionResult, err error) { + slog.Debug("TypeDefinition called") + defer slog.Debug("TypeDefinition finished") + res, err := s.typeDefinition(ctx, params) + return protocol.LocationSlice(res), err } func (s *Server) WillSave(ctx context.Context, params *protocol.WillSaveTextDocumentParams) (err error) { @@ -266,7 +269,7 @@ func (s *Server) SemanticTokensFull(ctx context.Context, params *protocol.Semant return nil, nil } -func (s *Server) SemanticTokensFullDelta(ctx context.Context, params *protocol.SemanticTokensDeltaParams) (result interface{}, err error) { +func (s *Server) SemanticTokensFullDelta(ctx context.Context, params *protocol.SemanticTokensDeltaParams) (result protocol.SemanticTokensDeltaResult, err error) { return nil, nil } @@ -287,6 +290,85 @@ func (s *Server) Moniker(ctx context.Context, params *protocol.MonikerParams) (r } // Request handles all no standard request -func (s *Server) Request(ctx context.Context, method string, params interface{}) (result interface{}, err error) { +func (s *Server) Request(ctx context.Context, method string, params any) (result any, err error) { + return nil, nil +} + +// The following methods satisfy the full protocol.Server interface. thrift-ls +// does not implement these features; they return empty results. + +func (s *Server) Progress(ctx context.Context, params *protocol.ProgressParams) error { + return nil +} + +func (s *Server) DidOpenNotebookDocument(ctx context.Context, params *protocol.DidOpenNotebookDocumentParams) error { + return nil +} + +func (s *Server) DidChangeNotebookDocument(ctx context.Context, params *protocol.DidChangeNotebookDocumentParams) error { + return nil +} + +func (s *Server) DidSaveNotebookDocument(ctx context.Context, params *protocol.DidSaveNotebookDocumentParams) error { + return nil +} + +func (s *Server) DidCloseNotebookDocument(ctx context.Context, params *protocol.DidCloseNotebookDocumentParams) error { + return nil +} + +func (s *Server) PrepareTypeHierarchy(ctx context.Context, params *protocol.TypeHierarchyPrepareParams) ([]protocol.TypeHierarchyItem, error) { + return nil, nil +} + +func (s *Server) Supertypes(ctx context.Context, params *protocol.TypeHierarchySupertypesParams) ([]protocol.TypeHierarchyItem, error) { + return nil, nil +} + +func (s *Server) Subtypes(ctx context.Context, params *protocol.TypeHierarchySubtypesParams) ([]protocol.TypeHierarchyItem, error) { + return nil, nil +} + +func (s *Server) SelectionRange(ctx context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) { + return nil, nil +} + +func (s *Server) InlineValue(ctx context.Context, params *protocol.InlineValueParams) ([]protocol.InlineValue, error) { + return nil, nil +} + +func (s *Server) InlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) { + return nil, nil +} + +func (s *Server) InlayHintResolve(ctx context.Context, params *protocol.InlayHint) (*protocol.InlayHint, error) { + return params, nil +} + +func (s *Server) Diagnostic(ctx context.Context, params *protocol.DocumentDiagnosticParams) (protocol.DocumentDiagnosticReport, error) { + return nil, nil +} + +func (s *Server) DiagnosticWorkspace(ctx context.Context, params *protocol.WorkspaceDiagnosticParams) (*protocol.WorkspaceDiagnosticReport, error) { + return nil, nil +} + +func (s *Server) CodeActionResolve(ctx context.Context, params *protocol.CodeAction) (*protocol.CodeAction, error) { + return params, nil +} + +func (s *Server) RangesFormatting(ctx context.Context, params *protocol.DocumentRangesFormattingParams) ([]protocol.TextEdit, error) { + return nil, nil +} + +func (s *Server) InlineCompletion(ctx context.Context, params *protocol.InlineCompletionParams) (protocol.InlineCompletionResult, error) { + return nil, nil +} + +func (s *Server) WorkspaceSymbolResolve(ctx context.Context, params *protocol.WorkspaceSymbol) (*protocol.WorkspaceSymbol, error) { + return params, nil +} + +func (s *Server) TextDocumentContent(ctx context.Context, params *protocol.TextDocumentContentParams) (*protocol.TextDocumentContentResult, error) { return nil, nil } diff --git a/lsp/stream.go b/lsp/stream.go index 4be5c43..19a8758 100644 --- a/lsp/stream.go +++ b/lsp/stream.go @@ -3,56 +3,50 @@ package lsp import ( "context" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/format" - "github.com/joyme123/thrift-ls/lsp/cache" - "github.com/joyme123/thrift-ls/lsp/memoize" "go.lsp.dev/jsonrpc2" - "go.lsp.dev/pkg/event" - "go.uber.org/zap" + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/formatter" + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/lsp/memoize" ) type StreamServer struct { - logger *zap.Logger - cache *cache.Cache - formatOpts format.Options + formatOpts formatter.Options } type Options struct { IncludePaths []string - Format format.Options + Format formatter.Options } func NewStreamServer(opts *Options) *StreamServer { - logger, _ := zap.NewProduction() - store := &memoize.Store{} return &StreamServer{ cache: cache.New(store, opts.IncludePaths), - logger: logger, formatOpts: opts.Format, } } func (s *StreamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) error { - client := protocol.ClientDispatcher(conn, s.logger) + client := protocol.ClientDispatcher(conn) server := NewServer(s.cache, client, s.formatOpts) // Clients may or may not send a shutdown message. Make sure the server is // shut down. - // TODO(rFindley): this shutdown should perhaps be on a disconnected context. defer func() { - if err := server.Shutdown(ctx); err != nil { - event.Error(ctx, "error shutting down", err) - } + _ = server.Shutdown(ctx) }() + ctx = protocol.WithClient(ctx, client) conn.Go(ctx, DebugHandler( protocol.Handlers( - protocol.ServerHandler(server, jsonrpc2.MethodNotFoundHandler)))) + protocol.ServerHandler(server, jsonrpc2.MethodNotFoundHandler), + ), + )) <-conn.Done() return conn.Err() } diff --git a/lsp/symbols.go b/lsp/symbols.go index fd50268..d222ff7 100644 --- a/lsp/symbols.go +++ b/lsp/symbols.go @@ -3,11 +3,12 @@ package lsp import ( "context" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/symbols" + "go.lsp.dev/protocol" + + "github.com/karitham/thrift-ls/lsp/symbols" ) -func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) (result []interface{}, err error) { +func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) (result protocol.DocumentSymbolSlice, err error) { file := params.TextDocument.URI view, err := s.session.ViewOf(file) if err != nil { @@ -16,11 +17,11 @@ func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSy ss, release := view.Snapshot() defer release() - symbols := symbols.DocumentSymbols(ctx, ss, file) + syms := symbols.DocumentSymbols(ctx, ss, file) - for i := range symbols { - result = append(result, symbols[i]) + for i := range syms { + result = append(result, *syms[i]) } - return + return result, err } diff --git a/lsp/symbols/const.go b/lsp/symbols/const.go deleted file mode 100644 index 89b9eb9..0000000 --- a/lsp/symbols/const.go +++ /dev/null @@ -1,22 +0,0 @@ -package symbols - -import ( - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" -) - -func ConstSymbol(cst *parser.Const) *protocol.DocumentSymbol { - if cst.IsBadNode() || cst.ChildrenBadNode() { - return nil - } - - res := &protocol.DocumentSymbol{ - Name: cst.Name.Name.Text, - Kind: protocol.SymbolKindConstant, - Range: lsputils.ASTNodeToRange(cst.Name.Name), - SelectionRange: lsputils.ASTNodeToRange(cst.Name.Name), - } - - return res -} diff --git a/lsp/symbols/document.go b/lsp/symbols/document.go index 875ae52..797c7fa 100644 --- a/lsp/symbols/document.go +++ b/lsp/symbols/document.go @@ -2,68 +2,160 @@ package symbols import ( "context" - "errors" - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/cache" + "go.lsp.dev/protocol" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/syntax" ) +// DocumentSymbols returns the document symbols of a file, in source order. func DocumentSymbols(ctx context.Context, ss *cache.Snapshot, file uri.URI) []*protocol.DocumentSymbol { res := make([]*protocol.DocumentSymbol, 0) pf, err := ss.Parse(ctx, file) if err != nil { return res } - if pf.AST() == nil { - err = errors.New("parse ast failed") return res } doc := pf.AST() - - for i := range doc.Typedefs { - child := TypedefSymbol(doc.Typedefs[i]) + for _, node := range doc.Nodes { + child := nodeSymbol(doc, node) if child != nil { res = append(res, child) } } + return res +} - for i := range doc.Consts { - child := ConstSymbol(doc.Consts[i]) - if child != nil { - res = append(res, child) - } +//go:fix inline + +// nameRange returns the LSP range of an identifier. +func nameRange(doc *syntax.Document, id *syntax.Identifier) protocol.Range { + if id == nil { + return protocol.Range{} } + start, end := doc.Range(id) + return protocol.Range{ + Start: protocol.Position{ + Line: uint32(start.Line - 1), + Character: uint32(start.Col - 1), + }, + End: protocol.Position{ + Line: uint32(end.Line - 1), + Character: uint32(end.Col - 1), + }, + } +} - for i := range doc.Structs { - child := StructSymbol(doc.Structs[i]) - if child != nil { - res = append(res, child) +// nodeSymbol builds the symbol for a top-level definition. +func nodeSymbol(doc *syntax.Document, node syntax.Node) *protocol.DocumentSymbol { + switch v := node.(type) { + case *syntax.Typedef: + return typedefSymbol(doc, v) + case *syntax.Const: + return constSymbol(doc, v) + case *syntax.Struct: + switch v.Kind { + case syntax.StructDecl: + return structSymbol(doc, v, "Struct", protocol.SymbolKindStruct) + case syntax.UnionDecl: + return structSymbol(doc, v, "Union", protocol.SymbolKindStruct) + case syntax.ExceptionDecl: + return structSymbol(doc, v, "Exception", protocol.SymbolKindStruct) } + case *syntax.Enum: + return enumSymbol(doc, v) + case *syntax.Service: + return serviceSymbol(doc, v) } + return nil +} - for i := range doc.Unions { - child := UnionSymbol(doc.Unions[i]) +func structSymbol(doc *syntax.Document, st *syntax.Struct, detail string, kind protocol.SymbolKind) *protocol.DocumentSymbol { + res := &protocol.DocumentSymbol{ + Name: st.Name.Text, + Detail: &detail, + Kind: kind, + Range: nameRange(doc, st.Name), + SelectionRange: nameRange(doc, st.Name), + } + for _, field := range st.Fields { + child := fieldSymbol(doc, field) if child != nil { - res = append(res, child) + res.Children = append(res.Children, *child) } } + return res +} - for i := range doc.Exceptions { - child := ExceptionSymbol(doc.Exceptions[i]) - if child != nil { - res = append(res, child) +func enumSymbol(doc *syntax.Document, enum *syntax.Enum) *protocol.DocumentSymbol { + res := &protocol.DocumentSymbol{ + Name: enum.Name.Text, + Detail: new("Enum"), + Kind: protocol.SymbolKindEnum, + Range: nameRange(doc, enum.Name), + SelectionRange: nameRange(doc, enum.Name), + } + for _, value := range enum.Values { + child := &protocol.DocumentSymbol{ + Name: value.Name.Text, + Kind: protocol.SymbolKindEnumMember, + Range: nameRange(doc, value.Name), + SelectionRange: nameRange(doc, value.Name), } + res.Children = append(res.Children, *child) } + return res +} - for i := range doc.Services { - child := ServiceSymbol(doc.Services[i]) - if child != nil { - res = append(res, child) +func serviceSymbol(doc *syntax.Document, svc *syntax.Service) *protocol.DocumentSymbol { + res := &protocol.DocumentSymbol{ + Name: svc.Name.Text, + Kind: protocol.SymbolKindInterface, + Range: nameRange(doc, svc.Name), + SelectionRange: nameRange(doc, svc.Name), + } + for _, fn := range svc.Functions { + child := &protocol.DocumentSymbol{ + Name: fn.Name.Text, + Kind: protocol.SymbolKindFunction, + Range: nameRange(doc, fn.Name), + SelectionRange: nameRange(doc, fn.Name), } + res.Children = append(res.Children, *child) } - return res } + +func fieldSymbol(doc *syntax.Document, field *syntax.Field) *protocol.DocumentSymbol { + return &protocol.DocumentSymbol{ + Name: field.Name.Text, + Kind: protocol.SymbolKindField, + Range: nameRange(doc, field.Name), + SelectionRange: nameRange(doc, field.Name), + } +} + +func typedefSymbol(doc *syntax.Document, td *syntax.Typedef) *protocol.DocumentSymbol { + return &protocol.DocumentSymbol{ + Name: td.Name.Text, + Detail: new("Typedef"), + Kind: protocol.SymbolKindTypeParameter, + Range: nameRange(doc, td.Name), + SelectionRange: nameRange(doc, td.Name), + } +} + +func constSymbol(doc *syntax.Document, cst *syntax.Const) *protocol.DocumentSymbol { + return &protocol.DocumentSymbol{ + Name: cst.Name.Text, + Detail: new("Const"), + Kind: protocol.SymbolKindConstant, + Range: nameRange(doc, cst.Name), + SelectionRange: nameRange(doc, cst.Name), + } +} diff --git a/lsp/symbols/enum.go b/lsp/symbols/enum.go deleted file mode 100644 index a0595f3..0000000 --- a/lsp/symbols/enum.go +++ /dev/null @@ -1,50 +0,0 @@ -package symbols - -import ( - "strconv" - - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" -) - -func EnumSymbol(enum *parser.Enum) *protocol.DocumentSymbol { - if enum.IsBadNode() || enum.ChildrenBadNode() { - return nil - } - - res := &protocol.DocumentSymbol{ - Name: enum.Name.Name.Text, - Detail: "Enum", - Kind: protocol.SymbolKindEnum, - Range: lsputils.ASTNodeToRange(enum.Name.Name), - SelectionRange: lsputils.ASTNodeToRange(enum.Name.Name), - } - - for i := range enum.Values { - child := EnumValueSymbol(enum.Values[i]) - if child == nil { - continue - } - res.Children = append(res.Children, *child) - } - - return res - -} - -func EnumValueSymbol(v *parser.EnumValue) *protocol.DocumentSymbol { - if v.IsBadNode() || v.ChildrenBadNode() { - return nil - } - - res := &protocol.DocumentSymbol{ - Name: v.Name.Name.Text, - Detail: strconv.FormatInt(v.Value, 10), - Kind: protocol.SymbolKindNumber, - Range: lsputils.ASTNodeToRange(v.Name.Name), - SelectionRange: lsputils.ASTNodeToRange(v.Name.Name), - } - - return res -} diff --git a/lsp/symbols/field.go b/lsp/symbols/field.go deleted file mode 100644 index e448dcc..0000000 --- a/lsp/symbols/field.go +++ /dev/null @@ -1,32 +0,0 @@ -package symbols - -import ( - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/format" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" -) - -func FieldSymbol(field *parser.Field) *protocol.DocumentSymbol { - if field.IsBadNode() || field.ChildrenBadNode() { - return nil - } - - detail := "" - if field.RequiredKeyword != nil { - detail = field.RequiredKeyword.Literal.Text + " " - } - // Use default options for symbol display - opts := format.Options{} - detail += format.MustFormatFieldType(field.FieldType, opts) - - res := &protocol.DocumentSymbol{ - Name: field.Identifier.Name.Text, - Detail: detail, - Kind: protocol.SymbolKindField, - Range: lsputils.ASTNodeToRange(field.Identifier.Name), - SelectionRange: lsputils.ASTNodeToRange(field.Identifier.Name), - } - - return res -} diff --git a/lsp/symbols/service.go b/lsp/symbols/service.go deleted file mode 100644 index 155827c..0000000 --- a/lsp/symbols/service.go +++ /dev/null @@ -1,45 +0,0 @@ -package symbols - -import ( - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" -) - -func ServiceSymbol(svc *parser.Service) *protocol.DocumentSymbol { - if svc.IsBadNode() || svc.ChildrenBadNode() { - return nil - } - - res := &protocol.DocumentSymbol{ - Name: svc.Name.Name.Text, - Kind: protocol.SymbolKindInterface, - Range: lsputils.ASTNodeToRange(svc.Name.Name), - SelectionRange: lsputils.ASTNodeToRange(svc.Name.Name), - } - - for i := range svc.Functions { - child := FunctionSymbol(svc.Functions[i]) - if child != nil { - res.Children = append(res.Children, *child) - } - - } - - return res -} - -func FunctionSymbol(fn *parser.Function) *protocol.DocumentSymbol { - if fn.IsBadNode() || fn.ChildrenBadNode() { - return nil - } - - res := &protocol.DocumentSymbol{ - Name: fn.Name.Name.Text, - Kind: protocol.SymbolKindFunction, - Range: lsputils.ASTNodeToRange(fn.Name.Name), - SelectionRange: lsputils.ASTNodeToRange(fn.Name.Name), - } - - return res -} diff --git a/lsp/symbols/struct_like.go b/lsp/symbols/struct_like.go deleted file mode 100644 index a7c6a19..0000000 --- a/lsp/symbols/struct_like.go +++ /dev/null @@ -1,81 +0,0 @@ -package symbols - -import ( - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" -) - -func StructSymbol(st *parser.Struct) *protocol.DocumentSymbol { - if st.IsBadNode() || st.ChildrenBadNode() { - return nil - } - - res := &protocol.DocumentSymbol{ - Name: st.Identifier.Name.Text, - Detail: "Struct", - Kind: protocol.SymbolKindStruct, - Range: lsputils.ASTNodeToRange(st.Identifier.Name), - SelectionRange: lsputils.ASTNodeToRange(st.Identifier.Name), - } - - for i := range st.Fields { - child := FieldSymbol(st.Fields[i]) - if child == nil { - continue - } - res.Children = append(res.Children, *child) - } - - return res - -} - -func UnionSymbol(un *parser.Union) *protocol.DocumentSymbol { - if un.IsBadNode() || un.ChildrenBadNode() { - return nil - } - - res := &protocol.DocumentSymbol{ - Name: un.Name.Name.Text, - Detail: "Union", - Kind: protocol.SymbolKindStruct, - Range: lsputils.ASTNodeToRange(un.Name.Name), - SelectionRange: lsputils.ASTNodeToRange(un.Name.Name), - } - - for i := range un.Fields { - child := FieldSymbol(un.Fields[i]) - if child == nil { - continue - } - res.Children = append(res.Children, *child) - } - - return res - -} - -func ExceptionSymbol(ex *parser.Exception) *protocol.DocumentSymbol { - if ex.IsBadNode() || ex.ChildrenBadNode() { - return nil - } - - res := &protocol.DocumentSymbol{ - Name: ex.Name.Name.Text, - Detail: "Exception", - Kind: protocol.SymbolKindStruct, - Range: lsputils.ASTNodeToRange(ex.Name.Name), - SelectionRange: lsputils.ASTNodeToRange(ex.Name.Name), - } - - for i := range ex.Fields { - child := FieldSymbol(ex.Fields[i]) - if child == nil { - continue - } - res.Children = append(res.Children, *child) - } - - return res -} diff --git a/lsp/symbols/typedef.go b/lsp/symbols/typedef.go deleted file mode 100644 index 293e9b2..0000000 --- a/lsp/symbols/typedef.go +++ /dev/null @@ -1,23 +0,0 @@ -package symbols - -import ( - "github.com/joyme123/protocol" - "github.com/joyme123/thrift-ls/format" - "github.com/joyme123/thrift-ls/lsp/lsputils" - "github.com/joyme123/thrift-ls/parser" -) - -func TypedefSymbol(td *parser.Typedef) *protocol.DocumentSymbol { - // Use default options for symbol display - opts := format.Options{} - - res := &protocol.DocumentSymbol{ - Name: td.Alias.Name.Text, - Detail: format.MustFormatFieldType(td.T, opts), - Kind: protocol.SymbolKindTypeParameter, - Range: lsputils.ASTNodeToRange(td.Alias.Name), - SelectionRange: lsputils.ASTNodeToRange(td.Alias.Name), - } - - return res -} diff --git a/lsp/testdata/fuzz/FuzzFormatRangeText/7f0ba3fa9729404d b/lsp/testdata/fuzz/FuzzFormatRangeText/7f0ba3fa9729404d new file mode 100644 index 0000000..5add488 --- /dev/null +++ b/lsp/testdata/fuzz/FuzzFormatRangeText/7f0ba3fa9729404d @@ -0,0 +1,4 @@ +go test fuzz v1 +[]byte(" #0000000000\n\n00") +int(0) +int(-49) diff --git a/main.go b/main.go index 577333e..f891fbd 100644 --- a/main.go +++ b/main.go @@ -3,201 +3,300 @@ package main import ( "context" "errors" - "flag" "fmt" "io" - "math/rand" "os" "path/filepath" - "time" + "strings" - "github.com/joyme123/thrift-ls/format" - tlog "github.com/joyme123/thrift-ls/log" - "github.com/joyme123/thrift-ls/lsp" - "github.com/joyme123/thrift-ls/parser" - "github.com/joyme123/thrift-ls/utils/diff" + "github.com/urfave/cli/v3" + + "github.com/karitham/thrift-ls/formatter" + tlog "github.com/karitham/thrift-ls/log" + "github.com/karitham/thrift-ls/lsp" + "github.com/karitham/thrift-ls/options" + "github.com/karitham/thrift-ls/syntax" "go.lsp.dev/jsonrpc2" "go.lsp.dev/pkg/fakenet" - "gopkg.in/yaml.v2" ) -type Options struct { - LogLevel int `yaml:"logLevel"` // 1: fatal, 2: error, 3: warn, 4: info, 5: debug, 6: trace - IncludePaths []string `yaml:"include_paths"` - Format format.Options `yaml:"format"` +func main() { + cmd := &cli.Command{ + Name: "thriftls", + Usage: "Thrift language server and formatter", + Version: lsp.ServerVersion, + Flags: lspFlags(), + // No subcommand: run the language server. + Action: lspAction, + Commands: []*cli.Command{ + { + Name: "lsp", + Usage: "run the language server on stdio", + Flags: lspFlags(), + Action: lspAction, + }, + { + Name: "format", + Usage: "format a thrift file", + ArgsUsage: "", + Flags: formatFlags(), + Action: formatAction, + }, + }, + } + + if err := cmd.Run(context.Background(), os.Args); err != nil { + fmt.Fprintln(os.Stderr, "thriftls:", err) + os.Exit(1) + } } -func main_format(opt format.Options, file string, includePaths []string) error { - if file == "" { - err := errors.New("must specified a thrift file to format") - fmt.Println(err) - return err +// lspFlags are the flags shared by the default and lsp subcommands. +func lspFlags() []cli.Flag { + return []cli.Flag{ + &cli.IntFlag{ + Name: "logLevel", + Usage: "set log level (1 fatal .. 6 trace)", + }, + &cli.StringFlag{ + Name: "config", + Usage: "path to a thriftls.json config file", + }, + &cli.StringSliceFlag{ + Name: "I", + Usage: "additional include path, like the thrift compiler (repeatable)", + }, } +} - absFile, err := filepath.Abs(file) - if err != nil { - fmt.Println(err) - return err +// formatFlags are the flags of the format subcommand. +func formatFlags() []cli.Flag { + return []cli.Flag{ + &cli.BoolFlag{ + Name: "w", + Usage: "overwrite the file with the formatted result", + }, + &cli.BoolFlag{ + Name: "d", + Usage: "print a diff instead of the formatted result", + }, + &cli.IntFlag{ + Name: "printWidth", + Usage: "target line width (default 80)", + }, + &cli.StringFlag{ + Name: "indent", + Usage: `indentation: a literal like " " or "\t", a number like 8, or a legacy spec like "2spaces"`, + }, + &cli.StringFlag{ + Name: "align", + Usage: `align fields: "field", "assign", or "disable"`, + }, + &cli.StringFlag{ + Name: "fieldLineComma", + Usage: `trailing commas: "add", "remove", or "disable" to keep as written`, + }, + &cli.StringFlag{ + Name: "config", + Usage: "path to a thriftls.json config file", + }, + &cli.StringSliceFlag{ + Name: "I", + Usage: "additional include path, like the thrift compiler (repeatable)", + }, } +} - content, err := os.ReadFile(file) +// lspAction serves the language server on stdio. +func lspAction(ctx context.Context, cmd *cli.Command) error { + cfg := loadConfig(cmd.String("config"), ".") + patch := options.Effective(cfg) + cli, err := lspPatch(cmd) if err != nil { - fmt.Println(err) return err } + patch = cli.Apply(patch) - thrift_file := filepath.Base(file) - ast, err := parser.Parse(thrift_file, content) - if err != nil { - fmt.Println(err) - return err + logLevelValue := 3 + if patch.LogLevel != nil { + logLevelValue = *patch.LogLevel } + tlog.Init(logLevelValue) - formated, err := format.FormatDocumentWithValidationFull(ast.(*parser.Document), opt, includePaths, absFile) + fopts, err := patch.Formatter() if err != nil { - fmt.Println(err) return err } - if opt.Write { - var perms os.FileMode - fileInfo, err := os.Stat(file) - if err != nil { - fmt.Println(err) - return err - } - perms = fileInfo.Mode() // 使用原文件的权限 - - // overwrite - err = os.WriteFile(file, []byte(formated), perms) - if err != nil { - fmt.Println(err) - return err - } - } else { - if opt.Diff { - diffLines := diff.Diff("old", content, "new", []byte(formated)) - fmt.Print(string(diffLines)) - } else { - fmt.Println(formated) - } - return err + lspOpts := &lsp.Options{ + IncludePaths: derefStrings(patch.IncludePaths), + Format: fopts, } - return nil - -} - -func main() { - rand.Seed(time.Now().UnixMilli()) - - formatter := false - formatFile := "" - flag.BoolVar(&formatter, "format", false, "use thrift-ls as a format tool") - flag.StringVar(&formatFile, "f", "", "file path to format") - formatOpts := format.Options{} - formatOpts.SetFlags() - flag.Parse() - - opts := configInit(&formatOpts) - formatOpts = formatOpts.InitDefault() - tlog.Init(opts.LogLevel) - - if formatter { - main_format(formatOpts, formatFile, opts.IncludePaths) - return - } - - ctx := context.Background() - // server := &lsp.Server{} - // handler := protocol.ServerHandler(server, nil) - // - // streamServer := jsonrpc2.HandlerServer(handler) - // if err := jsonrpc2.ListenAndServe(ctx, "tcp", "127.0.0.1:8000", streamServer, 60*time.Second); err != nil { - // panic(err) - // } - - ss := lsp.NewStreamServer(&lsp.Options{ - IncludePaths: opts.IncludePaths, - Format: formatOpts, - }) + ss := lsp.NewStreamServer(lspOpts) stream := jsonrpc2.NewStream(fakenet.NewConn("stdio", os.Stdin, os.Stdout)) conn := jsonrpc2.NewConn(stream) - err := ss.ServeStream(ctx, conn) + err = ss.ServeStream(ctx, conn) if errors.Is(err, io.EOF) { - return + return nil } - panic(err) + return err } -// defaultConfigPath returns the default config file path: -// THRIFTLS_CONFIG env var if set, otherwise ~/.thriftls/config.yaml -func defaultConfigPath() string { - if configFile := os.Getenv("THRIFTLS_CONFIG"); configFile != "" { - return configFile - } - dir, err := os.UserHomeDir() +// formatAction formats a single thrift file. +func formatAction(ctx context.Context, cmd *cli.Command) error { + file := cmd.Args().First() + cli, err := formatPatch(cmd) if err != nil { - dir = os.TempDir() + return err } - return dir + "/.thriftls/config.yaml" + return formatFile(file, cmd.Bool("w"), cmd.Bool("d"), cmd.String("config"), cli) } -// readConfig reads and parses the config file at the given path. -func readConfig(path string) (*Options, error) { - opts := &Options{} - data, err := os.ReadFile(path) - if err != nil { - return nil, err +// lspPatch builds an options patch from the explicitly set lsp flags. +func lspPatch(cmd *cli.Command) (options.Patch, error) { + p := options.Patch{} + if cmd.IsSet("logLevel") { + v := cmd.Int("logLevel") + p.LogLevel = &v } - if err := yaml.Unmarshal(data, opts); err != nil { - return nil, err + if paths := cmd.StringSlice("I"); len(paths) > 0 { + p.IncludePaths = &paths } - return opts, nil + return p, nil } -func configInit(formatOpts *format.Options) *Options { - opts := &Options{} +// formatPatch builds an options patch from the explicitly set format flags. +func formatPatch(cmd *cli.Command) (options.Patch, error) { + p := options.Patch{} + if cmd.IsSet("printWidth") { + v := cmd.Int("printWidth") + p.PrintWidth = &v + } + if cmd.IsSet("indent") { + ind, err := options.ParseIndentValue(cmd.String("indent")) + if err != nil { + return options.Patch{}, err + } + p.Indent = &ind + } + if cmd.IsSet("align") { + v := cmd.String("align") + p.Align = &v + } + if cmd.IsSet("fieldLineComma") { + v := cmd.String("fieldLineComma") + p.FieldLineComma = &v + } + if paths := cmd.StringSlice("I"); len(paths) > 0 { + p.IncludePaths = &paths + } + return p, nil +} - logLevel := -1 - flag.IntVar(&logLevel, "logLevel", -1, "set log level") - flag.Parse() +// loadConfig loads the explicit config path, or finds one walking up from +// dir. A missing config is not an error. +func loadConfig(path, dir string) *options.Patch { + if path == "" { + var err error + path, err = options.FindConfig(dir) + if err != nil { + fatal(err) + } + } + if path == "" { + return nil + } + cfg, err := options.Load(path) + if err != nil { + fatal(err) + } + return cfg +} - configFile := defaultConfigPath() - if cfg, err := readConfig(configFile); err == nil { - opts = cfg +func fatal(err error) { + fmt.Fprintln(os.Stderr, "thriftls:", err) + os.Exit(1) +} + +// formatFile formats a single file: read, parse, resolve options, format, +// self-validate, and write, diff, or print. +func formatFile(file string, write, diffOut bool, configPath string, cli options.Patch) error { + if file == "" { + return errors.New("must specify a thrift file to format, e.g. thriftls format file.thrift") + } + + src, err := os.ReadFile(file) + if err != nil { + return err + } + absFile, err := filepath.Abs(file) + if err != nil { + return err } - if logLevel >= 0 { - opts.LogLevel = logLevel // flag can override config file + cfg := loadConfig(configPath, filepath.Dir(absFile)) + patch := options.Effective(cfg) + patch = cli.Apply(patch) + fopts, err := patch.Formatter() + if err != nil { + return err } - if opts.LogLevel == 0 { - opts.LogLevel = 3 + + doc, errs := syntax.Parse(src) + if parseErrors(errs) { + return fmt.Errorf("%s: file does not parse:\n%s", file, formatErrors(errs)) } - // Track which flags were explicitly set via CLI - cliFlags := make(map[string]bool) - flag.Visit(func(f *flag.Flag) { - cliFlags[f.Name] = true - }) + out, err := formatter.Format(doc, fopts) + if err != nil { + return fmt.Errorf("%s: %w", file, err) + } - // Merge format config values (CLI > config > defaults) - // Only use config value if CLI flag wasn't explicitly set - if !cliFlags["indent"] && opts.Format.Indent != "" { - formatOpts.Indent = opts.Format.Indent + // Self-validation: the formatted output must parse cleanly. + if _, errs := syntax.Parse([]byte(out)); parseErrors(errs) { + return fmt.Errorf("%s: formatting produced invalid output", file) } - if !cliFlags["align"] && opts.Format.Align != "" { - formatOpts.Align = opts.Format.Align + + switch { + case write: + perms := os.FileMode(0o644) + if info, err := os.Stat(file); err == nil { + perms = info.Mode() + } + return os.WriteFile(file, []byte(out), perms) + case diffOut: + fmt.Print(string(Diff("old", src, "new", []byte(out)))) + return nil + default: + fmt.Print(out) + return nil } - if !cliFlags["fieldLineComma"] && opts.Format.FieldLineComma != "" { - formatOpts.FieldLineComma = opts.Format.FieldLineComma +} + +func parseErrors(errs []syntax.Error) bool { + for _, e := range errs { + if e.Severity == syntax.SeverityError { + return true + } } - // TrailingNewline is a boolean, so we need to check if the CLI flag was set - // The default is false, so we only override if config is true and CLI wasn't set - if !cliFlags["trailingNewline"] && opts.Format.TrailingNewline { - formatOpts.TrailingNewline = opts.Format.TrailingNewline + return false +} + +func formatErrors(errs []syntax.Error) string { + var b strings.Builder + for _, e := range errs { + if e.Severity == syntax.SeverityError { + fmt.Fprintf(&b, " %s\n", e) + } } + return b.String() +} - return opts +func derefStrings(p *[]string) []string { + if p == nil { + return nil + } + return *p } diff --git a/options/options.go b/options/options.go new file mode 100644 index 0000000..dec8bba --- /dev/null +++ b/options/options.go @@ -0,0 +1,312 @@ +// Package options is the configuration layer of thrift-ls. It owns a +// partial-options model (Patch): every field is optional so configuration +// sources can be layered — defaults, a JSON config file, CLI flags, and LSP +// workspace settings — each overriding the previous. +// +// The config file is thriftls.json, discovered by walking up from the file +// being formatted, like Biome's config discovery. THRIFTLS_CONFIG overrides +// the search with an explicit path. +package options + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + + "github.com/karitham/thrift-ls/formatter" +) + +// ConfigFileName is the JSON config file name. +const ConfigFileName = "thriftls.json" + +// Patch is a partial set of options; nil fields are unset. +type Patch struct { + PrintWidth *int `json:"printWidth"` + Indent *Indent `json:"indent"` + TabWidth *int `json:"tabWidth"` + Align *string `json:"align"` + FieldLineComma *string `json:"fieldLineComma"` + IncludePaths *[]string `json:"includePaths"` + LogLevel *int `json:"logLevel"` +} + +// Apply overlays p onto base: every set field of p replaces the +// corresponding field of base. +func (p Patch) Apply(base Patch) Patch { + out := base + if p.PrintWidth != nil { + out.PrintWidth = p.PrintWidth + } + if p.Indent != nil { + out.Indent = p.Indent + } + if p.TabWidth != nil { + out.TabWidth = p.TabWidth + } + if p.Align != nil { + out.Align = p.Align + } + if p.FieldLineComma != nil { + out.FieldLineComma = p.FieldLineComma + } + if p.IncludePaths != nil { + out.IncludePaths = p.IncludePaths + } + if p.LogLevel != nil { + out.LogLevel = p.LogLevel + } + return out +} + +// Default returns the default options as a fully-set patch. +func Default() Patch { + printWidth := 80 + indent := Indent{Value: " ", Width: 4} + tabWidth := 4 + align := "field" + comma := "disable" + return Patch{ + PrintWidth: &printWidth, + Indent: &indent, + TabWidth: &tabWidth, + Align: &align, + FieldLineComma: &comma, + } +} + +// Validate checks every set field for validity. +func (p Patch) Validate() error { + if p.PrintWidth != nil && *p.PrintWidth <= 0 { + return errors.New("printWidth must be positive") + } + if p.TabWidth != nil && *p.TabWidth <= 0 { + return errors.New("tabWidth must be positive") + } + if p.Align != nil && !oneOf(*p.Align, "field", "assign", "disable") { + return fmt.Errorf("align must be one of \"field\", \"assign\", \"disable\", got %q", *p.Align) + } + if p.FieldLineComma != nil && !oneOf(*p.FieldLineComma, "add", "remove", "disable", "preserve") { + return fmt.Errorf("fieldLineComma must be one of \"add\", \"remove\", \"disable\" (keep as written), got %q", *p.FieldLineComma) + } + if p.Indent != nil { + if p.Indent.Width <= 0 || !isWhitespaceOnly(p.Indent.Value) { + return errors.New("indent must be a string of spaces or tabs") + } + } + return nil +} + +func oneOf(s string, options ...string) bool { + return slices.Contains(options, s) +} + +// Formatter converts the patch to formatter options, validating first. +func (p Patch) Formatter() (formatter.Options, error) { + if err := p.Validate(); err != nil { + return formatter.Options{}, err + } + o := formatter.DefaultOptions() + if p.PrintWidth != nil { + o.PrintWidth = *p.PrintWidth + } + if p.Indent != nil { + o.Indent = p.Indent.Value + o.TabWidth = p.Indent.Width + } + if p.TabWidth != nil { + o.TabWidth = *p.TabWidth + } + if p.Align != nil { + switch *p.Align { + case "field": + o.Align = formatter.AlignField + case "assign": + o.Align = formatter.AlignAssign + case "disable": + o.Align = formatter.AlignDisable + } + } + if p.FieldLineComma != nil { + switch *p.FieldLineComma { + case "add": + o.FieldLineComma = formatter.CommaAdd + case "remove": + o.FieldLineComma = formatter.CommaRemove + case "disable", "preserve": + o.FieldLineComma = formatter.CommaPreserve + } + } + return o, nil +} + +// Indent is a resolved indentation: the string emitted for one level and +// its display width. It is set from a config value that may be a literal +// string of spaces or tabs, a number of spaces, or a legacy spec like +// "2spaces" or "1tab". +type Indent struct { + Value string // the indentation string, spaces or tabs + Width int // display width of one level +} + +// UnmarshalJSON accepts a number (spaces), a literal string of spaces or +// tabs, or a legacy spec string. +func (i *Indent) UnmarshalJSON(data []byte) error { + var n int + if err := json.Unmarshal(data, &n); err == nil { + ind, err := ParseIndentValue(strconv.Itoa(n)) + if err != nil { + return err + } + *i = ind + return nil + } + var s string + if err := json.Unmarshal(data, &s); err != nil { + return errors.New("indent must be a string of spaces or tabs, a number, or a legacy spec like \"2spaces\"") + } + ind, err := ParseIndentValue(s) + if err != nil { + return err + } + *i = ind + return nil +} + +// ParseIndentValue resolves a friendly indent spec: +// +// " " literal spaces, used as written +// "\t" literal tabs, used as written +// "8" a number of spaces +// "2spaces", "1tab", "tab" legacy specs, kept as aliases +// +// An empty spec yields the default of four spaces. +func ParseIndentValue(s string) (Indent, error) { + if s == "" { + return Indent{Value: " ", Width: 4}, nil + } + if n, err := strconv.Atoi(s); err == nil { + if n <= 0 { + return Indent{}, errors.New("indent must be a positive number of spaces") + } + return Indent{Value: strings.Repeat(" ", n), Width: n}, nil + } + if isWhitespaceOnly(s) { + spaces := strings.Count(s, " ") + tabs := strings.Count(s, "\t") + if spaces > 0 && tabs > 0 { + return Indent{}, fmt.Errorf("indent %q mixes spaces and tabs", s) + } + if tabs > 0 { + return Indent{Value: s, Width: tabs * 4}, nil + } + return Indent{Value: s, Width: spaces}, nil + } + return ParseLegacyIndent(s) +} + +func isWhitespaceOnly(s string) bool { + for _, r := range s { + if r != ' ' && r != '\t' { + return false + } + } + return true +} + +// ParseLegacyIndent parses the legacy indent specs ("4spaces", "1tab", +// "2tabs", "tab"), kept for compatibility. +func ParseLegacyIndent(s string) (Indent, error) { + lower := strings.ToLower(s) + num := 1 + unit := "" + for _, suffix := range []string{"spaces", "space", "tabs", "tab"} { + if strings.HasSuffix(lower, suffix) { + unit = suffix + prefix := strings.TrimSuffix(lower, suffix) + if prefix != "" { + n, err := strconv.Atoi(prefix) + if err != nil || n <= 0 { + return Indent{}, fmt.Errorf("invalid indent %q: use a literal like \" \", a number like 8, or a legacy spec like \"2spaces\"", s) + } + num = n + } + break + } + } + if unit == "" { + return Indent{}, fmt.Errorf("invalid indent %q: use a literal like \" \", a number like 8, or a legacy spec like \"2spaces\"", s) + } + if strings.HasPrefix(unit, "tab") { + return Indent{Value: strings.Repeat("\t", num), Width: num * 4}, nil + } + return Indent{Value: strings.Repeat(" ", num), Width: num}, nil +} + +// Load reads and parses a config file. Unknown keys are rejected so that +// typos and stale settings (e.g. the removed overrides feature) fail loudly. +func Load(path string) (*Patch, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var p Patch + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() + if err := dec.Decode(&p); err != nil { + return nil, fmt.Errorf("options: %s: %w", path, err) + } + if err := p.Validate(); err != nil { + return nil, fmt.Errorf("options: %s: %w", path, err) + } + // Include paths are relative to the config file, not the process CWD, + // so resolution works the same for the CLI and the LSP no matter where + // the server is launched from. + if p.IncludePaths != nil { + abs := make([]string, 0, len(*p.IncludePaths)) + for _, ip := range *p.IncludePaths { + if filepath.IsAbs(ip) { + abs = append(abs, ip) + } else { + abs = append(abs, filepath.Join(filepath.Dir(path), ip)) + } + } + p.IncludePaths = &abs + } + return &p, nil +} + +// FindConfig returns the config file path for dir: THRIFTLS_CONFIG when set, +// otherwise the nearest thriftls.json walking up from dir. It returns an +// empty path when no config exists. +func FindConfig(dir string) (string, error) { + if path := os.Getenv("THRIFTLS_CONFIG"); path != "" { + return path, nil + } + for d := dir; ; d = filepath.Dir(d) { + path := filepath.Join(d, ConfigFileName) + if _, err := os.Stat(path); err == nil { + return path, nil + } else if !os.IsNotExist(err) { + return "", err + } + if d == filepath.Dir(d) { + return "", nil + } + } +} + +// Effective returns the effective options for a file: defaults with the +// config patch applied. cfg may be nil. +func Effective(cfg *Patch) Patch { + p := Default() + if cfg != nil { + p = cfg.Apply(p) + } + return p +} diff --git a/options/options_test.go b/options/options_test.go new file mode 100644 index 0000000..a33fdd2 --- /dev/null +++ b/options/options_test.go @@ -0,0 +1,247 @@ +package options + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/karitham/thrift-ls/formatter" +) + +func TestParseIndentValue(t *testing.T) { + tests := []struct { + name string + in string + want Indent + wantErr bool + }{ + {"empty defaults", "", Indent{" ", 4}, false}, + {"literal two spaces", " ", Indent{" ", 2}, false}, + {"literal four spaces", " ", Indent{" ", 4}, false}, + {"literal tab", "\t", Indent{"\t", 4}, false}, + {"literal two tabs", "\t\t", Indent{"\t\t", 8}, false}, + {"number", "8", Indent{" ", 8}, false}, + {"number one", "1", Indent{" ", 1}, false}, + {"legacy spaces", "2spaces", Indent{" ", 2}, false}, + {"legacy space singular", "1space", Indent{" ", 1}, false}, + {"legacy tab", "1tab", Indent{"\t", 4}, false}, + {"legacy bare tab", "tab", Indent{"\t", 4}, false}, + {"legacy two tabs", "2tabs", Indent{"\t\t", 8}, false}, + {"zero number", "0", Indent{}, true}, + {"mixed spaces and tabs", " \t", Indent{}, true}, + {"garbage", "banana", Indent{}, true}, + {"negative", "-2", Indent{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseIndentValue(tt.in) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got %+v", got) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("got %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestIndentUnmarshal(t *testing.T) { + tests := []struct { + name string + json string + want Indent + }{ + {"string spaces", `" "`, Indent{" ", 2}}, + {"string tab", `"\t"`, Indent{"\t", 4}}, + {"number", `8`, Indent{" ", 8}}, + {"legacy", `"2spaces"`, Indent{" ", 2}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var i Indent + if err := json.Unmarshal([]byte(tt.json), &i); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if i != tt.want { + t.Errorf("got %+v, want %+v", i, tt.want) + } + }) + } +} + +func TestPatchApply(t *testing.T) { + base := Default() + + overlay := Patch{} + printWidth := 100 + overlay.PrintWidth = &printWidth + + got := overlay.Apply(base) + if got.PrintWidth == nil || *got.PrintWidth != 100 { + t.Errorf("PrintWidth not overridden: %v", got.PrintWidth) + } + if got.Align == nil || *got.Align != "field" { + t.Errorf("Align should stay from base: %v", got.Align) + } +} + +func TestPatchValidate(t *testing.T) { + intPtr := func(n int) *int { return &n } + strPtr := func(s string) *string { return &s } + + tests := []struct { + name string + patch Patch + wantErr bool + }{ + {"default is valid", Default(), false}, + {"bad printWidth", Patch{PrintWidth: intPtr(0)}, true}, + {"bad tabWidth", Patch{TabWidth: intPtr(-1)}, true}, + {"bad align", Patch{Align: strPtr("sideways")}, true}, + {"bad comma", Patch{FieldLineComma: strPtr("maybe")}, true}, + {"preserve alias", Patch{FieldLineComma: strPtr("preserve")}, false}, + {"bad indent value", Patch{Indent: &Indent{Value: "x", Width: 1}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.patch.Validate() + if tt.wantErr && err == nil { + t.Error("expected error") + } + if !tt.wantErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} + +func TestPatchFormatter(t *testing.T) { + indent := Indent{Value: " ", Width: 2} + p := Patch{Indent: &indent, PrintWidth: new(100)} + o, err := p.Formatter() + if err != nil { + t.Fatalf("Formatter: %v", err) + } + if o.PrintWidth != 100 || o.Indent != " " || o.TabWidth != 2 { + t.Errorf("got %+v", o) + } + if o.Align != formatter.AlignField || o.FieldLineComma != formatter.CommaPreserve { + t.Errorf("defaults wrong: %+v", o) + } + + comma := "add" + align := "assign" + p = Patch{FieldLineComma: &comma, Align: &align} + o, err = p.Formatter() + if err != nil { + t.Fatalf("Formatter: %v", err) + } + if o.FieldLineComma != formatter.CommaAdd || o.Align != formatter.AlignAssign { + t.Errorf("got %+v", o) + } +} + +func TestFindConfig(t *testing.T) { + dir := t.TempDir() + sub := filepath.Join(dir, "a", "b") + if err := os.MkdirAll(sub, 0o755); err != nil { + t.Fatal(err) + } + + // No config anywhere. + got, err := FindConfig(sub) + if err != nil || got != "" { + t.Fatalf("FindConfig = %q, %v; want empty", got, err) + } + + // Config in an ancestor directory is found walking up. + cfgPath := filepath.Join(dir, "thriftls.json") + if err := os.WriteFile(cfgPath, []byte("{}"), 0o644); err != nil { + t.Fatal(err) + } + got, err = FindConfig(sub) + if err != nil || got != cfgPath { + t.Fatalf("FindConfig = %q, %v; want %q", got, err, cfgPath) + } + + // A nearer config wins. + near := filepath.Join(dir, "a", "thriftls.json") + if err := os.WriteFile(near, []byte("{}"), 0o644); err != nil { + t.Fatal(err) + } + got, err = FindConfig(sub) + if err != nil || got != near { + t.Fatalf("FindConfig = %q, %v; want %q", got, err, near) + } +} + +func TestLoadAndEffective(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "thriftls.json") + content := `{ + "printWidth": 100, + "indent": " ", + "align": "disable" +}` + if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + + cfg, err := Load(cfgPath) + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.PrintWidth == nil || *cfg.PrintWidth != 100 { + t.Errorf("printWidth = %v, want 100", cfg.PrintWidth) + } + if cfg.Indent == nil || cfg.Indent.Value != " " { + t.Errorf("indent = %+v, want two spaces", cfg.Indent) + } + + p := Effective(cfg) + if p.PrintWidth == nil || *p.PrintWidth != 100 { + t.Errorf("effective printWidth = %v, want 100", p.PrintWidth) + } + // Unset config fields keep their defaults. + if p.FieldLineComma == nil || *p.FieldLineComma != "disable" { + t.Errorf("comma = %v, want default disable", p.FieldLineComma) + } + if p.Indent == nil || p.Indent.Value != " " { + t.Errorf("indent = %+v, want two spaces", p.Indent) + } + + // Effective with no config returns plain defaults. + d := Effective(nil) + if d.PrintWidth == nil || *d.PrintWidth != 80 { + t.Errorf("default printWidth = %v, want 80", d.PrintWidth) + } + if d.Indent == nil || d.Indent.Value != " " { + t.Errorf("default indent = %+v, want four spaces", d.Indent) + } +} + +func TestLoadRejectsUnknownOverrideKeys(t *testing.T) { + // Config files written for the old overrides feature should fail loudly + // rather than silently ignoring per-file settings. + dir := t.TempDir() + cfgPath := filepath.Join(dir, "thriftls.json") + content := `{ + "printWidth": 100, + "overrides": [ + { "files": ["**/gen/**"], "options": { "printWidth": 120 } } + ] +}` + if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + if _, err := Load(cfgPath); err == nil { + t.Fatal("Load accepted a config with an overrides key") + } +} diff --git a/parser/Makefile b/parser/Makefile deleted file mode 100644 index acd1fb9..0000000 --- a/parser/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -gen: - which pigeon >/dev/null || go install github.com/mna/pigeon@master - pigeon -o thrift.peg.go ./thrift.peg - -test-parser: - go test -v github.com/joyme123/thrift-ls/parser/test diff --git a/parser/ast.go b/parser/ast.go deleted file mode 100644 index 49e1524..0000000 --- a/parser/ast.go +++ /dev/null @@ -1,4431 +0,0 @@ -package parser - -import ( - "path" - "strings" - "unicode/utf8" -) - -type Node interface { - // position of first charactor of this node - Pos() Position - // position of first charactor immediately after this node - End() Position - - Contains(pos Position) bool - - Children() []Node - - Type() string - - IsBadNode() bool - ChildrenBadNode() bool - - // Equals checks ast equals between two node - Equals(node Node) bool -} - -type Document struct { - Filename string - - BadHeaders []*BadHeader - Includes []*Include - CPPIncludes []*CPPInclude - Namespaces []*Namespace - - Consts []*Const - Typedefs []*Typedef - Enums []*Enum - Services []*Service - Structs []*Struct - Unions []*Union - Exceptions []*Exception - BadDefinitions []*BadDefinition - - Comments []*Comment // Comments at end of doc - - Nodes []Node - - Location -} - -func NewDocument(headers []Header, defs []Definition, comments []*Comment, loc Location) *Document { - doc := &Document{ - Location: loc, - } - - for _, header := range headers { - switch header.Type() { - case "Include": - doc.Includes = append(doc.Includes, header.(*Include)) - case "CPPInclude": - doc.CPPIncludes = append(doc.CPPIncludes, header.(*CPPInclude)) - case "Namespace": - doc.Namespaces = append(doc.Namespaces, header.(*Namespace)) - case "BadHeader": - doc.BadHeaders = append(doc.BadHeaders, header.(*BadHeader)) - } - doc.Nodes = append(doc.Nodes, header) - } - - for _, def := range defs { - switch def.Type() { - case "Const": - doc.Consts = append(doc.Consts, def.(*Const)) - case "Typedef": - doc.Typedefs = append(doc.Typedefs, def.(*Typedef)) - case "Enum": - doc.Enums = append(doc.Enums, def.(*Enum)) - case "Service": - doc.Services = append(doc.Services, def.(*Service)) - case "Struct": - doc.Structs = append(doc.Structs, def.(*Struct)) - case "Union": - doc.Unions = append(doc.Unions, def.(*Union)) - case "Exception": - doc.Exceptions = append(doc.Exceptions, def.(*Exception)) - case "BadDefinition": - doc.BadDefinitions = append(doc.BadDefinitions, def.(*BadDefinition)) - } - doc.Nodes = append(doc.Nodes, def) - } - doc.Comments = comments - for _, comment := range comments { - doc.Nodes = append(doc.Nodes, comment) - } - return doc -} - -func (d *Document) Children() []Node { - return d.Nodes -} - -func (d *Document) Type() string { - return "Document" -} - -func (d *Document) IsBadNode() bool { - return false -} - -func (d *Document) ChildrenBadNode() bool { - children := d.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (d *Document) Equals(node Node) bool { - doc, ok := node.(*Document) - if !ok { - return false - } - - if (d == nil && doc != nil) || - (d != nil && doc == nil) { - return false - } else if d == nil && doc == nil { - return true - } - - if len(doc.Nodes) != len(d.Nodes) { - return false - } - - for i := range d.Nodes { - if !d.Nodes[i].Equals(doc.Nodes[i]) { - return false - } - } - - return true -} - -type Header interface { - Type() string - SetComments(comments []*Comment, endLineComments []*Comment) - SetLocation(loc Location) - Node -} - -type BadHeader struct { - BadNode bool - Location -} - -func NewBadHeader(loc Location) *BadHeader { - return &BadHeader{ - BadNode: true, - Location: loc, - } -} - -func (h *BadHeader) Type() string { - return "BadHeader" -} - -func (h *BadHeader) Children() []Node { - return nil -} - -func (h *BadHeader) IsBadNode() bool { - return true -} - -func (h *BadHeader) ChildrenBadNode() bool { - return false -} - -func (h *BadHeader) SetComments([]*Comment, []*Comment) { - -} - -func (h *BadHeader) Equals(node Node) bool { - hn, ok := node.(*BadHeader) - if !ok { - return false - } - - if (h != nil && hn == nil) || - (h == nil && hn != nil) { - return false - } - - return true -} - -func (h *BadHeader) SetLocation(loc Location) { - h.Location = loc -} - -type KeywordLiteral struct { - Text string - BadNode bool - Location -} - -func NewKeywordLiteral(c *current) *KeywordLiteral { - return &KeywordLiteral{ - Text: string(c.text), - Location: NewLocationFromCurrent(c), - } -} - -func NewBadKeywordLiteral(c *current) *KeywordLiteral { - return &KeywordLiteral{ - Text: string(c.text), - BadNode: true, - Location: NewLocationFromCurrent(c), - } -} - -func (k *KeywordLiteral) Type() string { - return "KeywordLiteral" -} - -func (k *KeywordLiteral) IsBadNode() bool { - return k.BadNode -} - -func (k *KeywordLiteral) Children() []Node { - return nil -} - -func (k *KeywordLiteral) ChildrenBadNode() bool { - return false -} - -func (k *KeywordLiteral) Equals(node Node) bool { - kl, ok := node.(*KeywordLiteral) - if !ok { - return false - } - - if (k == nil && kl != nil) || - (k != nil && kl == nil) { - return false - } else if k == nil && kl == nil { - return true - } - - if k.BadNode != kl.BadNode { - return false - } - - return k.Text == kl.Text -} - -type Keyword struct { - Comments []*Comment - Literal *KeywordLiteral - - BadNode bool - Location -} - -func NewKeyword(comments []*Comment, literal *KeywordLiteral, loc Location) Keyword { - return Keyword{ - Literal: literal, - Comments: comments, - Location: loc, - } -} - -func (i *Keyword) Children() []Node { - return nil -} - -func (i *Keyword) IsBadNode() bool { - return i.BadNode -} - -func (i *Keyword) ChildrenBadNode() bool { - return false -} - -func (i *Keyword) Equals(k *Keyword) bool { - if (i == nil && k != nil) || - (i != nil && k == nil) { - return false - } else if i == nil && k == nil { - return true - } - - if i.BadNode != k.BadNode { - return false - } - - if !i.Literal.Equals(k.Literal) { - return false - } - - if len(i.Comments) != len(k.Comments) { - return false - } - - for n := range i.Comments { - if !i.Comments[n].Equals(k.Comments[n]) { - return false - } - } - - return true -} - -type IncludeKeyword struct { - Keyword -} - -func (i *IncludeKeyword) Type() string { - return "IncludeKeyword" -} - -func (i *IncludeKeyword) Equals(node Node) bool { - ik, ok := node.(*IncludeKeyword) - if !ok { - return false - } - - if (i == nil && ik != nil) || - (i != nil && ik == nil) { - return false - } else if i == nil && ik == nil { - return true - } - - return ik.Keyword.Equals(&i.Keyword) -} - -type Include struct { - IncludeKeyword *IncludeKeyword - Path *Literal - - Comments []*Comment - EndLineComments []*Comment - - BadNode bool - Location -} - -func NewInclude(keyword *IncludeKeyword, path *Literal, loc Location) *Include { - return &Include{ - IncludeKeyword: keyword, - Location: loc, - Path: path, - } -} - -func NewBadInclude(loc Location) *Include { - return &Include{ - BadNode: true, - Location: loc, - } -} - -func (i *Include) Type() string { - return "Include" -} - -func (i *Include) SetComments(comments []*Comment, endLineComments []*Comment) { - i.Comments = comments - i.EndLineComments = endLineComments -} - -func (i *Include) Name() string { - _, file := path.Split(i.Path.Value.Text) - name := strings.TrimRight(file, path.Ext(file)) - return name -} - -func (i *Include) Children() []Node { - nodes := []Node{i.IncludeKeyword, i.Path} - - for _, com := range i.Comments { - nodes = append(nodes, com) - } - for _, com := range i.EndLineComments { - nodes = append(nodes, com) - } - - return nodes -} - -func (i *Include) IsBadNode() bool { - return i.BadNode -} - -func (i *Include) ChildrenBadNode() bool { - children := i.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (i *Include) SetLocation(loc Location) { - i.Location = loc -} - -func (i *Include) Equals(node Node) bool { - in, ok := node.(*Include) - if !ok { - return false - } - - if (i == nil && in != nil) || - (i != nil && in == nil) { - return false - } else if i == nil && in == nil { - return true - } - - if i.BadNode != in.BadNode { - return false - } - - if !i.IncludeKeyword.Equals(in.IncludeKeyword) { - return false - } - - if !i.Path.Equals(in.Path) { - return false - } - - if len(i.Comments) != len(in.Comments) { - return false - } - - for n := range i.Comments { - if !i.Comments[n].Equals(in.Comments[n]) { - return false - } - } - - if len(i.EndLineComments) != len(in.EndLineComments) { - return false - } - - for n := range i.EndLineComments { - if !i.EndLineComments[n].Equals(in.EndLineComments[n]) { - return false - } - } - - return true -} - -type CPPIncludeKeyword struct { - Keyword -} - -func (c *CPPIncludeKeyword) Type() string { - return "CPPIncludeKeyword" -} - -func (c *CPPIncludeKeyword) Equals(node Node) bool { - cn, ok := node.(*CPPIncludeKeyword) - if !ok { - return false - } - - if (c == nil && cn != nil) || - (c != nil && cn == nil) { - return false - } else if c == nil && cn == nil { - return true - } - - return c.Keyword.Equals(&cn.Keyword) -} - -type CPPInclude struct { - CPPIncludeKeyword *CPPIncludeKeyword - Path *Literal - - Comments []*Comment - EndLineComments []*Comment - - BadNode bool - Location -} - -func NewCPPInclude(keyword *CPPIncludeKeyword, path *Literal, loc Location) *CPPInclude { - return &CPPInclude{ - CPPIncludeKeyword: keyword, - Location: loc, - Path: path, - } -} - -func NewBadCPPInclude(loc Location) *CPPInclude { - return &CPPInclude{ - BadNode: true, - Location: loc, - } -} - -func (i *CPPInclude) Type() string { - return "CPPInclude" -} - -func (i *CPPInclude) SetComments(comments []*Comment, endLineComments []*Comment) { - i.Comments = comments - i.EndLineComments = endLineComments -} - -func (i *CPPInclude) Children() []Node { - res := []Node{i.CPPIncludeKeyword, i.Path} - for _, com := range i.Comments { - res = append(res, com) - } - for _, com := range i.EndLineComments { - res = append(res, com) - } - return res -} - -func (i *CPPInclude) IsBadNode() bool { - return i.BadNode -} - -func (i *CPPInclude) ChildrenBadNode() bool { - children := i.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - - return false -} - -func (i *CPPInclude) SetLocation(loc Location) { - i.Location = loc -} - -func (i *CPPInclude) Equals(node Node) bool { - cn, ok := node.(*CPPInclude) - if !ok { - return false - } - - if (i == nil && cn != nil) || - (i != nil && cn == nil) { - return false - } else if i == nil && cn == nil { - return true - } - - if i.BadNode != cn.BadNode { - return false - } - - if !i.CPPIncludeKeyword.Equals(cn.CPPIncludeKeyword) { - return false - } - - if !i.Path.Equals(cn.Path) { - return false - } - - if len(i.Comments) != len(cn.Comments) { - return false - } - - for n := range i.Comments { - if !i.Comments[n].Equals(cn.Comments[n]) { - return false - } - } - - if len(i.EndLineComments) != len(cn.EndLineComments) { - return false - } - - for n := range i.EndLineComments { - if !i.EndLineComments[n].Equals(cn.EndLineComments[n]) { - return false - } - } - - return true -} - -type NamespaceKeyword struct { - Keyword -} - -func (n *NamespaceKeyword) Type() string { - return "NamespaceKeyword" -} - -func (n *NamespaceKeyword) Equals(node Node) bool { - nn, ok := node.(*NamespaceKeyword) - if !ok { - return false - } - - if (n == nil && nn != nil) || - (n != nil && nn == nil) { - return false - } else if n == nil && nn == nil { - return true - } - - return n.Keyword.Equals(&nn.Keyword) -} - -type NamespaceScope struct { - Identifier -} - -func (ns *NamespaceScope) Equals(node Node) bool { - nn, ok := node.(*NamespaceScope) - if !ok { - return false - } - - if (ns == nil && nn != nil) || - (ns != nil && nn == nil) { - return false - } else if ns == nil && nn == nil { - return true - } - - return ns.Identifier.Equals(&nn.Identifier) -} - -type Namespace struct { - NamespaceKeyword *NamespaceKeyword - Language *NamespaceScope - Name *Identifier - - Annotations *Annotations - Comments []*Comment - EndLineComments []*Comment - - BadNode bool - Location -} - -func NewNamespace(keyword *NamespaceKeyword, language *NamespaceScope, name *Identifier, annotations *Annotations, loc Location) *Namespace { - return &Namespace{ - NamespaceKeyword: keyword, - Language: language, - Name: name, - Annotations: annotations, - - Location: loc, - } -} - -func NewBadNamespace(loc Location) *Namespace { - return &Namespace{ - BadNode: true, - Location: loc, - } -} - -func (n *Namespace) Type() string { - return "Namespace" -} - -func (n *Namespace) SetComments(comments []*Comment, endLineComments []*Comment) { - n.Comments = comments - n.EndLineComments = endLineComments -} - -func (n *Namespace) Children() []Node { - ret := []Node{n.NamespaceKeyword, n.Language, n.Name} - - for i := range n.Comments { - ret = append(ret, n.Comments[i]) - } - for i := range n.EndLineComments { - ret = append(ret, n.EndLineComments[i]) - } - - if n.Annotations != nil { - ret = append(ret, n.Annotations) - } - - return ret -} - -func (n *Namespace) IsBadNode() bool { - return n.BadNode -} - -func (n *Namespace) ChildrenBadNode() bool { - children := n.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (n *Namespace) SetLocation(loc Location) { - n.Location = loc -} - -func (n *Namespace) Equals(node Node) bool { - nn, ok := node.(*Namespace) - if !ok { - return false - } - - if (n == nil && nn != nil) || - (n != nil && nn == nil) { - return false - } else if n == nil && nn == nil { - return true - } - - if n.BadNode != nn.BadNode { - return false - } - - if !n.NamespaceKeyword.Equals(nn.NamespaceKeyword) { - return false - } - - if !n.Language.Equals(nn.Language) { - return false - } - - if !n.Name.Equals(nn.Name) { - return false - } - - if !n.Annotations.Equals(nn.Annotations) { - return false - } - - if len(n.Comments) != len(nn.Comments) { - return false - } - - for i := range n.Comments { - if !n.Comments[i].Equals(nn.Comments[i]) { - return false - } - } - - if len(n.EndLineComments) != len(nn.EndLineComments) { - return false - } - - for i := range n.EndLineComments { - if !n.EndLineComments[i].Equals(nn.EndLineComments[i]) { - return false - } - } - - return true -} - -type Definition interface { - Node - Type() string - SetComments(comments []*Comment, endLineComments []*Comment) - SetAnnotations(annotations *Annotations) - SetLocation(loc Location) -} - -type BadDefinition struct { - BadNode bool - Location -} - -func NewBadDefinition(loc Location) *BadDefinition { - return &BadDefinition{ - BadNode: true, - Location: loc, - } -} - -func (d *BadDefinition) Type() string { - return "Definition" -} - -func (d *BadDefinition) Children() []Node { - return nil -} - -func (d *BadDefinition) SetComments([]*Comment, []*Comment) { -} - -func (d *BadDefinition) SetAnnotations(annos *Annotations) { - -} - -func (d *BadDefinition) SetLocation(loc Location) { - d.Location = loc -} - -func (d *BadDefinition) IsBadNode() bool { - return true -} - -func (d *BadDefinition) ChildrenBadNode() bool { - return false -} - -func (d *BadDefinition) Equals(node Node) bool { - dn, ok := node.(*BadDefinition) - if !ok { - return false - } - - if (d == nil && dn != nil) || - (d != nil && dn == nil) { - return false - } else if d == nil && dn == nil { - return true - } - - if d.BadNode != dn.BadNode { - return false - } - - return true -} - -type StructKeyword struct { - Keyword -} - -func (s *StructKeyword) Type() string { - return "StructKeyword" -} - -func (s *StructKeyword) Equals(node Node) bool { - sn, ok := node.(*StructKeyword) - if !ok { - return false - } - - if (s == nil && sn != nil) || - (s != nil && sn == nil) { - return false - } else if s == nil && sn == nil { - return true - } - - return s.Keyword.Equals(&sn.Keyword) -} - -type LCurKeyword struct { - Keyword -} - -func (s *LCurKeyword) Type() string { - return "LCurKeyword" -} - -func (s *LCurKeyword) Equals(node Node) bool { - sn, ok := node.(*LCurKeyword) - if !ok { - return false - } - - if (s == nil && sn != nil) || - (s != nil && sn == nil) { - return false - } else if s == nil && sn == nil { - return true - } - - return s.Keyword.Equals(&sn.Keyword) -} - -type RCurKeyword struct { - Keyword -} - -func (s *RCurKeyword) Type() string { - return "RCurKeyword" -} - -func (s *RCurKeyword) Equals(node Node) bool { - sn, ok := node.(*RCurKeyword) - if !ok { - return false - } - - if (s == nil && sn != nil) || - (s != nil && sn == nil) { - return false - } else if s == nil && sn == nil { - return true - } - - return s.Keyword.Equals(&sn.Keyword) -} - -type Struct struct { - StructKeyword *StructKeyword - LCurKeyword *LCurKeyword - RCurKeyword *RCurKeyword - Identifier *Identifier - Fields []*Field - - Comments []*Comment - EndLineComments []*Comment - Annotations *Annotations - - BadNode bool - Location -} - -func NewStruct(structKeyword *StructKeyword, lCurKeyword *LCurKeyword, rCurKeyword *RCurKeyword, identifier *Identifier, fields []*Field, loc Location) *Struct { - return &Struct{ - StructKeyword: structKeyword, - LCurKeyword: lCurKeyword, - RCurKeyword: rCurKeyword, - Identifier: identifier, - Fields: fields, - Location: loc, - } -} - -func NewBadStruct(loc Location) *Struct { - return &Struct{ - BadNode: true, - Location: loc, - } -} - -func (s *Struct) Type() string { - return "Struct" -} - -func (s *Struct) SetComments(comments []*Comment, endLineComments []*Comment) { - s.Comments = comments - s.EndLineComments = endLineComments -} - -func (s *Struct) SetAnnotations(annos *Annotations) { - s.Annotations = annos -} - -func (s *Struct) Children() []Node { - nodes := []Node{s.StructKeyword, s.LCurKeyword, s.RCurKeyword, s.Identifier} - for i := range s.Fields { - nodes = append(nodes, s.Fields[i]) - } - - for i := range s.Comments { - nodes = append(nodes, s.Comments[i]) - } - for i := range s.EndLineComments { - nodes = append(nodes, s.EndLineComments[i]) - } - if s.Annotations != nil { - nodes = append(nodes, s.Annotations) - } - - return nodes -} - -func (s *Struct) IsBadNode() bool { - return s.BadNode -} - -func (s *Struct) ChildrenBadNode() bool { - children := s.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (s *Struct) SetLocation(loc Location) { - s.Location = loc -} - -func (s *Struct) Equals(node Node) bool { - sn, ok := node.(*Struct) - if !ok { - return false - } - - if (s == nil && sn != nil) || - (s != nil && sn == nil) { - return false - } else if s == nil && sn == nil { - return true - } - - if s.BadNode != sn.BadNode { - return false - } - - if !s.StructKeyword.Equals(sn.StructKeyword) { - return false - } - - if !s.LCurKeyword.Equals(sn.LCurKeyword) { - return false - } - - if !s.RCurKeyword.Equals(sn.RCurKeyword) { - return false - } - - if !s.Identifier.Equals(sn.Identifier) { - return false - } - - if len(s.Fields) != len(sn.Fields) { - return false - } - - for i := range s.Fields { - if !s.Fields[i].Equals(sn.Fields[i]) { - return false - } - } - - if len(s.Comments) != len(sn.Comments) { - return false - } - - for i := range s.Comments { - if !s.Comments[i].Equals(sn.Comments[i]) { - return false - } - } - - if len(s.EndLineComments) != len(sn.EndLineComments) { - return false - } - - for i := range s.EndLineComments { - if !s.EndLineComments[i].Equals(sn.EndLineComments[i]) { - return false - } - } - - if !s.Annotations.Equals(sn.Annotations) { - return false - } - - return true -} - -type ConstKeyword struct { - Keyword -} - -func (c *ConstKeyword) Type() string { - return "ConstKeyword" -} - -func (c *ConstKeyword) Equals(node Node) bool { - cn, ok := node.(*ConstKeyword) - if !ok { - return false - } - - if (c == nil && cn != nil) || - (c != nil && cn == nil) { - return false - } else if c == nil && cn == nil { - return true - } - - return c.Keyword.Equals(&cn.Keyword) -} - -type EqualKeyword struct { - Keyword -} - -func NewBadEqualKeyword() *EqualKeyword { - return &EqualKeyword{ - Keyword: Keyword{ - BadNode: true, - }, - } -} - -func (e *EqualKeyword) Type() string { - return "EqualKeyword" -} - -func (e *EqualKeyword) Equals(node Node) bool { - en, ok := node.(*EqualKeyword) - if !ok { - return false - } - - if (e == nil && en != nil) || - (e != nil && en == nil) { - return false - } else if e == nil && en == nil { - return true - } - - return e.Keyword.Equals(&en.Keyword) -} - -type ListSeparatorKeyword struct { - Keyword - Text string // , or ; -} - -func (e *ListSeparatorKeyword) Type() string { - return "ListSeparator" -} - -func (e *ListSeparatorKeyword) Equals(node Node) bool { - en, ok := node.(*ListSeparatorKeyword) - if !ok { - return false - } - - if (e == nil && en != nil) || - (e != nil && en == nil) { - return false - } else if e == nil && en == nil { - return true - } - - return e.Keyword.Equals(&en.Keyword) -} - -type Const struct { - ConstKeyword *ConstKeyword - EqualKeyword *EqualKeyword - ListSeparatorKeyword *ListSeparatorKeyword // can be nil - Name *Identifier - ConstType *FieldType - Value *ConstValue - - Comments []*Comment - EndLineComments []*Comment - Annotations *Annotations - - BadNode bool - Location -} - -func NewConst(constKeyword *ConstKeyword, equalKeyword *EqualKeyword, listSeparatorKeyword *ListSeparatorKeyword, name *Identifier, t *FieldType, v *ConstValue, loc Location) *Const { - return &Const{ - ConstKeyword: constKeyword, - EqualKeyword: equalKeyword, - ListSeparatorKeyword: listSeparatorKeyword, - Name: name, - ConstType: t, - Value: v, - Location: loc, - } -} - -func NewBadConst(loc Location) *Const { - return &Const{ - BadNode: true, - Location: loc, - } -} - -func (c *Const) Type() string { - return "Const" -} - -func (c *Const) SetComments(comments []*Comment, endLineComments []*Comment) { - c.Comments = comments - c.EndLineComments = endLineComments -} - -func (c *Const) SetAnnotations(annos *Annotations) { - c.Annotations = annos -} - -func (c *Const) Children() []Node { - res := []Node{c.ConstKeyword, c.EqualKeyword, c.Name, c.ConstType, c.Value} - if c.ListSeparatorKeyword != nil { - res = append(res, c.ListSeparatorKeyword) - } - - for i := range c.Comments { - res = append(res, c.Comments[i]) - } - for i := range c.EndLineComments { - res = append(res, c.EndLineComments[i]) - } - - if c.Annotations != nil { - res = append(res, c.Annotations) - } - - return res -} - -func (c *Const) IsBadNode() bool { - return c.BadNode -} - -func (c *Const) ChildrenBadNode() bool { - children := c.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (c *Const) SetLocation(loc Location) { - c.Location = loc -} - -func (c *Const) Equals(node Node) bool { - cn, ok := node.(*Const) - if !ok { - return false - } - - if (c == nil && cn != nil) || - (c != nil && cn == nil) { - return false - } else if c == nil && cn == nil { - return true - } - - if c.BadNode != cn.BadNode { - return false - } - - if !c.ConstKeyword.Equals(cn.ConstKeyword) { - return false - } - - if !c.EqualKeyword.Equals(cn.EqualKeyword) { - return false - } - - if !c.ListSeparatorKeyword.Equals(cn.ListSeparatorKeyword) { - return false - } - - if !c.Name.Equals(cn.Name) { - return false - } - - if !c.ConstType.Equals(cn.ConstType) { - return false - } - if !c.Value.Equals(cn.Value) { - return false - } - - if len(c.Comments) != len(cn.Comments) { - return false - } - - for i := range c.Comments { - if !c.Comments[i].Equals(cn.Comments[i]) { - return false - } - } - - if len(c.EndLineComments) != len(cn.EndLineComments) { - return false - } - - for i := range c.EndLineComments { - if !c.EndLineComments[i].Equals(cn.EndLineComments[i]) { - return false - } - } - - if !c.Annotations.Equals(cn.Annotations) { - return false - } - - return true -} - -type TypedefKeyword struct { - Keyword -} - -func (t *TypedefKeyword) Type() string { - return "TypedefKeyword" -} - -func (t *TypedefKeyword) Equals(node Node) bool { - tn, ok := node.(*TypedefKeyword) - if !ok { - return false - } - - if (t == nil && tn != nil) || - (t != nil && tn == nil) { - return false - } else if t == nil && tn == nil { - return true - } - - return t.Keyword.Equals(&tn.Keyword) -} - -type Typedef struct { - TypedefKeyword *TypedefKeyword - T *FieldType - Alias *Identifier - - Comments []*Comment - EndLineComments []*Comment - Annotations *Annotations - BadNode bool - - Location -} - -func NewTypedef(keyword *TypedefKeyword, t *FieldType, alias *Identifier, loc Location) *Typedef { - return &Typedef{ - TypedefKeyword: keyword, - T: t, - Alias: alias, - Location: loc, - } -} - -func NewBadTypedef(loc Location) *Typedef { - return &Typedef{ - BadNode: true, - Location: loc, - } -} - -func (t *Typedef) Type() string { - return "Typedef" -} - -func (t *Typedef) SetComments(comments []*Comment, endLineComments []*Comment) { - t.Comments = comments - t.EndLineComments = endLineComments -} - -func (t *Typedef) SetAnnotations(annos *Annotations) { - t.Annotations = annos -} - -func (t *Typedef) Children() []Node { - nodes := []Node{t.TypedefKeyword, t.T, t.Alias} - - for i := range t.Comments { - nodes = append(nodes, t.Comments[i]) - } - for i := range t.EndLineComments { - nodes = append(nodes, t.EndLineComments[i]) - } - if t.Annotations != nil { - nodes = append(nodes, t.Annotations) - } - - return nodes -} - -func (t *Typedef) IsBadNode() bool { - return t.BadNode -} - -func (t *Typedef) ChildrenBadNode() bool { - children := t.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (t *Typedef) SetLocation(loc Location) { - t.Location = loc -} - -func (t *Typedef) Equals(node Node) bool { - tn, ok := node.(*Typedef) - if !ok { - return false - } - - if (t == nil && tn != nil) || - (t != nil && tn == nil) { - return false - } else if t == nil && tn == nil { - return true - } - - if t.BadNode != tn.BadNode { - return false - } - - if !t.TypedefKeyword.Equals(tn.TypedefKeyword) { - return false - } - - if !t.T.Equals(tn.T) { - return false - } - - if !t.Alias.Equals(tn.Alias) { - return false - } - - if len(t.Comments) != len(tn.Comments) { - return false - } - - for i := range t.Comments { - if !t.Comments[i].Equals(tn.Comments[i]) { - return false - } - } - - if len(t.EndLineComments) != len(tn.EndLineComments) { - return false - } - - for i := range t.EndLineComments { - if !t.EndLineComments[i].Equals(tn.EndLineComments[i]) { - return false - } - } - - if !t.Annotations.Equals(tn.Annotations) { - return false - } - - return true -} - -type EnumKeyword struct { - Keyword -} - -func (e *EnumKeyword) Type() string { - return "EnumKeyword" -} - -func (e *EnumKeyword) Equals(node Node) bool { - en, ok := node.(*EnumKeyword) - if !ok { - return false - } - - if (e == nil && en != nil) || - (e != nil && en == nil) { - return false - } else if e == nil && en == nil { - return true - } - - return e.Keyword.Equals(&en.Keyword) -} - -type Enum struct { - EnumKeyword *EnumKeyword - LCurKeyword *LCurKeyword - RCurKeyword *RCurKeyword - Name *Identifier - Values []*EnumValue - - Comments []*Comment - EndLineComments []*Comment - Annotations *Annotations - - BadNode bool - Location -} - -func NewEnum(enumKeyword *EnumKeyword, lCurKeyword *LCurKeyword, rCurKeyword *RCurKeyword, name *Identifier, values []*EnumValue, loc Location) *Enum { - return &Enum{ - EnumKeyword: enumKeyword, - LCurKeyword: lCurKeyword, - RCurKeyword: rCurKeyword, - Name: name, - Values: values, - Location: loc, - } -} - -func NewBadEnum(loc Location) *Enum { - return &Enum{ - BadNode: true, - Location: loc, - } -} - -func (e *Enum) Type() string { - return "Enum" -} - -func (e *Enum) SetComments(comments []*Comment, endlineComments []*Comment) { - e.Comments = comments - e.EndLineComments = endlineComments -} - -func (e *Enum) SetAnnotations(annos *Annotations) { - e.Annotations = annos -} - -func (e *Enum) Children() []Node { - nodes := []Node{e.Name} - for i := range e.Values { - nodes = append(nodes, e.Values[i]) - } - - for i := range e.Comments { - nodes = append(nodes, e.Comments[i]) - } - for i := range e.EndLineComments { - nodes = append(nodes, e.EndLineComments[i]) - } - if e.Annotations != nil { - nodes = append(nodes, e.Annotations) - } - - return nodes -} - -func (e *Enum) IsBadNode() bool { - return e.BadNode -} - -func (e *Enum) ChildrenBadNode() bool { - children := e.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (e *Enum) SetLocation(loc Location) { - e.Location = loc -} - -func (e *Enum) Equals(node Node) bool { - en, ok := node.(*Enum) - if !ok { - return false - } - - if (e == nil && en != nil) || - (e != nil && en == nil) { - return false - } else if e == nil && en == nil { - return true - } - - if e.BadNode != en.BadNode { - return false - } - - if !e.EnumKeyword.Equals(en.EnumKeyword) { - return false - } - - if !e.LCurKeyword.Equals(en.LCurKeyword) { - return false - } - - if !e.RCurKeyword.Equals(en.RCurKeyword) { - return false - } - - if !e.Name.Equals(en.Name) { - return false - } - if len(e.Values) != len(en.Values) { - return false - } - for i := range e.Values { - if !e.Values[i].Equals(en.Values[i]) { - return false - } - } - - if len(e.Comments) != len(en.Comments) { - return false - } - - for i := range e.Comments { - if !e.Comments[i].Equals(en.Comments[i]) { - return false - } - } - - if len(e.EndLineComments) != len(en.EndLineComments) { - return false - } - - for i := range e.EndLineComments { - if !e.EndLineComments[i].Equals(en.EndLineComments[i]) { - return false - } - } - - if !e.Annotations.Equals(en.Annotations) { - return false - } - - return true -} - -type EnumValue struct { - ListSeparatorKeyword *ListSeparatorKeyword // can be nil - EqualKeyword *EqualKeyword // can be nil - Name *Identifier - ValueNode *ConstValue - Value int64 // Value only record enum value. it is not a ast node - Annotations *Annotations - Comments []*Comment - EndLineComments []*Comment - - BadNode bool - Location -} - -func NewBadEnumValue(loc Location) *EnumValue { - return &EnumValue{ - BadNode: true, - Location: loc, - } -} - -func NewEnumValue(listSeparatorKeyword *ListSeparatorKeyword, equalKeyword *EqualKeyword, name *Identifier, valueNode *ConstValue, value int64, annotations *Annotations, loc Location) *EnumValue { - return &EnumValue{ - ListSeparatorKeyword: listSeparatorKeyword, - EqualKeyword: equalKeyword, - Name: name, - ValueNode: valueNode, - Value: value, - Annotations: annotations, - Location: loc, - } -} - -func (e *EnumValue) Children() []Node { - nodes := []Node{e.Name} - if e.ValueNode != nil { - nodes = append(nodes, e.ValueNode) - } - if e.ListSeparatorKeyword != nil { - nodes = append(nodes, e.ListSeparatorKeyword) - } - if e.EqualKeyword != nil { - nodes = append(nodes, e.EqualKeyword) - } - for i := range e.Comments { - nodes = append(nodes, e.Comments[i]) - } - for i := range e.EndLineComments { - nodes = append(nodes, e.EndLineComments[i]) - } - if e.Annotations != nil { - nodes = append(nodes, e.Annotations) - } - - return nodes -} - -func (e *EnumValue) Type() string { - return "EnumValue" -} - -func (e *EnumValue) SetComments(comments []*Comment, endLineComments []*Comment) { - e.Comments = comments - e.EndLineComments = endLineComments -} - -func (e *EnumValue) IsBadNode() bool { - return e.BadNode -} - -func (e *EnumValue) ChildrenBadNode() bool { - children := e.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (e *EnumValue) Equals(node Node) bool { - en, ok := node.(*EnumValue) - if !ok { - return false - } - - if (e == nil && en != nil) || - (e != nil && en == nil) { - return false - } else if e == nil && en == nil { - return true - } - - if e.BadNode != en.BadNode { - return true - } - - // comma 不影响语义 - // if !e.ListSeparatorKeyword.Equals(en.ListSeparatorKeyword) { - // return false - // } - - if !e.EqualKeyword.Equals(en.EqualKeyword) { - return false - } - - if !e.Name.Equals(en.Name) { - return false - } - - if !e.ValueNode.Equals(en.ValueNode) { - return false - } - - if e.Value != en.Value { - return false - } - - if len(e.Comments) != len(en.Comments) { - return false - } - - for i := range e.Comments { - if !e.Comments[i].Equals(en.Comments[i]) { - return false - } - } - - if len(e.EndLineComments) != len(en.EndLineComments) { - return false - } - - for i := range e.EndLineComments { - if !e.EndLineComments[i].Equals(en.EndLineComments[i]) { - return false - } - } - - if !e.Annotations.Equals(en.Annotations) { - return false - } - - return true -} - -type ServiceKeyword struct { - Keyword -} - -func (s *ServiceKeyword) Type() string { - return "ServiceKeyword" -} - -func (s *ServiceKeyword) Equals(node Node) bool { - sn, ok := node.(*ServiceKeyword) - if !ok { - return false - } - - if (s == nil && sn != nil) || - (s != nil && sn == nil) { - return false - } else if s == nil && sn == nil { - return true - } - - return s.Keyword.Equals(&sn.Keyword) -} - -type ExtendsKeyword struct { - Keyword -} - -func (s *ExtendsKeyword) Type() string { - return "ExtendsKeyword" -} - -func (s *ExtendsKeyword) Equals(node Node) bool { - sn, ok := node.(*ExtendsKeyword) - if !ok { - return false - } - - if (s == nil && sn != nil) || - (s != nil && sn == nil) { - return false - } else if s == nil && sn == nil { - return true - } - - return s.Keyword.Equals(&sn.Keyword) -} - -type Service struct { - ServiceKeyword *ServiceKeyword - ExtendsKeyword *ExtendsKeyword // can be nil - LCurKeyword *LCurKeyword - RCurKeyword *RCurKeyword - Name *Identifier - Extends *Identifier - Functions []*Function - - Comments []*Comment - EndLineComments []*Comment - Annotations *Annotations - - BadNode bool - Location -} - -func NewService(serviceKeyword *ServiceKeyword, extendsKeyword *ExtendsKeyword, lCurKeyword *LCurKeyword, rCurKeyword *RCurKeyword, name *Identifier, extends *Identifier, fns []*Function, loc Location) *Service { - return &Service{ - ServiceKeyword: serviceKeyword, - ExtendsKeyword: extendsKeyword, - LCurKeyword: lCurKeyword, - RCurKeyword: rCurKeyword, - Name: name, - Extends: extends, - Functions: fns, - Location: loc, - } -} - -func NewBadService(loc Location) *Service { - return &Service{ - BadNode: true, - Location: loc, - } -} - -func (s *Service) Type() string { - return "Service" -} - -func (s *Service) SetComments(comments []*Comment, endLineComments []*Comment) { - s.Comments = comments - s.EndLineComments = endLineComments -} - -func (s *Service) SetAnnotations(annos *Annotations) { - s.Annotations = annos -} - -func (s *Service) Children() []Node { - nodes := []Node{s.ServiceKeyword, s.LCurKeyword, s.RCurKeyword} - if s.ExtendsKeyword != nil { - nodes = append(nodes, s.ExtendsKeyword) - } - if s.Name != nil { - nodes = append(nodes, s.Name) - } - if s.Extends != nil { - nodes = append(nodes, s.Extends) - } - for i := range s.Functions { - nodes = append(nodes, s.Functions[i]) - } - - for i := range s.Comments { - nodes = append(nodes, s.Comments[i]) - } - for i := range s.EndLineComments { - nodes = append(nodes, s.EndLineComments[i]) - } - if s.Annotations != nil { - nodes = append(nodes, s.Annotations) - } - - return nodes -} - -func (s *Service) IsBadNode() bool { - return s.BadNode -} - -func (s *Service) ChildrenBadNode() bool { - children := s.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (s *Service) SetLocation(loc Location) { - s.Location = loc -} - -func (s *Service) Equals(node Node) bool { - sn, ok := node.(*Service) - if !ok { - return false - } - - if (s == nil && sn != nil) || - (s != nil && sn == nil) { - return false - } else if s == nil && sn == nil { - return true - } - - if s.BadNode != sn.BadNode { - return false - } - - if !s.ServiceKeyword.Equals(sn.ServiceKeyword) { - return false - } - - if !s.ExtendsKeyword.Equals(sn.ExtendsKeyword) { - return false - } - - if !s.LCurKeyword.Equals(sn.LCurKeyword) { - return false - } - - if !s.RCurKeyword.Equals(sn.RCurKeyword) { - return false - } - - if !s.Name.Equals(sn.Name) { - return false - } - - if !s.Extends.Equals(sn.Extends) { - return false - } - - if len(s.Functions) != len(sn.Functions) { - return false - } - - for i := range s.Functions { - if !s.Functions[i].Equals(sn.Functions[i]) { - return false - } - } - - if len(s.Comments) != len(sn.Comments) { - return false - } - - for i := range s.Comments { - if !s.Comments[i].Equals(sn.Comments[i]) { - return false - } - } - - if len(s.EndLineComments) != len(sn.EndLineComments) { - return false - } - - for i := range s.EndLineComments { - if !s.EndLineComments[i].Equals(sn.EndLineComments[i]) { - return false - } - } - - if !s.Annotations.Equals(sn.Annotations) { - return false - } - - return true -} - -type OnewayKeyword struct { - Keyword -} - -func (o *OnewayKeyword) Type() string { - return "OnewayKeyword" -} - -func (o *OnewayKeyword) Equals(node Node) bool { - on, ok := node.(*OnewayKeyword) - if !ok { - return false - } - - if (o == nil && on != nil) || - (o != nil && on == nil) { - return false - } else if o == nil && on == nil { - return true - } - - return o.Keyword.Equals(&on.Keyword) -} - -type LParKeyword struct { - Keyword -} - -func (l *LParKeyword) Type() string { - return "LParKeyword" -} - -func (l *LParKeyword) Equals(node Node) bool { - ln, ok := node.(*LParKeyword) - if !ok { - return false - } - - if (l == nil && ln != nil) || - (l != nil && ln == nil) { - return false - } else if l == nil && ln == nil { - return true - } - - return l.Keyword.Equals(&ln.Keyword) -} - -type RParKeyword struct { - Keyword -} - -func (r *RParKeyword) Type() string { - return "RParKeyword" -} - -func (r *RParKeyword) Equals(node Node) bool { - rn, ok := node.(*RParKeyword) - if !ok { - return false - } - - if (r == nil && rn != nil) || - (r != nil && rn == nil) { - return false - } else if r == nil && rn == nil { - return true - } - - return r.Keyword.Equals(&rn.Keyword) -} - -type VoidKeyword struct { - Keyword -} - -func (v *VoidKeyword) Type() string { - return "VoidKeyword" -} - -func (v *VoidKeyword) Equals(node Node) bool { - vn, ok := node.(*VoidKeyword) - if !ok { - return false - } - - if (v == nil && vn != nil) || - (v != nil && vn == nil) { - return false - } else if v == nil && vn == nil { - return true - } - - return v.Keyword.Equals(&vn.Keyword) -} - -type ThrowsKeyword struct { - Keyword -} - -func (t *ThrowsKeyword) Type() string { - return "ThrowsKeyword" -} - -func (t *ThrowsKeyword) Equals(node Node) bool { - tn, ok := node.(*ThrowsKeyword) - if !ok { - return false - } - - if (t == nil && tn != nil) || - (t != nil && tn == nil) { - return false - } else if t == nil && tn == nil { - return true - } - - return t.Keyword.Equals(&tn.Keyword) -} - -type Throws struct { - ThrowsKeyword *ThrowsKeyword - LParKeyword *LParKeyword - RParKeyword *RParKeyword - - Fields []*Field - - BadNode bool - Location -} - -func NewThrows(throwsKeyword *ThrowsKeyword, lparKeyword *LParKeyword, rparKeyword *RParKeyword, fields []*Field, loc Location) *Throws { - return &Throws{ - ThrowsKeyword: throwsKeyword, - LParKeyword: lparKeyword, - RParKeyword: rparKeyword, - Fields: fields, - Location: loc, - } -} - -func (t *Throws) Type() string { - return "Throws" -} - -func (t *Throws) IsBadNode() bool { - return t.BadNode -} - -func (t *Throws) ChildrenBadNode() bool { - children := t.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (t *Throws) Children() []Node { - nodes := []Node{t.ThrowsKeyword, t.LParKeyword, t.RParKeyword} - for i := range t.Fields { - nodes = append(nodes, t.Fields[i]) - } - return nodes -} - -func (t *Throws) Equals(node Node) bool { - tn, ok := node.(*Throws) - if !ok { - return false - } - - if (t == nil && tn != nil) || - (t != nil && tn == nil) { - return false - } else if t == nil && tn == nil { - return true - } - - if t.BadNode != tn.BadNode { - return false - } - - if !t.ThrowsKeyword.Equals(tn.ThrowsKeyword) { - return false - } - - if !t.LParKeyword.Equals(tn.LParKeyword) { - return false - } - - if !t.RParKeyword.Equals(tn.RParKeyword) { - return false - } - - if len(t.Fields) != len(tn.Fields) { - return false - } - - for i := range t.Fields { - if !t.Fields[i].Equals(tn.Fields[i]) { - return false - } - } - - return true -} - -type Function struct { - LParKeyword *LParKeyword - RParKeyword *RParKeyword - ListSeparatorKeyword *ListSeparatorKeyword // can be nil - Name *Identifier - Oneway *OnewayKeyword // can be nil - Void *VoidKeyword // can be nil - FunctionType *FieldType - Arguments []*Field - Throws *Throws - Comments []*Comment - EndLineComments []*Comment - Annotations *Annotations - - BadNode bool - Location -} - -func NewFunction(lParKeyword *LParKeyword, rParKeyword *RParKeyword, listSeparatorKeyword *ListSeparatorKeyword, name *Identifier, oneway *OnewayKeyword, void *VoidKeyword, ft *FieldType, args []*Field, throws *Throws, comments []*Comment, endlineComments []*Comment, annotations *Annotations, loc Location) *Function { - return &Function{ - LParKeyword: lParKeyword, - RParKeyword: rParKeyword, - ListSeparatorKeyword: listSeparatorKeyword, - Name: name, - Oneway: oneway, - Void: void, - FunctionType: ft, - Arguments: args, - Throws: throws, - Comments: comments, - EndLineComments: endlineComments, - Annotations: annotations, - Location: loc, - } -} - -func NewBadFunction(loc Location) *Function { - return &Function{ - BadNode: true, - Location: loc, - } -} - -func (f *Function) Children() []Node { - nodes := []Node{f.LParKeyword, f.RParKeyword} - if f.Oneway != nil { - nodes = append(nodes, f.Oneway) - } - if f.Void != nil { - nodes = append(nodes, f.Void) - } - if f.ListSeparatorKeyword != nil { - nodes = append(nodes, f.ListSeparatorKeyword) - } - if f.Name != nil { - nodes = append(nodes, f.Name) - } - if f.FunctionType != nil { - nodes = append(nodes, f.FunctionType) - } - for i := range f.Arguments { - nodes = append(nodes, f.Arguments[i]) - } - if f.Throws != nil { - nodes = append(nodes, f.Throws) - } - for i := range f.Comments { - nodes = append(nodes, f.Comments[i]) - } - for i := range f.EndLineComments { - nodes = append(nodes, f.EndLineComments[i]) - } - if f.Annotations != nil { - nodes = append(nodes, f.Annotations) - } - - return nodes -} - -func (f *Function) Type() string { - return "Function" -} - -func (f *Function) IsBadNode() bool { - return f.BadNode -} - -func (f *Function) ChildrenBadNode() bool { - children := f.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (f *Function) Equals(node Node) bool { - fn, ok := node.(*Function) - if !ok { - return false - } - - if (f == nil && fn != nil) || - (f != nil && fn == nil) { - return false - } else if f == nil && fn == nil { - return true - } - - if f.BadNode != fn.BadNode { - return false - } - - if !f.LParKeyword.Equals(fn.LParKeyword) { - return false - } - - if !f.RParKeyword.Equals(fn.RParKeyword) { - return false - } - - // 格式化场景,会变更默认的分隔符,所以在 equals 时不需要比较 - // if !f.ListSeparatorKeyword.Equals(fn.ListSeparatorKeyword) { - // return false - // } - - if !f.Name.Equals(fn.Name) { - return false - } - - if !f.Oneway.Equals(fn.Oneway) { - return false - } - - if !f.Void.Equals(fn.Void) { - return false - } - - if !f.FunctionType.Equals(fn.FunctionType) { - return false - } - - if len(f.Arguments) != len(fn.Arguments) { - return false - } - - for i := range f.Arguments { - if !f.Arguments[i].Equals(fn.Arguments[i]) { - return false - } - } - - if !f.Throws.Equals(fn.Throws) { - return false - } - - if len(f.Comments) != len(fn.Comments) { - return false - } - - for i := range f.Comments { - if !f.Comments[i].Equals(fn.Comments[i]) { - return false - } - } - - if len(f.EndLineComments) != len(fn.EndLineComments) { - return false - } - - for i := range f.EndLineComments { - if !f.EndLineComments[i].Equals(fn.EndLineComments[i]) { - return false - } - } - - if !f.Annotations.Equals(fn.Annotations) { - return false - } - - return true -} - -type UnionKeyword struct { - Keyword -} - -func (u *UnionKeyword) Type() string { - return "UnionKeyword" -} - -func (u *UnionKeyword) Equals(node Node) bool { - un, ok := node.(*UnionKeyword) - if !ok { - return false - } - - if (u == nil && un != nil) || - (u != nil && un == nil) { - return false - } else if u == nil && un == nil { - return true - } - - return u.Keyword.Equals(&un.Keyword) -} - -type Union struct { - UnionKeyword *UnionKeyword - LCurKeyword *LCurKeyword - RCurKeyword *RCurKeyword - Name *Identifier - Fields []*Field - - Comments []*Comment - EndLineComments []*Comment - Annotations *Annotations - - BadNode bool - Location -} - -func NewUnion(unionKeyword *UnionKeyword, lCurKeyword *LCurKeyword, rCurKeyword *RCurKeyword, name *Identifier, fields []*Field, loc Location) *Union { - return &Union{ - UnionKeyword: unionKeyword, - LCurKeyword: lCurKeyword, - RCurKeyword: rCurKeyword, - Name: name, - Fields: fields, - Location: loc, - } -} - -func NewBadUnion(loc Location) *Union { - return &Union{ - BadNode: true, - Location: loc, - } -} - -func (u *Union) Type() string { - return "Union" -} - -func (u *Union) SetComments(comments []*Comment, endLineComments []*Comment) { - u.Comments = comments - u.EndLineComments = endLineComments -} - -func (u *Union) SetAnnotations(annos *Annotations) { - u.Annotations = annos -} - -func (u *Union) Children() []Node { - nodes := []Node{u.Name, u.UnionKeyword, u.LCurKeyword, u.RCurKeyword} - for i := range u.Fields { - nodes = append(nodes, u.Fields[i]) - } - for i := range u.Comments { - nodes = append(nodes, u.Comments[i]) - } - for i := range u.EndLineComments { - nodes = append(nodes, u.EndLineComments[i]) - } - - for i := range u.Comments { - nodes = append(nodes, u.Comments[i]) - } - for i := range u.EndLineComments { - nodes = append(nodes, u.EndLineComments[i]) - } - if u.Annotations != nil { - nodes = append(nodes, u.Annotations) - } - - return nodes -} - -func (u *Union) IsBadNode() bool { - return u.BadNode -} - -func (u *Union) ChildrenBadNode() bool { - children := u.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (u *Union) SetLocation(loc Location) { - u.Location = loc -} - -func (u *Union) Equals(node Node) bool { - un, ok := node.(*Union) - if !ok { - return false - } - - if (u == nil && un != nil) || - (u != nil && un == nil) { - return false - } else if u == nil && un == nil { - return true - } - - if !u.UnionKeyword.Equals(un.UnionKeyword) { - return false - } - - if !u.LCurKeyword.Equals(un.LCurKeyword) { - return false - } - - if !u.RCurKeyword.Equals(un.RCurKeyword) { - return false - } - - if !u.Name.Equals(un.Name) { - return false - } - - if len(u.Fields) != len(un.Fields) { - return false - } - - for i := range u.Fields { - if !u.Fields[i].Equals(un.Fields[i]) { - return false - } - } - - if len(u.Comments) != len(un.Comments) { - return false - } - - for i := range u.Comments { - if !u.Comments[i].Equals(un.Comments[i]) { - return false - } - } - - if len(u.EndLineComments) != len(un.EndLineComments) { - return false - } - - for i := range u.EndLineComments { - if !u.EndLineComments[i].Equals(un.EndLineComments[i]) { - return false - } - } - - if !u.Annotations.Equals(un.Annotations) { - return false - } - - return true -} - -type ExceptionKeyword struct { - Keyword -} - -func (e *ExceptionKeyword) Type() string { - return "ExceptionKeyword" -} - -func (e *ExceptionKeyword) Equals(node Node) bool { - en, ok := node.(*ExceptionKeyword) - if !ok { - return false - } - - if (e == nil && en != nil) || - (e != nil && en == nil) { - return false - } else if e == nil && en == nil { - return true - } - - return e.Keyword.Equals(&en.Keyword) -} - -type Exception struct { - ExceptionKeyword *ExceptionKeyword - LCurKeyword *LCurKeyword - RCurKeyword *RCurKeyword - Name *Identifier - Fields []*Field - - Comments []*Comment - EndLineComments []*Comment - Annotations *Annotations - - BadNode bool - Location -} - -func NewException(exceptionKeyword *ExceptionKeyword, lCurKeyword *LCurKeyword, rCurKeyword *RCurKeyword, name *Identifier, fields []*Field, loc Location) *Exception { - return &Exception{ - ExceptionKeyword: exceptionKeyword, - LCurKeyword: lCurKeyword, - RCurKeyword: rCurKeyword, - Name: name, - Fields: fields, - Location: loc, - } -} - -func NewBadException(loc Location) *Exception { - return &Exception{ - BadNode: true, - Location: loc, - } -} - -func (e *Exception) Type() string { - return "Exception" -} - -func (e *Exception) SetComments(comments []*Comment, endLineComments []*Comment) { - e.Comments = comments - e.EndLineComments = endLineComments -} - -func (e *Exception) SetAnnotations(annos *Annotations) { - e.Annotations = annos -} - -func (e *Exception) Children() []Node { - nodes := []Node{e.Name, e.ExceptionKeyword, e.LCurKeyword, e.RCurKeyword} - for i := range e.Fields { - nodes = append(nodes, e.Fields[i]) - } - for i := range e.Comments { - nodes = append(nodes, e.Comments[i]) - } - for i := range e.EndLineComments { - nodes = append(nodes, e.EndLineComments[i]) - } - - if e.Annotations != nil { - nodes = append(nodes, e.Annotations) - } - - return nodes -} - -func (e *Exception) IsBadNode() bool { - return e.BadNode -} - -func (e *Exception) ChildrenBadNode() bool { - children := e.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (e *Exception) SetLocation(loc Location) { - e.Location = loc -} - -func (e *Exception) Equals(node Node) bool { - en, ok := node.(*Exception) - if !ok { - return false - } - - if (e == nil && en != nil) || - (e != nil && en == nil) { - return false - } else if e == nil && en == nil { - return true - } - - if e.BadNode != en.BadNode { - return false - } - - if !e.ExceptionKeyword.Equals(en.ExceptionKeyword) { - return false - } - - if !e.LCurKeyword.Equals(en.LCurKeyword) { - return false - } - - if !e.RCurKeyword.Equals(en.RCurKeyword) { - return false - } - - if !e.Name.Equals(en.Name) { - return false - } - - if len(e.Fields) != len(en.Fields) { - return false - } - - for i := range e.Fields { - if !e.Fields[i].Equals(en.Fields[i]) { - return false - } - } - - if len(e.Comments) != len(en.Comments) { - return false - } - - for i := range e.Comments { - if !e.Comments[i].Equals(en.Comments[i]) { - return false - } - } - - if len(e.EndLineComments) != len(en.EndLineComments) { - return false - } - - for i := range e.EndLineComments { - if !e.EndLineComments[i].Equals(en.EndLineComments[i]) { - return false - } - } - - if !e.Annotations.Equals(en.Annotations) { - return false - } - - return true -} - -type IdentifierName struct { - Text string - - BadNode bool - Location -} - -func NewIdentifierName(name string, loc Location) *IdentifierName { - return &IdentifierName{ - Text: name, - Location: loc, - BadNode: name == "", - } -} - -func (i *IdentifierName) Children() []Node { - return nil -} - -func (i *IdentifierName) Type() string { - return "IdentifierName" -} - -func (i *IdentifierName) IsBadNode() bool { - return i.BadNode -} - -func (i *IdentifierName) ChildrenBadNode() bool { - children := i.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (i *IdentifierName) Equals(node Node) bool { - in, ok := node.(*IdentifierName) - if !ok { - return false - } - - if (i == nil && in != nil) || - (i != nil && in == nil) { - return false - } else if i == nil && in == nil { - return true - } - - if i.BadNode != in.BadNode { - return false - } - - if i.Text != in.Text { - return false - } - - return true -} - -type Identifier struct { - Name *IdentifierName - Comments []*Comment - - BadNode bool - Location -} - -func NewIdentifier(name *IdentifierName, comments []*Comment, loc Location) *Identifier { - id := &Identifier{ - Name: name, - Comments: comments, - Location: loc, - BadNode: name == nil || name.BadNode, - } - - return id -} - -func NewBadIdentifier(loc Location) *Identifier { - return &Identifier{ - BadNode: true, - Location: loc, - } -} - -func (i *Identifier) ToFieldType() *FieldType { - t := &FieldType{ - TypeName: &TypeName{ - Name: i.Name.Text, - Location: i.Name.Location, - }, - Location: i.Location, - } - - return t -} - -func (i *Identifier) Children() []Node { - var nodes []Node - for _, com := range i.Comments { - nodes = append(nodes, com) - } - nodes = append(nodes, i.Name) - return nodes -} - -func (i *Identifier) Type() string { - return "Identifier" -} - -func (i *Identifier) IsBadNode() bool { - return i.BadNode -} - -func (i *Identifier) ChildrenBadNode() bool { - children := i.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (i *Identifier) Equals(node Node) bool { - in, ok := node.(*Identifier) - if !ok { - return false - } - - if (i == nil && in != nil) || - (i != nil && in == nil) { - return false - } else if i == nil && in == nil { - return true - } - - if i.BadNode != in.BadNode { - return false - } - - if !i.Name.Equals(in.Name) { - return false - } - - if len(i.Comments) != len(in.Comments) { - return false - } - - for n := range i.Comments { - if !i.Comments[n].Equals(in.Comments[n]) { - return false - } - } - - return true -} - -func ConvertPosition(pos position) Position { - return Position{ - Line: pos.line, - Col: pos.col, - Offset: pos.offset, - } -} - -type Field struct { - Index *FieldIndex - RequiredKeyword *RequiredKeyword - FieldType *FieldType - Identifier *Identifier - ConstValue *ConstValue - - EqualKeyword *EqualKeyword // can be nil - ListSeparatorKeyword *ListSeparatorKeyword // can be nil - - Comments []*Comment - EndLineComments []*Comment - Annotations *Annotations - - BadNode bool - Location -} - -func NewField(equalKeyword *EqualKeyword, listSeparatorKeyword *ListSeparatorKeyword, comments []*Comment, endLineComments []*Comment, annotations *Annotations, index *FieldIndex, required *RequiredKeyword, fieldType *FieldType, identifier *Identifier, constValue *ConstValue, loc Location) *Field { - field := &Field{ - EqualKeyword: equalKeyword, - ListSeparatorKeyword: listSeparatorKeyword, - Comments: comments, - EndLineComments: endLineComments, - Annotations: annotations, - Index: index, - RequiredKeyword: required, - FieldType: fieldType, - Identifier: identifier, - ConstValue: constValue, - Location: loc, - } - return field -} - -func NewBadField(loc Location) *Field { - return &Field{ - BadNode: true, - Location: loc, - } -} - -func (f *Field) Children() []Node { - var res []Node - if f.RequiredKeyword != nil { - res = append(res, f.RequiredKeyword) - } - if f.FieldType != nil { - res = append(res, f.FieldType) - } - if f.Identifier != nil { - res = append(res, f.Identifier) - } - if f.ConstValue != nil { - res = append(res, f.ConstValue) - } - if f.EqualKeyword != nil { - res = append(res, f.EqualKeyword) - } - if f.ListSeparatorKeyword != nil { - res = append(res, f.ListSeparatorKeyword) - } - for i := range f.Comments { - res = append(res, f.Comments[i]) - } - for i := range f.EndLineComments { - res = append(res, f.EndLineComments[i]) - } - if f.Annotations != nil { - res = append(res, f.Annotations) - } - return res -} - -func (f *Field) Type() string { - return "Field" -} - -func (f *Field) IsBadNode() bool { - return f.BadNode -} - -func (f *Field) ChildrenBadNode() bool { - children := f.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (f *Field) Equals(node Node) bool { - fn, ok := node.(*Field) - if !ok { - return false - } - - if (f == nil && fn != nil) || - (f != nil && fn == nil) { - return false - } else if f == nil && fn == nil { - return true - } - - if f.BadNode != fn.BadNode { - return false - } - - if !f.Index.Equals(fn.Index) { - return false - } - - if !f.RequiredKeyword.Equals(fn.RequiredKeyword) { - return false - } - - if !f.FieldType.Equals(fn.FieldType) { - return false - } - - if !f.Identifier.Equals(fn.Identifier) { - return false - } - - if !f.ConstValue.Equals(fn.ConstValue) { - return false - } - - if !f.EqualKeyword.Equals(fn.EqualKeyword) { - return false - } - - // 末尾的 , 不影响语义,暂时注释掉 - // if !f.ListSeparatorKeyword.Equals(fn.ListSeparatorKeyword) { - // return false - // } - - if len(f.Comments) != len(fn.Comments) { - return false - } - - for i := range f.Comments { - if !f.Comments[i].Equals(fn.Comments[i]) { - return false - } - } - - if len(f.EndLineComments) != len(fn.EndLineComments) { - return false - } - - for i := range f.EndLineComments { - if !f.EndLineComments[i].Equals(fn.EndLineComments[i]) { - return false - } - } - - if !f.Annotations.Equals(fn.Annotations) { - return false - } - - return true -} - -type ColonKeyword struct { - Keyword -} - -func (c *ColonKeyword) Type() string { - return "ColonKeyword" -} - -func (c *ColonKeyword) Equals(node Node) bool { - cn, ok := node.(*ColonKeyword) - if !ok { - return false - } - - if (c == nil && cn != nil) || - (c != nil && cn == nil) { - return false - } else if c == nil && cn == nil { - return true - } - - return c.Keyword.Equals(&cn.Keyword) -} - -type FieldIndex struct { - ColonKeyword *ColonKeyword - Value int - - Comments []*Comment - - BadNode bool - Location -} - -func NewFieldIndex(ColonKeyword *ColonKeyword, v int, comments []*Comment, loc Location) *FieldIndex { - return &FieldIndex{ - ColonKeyword: ColonKeyword, - Value: v, - Comments: comments, - Location: loc, - } -} - -func NewBadFieldIndex(loc Location) *FieldIndex { - return &FieldIndex{ - BadNode: true, - Location: loc, - } -} - -func (f *FieldIndex) Children() []Node { - return nil -} - -func (f *FieldIndex) Type() string { - return "FieldIndex" -} - -func (f *FieldIndex) IsBadNode() bool { - return f.BadNode -} - -func (f *FieldIndex) ChildrenBadNode() bool { - children := f.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (f *FieldIndex) Equals(node Node) bool { - fn, ok := node.(*FieldIndex) - if !ok { - return false - } - - if (f == nil && fn != nil) || - (f != nil && fn == nil) { - return false - } else if f == nil && fn == nil { - return true - } - - if f.BadNode != fn.BadNode { - return false - } - - if !f.ColonKeyword.Equals(fn.ColonKeyword) { - return false - } - - if f.Value != fn.Value { - return false - } - - if len(f.Comments) != len(fn.Comments) { - return false - } - - for i := range f.Comments { - if !f.Comments[i].Equals(fn.Comments[i]) { - return false - } - } - - return true -} - -type RequiredKeyword struct { - Keyword -} - -func (r *RequiredKeyword) Type() string { - return "RequiredKeyword" -} - -func (r *RequiredKeyword) Equals(node Node) bool { - rn, ok := node.(*RequiredKeyword) - if !ok { - return false - } - - if (r == nil && rn != nil) || - (r != nil && rn == nil) { - return false - } else if r == nil && rn == nil { - return true - } - - return r.Keyword.Equals(&rn.Keyword) -} - -type LPointKeyword struct { - Keyword -} - -func (l *LPointKeyword) Type() string { - return "LPointKeyword" -} - -func (l *LPointKeyword) Equals(node Node) bool { - ln, ok := node.(*LPointKeyword) - if !ok { - return false - } - - if (l == nil && ln != nil) || - (l != nil && ln == nil) { - return false - } else if l == nil && ln == nil { - return true - } - - return l.Keyword.Equals(&ln.Keyword) -} - -type RPointKeyword struct { - Keyword -} - -func (r *RPointKeyword) Type() string { - return "RPointKeyword" -} - -func (r *RPointKeyword) Equals(node Node) bool { - rn, ok := node.(*RPointKeyword) - if !ok { - return false - } - - if (r == nil && rn != nil) || - (r != nil && rn == nil) { - return false - } else if r == nil && rn == nil { - return true - } - - return r.Keyword.Equals(&rn.Keyword) -} - -type CommaKeyword struct { - Keyword -} - -func (r *CommaKeyword) Type() string { - return "CommaKeyword" -} - -func (r *CommaKeyword) Equals(node Node) bool { - rn, ok := node.(*CommaKeyword) - if !ok { - return false - } - - if (r == nil && rn != nil) || - (r != nil && rn == nil) { - return false - } else if r == nil && rn == nil { - return true - } - - return r.Keyword.Equals(&rn.Keyword) -} - -type CppTypeKeyword struct { - Keyword -} - -func (r *CppTypeKeyword) Type() string { - return "CppTypeKeyword" -} - -func (r *CppTypeKeyword) Equals(node Node) bool { - rn, ok := node.(*CppTypeKeyword) - if !ok { - return false - } - - if (r == nil && rn != nil) || - (r != nil && rn == nil) { - return false - } else if r == nil && rn == nil { - return true - } - - return r.Keyword.Equals(&rn.Keyword) -} - -type CppType struct { - CppTypeKeyword *CppTypeKeyword - Literal *Literal - - BadNode bool - Location -} - -func NewCppType(cppTypeKeyword *CppTypeKeyword, literal *Literal, loc Location) *CppType { - return &CppType{ - CppTypeKeyword: cppTypeKeyword, - Literal: literal, - Location: loc, - } -} - -func (c *CppType) Type() string { - return "CppType" -} - -func (c *CppType) Children() []Node { - return []Node{c.CppTypeKeyword, c.Literal} -} - -func (c *CppType) IsBadNode() bool { - return c.IsBadNode() -} - -func (c *CppType) ChildrenBadNode() bool { - children := c.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (c *CppType) Equals(node Node) bool { - cn, ok := node.(*CppType) - if !ok { - return false - } - if (c == nil && cn != nil) || - (c != nil && cn == nil) { - return false - } else if c == nil && cn == nil { - return true - } - - if c.BadNode != cn.BadNode { - return false - } - - if !c.CppTypeKeyword.Equals(cn.CppTypeKeyword) { - return false - } - - if !c.Literal.Equals(cn.Literal) { - return false - } - - return true -} - -type FieldType struct { - TypeName *TypeName - // only exist when TypeName is map or set or list - KeyType *FieldType - // only exist when TypeName is map - ValueType *FieldType - - // only exist in map, set, list. can be nil - CppType *CppType - - // only exist in map, set, list - LPointKeyword *LPointKeyword - // only exist in map, set, list - RPointKeyword *RPointKeyword - // only exist in map - CommaKeyword *CommaKeyword - - Annotations *Annotations - - BadNode bool - - Location -} - -func NewFieldType(lpointKeyword *LPointKeyword, rpointKeyword *RPointKeyword, commaKeyword *CommaKeyword, cppType *CppType, typeName *TypeName, keyType *FieldType, valueType *FieldType, loc Location) *FieldType { - return &FieldType{ - LPointKeyword: lpointKeyword, - RPointKeyword: rpointKeyword, - CommaKeyword: commaKeyword, - - CppType: cppType, - - TypeName: typeName, - KeyType: keyType, - ValueType: valueType, - Location: loc, - } -} - -func (c *FieldType) Children() []Node { - nodes := make([]Node, 0, 1) - nodes = append(nodes, c.TypeName) - if c.KeyType != nil { - nodes = append(nodes, c.KeyType) - } - if c.ValueType != nil { - nodes = append(nodes, c.ValueType) - } - - return nodes -} - -func (c *FieldType) Type() string { - return "FieldType" -} - -func (c *FieldType) IsBadNode() bool { - return c.BadNode -} - -func (c *FieldType) ChildrenBadNode() bool { - children := c.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (c *FieldType) Equals(node Node) bool { - fn, ok := node.(*FieldType) - if !ok { - return false - } - - if (c == nil && fn != nil) || - (c != nil && fn == nil) { - return false - } else if c == nil && fn == nil { - return true - } - - if c.BadNode != fn.BadNode { - return false - } - - if !c.TypeName.Equals(fn.TypeName) { - return false - } - - if !c.KeyType.Equals(fn.KeyType) { - return false - } - - if !c.ValueType.Equals(fn.ValueType) { - return false - } - - if !c.CppType.Equals(fn.CppType) { - return false - } - - if !c.LPointKeyword.Equals(fn.LPointKeyword) { - return false - } - - if !c.RPointKeyword.Equals(fn.RPointKeyword) { - return false - } - - if !c.CommaKeyword.Equals(fn.CommaKeyword) { - return false - } - - if !c.Annotations.Equals(fn.Annotations) { - return false - } - - return true -} - -type TypeName struct { - // TypeName can be: - // container type: map, set, list - // base type: bool, byte, i8, i16, i32, i64, double, string, binary, uuid - // struct, enum, union, exception, identifier - Name string - Comments []*Comment - - BadNode bool - Location -} - -func NewTypeName(name string, pos position) *TypeName { - t := &TypeName{ - Name: name, - Location: NewLocation(pos, name), - } - - return t -} - -func (t *TypeName) Children() []Node { - var nodes []Node - for i := range t.Comments { - nodes = append(nodes, t.Comments[i]) - } - return nodes -} - -func (t *TypeName) Type() string { - return "TypeName" -} - -func (t *TypeName) IsBadNode() bool { - return t.BadNode -} - -func (t *TypeName) ChildrenBadNode() bool { - children := t.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (t *TypeName) Equals(node Node) bool { - tn, ok := node.(*TypeName) - if !ok { - return false - } - - if (t == nil && tn != nil) || - (t != nil && tn == nil) { - return false - } else if t == nil && tn == nil { - return true - } - - if t.BadNode != tn.BadNode { - return false - } - - if t.Name != tn.Name { - return false - } - - if len(t.Comments) != len(tn.Comments) { - return false - } - - for i := range t.Comments { - if !t.Comments[i].Equals(tn.Comments[i]) { - return false - } - } - - return true -} - -type LBrkKeyword struct { - Keyword -} - -func (l *LBrkKeyword) Type() string { - return "LBrkKeyword" -} - -func (l *LBrkKeyword) Equals(node Node) bool { - ln, ok := node.(*LBrkKeyword) - if !ok { - return false - } - - if (l == nil && ln != nil) || - (l != nil && ln == nil) { - return false - } else if l == nil && ln == nil { - return true - } - - return l.Keyword.Equals(&ln.Keyword) -} - -type RBrkKeyword struct { - Keyword -} - -func (l *RBrkKeyword) Type() string { - return "RBrkKeyword" -} - -func (l *RBrkKeyword) Equals(node Node) bool { - ln, ok := node.(*RBrkKeyword) - if !ok { - return false - } - - if (l == nil && ln != nil) || - (l != nil && ln == nil) { - return false - } else if l == nil && ln == nil { - return true - } - - return l.Keyword.Equals(&ln.Keyword) -} - -type ConstValue struct { - // TypeName can be: list, map, pair, string, identifier, i64, double - TypeName string - // Value is the actual value or identifier name - Value any - - // ValueInText is the user input value - // it is used for i64 and double type value - ValueInText string - - // only exist when TypeName is map - Key any - - // exist in list - LBrkKeyword *LBrkKeyword - RBrkKeyword *RBrkKeyword - - // exist in map - LCurKeyword *LCurKeyword - RCurKeyword *RCurKeyword - - // exist in list, map item - ListSeparatorKeyword *ListSeparatorKeyword - - // exist in map item - ColonKeyword *ColonKeyword - - Comments []*Comment - - BadNode bool - Location -} - -func NewConstValue(typeName string, value any, loc Location) *ConstValue { - return &ConstValue{ - TypeName: typeName, - Value: value, - Location: loc, - } -} - -func NewBadConstValue(loc Location) *ConstValue { - return &ConstValue{ - BadNode: true, - Location: loc, - } -} - -func NewBadIntConstValue(loc Location) *ConstValue { - return &ConstValue{ - TypeName: "i64", - BadNode: true, - Value: int64(0), - Location: loc, - } -} - -func NewMapConstValue(key, value *ConstValue, loc Location) *ConstValue { - return &ConstValue{ - TypeName: "pair", - Key: key, - Value: value, - Location: loc, - } -} - -func (c *ConstValue) SetComments(comments []*Comment) { - c.Comments = comments -} - -// Children returns child nodes for list and map const values. -// This enables AST traversal to descend into list/map items. -func (c *ConstValue) Children() []Node { - if c == nil { - return nil - } - - switch c.TypeName { - case "list": - if c.Value == nil { - return nil - } - items := c.Value.([]*ConstValue) - nodes := make([]Node, len(items)) - for i, item := range items { - nodes[i] = item - } - return nodes - case "map": - if c.Value == nil { - return nil - } - items := c.Value.([]*ConstValue) - nodes := make([]Node, len(items)) - for i, item := range items { - nodes[i] = item - } - return nodes - case "pair": - // Map entries have Key and Value - var nodes []Node - if c.Key != nil { - nodes = append(nodes, c.Key.(*ConstValue)) - } - if c.Value != nil { - nodes = append(nodes, c.Value.(*ConstValue)) - } - return nodes - default: - return nil - } -} - -func (c *ConstValue) Type() string { - return "ConstValue" -} - -func (c *ConstValue) IsBadNode() bool { - return c.BadNode -} - -func (c *ConstValue) ChildrenBadNode() bool { - children := c.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (c *ConstValue) Equals(node Node) bool { - cn, ok := node.(*ConstValue) - if !ok { - return false - } - - if (c == nil && cn != nil) || - (c != nil && cn == nil) { - return false - } else if c == nil && cn == nil { - return true - } - - if c.BadNode != cn.BadNode { - return false - } - - if c.TypeName != cn.TypeName { - return false - } - - // TODO(jpf): any 的比对 - // if c.Value != cn.Value { - // return false - // } - - if c.ValueInText != cn.ValueInText { - return false - } - - // TODO(jpf): any 的比对 - // if c.Key != cn.Key { - // return false - // } - - if !c.LBrkKeyword.Equals(cn.LBrkKeyword) { - return false - } - - if !c.RBrkKeyword.Equals(cn.RBrkKeyword) { - return false - } - - if !c.LCurKeyword.Equals(cn.LCurKeyword) { - return false - } - - if !c.RCurKeyword.Equals(cn.RCurKeyword) { - return false - } - - if !c.ListSeparatorKeyword.Equals(cn.ListSeparatorKeyword) { - return false - } - - if !c.ColonKeyword.Equals(cn.ColonKeyword) { - return false - } - - if len(c.Comments) != len(cn.Comments) { - return false - } - - for i := range c.Comments { - if !c.Comments[i].Equals(cn.Comments[i]) { - return false - } - } - - return true -} - -type LiteralValue struct { - Text string - - BadNode bool - Location -} - -func NewLiteralValue(text string, loc Location) *LiteralValue { - return &LiteralValue{ - Text: text, - Location: loc, - } -} - -func NewBadLiteralValue(loc Location) *LiteralValue { - return &LiteralValue{ - BadNode: true, - Location: loc, - } -} - -func (l *LiteralValue) Children() []Node { - return nil -} - -func (l *LiteralValue) Type() string { - return "LiteralValue" -} - -func (l *LiteralValue) IsBadNode() bool { - return l.BadNode -} - -func (l *LiteralValue) ChildrenBadNode() bool { - children := l.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (l *LiteralValue) Equals(node Node) bool { - ln, ok := node.(*LiteralValue) - if !ok { - return false - } - - if (l == nil && ln != nil) || - (l != nil && ln == nil) { - return false - } else if l == nil && ln == nil { - return true - } - - if l.BadNode != ln.BadNode { - return false - } - - if l.Text != ln.Text { - return false - } - - return true -} - -type Literal struct { - Value *LiteralValue - - Quote string // single for ', double for " - - Comments []*Comment - - BadNode bool - Location -} - -// TODO: 区分单引号还是双引号? -func NewLiteral(comments []*Comment, v *LiteralValue, quote string, loc Location) *Literal { - return &Literal{ - Value: v, - Quote: quote, - Comments: comments, - Location: loc, - } -} - -func NewBadLiteral(loc Location) *Literal { - return &Literal{ - Location: loc, - BadNode: true, - } -} - -func (l *Literal) Children() []Node { - var nodes []Node - for i := range l.Comments { - nodes = append(nodes, l.Comments[i]) - } - if l.Value != nil { - nodes = append(nodes, l.Value) - } - return nodes -} - -func (l *Literal) Type() string { - return "Literal" -} - -func (l *Literal) IsBadNode() bool { - return l.BadNode -} - -func (l *Literal) ChildrenBadNode() bool { - children := l.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (l *Literal) Equals(node Node) bool { - ln, ok := node.(*Literal) - if !ok { - return false - } - - if (l == nil && ln != nil) || - (l != nil && ln == nil) { - return false - } else if l == nil && ln == nil { - return true - } - - if l.BadNode != ln.BadNode { - return false - } - - if !l.Value.Equals(ln.Value) { - return false - } - - if l.Quote != ln.Quote { - return false - } - - if len(l.Comments) != len(ln.Comments) { - return false - } - - for i := range l.Comments { - if !l.Comments[i].Equals(ln.Comments[i]) { - return false - } - } - - return true -} - -type Annotations struct { - Annotations []*Annotation - LParKeyword *LParKeyword - RParKeyword *RParKeyword - - BadNode bool - Location -} - -func NewAnnotations(lpar *LParKeyword, rpar *RParKeyword, annos []*Annotation, loc Location) *Annotations { - return &Annotations{ - LParKeyword: lpar, - RParKeyword: rpar, - Annotations: annos, - Location: loc, - } -} - -func (a *Annotations) Type() string { - return "Annotations" -} - -func (a *Annotations) Children() []Node { - nodes := []Node{a.LParKeyword, a.RParKeyword} - for i := range a.Annotations { - nodes = append(nodes, a.Annotations[i]) - } - - return nodes -} - -func (a *Annotations) IsBadNode() bool { - return a.BadNode -} - -func (a *Annotations) ChildrenBadNode() bool { - children := a.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (a *Annotations) Equals(node Node) bool { - an, ok := node.(*Annotations) - if !ok { - return false - } - - if (a == nil && an != nil) || - (a != nil && an == nil) { - return false - } else if a == nil && an == nil { - return true - } - - if a.BadNode != an.BadNode { - return false - } - - if len(a.Annotations) != len(an.Annotations) { - return false - } - - for i := range a.Annotations { - if !a.Annotations[i].Equals(an.Annotations[i]) { - return false - } - } - - if !a.LParKeyword.Equals(an.LParKeyword) { - return false - } - - if !a.RParKeyword.Equals(an.RParKeyword) { - return false - } - - return true - -} - -type Annotation struct { - EqualKeyword *EqualKeyword - ListSeparatorKeyword *ListSeparatorKeyword - - Identifier *Identifier - Value *Literal - - BadNode bool - Location -} - -func NewAnnotation(equalKeyword *EqualKeyword, listSeparatorKeyword *ListSeparatorKeyword, id *Identifier, value *Literal, loc Location) *Annotation { - return &Annotation{ - EqualKeyword: equalKeyword, - ListSeparatorKeyword: listSeparatorKeyword, - Identifier: id, - Value: value, - Location: loc, - } -} - -func NewBadAnnotation(loc Location) *Annotation { - return &Annotation{ - BadNode: true, - Location: loc, - } -} - -func (a *Annotation) Children() []Node { - nodes := []Node{a.Identifier, a.Value, a.EqualKeyword} - if a.ListSeparatorKeyword != nil { - nodes = append(nodes, a.ListSeparatorKeyword) - } - - return nodes -} - -func (a *Annotation) Type() string { - return "Annotation" -} - -func (a *Annotation) IsBadNode() bool { - return a.BadNode -} - -func (a *Annotation) ChildrenBadNode() bool { - children := a.Children() - for i := range children { - if children[i].IsBadNode() { - return true - } - if children[i].ChildrenBadNode() { - return true - } - } - return false -} - -func (a *Annotation) Equals(node Node) bool { - an, ok := node.(*Annotation) - if !ok { - return false - } - - if (a == nil && an != nil) || - (a != nil && an == nil) { - return false - } else if a == nil && an == nil { - return true - } - - if a.BadNode != an.BadNode { - return false - } - - if !a.EqualKeyword.Equals(an.EqualKeyword) { - return false - } - - if !a.ListSeparatorKeyword.Equals(an.ListSeparatorKeyword) { - return false - } - - if !a.Identifier.Equals(an.Identifier) { - return false - } - - if !a.Value.Equals(an.Value) { - return false - } - - return true - -} - -type CommentStyle string - -const ( - CommentStyleShell CommentStyle = "shell" - CommentStyleMultiLine CommentStyle = "multiline" - CommentStyleSingleLine CommentStyle = "singleline" -) - -type Comment struct { - Text string - Style CommentStyle // shell: #xxx, multiline: /* *** */, singleline: // xxxxx - - BadNode bool - Location -} - -func NewComment(text string, style CommentStyle, loc Location) *Comment { - return &Comment{ - Text: text, - Style: style, - Location: loc, - } -} - -func NewBadComment(loc Location) *Comment { - return &Comment{ - BadNode: true, - Location: loc, - } -} - -func (c *Comment) Children() []Node { - return nil -} - -func (c *Comment) Type() string { - return "Comment" -} - -func (c *Comment) IsBadNode() bool { - return c.BadNode -} - -func (c *Comment) ChildrenBadNode() bool { - return false -} - -func (c *Comment) Equals(node Node) bool { - cn, ok := node.(*Comment) - if !ok { - return false - } - - if (c == nil && cn != nil) || - (c != nil && cn == nil) { - return false - } else if c == nil && cn == nil { - return true - } - - if c.BadNode != cn.BadNode { - return false - } - - aLines := strings.Split(c.Text, "\n") - bLines := strings.Split(cn.Text, "\n") - - if len(aLines) != len(bLines) { - return false - } - - for i := range aLines { - if strings.TrimSpace(aLines[i]) != strings.TrimSpace(bLines[i]) { - return false - } - } - - if c.Style != cn.Style { - return false - } - - return true -} - -type Location struct { - StartPos Position - EndPos Position -} - -func (l Location) MoveStartInLine(n int) Location { - newL := l - newL.StartPos.Col += n - newL.StartPos.Offset += n - - return newL -} - -func (l *Location) Pos() Position { - return l.StartPos -} - -// end col and offset is excluded -func (l *Location) End() Position { - return l.EndPos -} - -func (l *Location) Contains(pos Position) bool { - if l == nil { - return false - } - // TODO(jpf): ut - return (l.StartPos.Less(pos) || l.StartPos.Equal(pos)) && l.EndPos.Greater(pos) -} - -func NewLocationFromPos(start, end Position) Location { - return Location{StartPos: start, EndPos: end} -} - -func NewLocationFromCurrent(c *current) Location { - return NewLocation(c.pos, string(c.text)) -} - -func NewLocation(startPos position, text string) Location { - start := ConvertPosition(startPos) - - nLine := strings.Count(text, "\n") // "\r\nline 1", this will start with line 1,0 in parsed ast - if startPos.col == 0 { - nLine = nLine - 1 - } - lastLineOffset := strings.LastIndexByte(text, '\n') - if lastLineOffset == -1 { - lastLineOffset = 0 - } - lastLine := []byte(text)[lastLineOffset:] - col := utf8.RuneCount(lastLine) + 1 - if nLine == 0 { - col += start.Col - 1 - } - end := Position{ - Line: start.Line + nLine, - Col: col, - Offset: start.Offset + len(text), - } - - return Location{ - StartPos: start, - EndPos: end, - } -} - -var InvalidPosition = Position{ - Line: -1, - Col: -1, - Offset: -1, -} - -type Position struct { - Line int // 1-based line number - Col int // 1-based rune count from start of line. - Offset int // 0-based byte offset -} - -func (p *Position) Less(other Position) bool { - if p.Line < other.Line { - return true - } else if p.Line == other.Line { - return p.Col < other.Col - } - return false -} - -func (p *Position) Equal(other Position) bool { - return p.Line == other.Line && p.Col == other.Col -} - -func (p *Position) Greater(other Position) bool { - if p.Line > other.Line { - return true - } else if p.Line == other.Line { - return p.Col > other.Col - } - return false -} - -func (p *Position) Invalid() bool { - return p.Line < 1 || p.Col < 1 || p.Offset < 0 -} diff --git a/parser/ast_test.go b/parser/ast_test.go deleted file mode 100644 index fca91cb..0000000 --- a/parser/ast_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewLocationFromCurrent(t *testing.T) { - type args struct { - c *current - } - tests := []struct { - name string - args args - want Location - }{ - { - name: "oneline", - args: args{ - c: ¤t{ - pos: position{ - line: 1, - col: 1, - offset: 0, - }, - text: []byte("aaaaa"), - }, - }, - want: Location{ - StartPos: Position{ - Line: 1, - Col: 1, - Offset: 0, - }, - EndPos: Position{ - Line: 1, - Col: 6, - Offset: 5, - }, - }, - }, - { - name: "multiline with CRLF", - args: args{ - c: ¤t{ - pos: position{ - line: 2, - col: 0, - offset: 1, - }, - text: []byte("\r\naa\r\n\r\naaa"), - }, - }, - want: Location{ - StartPos: Position{ - Line: 2, - Col: 0, - Offset: 1, - }, - EndPos: Position{ - Line: 4, - Col: 5, - Offset: 12, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, NewLocationFromCurrent(tt.args.c)) - }) - } -} diff --git a/parser/ast_visit.go b/parser/ast_visit.go deleted file mode 100644 index aa6e5af..0000000 --- a/parser/ast_visit.go +++ /dev/null @@ -1,27 +0,0 @@ -package parser - -import ( - "github.com/joyme123/thrift-ls/utils" -) - -func SearchNodePathByPosition(root Node, pos Position) []Node { - path := make([]Node, 0) - searchNodePath(root, pos, &path) - - return path -} - -func searchNodePath(root Node, pos Position, path *[]Node) { - if utils.IsNil(root) { - return - } - if !root.Contains(pos) { - return - } - - *path = append(*path, root) - - for _, child := range root.Children() { - searchNodePath(child, pos, path) - } -} diff --git a/parser/ast_visit_test.go b/parser/ast_visit_test.go deleted file mode 100644 index 8113fee..0000000 --- a/parser/ast_visit_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package parser - -import ( - "fmt" - "testing" - - "github.com/joyme123/thrift-ls/utils" - "github.com/stretchr/testify/assert" -) - -func TestSearchNodePath(t *testing.T) { - demoContent := `struct Demo { -1: optional set with_default = [ "😀", "aaa" ] -} - -service Demo { - user.Test Api(1:user.Test2 arg1, 2:user.Test3 arg2) throws (1:user.Error1 err) -} - -enum Test { - ONE - TWO = 2 -} -` - - ast, err := Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - doc := ast.(*Document) - - utils.MustDumpJsonToFile(doc, "/tmp/ast") - - type args struct { - root Node - pos Position - } - tests := []struct { - name string - args args - want []string - }{ - { - name: "struct name", - args: args{ - root: doc, - pos: Position{ - Line: 1, - Col: 11, - }, - }, - want: []string{"Document", "Struct", "Identifier", "IdentifierName"}, - }, - { - name: "field type", - args: args{ - root: doc, - pos: Position{ - Line: 2, - Col: 13, - }, - }, - want: []string{"Document", "Struct", "Field", "FieldType", "TypeName"}, - }, - { - name: "struct field value", - args: args{ - root: doc, - pos: Position{ - Line: 2, - Col: 44, - }, - }, - want: []string{"Document", "Struct", "Field", "ConstValue", "ConstValue"}, - }, - { - name: "function type", - args: args{ - root: doc, - pos: Position{ - Line: 6, - Col: 3, - }, - }, - want: []string{"Document", "Service", "Function", "FieldType", "TypeName"}, - }, - { - name: "function type", - args: args{ - root: doc, - pos: Position{ - Line: 6, - Col: 8, - }, - }, - want: []string{"Document", "Service", "Function", "FieldType", "TypeName"}, - }, - { - name: "function argument", - args: args{ - root: doc, - pos: Position{ - Line: 6, - Col: 24, - }, - }, - want: []string{"Document", "Service", "Function", "Field", "FieldType", "TypeName"}, - }, - { - name: "function throws", - args: args{ - root: doc, - pos: Position{ - Line: 6, - Col: 70, - }, - }, - want: []string{"Document", "Service", "Function", "Throws", "Field", "FieldType", "TypeName"}, - }, - { - name: "enum value", - args: args{ - root: doc, - pos: Position{ - Line: 11, - Col: 3, - }, - }, - want: []string{"Document", "Enum", "EnumValue", "Identifier", "IdentifierName"}, - }, - { - name: "enum value const value", - args: args{ - root: doc, - pos: Position{ - Line: 11, - Col: 9, - }, - }, - want: []string{"Document", "Enum", "EnumValue", "ConstValue"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.name == "enum value" { - fmt.Println() - } - path := SearchNodePathByPosition(tt.args.root, tt.args.pos) - pathStr := make([]string, 0, len(path)) - for i := range path { - nodeType := path[i].Type() - pathStr = append(pathStr, nodeType) - } - assert.Equal(t, tt.want, pathStr) - }) - } -} diff --git a/parser/consts.go b/parser/consts.go deleted file mode 100644 index 8f9e8a0..0000000 --- a/parser/consts.go +++ /dev/null @@ -1,6 +0,0 @@ -package parser - -const ( - LCUR = "{" - RCUR = "}" -) diff --git a/parser/errors.go b/parser/errors.go deleted file mode 100644 index 51fbbf6..0000000 --- a/parser/errors.go +++ /dev/null @@ -1,98 +0,0 @@ -package parser - -import ( - "errors" -) - -// ErrorLister is the public interface to access the inner errors -// included in a errList -type ErrorLister interface { - Errors() []error -} - -func (e errList) Errors() []error { - return e -} - -// ParserError is the public interface to errors of type parserError -type ParserError interface { - Error() string - InnerError() error - Pos() (int, int, int) - Expected() []string -} - -func (p *parserError) InnerError() error { - return p.Inner -} - -func (p *parserError) Pos() (line, col, offset int) { - return p.pos.line, p.pos.col, p.pos.offset -} - -func (p *parserError) Expected() []string { - return p.expected -} - -var ( - RequiredError error = errors.New("expecting 'required' or 'optional'") - InvalidFieldTypeError error = errors.New("expecting a valid field type") - InvalidFieldIndexError error = errors.New("expecting a valid int16 field index") - - InvalidStructError error = errors.New("expecting a valid struct definition") - InvalidStructIdentifierError error = errors.New("expecting a valid struct identifier") - InvalidStructBlockLCURError error = errors.New("expecting a starting '{' of struct block") - InvalidStructBlockRCURError error = errors.New("expecting an ending '}' of struct block") - InvalidStructFieldError error = errors.New("expecting a valid struct field") - - InvalidUnionError error = errors.New("expecting a valid union definition") - InvalidUnionIdentifierError error = errors.New("expecting a valid union identifier") - InvalidUnionBlockLCURError error = errors.New("expecting a starting '{' of union block") - InvalidUnionBlockRCURError error = errors.New("expecting a ending '}' of union block") - InvalidUnionFieldError error = errors.New("expecting a valid union field") - - InvalidExceptionError error = errors.New("expecting a valid exception definition") - InvalidExceptionIdentifierError error = errors.New("expecting a valid exception identifier") - InvalidExceptionBlockLCURError error = errors.New("expecting a starting '{' of exception block") - InvalidExceptionBlockRCURError error = errors.New("expecting a ending '}' of exception block") - InvalidExceptionFieldError error = errors.New("expecting a valid exception field") - - InvalidEnumError error = errors.New("expecting a valid enum definition") - InvalidEnumIdentifierError error = errors.New("expecting a valid enum identifier") - InvalidEnumBlockLCURError error = errors.New("expecting a starting '{' of enum block") - InvalidEnumBlockRCURError error = errors.New("expecting a ending '}' of enum block") - InvalidEnumValueError error = errors.New("expecting a valid enum field") - InvalidEnumValueIntConstantError error = errors.New("expecting a valid int contant") - - InvalidTypedefError error = errors.New("expecting a valid typedef definition") - InvalidTypedefIdentifierError error = errors.New("expecting a valid typedef identifier") - - InvalidConstError error = errors.New("expecting a valid const definition") - InvalidConstConstValueError error = errors.New("expecting a valid const value") - InvalidConstMissingValueError error = errors.New("expecting a const value") - InvalidConstIdentifierError error = errors.New("expecting a valid const identifier") - - InvalidServiceIdentifierError error = errors.New("expecting a valid service identifier") - InvalidServiceBlockRCURError error = errors.New("expecting a ending '}' of service block") - InvalidServiceFunctionError error = errors.New("expecting a valid service function") - - InvalidFunctionIdentifierError error = errors.New("expecting a valid function identifier") - InvalidFunctionArgumentError error = errors.New("expecting a valid function argument") - - InvalidIdentifierError error = errors.New("expecting a valid identifier") - - InvalidLiteral1MissingRightError error = errors.New("expecting a right \" ") - InvalidLiteral1Error error = errors.New("expecting a valid literal") - InvalidLiteral2MissingRightError error = errors.New("expecting a right ' ") - InvalidLiteral2Error error = errors.New("expecting a valid literal") - - InvalidHeaderError error = errors.New("expecting a valid header") - - InvalidIncludeError error = errors.New("expecting a valid include header") - InvalidCppIncludeError error = errors.New("expecting a valid cpp include header") - InvalidNamespaceError error = errors.New("expecting a valid namespace header") - - InvalidDefinitionError error = errors.New("expecting a valid definition") - - InvalidServiceError error = errors.New("expecting a valid service definition") -) diff --git a/parser/parser.go b/parser/parser.go deleted file mode 100644 index 7f4e4f5..0000000 --- a/parser/parser.go +++ /dev/null @@ -1,82 +0,0 @@ -package parser - -type IncludeCall func(include string) (filename string, content []byte, err error) - -type Parser interface { - Parse(filename string, content []byte) *ParseResult - ParseRecursively(filename string, content []byte, maxDepth int, call IncludeCall) []*ParseResult -} - -// PEGParser use PEG as a parser implementation -type PEGParser struct { - parsed map[string]struct{} -} - -func (p *PEGParser) Parse(filename string, content []byte) (*Document, []error) { - if p.parsed == nil { - p.parsed = make(map[string]struct{}) - } - p.parsed[filename] = struct{}{} - - doc, err := Parse(filename, content) - if err != nil { - var errors []error - errList, ok := err.(ErrorLister) - if ok { - errors = errList.Errors() - } else { - errors = append(errors, err) - } - - var res *Document - if doc != nil { - res = doc.(*Document) - } - return res, errors - } - - return doc.(*Document), nil -} - -func (p *PEGParser) ParseRecursively(filename string, content []byte, maxDepth int, call IncludeCall) []*ParseResult { - return p.parseRecursively(filename, content, 0, maxDepth, call) -} - -func (p *PEGParser) parseRecursively(filename string, content []byte, curDepth int, maxDepth int, call IncludeCall) []*ParseResult { - if curDepth > maxDepth && maxDepth > 0 { - return nil - } - - results := make([]*ParseResult, 0) - - doc, errs := p.Parse(filename, content) - results = append(results, &ParseResult{ - Doc: doc, - Errors: errs, - }) - - if doc != nil { - for _, include := range doc.Includes { - if include.Path == nil || include.Path.ChildrenBadNode() { - continue - } - f, c, err := call(include.Path.Value.Text) - if err != nil { - // Include file could not be resolved, skip it - continue - } - if _, ok := p.parsed[f]; ok { - continue - } - subRes := p.parseRecursively(f, c, curDepth+1, maxDepth, call) - results = append(results, subRes...) - } - } - - return results -} - -type ParseResult struct { - Doc *Document - Errors []error -} diff --git a/parser/parser_test.go b/parser/parser_test.go deleted file mode 100644 index 88170ef..0000000 --- a/parser/parser_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package parser - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_ParseRecursively(t *testing.T) { - baseFile := ` -struct User { - 1: required string Name, - 2: optional i32 Age, -} -struct GetUserRequest { - 1: required string Name, -} -exception Error { - 1: string Code, - 2: string Message, - 3: map Data, -} -` - serviceFile := ` -include "base.thrift" - -service APIService { - base.User GetUser(1: base.GetUserRequest req) throws (1: base.Error err), -} -` - - parser := &PEGParser{} - parseResult := parser.ParseRecursively("service.thrift", []byte(serviceFile), 10, func(include string) (filename string, content []byte, err error) { - if include == "base.thrift" { - return "base.thrift", []byte(baseFile), nil - } - return "", nil, errors.New("file not found") - }) - - assert.Len(t, parseResult, 2) - for _, res := range parseResult { - if res.Doc.Filename == "service.thrift" { - assert.Len(t, res.Errors, 0) - assert.Len(t, res.Doc.Services, 1) - assert.Len(t, res.Doc.Services[0].Functions, 1) - } else if res.Doc.Filename == "base.thrift" { - assert.Len(t, res.Errors, 0) - assert.Len(t, res.Doc.Structs, 2) - assert.Len(t, res.Doc.Exceptions, 1) - } - } -} diff --git a/parser/test/bad_definition_test.go b/parser/test/bad_definition_test.go deleted file mode 100644 index ae4dd42..0000000 --- a/parser/test/bad_definition_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseBadDefinition(t *testing.T) { - content := `include "test" - -struct test {} -str` - - ast, err := parser.Parse("test.thrift", []byte(content)) - assert.Error(t, err) - assert.NotNil(t, ast) - - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"4:1"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidDefinitionError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - -} diff --git a/parser/test/bad_header_test.go b/parser/test/bad_header_test.go deleted file mode 100644 index b744771..0000000 --- a/parser/test/bad_header_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseBadHeader(t *testing.T) { - demoContent := `include "aaa" -aaaaa` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - assert.NotNil(t, ast) - - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"2:1"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidHeaderError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - demoContent2 := `inc` - ast, err = parser.Parse("test.thrift", []byte(demoContent2)) - assert.Error(t, err) - assert.NotNil(t, ast) - - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"1:1"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidHeaderError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } -} diff --git a/parser/test/const_test.go b/parser/test/const_test.go deleted file mode 100644 index bfa534d..0000000 --- a/parser/test/const_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseConstError(t *testing.T) { - - demoContent := `const string a = 1aa // err1: InvalidConstValueError -const string b // err2: InvalidConstMissingValueError -const string c = "hello // err3: InvalidLiteral1MissingRightError -const string d = 'hello // err4: InvalidLiteral2MissingRightError -const i32 111e = 1 // err5: InvalidConstIdentifierError -const string f = "hello"` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"1:18", "2:16", "3:18", "4:18", "5:11"} - errs := []error{ - parser.InvalidConstConstValueError, - parser.InvalidConstMissingValueError, - parser.InvalidLiteral1MissingRightError, - parser.InvalidLiteral2MissingRightError, - parser.InvalidConstIdentifierError} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, equalErrors(errList.Errors(), errs)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} diff --git a/parser/test/constvalue_test.go b/parser/test/constvalue_test.go deleted file mode 100644 index 06f9335..0000000 --- a/parser/test/constvalue_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseConstValue(t *testing.T) { - - demoContent := `const i16 a = 1 -const i32 b = 0o1 -const i64 c = 0xa1 -const double d = 1.33333333 -const double e = 1.3333e11 -const double f = 1e11 -const double g = 1.3333E11 -const double h = 1E11` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - consts := ast.(*parser.Document).Consts - assert.Len(t, consts, 8) - assert.Equal(t, "1", consts[0].Value.ValueInText) - assert.Equal(t, "0o1", consts[1].Value.ValueInText) - assert.Equal(t, "0xa1", consts[2].Value.ValueInText) - assert.Equal(t, "1.33333333", consts[3].Value.ValueInText) - assert.Equal(t, "1.3333e11", consts[4].Value.ValueInText) - assert.Equal(t, "1e11", consts[5].Value.ValueInText) - assert.Equal(t, "1.3333E11", consts[6].Value.ValueInText) - assert.Equal(t, "1E11", consts[7].Value.ValueInText) -} diff --git a/parser/test/enum_test.go b/parser/test/enum_test.go deleted file mode 100644 index f17c27c..0000000 --- a/parser/test/enum_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseEnum(t *testing.T) { - demoContent := `enum TweetType { - TWEET, // 0 - RETWEET = 2, // 2 - DM = 0xa, // 10 - REPLY // 11 - POST = 0o21 // 17 -}` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - enums := ast.(*parser.Document).Enums - assert.Len(t, enums, 1) - assert.Equal(t, "TweetType", enums[0].Name.Name.Text) - assert.Len(t, enums[0].Values, 5) - assert.Equal(t, "TWEET", enums[0].Values[0].Name.Name.Text) - assert.Equal(t, int64(0), enums[0].Values[0].Value) - assert.Equal(t, "RETWEET", enums[0].Values[1].Name.Name.Text) - assert.Equal(t, int64(2), enums[0].Values[1].Value) - assert.Equal(t, "DM", enums[0].Values[2].Name.Name.Text) - assert.Equal(t, int64(10), enums[0].Values[2].Value) - assert.Equal(t, "REPLY", enums[0].Values[3].Name.Name.Text) - assert.Equal(t, int64(11), enums[0].Values[3].Value) - assert.Equal(t, "POST", enums[0].Values[4].Name.Name.Text) - assert.Equal(t, int64(17), enums[0].Values[4].Value) -} - -func Test_ParseEnumIdentifierError(t *testing.T) { - - demoContent := `enum { // err1, line 1, col 6 - TWEET, // 0 - RETWEET = 2, // 2 - DM = 0xa, // 10 - REPLY // 11 - POST = 0o21 // 17 -} - -enum 123Demos { // err2, line 9, col 6 -} - -enum Demos{ -} -` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"1:6", "9:6"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidEnumIdentifierError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseEnumBlockRCURError(t *testing.T) { - - demoContent := `enum Demo { - TWEET, // 0 - RETWEET = 2, // 2 - DM = 0xa, // 10 - REPLY // 11 - POST = 0o21 // 17 // err1, line 8, col 1 - -enum Demos1 {} -enum Demos{ // err2, line 10, col 1 -` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"8:1", "10:1"} - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidEnumBlockRCURError)) - - } - - assert.NotNil(t, ast) -} - -func Test_ParseEnumValueError(t *testing.T) { - demoContent := `enum Demo { - TWEET, // 0 - RETWEET = "aaa", // 2, err1, line 3, col 13 - DM = 0xa, // 10 - REPLY // 11 -} - -enum Demos1 {} -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"3:13"} - errs := []error{parser.InvalidEnumValueIntConstantError} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, equalErrors(errList.Errors(), errs)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseEnumLocation(t *testing.T) { - demoContent := `enum Demo { - TWEET, // 0 - RETWEET = 2, // 2, err1, line 3, col 13 - DM = 0xa, // 10 - REPLY // 11 -}` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - doc := ast.(*parser.Document) - docPos := doc.Location.Pos() - docEnd := doc.Location.End() - - // doc pos - assert.Equal(t, 1, docPos.Line) - assert.Equal(t, 1, docPos.Col) - assert.Equal(t, 0, docPos.Offset) - // doc end - assert.Equal(t, 6, docEnd.Line) - assert.Equal(t, 3, docEnd.Col) - assert.Equal(t, len(demoContent), docEnd.Offset) - - assert.Len(t, doc.Enums, 1) - namePos := doc.Enums[0].Name.Name.Pos() - nameEnd := doc.Enums[0].Name.Name.End() - - // name pos - assert.Equal(t, 1, namePos.Line) - assert.Equal(t, 6, namePos.Col) - assert.Equal(t, 5, namePos.Offset) - // name end - assert.Equal(t, 1, nameEnd.Line) - assert.Equal(t, 10, nameEnd.Col) - assert.Equal(t, 9, nameEnd.Offset) - - // enum value - assert.Len(t, doc.Enums[0].Values, 4) - enumValue := doc.Enums[0].Values[0] - enumValuePos := enumValue.Pos() - enumValueEnd := enumValue.End() - - // enum value pos - assert.Equal(t, 2, enumValuePos.Line) - assert.Equal(t, 3, enumValuePos.Col) - assert.Equal(t, 14, enumValuePos.Offset) - - // enum value end - assert.Equal(t, 2, enumValueEnd.Line) - assert.Equal(t, 18, enumValueEnd.Col) - assert.Equal(t, 29, enumValueEnd.Offset) -} diff --git a/parser/test/exception_test.go b/parser/test/exception_test.go deleted file mode 100644 index 3fb4da1..0000000 --- a/parser/test/exception_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseExceptionIdentifierError(t *testing.T) { - - demoContent := `exception { // err1, line 1, col 11 - // Name is demo name - 1: required string Name; - 2: optional boo Required = true; -} - -exception 123Demos { // err2, line 7, col 11 -} - -exception Demos{ -} -` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"1:11", "7:11"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidExceptionIdentifierError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseExceptionBlockRCURError(t *testing.T) { - - demoContent := `exception Demo { - // Name is demo name - - 2: optional boo Required = true; // err1, line 6, col 1 - -exception Demos{} -exception Demos{ // err2, line 8, col 1 -exception Demos{} -` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"6:1", "8:1"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidExceptionBlockRCURError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseExceptionFieldError(t *testing.T) { - demoContent := `exception Demo { - 1: optional i64 count - a: optional boo Required = true; // err1, line 3, col 3 - 2: required i32 test4; - required string test; // err2, line 5, col 3 - 4: required i32 test; - 5 required string test; // err3, line 7, col 3 - 6: required test test; - no comment // err4, line 9, col 3 -} -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"3:3", "5:3", "7:3", "9:3"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidExceptionFieldError)) - assert.True(t, containsError(errList.Errors(), parser.InvalidFieldIndexError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseExceptionFieldDefault(t *testing.T) { - demoContent := `exception Demo { -1: optional set with_default = [ "test", "aaa" ] -2: optional set bin_set = {} -3: optional map bin_map = {} -} -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - - assert.NotNil(t, ast) -} - -func Test_ParseExceptionLocation(t *testing.T) { - demoContent := `exception Demo { -1: optional set with_default = [ "😀", "aaa" ] -}` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - doc := ast.(*parser.Document) - docPos := doc.Location.Pos() - docEnd := doc.Location.End() - - // doc pos - assert.Equal(t, 1, docPos.Line) - assert.Equal(t, 1, docPos.Col) - assert.Equal(t, 0, docPos.Offset) - // doc end - assert.Equal(t, 3, docEnd.Line) - assert.Equal(t, 3, docEnd.Col) - assert.Equal(t, 75, docEnd.Offset) - - assert.Len(t, doc.Exceptions, 1) - namePos := doc.Exceptions[0].Name.Name.Pos() - nameEnd := doc.Exceptions[0].Name.Name.End() - - // pos - assert.Equal(t, 1, namePos.Line) - assert.Equal(t, 11, namePos.Col) - assert.Equal(t, 10, namePos.Offset) - // end - assert.Equal(t, 1, nameEnd.Line) - assert.Equal(t, 15, nameEnd.Col) - assert.Equal(t, 14, nameEnd.Offset) -} - -func Test_ParseExceptionErr(t *testing.T) { - demoContent := `exception Demo {` - _, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) -} diff --git a/parser/test/failure_recovery_test.go b/parser/test/failure_recovery_test.go deleted file mode 100644 index cb59aaf..0000000 --- a/parser/test/failure_recovery_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package test - -import "testing" - -func Test_BadHeader(t *testing.T) { - -} - -func Test_BadIncludePath(t *testing.T) { - -} - -func Test_BadDefinition(t *testing.T) { - -} diff --git a/parser/test/function_test.go b/parser/test/function_test.go deleted file mode 100644 index f05da2f..0000000 --- a/parser/test/function_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseFunctionIdentifierError(t *testing.T) { - demoContent := `service Demo { - string 11GetName(1:required string name) throws(1:required error err1) -} -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"2:10"} - errs := []error{parser.InvalidFunctionIdentifierError} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, equalErrors(errList.Errors(), errs)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseFunctionArgumentError(t *testing.T) { - demoContent := `service Demo -{ - string GetName(1:required string 11name) throws(1:required error err1) - string GetName(1:required string name) throws(1:required error err1) -} -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"3:18"} - errs := []error{parser.InvalidFunctionArgumentError} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, equalErrors(errList.Errors(), errs)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} diff --git a/parser/test/include_test.go b/parser/test/include_test.go deleted file mode 100644 index f2f5c85..0000000 --- a/parser/test/include_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseInclude(t *testing.T) { - demoContent := `include "../user.thrift" -include "../base.thrift" -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - includes := ast.(*parser.Document).Includes - assert.Len(t, includes, 2) - assert.Equal(t, "../user.thrift", includes[0].Path.Value.Text) - assert.Equal(t, "../base.thrift", includes[1].Path.Value.Text) -} - -func Test_ParseIncludeWithError(t *testing.T) { - demoContent := `include "../user.thrift" -include "../base.thrift -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - assert.NotNil(t, ast) - - includes := ast.(*parser.Document).Includes - assert.Len(t, includes, 2) - assert.Equal(t, "../user.thrift", includes[0].Path.Value.Text) - assert.Equal(t, true, includes[1].Path.BadNode) -} diff --git a/parser/test/service_test.go b/parser/test/service_test.go deleted file mode 100644 index 0af9df5..0000000 --- a/parser/test/service_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseServiceIdentifierError(t *testing.T) { - - demoContent := `service "aa" - {} // err1, line 1, col 8 - -` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"1:9"} - errs := []error{parser.InvalidServiceIdentifierError} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, equalErrors(errList.Errors(), errs)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseServiceBlockRCURError(t *testing.T) { - - demoContent := `service aa {` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"1:13"} - errs := []error{parser.InvalidServiceBlockRCURError} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, equalErrors(errList.Errors(), errs)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseServiceFunctionError(t *testing.T) { - - demoContent := `service aa { - string Tes -}` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"2:3"} - errs := []error{parser.InvalidServiceFunctionError} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, equalErrors(errList.Errors(), errs)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseServiceOK(t *testing.T) { - - demoContent := `service aa extends bb -{ - string Test(1:required string test) throws (1:required error err) - -}` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) -} diff --git a/parser/test/struct_test.go b/parser/test/struct_test.go deleted file mode 100644 index a05432c..0000000 --- a/parser/test/struct_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseStructIdentifierError(t *testing.T) { - - demoContent := `struct { // err1, line 1, col 8 - // Name is demo name - 1: required string Name; - 2: optional boo Required = true; -} - -struct 123Demos { // err3, line 7, col 8 -} - -struct Demos{ -} -` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"1:8", "7:8"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidStructIdentifierError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseStructBlockRCURError(t *testing.T) { - - demoContent := `struct Demo { - // Name is demo name - - 2: optional boo Required = true; // err1, line 6, col 1 - -struct Demos{} -struct Demos{ // err2, line 8, col 1 -struct Demos{} -` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"6:1", "8:1"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidStructBlockRCURError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseStructFieldError(t *testing.T) { - demoContent := `struct Demo { - 1: optional i64 count - a: optional boo Required = true; // err1, line 3, col 3 - 2: required i32 test4; - required string test; // err2, line 5, col 3 - 4: required i32 test; - 5 required string test; // err3, line 7, col 3 - 6: required test test; - no comment // err4, line 9, col 3 -} -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"3:3", "5:3", "7:3", "9:3"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidStructFieldError)) - assert.True(t, containsError(errList.Errors(), parser.InvalidFieldIndexError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseStructFieldDefault(t *testing.T) { - demoContent := `struct Demo { -1: optional set with_default = [ "test", "aaa" ] -2: optional set bin_set = {} -3: optional map bin_map = {} -} -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - - assert.NotNil(t, ast) -} - -func Test_ParseLocation(t *testing.T) { - demoContent := `struct Demo { -1: optional set with_default = [ "😀", "aaa" ] -}` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - doc := ast.(*parser.Document) - docPos := doc.Location.Pos() - docEnd := doc.Location.End() - - // doc pos - assert.Equal(t, 1, docPos.Line) - assert.Equal(t, 1, docPos.Col) - assert.Equal(t, 0, docPos.Offset) - // doc end - assert.Equal(t, 3, docEnd.Line) - assert.Equal(t, 3, docEnd.Col) - assert.Equal(t, 72, docEnd.Offset) - - assert.Len(t, doc.Structs, 1) - structNamePos := doc.Structs[0].Identifier.Name.Pos() - structNameEnd := doc.Structs[0].Identifier.Name.End() - - // struct pos - assert.Equal(t, 1, structNamePos.Line) - assert.Equal(t, 8, structNamePos.Col) - assert.Equal(t, 7, structNamePos.Offset) - // struct end - assert.Equal(t, 1, structNameEnd.Line) - assert.Equal(t, 12, structNameEnd.Col) - assert.Equal(t, 11, structNameEnd.Offset) -} - -func Test_ParseStructErr(t *testing.T) { - demoContent := `struct Demo {` - _, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) -} diff --git a/parser/test/testutils.go b/parser/test/testutils.go deleted file mode 100644 index 7755e4e..0000000 --- a/parser/test/testutils.go +++ /dev/null @@ -1,26 +0,0 @@ -package test - -import "github.com/joyme123/thrift-ls/parser" - -func containsError(errs []error, target error) bool { - for _, err := range errs { - err = err.(parser.ParserError).InnerError() - if err == target { - return true - } - } - return false -} - -func equalErrors(errs []error, target []error) bool { - if len(errs) != len(target) { - return false - } - for i, err := range errs { - err = err.(parser.ParserError).InnerError() - if err != target[i] { - return false - } - } - return true -} diff --git a/parser/test/thrift_src.peg b/parser/test/thrift_src.peg deleted file mode 100644 index a100870..0000000 --- a/parser/test/thrift_src.peg +++ /dev/null @@ -1,149 +0,0 @@ -{ -package parser - - -} - - -Document <- Header* Definition* Skip !. - -Header <- Skip (Include / CppInclude / Namespace) SkipLine - -Include <- INCLUDE Literal - -CppInclude <- CPPINCLUDE Literal - -Namespace <- NAMESPACE NamespaceScope Identifier Annotations? - -NamespaceScope <- Skip - '*' Indent* - / Identifier - -Definition <- ReservedComments Skip (Const / Typedef / Enum / Service / Struct / Union / Exception) Annotations? SkipLine - -Const <- CONST FieldType Identifier EQUAL ConstValue ListSeparator? - -Typedef <- TYPEDEF FieldType Identifier - -Enum <- ENUM Identifier LWING (ReservedComments Identifier (EQUAL IntConstant)? Annotations? ListSeparator? SkipLine)* RWING - -Service <- SERVICE Identifier ( EXTENDS Identifier )? LWING Function* RWING - -Struct <- STRUCT Identifier LWING Field* RWING - -Union <- UNION Identifier LWING Field* RWING - -Exception <- EXCEPTION Identifier LWING Field* RWING - -Field <- ReservedComments Skip FieldId? FieldReq? FieldType Identifier (EQUAL ConstValue)? Annotations? ListSeparator? ReservedEndLineComments SkipLine - -FieldId <- Skip IntConstant COLON Indent* - -FieldReq <- Skip <('required' / 'optional')> Indent* - -Function <- ReservedComments Skip ONEWAY? FunctionType Identifier LPAR Field* RPAR Throws? Annotations? ListSeparator? SkipLine - -FunctionType <- VOID / FieldType - -Throws <- THROWS LPAR Field* RPAR - -FieldType <- (ContainerType / BaseType / Identifier) Annotations? - -BaseType <- (BOOL / BYTE / I8 / I16 / I32 / I64 / DOUBLE / STRING / BINARY) - -ContainerType <- MapType / SetType / ListType - -MapType <- MAP CppType? LPOINT FieldType COMMA FieldType RPOINT - -SetType <- SET CppType? LPOINT FieldType RPOINT - -ListType <- LIST LPOINT FieldType RPOINT CppType? - -CppType <- CPPTYPE Literal - -ConstValue <- DoubleConstant / IntConstant / Literal / Identifier / ConstList / ConstMap - -IntConstant <- Skip < '0x' ([0-9] / [A-Z] / [a-z])+ / '0o' Digit+ / [+\-]? Digit+ > Indent* - -DoubleConstant <- Skip <[+\-]? ( - Digit* '.' Digit+ Exponent? - / Digit+ Exponent - )> Indent* - -Exponent <- ('e' / 'E') IntConstant - -Annotations <- LPAR Annotation+ RPAR - -Annotation <- Identifier EQUAL Literal ListSeparator? - -ConstList <- LBRK (ConstValue ListSeparator?)* RBRK - -ConstMap <- LWING (ConstValue COLON ConstValue ListSeparator?)* RWING - -EscapeLiteralChar <- '\\' ["'] - -Literal <- Skip '"' <(EscapeLiteralChar / !'"' .)*> '"' Indent* - / Skip "'" <(EscapeLiteralChar / !"'" .)*> "'" Indent* - -Identifier <- Skip Indent* - -ListSeparator <- Skip (',' / ';') Indent* - -Letter <- [A-Z] / [a-z] / '_' -LetterOrDigit <- [a-z] / [A-Z] / [0-9] / [_$] - -Digit <- [0-9] - -ReservedComments <- Skip -ReservedEndLineComments <- SkipLine - -Skip <- (Space / Comment)* -SkipLine <- (Indent / Comment)* - -Space <- (Indent / CarriageReturnLineFeed)+ -Indent <- [ \t\v] -CarriageReturnLineFeed <- [\r\n] - -Comment <- LongComment / LineComment / UnixComment -LongComment <- '/*' (!'*/' .)* '*/' -LineComment <- '//' (![\r\n] .)* -UnixComment <- '#' (![\r\n] .)* - -BOOL <- Skip <'bool'> !LetterOrDigit Indent* -BYTE <- Skip <'byte'> !LetterOrDigit Indent* -I8 <- Skip <'i8'> !LetterOrDigit Indent* -I16 <- Skip <'i16'> !LetterOrDigit Indent* -I32 <- Skip <'i32'> !LetterOrDigit Indent* -I64 <- Skip <'i64'> !LetterOrDigit Indent* -DOUBLE <- Skip <'double'> !LetterOrDigit Indent* -STRING <- Skip <'string'> !LetterOrDigit Indent* -BINARY <- Skip <'binary'> !LetterOrDigit Indent* -CONST <- Skip 'const' !LetterOrDigit Indent* -ONEWAY <- Skip 'oneway' !LetterOrDigit Indent* -TYPEDEF <- Skip 'typedef' !LetterOrDigit Indent* -MAP <- Skip 'map' !LetterOrDigit Indent* -SET <- Skip 'set' !LetterOrDigit Indent* -LIST <- Skip 'list' !LetterOrDigit Indent* -VOID <- Skip 'void' !LetterOrDigit Indent* -THROWS <- Skip 'throws' !LetterOrDigit Indent* -EXCEPTION <- Skip 'exception' !LetterOrDigit Indent* -EXTENDS <- Skip 'extends' !LetterOrDigit Indent* -SERVICE <- Skip 'service' !LetterOrDigit Indent* -STRUCT <- Skip 'struct' !LetterOrDigit Indent* -UNION <- Skip 'union' !LetterOrDigit Indent* -ENUM <- Skip 'enum' !LetterOrDigit Indent* -INCLUDE <- Skip 'include' !LetterOrDigit Indent* -CPPINCLUDE <- Skip 'cpp_include' !LetterOrDigit Indent* -NAMESPACE <- Skip 'namespace' !LetterOrDigit Indent* -CPPTYPE <- Skip 'cpp_type' !LetterOrDigit Indent* -LBRK <- Skip '[' Indent* -RBRK <- Skip ']' Indent* -LWING <- Skip '{' Indent* -RWING <- Skip '}' Indent* -EQUAL <- Skip '=' Indent* -LPOINT <- Skip '<' Indent* -RPOINT <- Skip '>' Indent* -COMMA <- Skip ',' Indent* -LPAR <- Skip '(' Indent* -RPAR <- Skip ')' Indent* -COLON <- Skip ':' Indent* diff --git a/parser/test/thrift_test.go b/parser/test/thrift_test.go deleted file mode 100644 index 13b32ef..0000000 --- a/parser/test/thrift_test.go +++ /dev/null @@ -1,454 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_Parse(t *testing.T) { - ast, err := parser.Parse("thrifttest.thrift", []byte(ThriftTestContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) -} - -func Test_Equals(t *testing.T) { - psr := parser.PEGParser{} - doc, err := psr.Parse("thrifttest.thrift", []byte(ThriftTestContent)) - assert.Zero(t, len(err)) - assert.NotNil(t, doc) - - assert.True(t, doc.Equals(doc)) -} - -const ThriftTestContent = ` -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * Contains some contributions under the Thrift Software License. - * Please see doc/old-thrift-license.txt in the Thrift distribution for - * details. - */ - -namespace c_glib TTest -namespace cpp thrift.test -namespace delphi Thrift.Test -namespace go thrifttest -namespace java thrift.test -namespace js ThriftTest -namespace lua ThriftTest -namespace netstd ThriftTest -namespace perl ThriftTest -namespace php ThriftTest -namespace py ThriftTest -namespace py.twisted ThriftTest -namespace rb Thrift.Test -namespace st ThriftTest -namespace xsd test (uri = 'http://thrift.apache.org/ns/ThriftTest') - -// Presence of namespaces and sub-namespaces for which there is -// no generator should compile with warnings only -namespace noexist ThriftTest -namespace cpp.noexist ThriftTest - -namespace * thrift.test - -/** - * Docstring! - */ -enum Numberz -{ - ONE = 1, - TWO, - THREE, - FIVE = 5, - SIX, - EIGHT = 8 -} - -const Numberz myNumberz = Numberz.ONE; -// the following is expected to fail: -// const Numberz urNumberz = ONE; - -typedef i64 UserId - -struct Bonk -{ - 1: string message, - 2: i32 type -} - -typedef map MapType - -struct Bools { - 1: bool im_true, - 2: bool im_false, -} - -struct Xtruct -{ - 1: string string_thing, - 4: i8 byte_thing, - 9: i32 i32_thing, - 11: i64 i64_thing -} - -struct Xtruct2 -{ - 1: i8 byte_thing, // used to be byte, hence the name - 2: Xtruct struct_thing, - 3: i32 i32_thing -} - -struct Xtruct3 -{ - 1: string string_thing, - 4: i32 changed, - 9: i32 i32_thing, - 11: i64 i64_thing -} - - -struct Insanity -{ - 1: map userMap, - 2: list xtructs -} (python.immutable= "") - -struct CrazyNesting { - 1: string string_field, - 2: optional set set_field, - // Do not insert line break as test/go/Makefile.am is removing this line with pattern match - 3: required list (python.immutable = ""), map(python.immutable = "")> (python.immutable = "")>>>> list_field, - 4: binary binary_field - 5: uuid uuid_field -} - -union SomeUnion { - 1: map map_thing, - 2: string string_thing, - 3: i32 i32_thing, - 4: Xtruct3 xtruct_thing, - 5: Insanity insanity_thing -} - -exception Xception { - 1: i32 errorCode, - 2: string message -} - -exception Xception2 { - 1: i32 errorCode, - 2: Xtruct struct_thing -} - -struct EmptyStruct {} - -struct OneField { - 1: EmptyStruct field -} - -service ThriftTest -{ - /** - * Prints "testVoid()" and returns nothing. - */ - void testVoid(), - - /** - * Prints 'testString("%s")' with thing as '%s' - * @param string thing - the string to print - * @return string - returns the string 'thing' - */ - string testString(1: string thing), - - /** - * Prints 'testBool("%s")' where '%s' with thing as 'true' or 'false' - * @param bool thing - the bool data to print - * @return bool - returns the bool 'thing' - */ - bool testBool(1: bool thing), - - /** - * Prints 'testByte("%d")' with thing as '%d' - * The types i8 and byte are synonyms, use of i8 is encouraged, byte still exists for the sake of compatibility. - * @param byte thing - the i8/byte to print - * @return i8 - returns the i8/byte 'thing' - */ - i8 testByte(1: i8 thing), - - /** - * Prints 'testI32("%d")' with thing as '%d' - * @param i32 thing - the i32 to print - * @return i32 - returns the i32 'thing' - */ - i32 testI32(1: i32 thing), - - /** - * Prints 'testI64("%d")' with thing as '%d' - * @param i64 thing - the i64 to print - * @return i64 - returns the i64 'thing' - */ - i64 testI64(1: i64 thing), - - /** - * Prints 'testDouble("%f")' with thing as '%f' - * @param double thing - the double to print - * @return double - returns the double 'thing' - */ - double testDouble(1: double thing), - - /** - * Prints 'testBinary("%s")' where '%s' is a hex-formatted string of thing's data - * @param binary thing - the binary data to print - * @return binary - returns the binary 'thing' - */ - binary testBinary(1: binary thing), - - /** - * Prints 'testUuid("%s")' where '%s' is the uuid given. Note that the uuid byte order should be correct. - * @param uuid thing - the uuid to print - * @return uuid - returns the uuid 'thing' - */ - uuid testUuid(1: uuid thing), - - /** - * Prints 'testStruct("{%s}")' where thing has been formatted into a string of comma separated values - * @param Xtruct thing - the Xtruct to print - * @return Xtruct - returns the Xtruct 'thing' - */ - Xtruct testStruct(1: Xtruct thing), - - /** - * Prints 'testNest("{%s}")' where thing has been formatted into a string of the nested struct - * @param Xtruct2 thing - the Xtruct2 to print - * @return Xtruct2 - returns the Xtruct2 'thing' - */ - Xtruct2 testNest(1: Xtruct2 thing), - - /** - * Prints 'testMap("{%s")' where thing has been formatted into a string of 'key => value' pairs - * separated by commas and new lines - * @param map thing - the map to print - * @return map - returns the map 'thing' - */ - map testMap(1: map thing), - - /** - * Prints 'testStringMap("{%s}")' where thing has been formatted into a string of 'key => value' pairs - * separated by commas and new lines - * @param map thing - the map to print - * @return map - returns the map 'thing' - */ - map testStringMap(1: map thing), - - /** - * Prints 'testSet("{%s}")' where thing has been formatted into a string of values - * separated by commas and new lines - * @param set thing - the set to print - * @return set - returns the set 'thing' - */ - set testSet(1: set thing), - - /** - * Prints 'testList("{%s}")' where thing has been formatted into a string of values - * separated by commas and new lines - * @param list thing - the list to print - * @return list - returns the list 'thing' - */ - list testList(1: list thing), - - /** - * Prints 'testEnum("%d")' where thing has been formatted into its numeric value - * @param Numberz thing - the Numberz to print - * @return Numberz - returns the Numberz 'thing' - */ - Numberz testEnum(1: Numberz thing), - - /** - * Prints 'testTypedef("%d")' with thing as '%d' - * @param UserId thing - the UserId to print - * @return UserId - returns the UserId 'thing' - */ - UserId testTypedef(1: UserId thing), - - /** - * Prints 'testMapMap("%d")' with hello as '%d' - * @param i32 hello - the i32 to print - * @return map> - returns a dictionary with these values: - * {-4 => {-4 => -4, -3 => -3, -2 => -2, -1 => -1, }, 4 => {1 => 1, 2 => 2, 3 => 3, 4 => 4, }, } - */ - map> testMapMap(1: i32 hello), - - /** - * So you think you've got this all worked out, eh? - * - * Creates a map with these values and prints it out: - * { 1 => { 2 => argument, - * 3 => argument, - * }, - * 2 => { 6 => , }, - * } - * @return map> - a map with the above values - */ - map> testInsanity(1: Insanity argument), - - /** - * Prints 'testMulti()' - * @param i8 arg0 - - * @param i32 arg1 - - * @param i64 arg2 - - * @param map arg3 - - * @param Numberz arg4 - - * @param UserId arg5 - - * @return Xtruct - returns an Xtruct with string_thing = "Hello2, byte_thing = arg0, i32_thing = arg1 - * and i64_thing = arg2 - */ - Xtruct testMulti(1: i8 arg0, 2: i32 arg1, 3: i64 arg2, 4: map arg3, 5: Numberz arg4, 6: UserId arg5), - - /** - * Print 'testException(%s)' with arg as '%s' - * @param string arg - a string indication what type of exception to throw - * if arg == "Xception" throw Xception with errorCode = 1001 and message = arg - * else if arg == "TException" throw TException - * else do not throw anything - */ - void testException(1: string arg) throws(1: Xception err1), - - /** - * Print 'testMultiException(%s, %s)' with arg0 as '%s' and arg1 as '%s' - * @param string arg - a string indicating what type of exception to throw - * if arg0 == "Xception" throw Xception with errorCode = 1001 and message = "This is an Xception" - * else if arg0 == "Xception2" throw Xception2 with errorCode = 2002 and struct_thing.string_thing = "This is an Xception2" - * else do not throw anything - * @return Xtruct - an Xtruct with string_thing = arg1 - */ - Xtruct testMultiException(1: string arg0, 2: string arg1) throws(1: Xception err1, 2: Xception2 err2) - - /** - * Print 'testOneway(%d): Sleeping...' with secondsToSleep as '%d' - * sleep 'secondsToSleep' - * Print 'testOneway(%d): done sleeping!' with secondsToSleep as '%d' - * @param i32 secondsToSleep - the number of seconds to sleep - */ - oneway void testOneway(1:i32 secondsToSleep) -} - -service SecondService -{ - /** - * Prints 'testString("%s")' with thing as '%s' - * @param string thing - the string to print - * @return string - returns the string 'thing' - */ - string secondtestString(1: string thing) -} - -struct VersioningTestV1 { - 1: i32 begin_in_both, - 3: string old_string, - 12: i32 end_in_both -} - -struct VersioningTestV2 { - 1: i32 begin_in_both, - - 2: i32 newint, - 3: i8 newbyte, - 4: i16 newshort, - 5: i64 newlong, - 6: double newdouble - 7: Bonk newstruct, - 8: list newlist, - 9: set newset, - 10: map newmap, - 11: string newstring, - 12: i32 end_in_both -} - -struct ListTypeVersioningV1 { - 1: list myints; - 2: string hello; -} - -struct ListTypeVersioningV2 { - 1: list strings; - 2: string hello; -} - -struct GuessProtocolStruct { - 7: map map_field, -} - -struct LargeDeltas { - 1: Bools b1, - 10: Bools b10, - 100: Bools b100, - 500: bool check_true, - 1000: Bools b1000, - 1500: bool check_false, - 2000: VersioningTestV2 vertwo2000, - 2500: set a_set2500, - 3000: VersioningTestV2 vertwo3000, - 4000: list big_numbers -} - -struct NestedListsI32x2 { - 1: list> integerlist -} -struct NestedListsI32x3 { - 1: list>> integerlist -} -struct NestedMixedx2 { - 1: list> int_set_list - 2: map> map_int_strset - 3: list>> map_int_strset_list -} -struct ListBonks { - 1: list bonk -} -struct NestedListsBonk { - 1: list>> bonk -} - -struct BoolTest { - 1: optional bool b = true; - 2: optional string s = "true"; -} - -struct StructA { - 1: required string s; -} - -struct StructB { - 1: optional StructA aa; - 2: required StructA ab; -} - -struct OptionalSetDefaultTest { - 1: optional set with_default = [ "test" ] -} - -struct OptionalBinary { - 1: optional set bin_set = {} - 2: optional map bin_map = {} -} - -// comments -` diff --git a/parser/test/typedef_test.go b/parser/test/typedef_test.go deleted file mode 100644 index f57cf12..0000000 --- a/parser/test/typedef_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseTypedefError(t *testing.T) { - - demoContent := `typedef a "b" -typedef a b -typedef a 1a` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"1:11", "3:11"} - errs := []error{parser.InvalidTypedefIdentifierError, parser.InvalidTypedefIdentifierError} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, equalErrors(errList.Errors(), errs)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseTypedefLocation(t *testing.T) { - - demoContent := `typedef a b` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - doc := ast.(*parser.Document) - docPos := doc.Location.Pos() - docEnd := doc.Location.End() - - // doc pos - assert.Equal(t, 1, docPos.Line) - assert.Equal(t, 1, docPos.Col) - assert.Equal(t, 0, docPos.Offset) - // doc end - assert.Equal(t, 1, docEnd.Line) - assert.Equal(t, 12, docEnd.Col) - assert.Equal(t, len(demoContent), docEnd.Offset) - - assert.Len(t, doc.Typedefs, 1) - aliasPos := doc.Typedefs[0].Alias.Name.Pos() - aliasEnd := doc.Typedefs[0].Alias.Name.End() - - // union pos - assert.Equal(t, 1, aliasPos.Line) - assert.Equal(t, 11, aliasPos.Col) - assert.Equal(t, 10, aliasPos.Offset) - // union end - assert.Equal(t, 1, aliasEnd.Line) - assert.Equal(t, 12, aliasEnd.Col) - assert.Equal(t, 11, aliasEnd.Offset) -} diff --git a/parser/test/union_test.go b/parser/test/union_test.go deleted file mode 100644 index 0cc365c..0000000 --- a/parser/test/union_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package test - -import ( - "testing" - - "github.com/joyme123/thrift-ls/parser" - "github.com/stretchr/testify/assert" -) - -func Test_ParseUnionIdentifierError(t *testing.T) { - - demoContent := `union { // err1, line 1, col 7 - // Name is demo name - 1: required string Name; - 2: optional boo Required = true; -} - -union 123Demos { // err3, line 7, col 7 -} - -union Demos{ -} -` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - - errPos := []string{"1:7", "7:7"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidUnionIdentifierError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseUnionBlockRCURError(t *testing.T) { - - demoContent := `union Demo { - // Name is demo name - - 2: optional boo Required = true; // err1, line 6, col 1 - -struct Demos{} -struct Demos{ // err2, line 8, col 1 -struct Demos{} -` - - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"6:1", "8:1"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidUnionBlockRCURError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseUnionFieldError(t *testing.T) { - demoContent := `union Demo { - 1: optional i64 count - a: optional boo Required = true; // err1, line 3, col 3 - 2: required i32 test4; - required string test; // err2, line 5, col 3 - 4: required i32 test; - 5 required string test; // err3, line 7, col 3 - 6: required test test; - no comment // err4, line 9, col 3 -} -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.Error(t, err) - if err != nil { - errList, ok := err.(parser.ErrorLister) - assert.True(t, ok) - errPos := []string{"3:3", "5:3", "7:3", "9:3"} - assert.Len(t, errList.Errors(), len(errPos)) - assert.True(t, containsError(errList.Errors(), parser.InvalidUnionFieldError)) - assert.True(t, containsError(errList.Errors(), parser.InvalidFieldIndexError)) - - for i, err := range errList.Errors() { - assert.Contains(t, err.Error(), errPos[i]) - t.Logf("error %d: %v\n", i, err) - } - } - - assert.NotNil(t, ast) -} - -func Test_ParseUnionFieldDefault(t *testing.T) { - demoContent := `union Demo { -1: optional set with_default = [ "test", "aaa" ] -2: optional set bin_set = {} -3: optional map bin_map = {} -} -` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - - assert.NotNil(t, ast) -} - -func Test_ParseUnionLocation(t *testing.T) { - demoContent := `union Demo { -1: optional set with_default = [ "😀", "aaa" ] -}` - ast, err := parser.Parse("test.thrift", []byte(demoContent)) - assert.NoError(t, err) - assert.NotNil(t, ast) - - doc := ast.(*parser.Document) - docPos := doc.Location.Pos() - docEnd := doc.Location.End() - - // doc pos - assert.Equal(t, 1, docPos.Line) - assert.Equal(t, 1, docPos.Col) - assert.Equal(t, 0, docPos.Offset) - // doc end - assert.Equal(t, 3, docEnd.Line) - assert.Equal(t, 3, docEnd.Col) - assert.Equal(t, 71, docEnd.Offset) - - assert.Len(t, doc.Unions, 1) - unionNamePos := doc.Unions[0].Name.Name.Pos() - unionNameEnd := doc.Unions[0].Name.Name.End() - - // union pos - assert.Equal(t, 1, unionNamePos.Line) - assert.Equal(t, 7, unionNamePos.Col) - assert.Equal(t, 6, unionNamePos.Offset) - // union end - assert.Equal(t, 1, unionNameEnd.Line) - assert.Equal(t, 11, unionNameEnd.Col) - assert.Equal(t, 10, unionNameEnd.Offset) -} diff --git a/parser/thrift.peg b/parser/thrift.peg deleted file mode 100644 index 36051fe..0000000 --- a/parser/thrift.peg +++ /dev/null @@ -1,1386 +0,0 @@ -{ -package parser - -func toStringSlice(strs any) []string { - if strs == nil { - return nil - } - - items := strs.([]any) - ret := make([]string, 0, len(items)) - for i := range items { - var item string - if _, ok := items[i].([]interface{}); ok { - data := items[i].([]interface{})[1] - if bs, ok := data.([]uint8); ok { - item = string(bs) - } else { - item = items[i].([]interface{})[1].(string) - } - } else { - item = items[i].(string) - } - - ret = append(ret, item) - } - - return ret -} - -func toFieldSlice(fields any) []*Field { - if fields == nil { - return nil - } - - items := fields.([]any) - ret := make([]*Field, 0, len(items)) - for i := range items { - item := items[i].(*Field) - ret = append(ret, item) - } - return ret -} - -func toConstValueSlice(values any) []*ConstValue { - if values == nil { - return nil - } - - items := values.([]any) - ret := make([]*ConstValue, 0, len(items)) - for i := range items { - item := items[i].(*ConstValue) - ret = append(ret, item) - } - return ret -} - -func toString(text any) string { - if text == nil { - return "" - } - - data := text.([]interface{}) - - ret := bytes.NewBuffer(nil) - for i := range data { - ret.WriteString(data[i].(string)) - } - - return ret.String() -} - -func toAnnotationSlice(annos any) []*Annotation { - if annos == nil { - return nil - } - items := annos.([]any) - ret := make([]*Annotation, 0, len(items)) - for i := range items { - ret = append(ret, items[i].(*Annotation)) - } - - return ret -} - -func toHeaderSlice(headers any) []Header { - if headers == nil { - return nil - } - items := headers.([]any) - ret := make([]Header, 0, len(items)) - for i := range items { - ret = append(ret, items[i].(Header)) - } - return ret -} - -func toDefinitionSlice(defs any) []Definition { - if defs == nil { - return nil - } - items := defs.([]any) - ret := make([]Definition, 0, len(items)) - for i := range items { - ret = append(ret, items[i].(Definition)) - } - return ret -} - -func toEnumValueSlice(v any) []*EnumValue { - if v == nil { - return nil - } - values := v.([]any) - - ret := make([]*EnumValue, 0, len(values)) - value := int64(0) - for i := range values { - enumV := values[i].(*EnumValue) - if enumV.ValueNode == nil { - enumV.Value = value - } else { - value = enumV.Value - } - value++ - ret = append(ret, enumV) - } - return ret -} - -func toFunctionSlice(fns any) []*Function { - if fns == nil { - return nil - } - items := fns.([]any) - ret := make([]*Function, 0, len(items)) - for i := range items { - ret = append(ret, items[i].(*Function)) - } - return ret -} - -func toCommentSlice(comments any) []*Comment { - if comments == nil { - return nil - } - items := comments.([]any) - ret := make([]*Comment, 0, len(items)) - for i := range items { - item := items[i] - commentItem, ok := item.(*Comment) - if ok { - ret = append(ret, commentItem) - } - } - - if len(ret) == 0 { - return nil - } - - return ret -} - -func toAnnotations(annos any) *Annotations { - if annos == nil { - return nil - } - - return annos.(*Annotations) -} - -func toListSeparatorKeyword(sep any) *ListSeparatorKeyword { - if sep == nil { - return nil - } - - return sep.(*ListSeparatorKeyword) -} - -} - -Document = headers:Header* defs:Definition* comments:ReservedComments !. { - return NewDocument(toHeaderSlice(headers), toDefinitionSlice(defs), comments.([]*Comment), NewLocationFromCurrent(c)), nil -} //{errHeader} ErrHeader //{errDefinition} ErrDefinition - -Header = comments:ReservedComments v:(Include / CppInclude / Namespace) endLineComments:ReservedEndLineComments { - c.globalStore["parse"] = "header" - v.(Header).SetComments(comments.([]*Comment), endLineComments.([]*Comment)) - return v, nil -} / x:(!(Definition) ReservedComments &(.+) &{ - /* fmt.Println("header predict:", c.pos, "text:", string(c.text)) */ - if state, ok := c.globalStore["parse"]; (!ok || ok && state.(string) == "header") { - return true, nil - } - return false, nil -} %{errHeader}) { - /* fmt.Println("header return:", c.pos, "text:", string(c.text)) */ - badHeader := x.([]any)[4].(*BadHeader) - return badHeader, nil -} //{errInclude} ErrInclude //{errCppInclude} ErrorCppInclude //{errNamespace} ErrorNamespace - -Include = includeKeyword:INCLUDE include:Literal { - includeV, ok := include.(*Literal) - if !ok { - includeV = include.([]interface{})[0].(*Literal) - } - return NewInclude(includeKeyword.(*IncludeKeyword), includeV, NewLocationFromCurrent(c)), nil -} / x:(&(INCLUDE .*) %{errInclude}) { - return x.([]any)[1], nil -} - - -CppInclude <- cppIncludeKeyword:CPPINCLUDE include:Literal { - includeV, ok := include.(*Literal) - if !ok { - includeV = include.([]interface{})[0].(*Literal) - } - return NewCPPInclude(cppIncludeKeyword.(*CPPIncludeKeyword), includeV, NewLocationFromCurrent(c)), nil -} / x:(&(CPPINCLUDE .*) %{errCppInclude}) { - return x.([]any)[1], nil -} - - -Namespace <- namespaceKeyword:NAMESPACE language:NamespaceScope name:Identifier annotations:Annotations? { - return NewNamespace(namespaceKeyword.(*NamespaceKeyword), language.(*NamespaceScope), name.(*Identifier), toAnnotations(annotations), NewLocationFromCurrent(c)), nil -} / x:(&(NAMESPACE .*) %{errNamespace}) { - return x.([]any)[1], nil -} - -NamespaceScope <- v:(NamespaceScopeAny / Identifier) { - id := v.(*Identifier) - res := &NamespaceScope{ - Identifier: *id, - } - - return res, nil -} - -NamespaceScopeAny = comments:ReservedComments idName:NamespaceScopeAnyToken Indent* { - return NewIdentifier(idName.(*IdentifierName), comments.([]*Comment), NewLocationFromCurrent(c)), nil -} - -NamespaceScopeAnyToken = '*' { - return NewIdentifierName("*", NewLocationFromCurrent(c)), nil -} - -Definition = comments:ReservedComments v:(Const / Typedef / Enum / Service / Struct / Union / Exception) annos:Annotations? endLineComments:ReservedEndLineComments { - c.globalStore["parse"] = "definition" - def := v.(Definition) - def.SetComments(comments.([]*Comment), endLineComments.([]*Comment)) - def.SetAnnotations(toAnnotations(annos)) - def.SetLocation(NewLocationFromCurrent(c)) - return def, nil -} / x:(ReservedComments &(.+) &{ - /* fmt.Println("definition predict:", c.pos, "text:", string(c.text)) */ - if state, ok := c.globalStore["parse"]; ok && state.(string) == "definition" { - return true, nil - } - return false, nil -} %{errDefinition}) { - /* fmt.Println("definition return:", c.pos, "text:", string(c.text)) */ - return x.([]any)[3], nil -} //{errConst} ErrConst //{errTypedef} ErrTypedef //{errEnum} ErrEnum //{errService} ErrService //{errStruct} ErrStruct //{errUnion} ErrUnion //{errException} ErrException - -Const = constKeyword:CONST t:FieldType name:DefinitionIdentifier v:ConstEqualValue sep:ListSeparator? { - equalAndValue := v.([]any) - return NewConst(constKeyword.(*ConstKeyword), equalAndValue[0].(*EqualKeyword), toListSeparatorKeyword(sep), name.(*Identifier), t.(*FieldType), equalAndValue[1].(*ConstValue), NewLocationFromCurrent(c)), nil -} / x:(&(CONST .*) %{errConst}) { - return x.([]any)[1], nil -} //{errIdentifier} ErrConstIdentifier //{errConstMissingValue} ErrConstMissingValue //{errConstConstValue} ErrConstConstValue - -ConstEqualValue = v:(EQUAL ConstValue) { - return v, nil -} / x:(!EQUAL %{errConstMissingValue}) { - return []any{NewBadEqualKeyword(), x.([]any)[1]}, nil -} / x:(EQUAL %{errConstConstValue}) { - return x, nil -} - -Typedef = typedefKeyword:TYPEDEF t:FieldType alias:DefinitionIdentifier { - return NewTypedef(typedefKeyword.(*TypedefKeyword), t.(*FieldType), alias.(*Identifier), NewLocationFromCurrent(c)), nil -} / x:(&(TYPEDEF .*) %{errTypedef}) { - return x.([]any)[1], nil -} //{errIdentifier} ErrTypedefIdentifier - -Enum = enum:ENUM name:DefinitionIdentifier lcur:LCUR v:EnumValueLine* rcur:RCUR { - values := toEnumValueSlice(v) - return NewEnum(enum.(*EnumKeyword), lcur.(*LCurKeyword), rcur.(*RCurKeyword), name.(*Identifier), values, NewLocationFromCurrent(c)), nil -} / x:(&(ENUM .*) %{errEnum}) { - return x.([]any)[1], nil -} //{errIdentifier} ErrEnumIdentifier //{errRCUR} ErrEnumRCUR //{errEnumValue} ErrEnumValue - -EnumValueLine = comments:ReservedComments v:EnumValue endLineComments:ReservedEndLineComments { - v.(*EnumValue).SetComments(comments.([]*Comment), endLineComments.([]*Comment)) - return v, nil -} - -EnumValue = name:Identifier value:(EnumValueIntConstant)? annos:Annotations? sep:ListSeparator? { - var valueNode *ConstValue - var equalNode *EqualKeyword - intV := int64(-1) - if value != nil { - //fmt.Println(value.(Node).Type(), value) - equalNode = value.([]any)[0].(*EqualKeyword) - valueNode = value.([]any)[1].(*ConstValue) - intV = valueNode.Value.(int64) - } - - return NewEnumValue(toListSeparatorKeyword(sep), equalNode, name.(*Identifier), valueNode, intV, toAnnotations(annos), NewLocationFromCurrent(c)), nil -} //{errIntConstant} ErrEnumValueIntConstant - -Service = svc:SERVICE name:DefinitionIdentifier extends:( EXTENDS Identifier )? lcur:LCUR fns:Function* rcur:RCUR { - var extendsVal *Identifier - var extendsKeyword *ExtendsKeyword - if extends != nil { - extendsKeyword = extends.([]any)[0].(*ExtendsKeyword) - extendsVal = extends.([]any)[1].(*Identifier) - } - fnsVal := toFunctionSlice(fns) - return NewService(svc.(*ServiceKeyword), extendsKeyword, lcur.(*LCurKeyword), rcur.(*RCurKeyword), name.(*Identifier), extendsVal, fnsVal, NewLocationFromCurrent(c)), nil -} / x:(&(SERVICE .*) %{errService}) { - return x.([]any)[1], nil -} //{errIdentifier} ErrServiceIdentifier //{errRCUR} ErrServiceRCUR //{errFunction} ErrServiceFunction - -Struct = st:STRUCT id:DefinitionIdentifier lcur:LCUR fields:FieldWithThrow* rcur:RCUR { - return NewStruct(st.(*StructKeyword), lcur.(*LCurKeyword), rcur.(*RCurKeyword), id.(*Identifier), toFieldSlice(fields), NewLocationFromCurrent(c)), nil -} / x:(&(STRUCT .*) %{errStruct}) { - return x.([]any)[1], nil -} //{errIdentifier} ErrStructIdentifier //{errRCUR} ErrStructRCUR //{errField} ErrStructField - -Union = union:UNION name:DefinitionIdentifier lcur:LCUR fields:FieldWithThrow* rcur:RCUR { - return NewUnion(union.(*UnionKeyword), lcur.(*LCurKeyword), rcur.(*RCurKeyword), name.(*Identifier), toFieldSlice(fields), NewLocationFromCurrent(c)), nil -} / x:(&(UNION .*) %{errUnion}) { - return x.([]any)[1], nil -} //{errIdentifier} ErrUnionIdentifier //{errRCUR} ErrUnionRCUR //{errField} ErrUnionField - - -Exception <- excep:EXCEPTION name:DefinitionIdentifier lcur:LCUR fields:FieldWithThrow* rcur:RCUR { - return NewException(excep.(*ExceptionKeyword), lcur.(*LCurKeyword), rcur.(*RCurKeyword), name.(*Identifier), toFieldSlice(fields), NewLocationFromCurrent(c)), nil -} / x:(&(EXCEPTION .*) %{errException}) { - return x.([]any)[1], nil -} //{errIdentifier} ErrExceptionIdentifier //{errRCUR} ErrExceptionRCUR //{errField} ErrExceptionField - - -FieldWithThrow = Field / x:( ReservedComments !(( '}' Indent*) / DefinitionStart ) %{errField} ) { - return x.([]any)[2], nil -} - -Field = comments:ReservedComments index:FieldId required:FieldReq? fieldType:FieldType id:Identifier value:(EQUAL ConstValue)? annos:Annotations? sep:ListSeparator? lineComments:ReservedEndLineComments { - var constV *ConstValue - var equalKeyword *EqualKeyword - if value != nil { - equalKeyword = value.([]any)[0].(*EqualKeyword) - constV = value.([]any)[1].(*ConstValue) - } - var requiredV *RequiredKeyword - if required != nil { - requiredV = required.(*RequiredKeyword) - } - - return NewField(equalKeyword, toListSeparatorKeyword(sep), comments.([]*Comment), lineComments.([]*Comment), toAnnotations(annos), index.(*FieldIndex), requiredV, fieldType.(*FieldType), id.(*Identifier), constV, NewLocationFromCurrent(c)), nil -} - - -FieldId = comments:ReservedComments i:FieldIndex colon:COLON Indent* { - fieldIndex := i.(*FieldIndex) - - - return NewFieldIndex(colon.(*ColonKeyword), fieldIndex.Value, comments.([]*Comment), fieldIndex.Location), nil -} //{errFieldIndex} ErrFieldIndex - -FieldReq = comments:ReservedComments r:IsRequired Indent* { - kw := NewKeyword(comments.([]*Comment), r.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &RequiredKeyword{Keyword: kw}, nil -} -IsRequired = v:(RequiredToken / OptionalToken) { - return v, nil -} - -RequiredToken = "required" { - return NewKeywordLiteral(c), nil -} - -OptionalToken = "optional" { - return NewKeywordLiteral(c), nil -} - -Function = comments:ReservedComments oneway:ONEWAY? ft:FunctionType name:DefinitionIdentifier lpar:LPAR args:FunctionFieldWithThrow* rpar:RPAR throws:Throws? annos:Annotations? sep:ListSeparator? endLineComments:ReservedEndLineComments { - var ftype *FieldType - var voidKeyword *VoidKeyword - if _, ok := ft.(*VoidKeyword); ok { - voidKeyword = ft.(*VoidKeyword) - } else { - ftype = ft.(*FieldType) - } - - var throwsV *Throws - if throws != nil { - throwsV = throws.(*Throws) - } - - var onewayKeyword *OnewayKeyword - if oneway != nil { - onewayKeyword = oneway.(*OnewayKeyword) - } - - return NewFunction(lpar.(*LParKeyword), rpar.(*RParKeyword), toListSeparatorKeyword(sep), name.(*Identifier), onewayKeyword, voidKeyword, ftype, toFieldSlice(args), throwsV, comments.([]*Comment), endLineComments.([]*Comment), toAnnotations(annos), NewLocationFromCurrent(c)), nil -} / x:(ReservedComments &(oneway:ONEWAY? ft:FunctionType) %{errFunction}) { - return x.([]any)[2], nil -} //{errIdentifier} ErrFunctionIdentifier //{errField} ErrFunctionArgument - -FunctionFieldWithThrow = v:Field { - return v, nil -} / x:(comments:ReservedComments &(index:FieldId required:FieldReq? fieldType:FieldType) %{errField}) { - return x.([]any)[2], nil -} - - -FunctionType <- VOID / FieldType - -Throws <- throws:THROWS lpar:LPAR fields:Field* rpar:RPAR { - return NewThrows(throws.(*ThrowsKeyword), lpar.(*LParKeyword), rpar.(*RParKeyword), toFieldSlice(fields), NewLocationFromCurrent(c)), nil -} - -FieldType = v:(ContainerType / BaseType / IdentifierType) annos:Annotations? { - ft := v.(*FieldType) - ft.Annotations = toAnnotations(annos) - - return ft, nil -} - -IdentifierType = v:Identifier { - return v.(*Identifier).ToFieldType(), nil -} - -BaseType = v:(BOOL / BYTE / I8 / I16 / I32 / I64 / DOUBLE / STRING / BINARY / UUID) { - return NewFieldType(nil, nil, nil, nil, v.(*TypeName), nil, nil, NewLocationFromCurrent(c)), nil -} - -ContainerType = v:(MapType / SetType / ListType) { - return v.(*FieldType), nil -} - -MapType = t:MAP cpp:CppType? lp:LPOINT key:FieldType comma:COMMA value:FieldType rp:RPOINT { - var cppType *CppType - if cpp != nil { - cppType = cpp.(*CppType) - } - - - return NewFieldType(lp.(*LPointKeyword), rp.(*RPointKeyword), comma.(*CommaKeyword), cppType, t.(*TypeName), key.(*FieldType), value.(*FieldType), NewLocationFromCurrent(c)), nil -} - -SetType = t:SET cpp:CppType? lp:LPOINT key:FieldType rp:RPOINT { - var cppType *CppType - if cpp != nil { - cppType = cpp.(*CppType) - } - - return NewFieldType(lp.(*LPointKeyword), rp.(*RPointKeyword), nil, cppType, t.(*TypeName), key.(*FieldType), nil, NewLocationFromCurrent(c)), nil -} - -ListType = t:LIST lp:LPOINT key:FieldType rp:RPOINT cpp:CppType? { - var cppType *CppType - if cpp != nil { - cppType = cpp.(*CppType) - } - - return NewFieldType(lp.(*LPointKeyword), rp.(*RPointKeyword), nil, cppType, t.(*TypeName), key.(*FieldType), nil, NewLocationFromCurrent(c)), nil -} - -CppType = cpp:CPPTYPE l:Literal { - return NewCppType(cpp.(*CppTypeKeyword), l.(*Literal), NewLocationFromCurrent(c)), nil -} - -ConstValue = v:(DoubleConstant / IntConstant / Literal / IdentifierConst / ConstMap / ConstList ) { - if literal, ok := v.(*Literal); ok { - return NewConstValue("string", literal, NewLocationFromCurrent(c)), nil - } - return v.(*ConstValue), nil -} - -IdentifierConst = comments:ReservedComments cv:IdentifierConstValue { - node := cv.(*ConstValue) - node.SetComments(comments.([]*Comment)) - return node, nil -} - -IdentifierConstValue = id:Identifier { - cv := NewConstValue("identifier", id.(*Identifier).Name.Text, NewLocationFromCurrent(c)) - return cv, nil -} - -EnumValueIntConstant = v:(EQUAL IntConstant) { - return v, nil -} / x:(EQUAL ReservedComments %{errIntConstant} Indent*) { - return []any{x.([]any)[0], x.([]any)[2]}, nil -} - -IntConstant = comments:ReservedComments v:(HexIntConstant / OctIntConstant / NormalIntConstant) ![a-zA-Z] Indent* { - cv := v.(*ConstValue) - cv.SetComments(comments.([]*Comment)) - - return cv, nil -} / x:(ReservedComments &("0x" / "0o" / (('+' / '-')? Digit)) %{errIntConstant}) { - return x.([]any)[2], nil -} - -HexIntConstant = "0x" ([0-9] / [A-Z] / [a-z])+ { - v, err := strconv.ParseInt(strings.TrimPrefix(string(c.text), "0x"), 16, 64) - if err != nil { - return nil, err - } - - cv := NewConstValue("i64", v, NewLocationFromCurrent(c)) - cv.ValueInText = string(c.text) - - return cv, nil -} - -OctIntConstant = "0o" Digit+ { - v, err := strconv.ParseInt(strings.TrimPrefix(string(c.text), "0o"), 8, 64) - if err != nil { - return nil, err - } - cv := NewConstValue("i64", v, NewLocationFromCurrent(c)) - cv.ValueInText = string(c.text) - - return cv, nil -} - -NormalIntConstant = ('+' / '-')? Digit+ { - v, err := strconv.ParseInt(string(c.text), 10, 64) - if err != nil { - return nil, err - } - cv := NewConstValue("i64", v, NewLocationFromCurrent(c)) - cv.ValueInText = string(c.text) - - return cv, nil -} - -FieldIndex = Digit+ { - v, err := strconv.ParseInt(string(c.text), 10, 64) - if err != nil { - return nil, err - } - return NewFieldIndex(nil, int(v), nil, NewLocationFromCurrent(c)), nil -} / x:(ReservedComments &([a-zA-Z]+ COLON) %{errFieldIndex}) { - return x.([]any)[2], nil -} - -DoubleConstant = comments:ReservedComments v:DoubleConstantValue Indent* { - cv := v.(*ConstValue) - cv.SetComments(comments.([]*Comment)) - - return cv, nil -} - -DoubleConstantValue = ('+' / '-')? (Digit* '.' Digit+ Exponent? / Digit+ Exponent) { - v, err := strconv.ParseFloat(string(c.text), 64) - if err != nil { - return nil, err - } - cv := NewConstValue("double", v, NewLocationFromCurrent(c)) - cv.ValueInText = string(c.text) - - return cv, nil -} - -Exponent = ('e' / 'E') IntConstant - -Annotations <- lpar:LPAR annos:Annotation+ rpar:RPAR { - return NewAnnotations(lpar.(*LParKeyword), rpar.(*RParKeyword), toAnnotationSlice(annos), NewLocationFromCurrent(c)), nil -} - -Annotation <- id:Identifier eq:EQUAL value:Literal sep:ListSeparator? { - return NewAnnotation(eq.(*EqualKeyword), toListSeparatorKeyword(sep), id.(*Identifier), value.(*Literal), NewLocationFromCurrent(c)), nil -} - -ConstList = lbrk:LBRK v:ConstListItem* rbrk:RBRK { - cv := NewConstValue("list", toConstValueSlice(v), NewLocationFromCurrent(c)) - - cv.LBrkKeyword = lbrk.(*LBrkKeyword) - cv.RBrkKeyword = rbrk.(*RBrkKeyword) - - return cv, nil -} - -ConstListItem = v:ConstValue sep:ListSeparator? { - cv := v.(*ConstValue) - - if sep != nil { - cv.ListSeparatorKeyword = sep.(*ListSeparatorKeyword) - } - - return cv, nil -} - -ConstMap = lcur:LCUR v:ConstMapItem* rcur:RCUR { - cv := NewConstValue("map", toConstValueSlice(v), NewLocationFromCurrent(c)) - - cv.LCurKeyword = lcur.(*LCurKeyword) - cv.RCurKeyword = rcur.(*RCurKeyword) - - return cv, nil -} - -ConstMapItem = key:ConstValue colon:COLON value:ConstValue sep:ListSeparator? { - cv := NewMapConstValue(key.(*ConstValue), value.(*ConstValue), NewLocationFromCurrent(c)) - - cv.ColonKeyword = colon.(*ColonKeyword) - if sep != nil { - cv.ListSeparatorKeyword = sep.(*ListSeparatorKeyword) - } - - return cv, nil -} - -EscapeLiteralChar = '\\' ["'] { - return string(c.text), nil -} - -Literal = l:(Literal1 / Literal2) { - return l, nil -} //{errLiteral1MissingRight} ErrLiteral1MissingRight //{errLiteral1} ErrLiteral1 //{errLiteral2MissingRight} ErrLiteral2MissingRight //{errLiteral2} ErrLiteral2 - -Literal1 = comments:ReservedComments '"' t:Literal1Val '"' Indent* { - return NewLiteral(comments.([]*Comment), t.(*LiteralValue), "\"", NewLocationFromCurrent(c)), nil -} / x:(&(ReservedComments '"' t:(EscapeLiteralChar / !'"' .)* Indent*) %{errLiteral1MissingRight}) { - return x.([]any)[1], nil -} - -Literal2 = comments:ReservedComments "'" t:Literal2Val "'" Indent* { - return NewLiteral(comments.([]*Comment), t.(*LiteralValue), "'", NewLocationFromCurrent(c)), nil -} / x:(&(ReservedComments "'" t:(EscapeLiteralChar / !"'" .)* Indent*) %{errLiteral2MissingRight}) { - return x.([]any)[1], nil -} - -Literal1Val = (EscapeLiteralChar / !["\r\n] .)* { - return NewLiteralValue(string(c.text), NewLocationFromCurrent(c)), nil -} - -Literal2Val = (EscapeLiteralChar / !['\r\n] .)* { - return NewLiteralValue(string(c.text), NewLocationFromCurrent(c)), nil -} - -DefinitionIdentifier = id:Identifier { - return id.(*Identifier), nil -} / %{errIdentifier} - -Identifier = comments:ReservedComments id:IdentifierToken Indent* { - idName := id.(*IdentifierName) - - return NewIdentifier(idName, comments.([]*Comment), NewLocationFromCurrent(c)), nil -} - -IdentifierToken = Letter ( Letter / Digit / '.' )* { - return NewIdentifierName(string(c.text), NewLocationFromCurrent(c)), nil -} - -ListSeparator = comments:ReservedComments t:ListSeparatorToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ListSeparatorKeyword{Keyword: kw}, nil -} - -ListSeparatorToken = (',' / ';') { - return NewKeywordLiteral(c), nil -} - -Letter = [A-Z] / [a-z] / '_' { - return string(c.text), nil -} -LetterOrDigit = [a-z] / [A-Z] / [0-9] / [_$] { - return string(c.text), nil -} - -Digit = [0-9] { - return string(c.text), nil -} - -ReservedComments = comments:(Space / Comment)* { - return toCommentSlice(comments), nil -} -ReservedEndLineComments = comments:(Indent / Comment)* { - return toCommentSlice(comments), nil -} - -Space = (Indent / CarriageReturnLineFeed)+ { - return "", nil -} -Indent = [ \t\v] { - return "", nil -} -CarriageReturnLineFeed = [\r\n] - -Comment = v:(LongComment / LineComment / UnixComment) { - return v.(*Comment), nil -} -LongComment = "/*" LongCommentMatch "*/" { - return NewComment(string(c.text), CommentStyleMultiLine, NewLocationFromCurrent(c)), nil -} -LongCommentMatch = (!"*/" .)* { - return string(c.text), nil -} - -LineComment = "//" LineCommentMatch { - return NewComment(string(c.text), CommentStyleSingleLine, NewLocationFromCurrent(c)), nil -} -LineCommentMatch = (![\r\n] .)* { - return string(c.text), nil -} - -UnixComment = '#' UnixCommentMatch { - return NewComment(string(c.text), CommentStyleShell, NewLocationFromCurrent(c)), nil -} -UnixCommentMatch = (![\r\n] .)* { - return string(c.text), nil -} - -BOOL = comments:ReservedComments t:BOOLToken !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -BOOLToken = "bool" { - return NewTypeName(string(c.text), c.pos), nil -} - -BYTE = comments:ReservedComments t:BYTEToken !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -BYTEToken = "byte" { - return NewTypeName(string(c.text), c.pos), nil -} - -I8 = comments:ReservedComments t:I8Token !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -I8Token = "i8" { - return NewTypeName(string(c.text), c.pos), nil -} - - -I16 = comments:ReservedComments t:I16Token !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -I16Token = "i16" { - return NewTypeName(string(c.text), c.pos), nil -} - -I32 = comments:ReservedComments t:I32Token !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -I32Token = "i32" { - return NewTypeName(string(c.text), c.pos), nil -} - -I64 = comments:ReservedComments t:I64Token !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -I64Token = "i64" { - return NewTypeName(string(c.text), c.pos), nil -} - -DOUBLE = comments:ReservedComments t:DOUBLEToken !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -DOUBLEToken = "double" { - return NewTypeName(string(c.text), c.pos), nil -} - -STRING = comments:ReservedComments t:STRINGToken !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -STRINGToken = "string" { - return NewTypeName(string(c.text), c.pos), nil -} - -BINARY = comments:ReservedComments t:BINARYToken !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -BINARYToken = "binary" { - return NewTypeName(string(c.text), c.pos), nil -} - -UUID = comments:ReservedComments t:UUIDToken !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -UUIDToken = "uuid" { - return NewTypeName(string(c.text), c.pos), nil -} - -MAP = comments:ReservedComments t:MAPToken !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -MAPToken = "map" { - return NewTypeName(string(c.text), c.pos), nil -} - -SET = comments:ReservedComments t:SETToken !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -SETToken = "set" { - return NewTypeName(string(c.text), c.pos), nil -} - -LIST = comments:ReservedComments t:ListToken !LetterOrDigit Indent* { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} -ListToken = "list" { - return NewTypeName(string(c.text), c.pos), nil -} - -CONST = comments:ReservedComments t:CONSTToken !LetterOrDigit Indent* { - - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ConstKeyword{Keyword: kw}, nil -} -CONSTToken = "const" { - return NewKeywordLiteral(c), nil -} - -ONEWAY = comments:ReservedComments t:ONEWAYToken !LetterOrDigit Indent* { - - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &OnewayKeyword{Keyword: kw}, nil -} -ONEWAYToken = "oneway" { - return NewKeywordLiteral(c), nil -} - -TYPEDEF = comments:ReservedComments t:TYPEDEFToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &TypedefKeyword{Keyword: kw}, nil -} - -TYPEDEFToken = "typedef" { - return NewKeywordLiteral(c), nil -} - - -VOID = comments:ReservedComments t:VOIDToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &VoidKeyword{Keyword: kw}, nil -} -VOIDToken = "void" { - return NewKeywordLiteral(c), nil -} - -THROWS = comments:ReservedComments t:THROWSToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ThrowsKeyword{Keyword: kw}, nil -} -THROWSToken = "throws" { - return NewKeywordLiteral(c), nil -} - -EXCEPTION = comments:ReservedComments t:EXCEPTIONToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ExceptionKeyword{Keyword: kw}, nil -} -EXCEPTIONToken = "exception" { - return NewKeywordLiteral(c), nil -} - - -EXTENDS = comments:ReservedComments t:EXTENDSToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ExtendsKeyword{Keyword: kw}, nil -} -EXTENDSToken = "extends" { - return NewKeywordLiteral(c), nil -} - -SERVICE = comments:ReservedComments t:SERVICEToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ServiceKeyword{Keyword: kw}, nil -} -SERVICEToken = "service" { - return NewKeywordLiteral(c), nil -} - -STRUCT = comments:ReservedComments t:STRUCTToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &StructKeyword{Keyword: kw}, nil -} -STRUCTToken = "struct" { - return NewKeywordLiteral(c), nil -} - -UNION = comments:ReservedComments t:UNIONToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &UnionKeyword{Keyword: kw}, nil -} -UNIONToken = "union" { - return NewKeywordLiteral(c), nil -} - -ENUM = comments:ReservedComments t:ENUMToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &EnumKeyword{Keyword: kw}, nil -} -ENUMToken = "enum" { - return NewKeywordLiteral(c), nil -} - -INCLUDE = comments:ReservedComments t:INCLUDEToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &IncludeKeyword{Keyword: kw}, nil -} -INCLUDEToken = "include" { - return NewKeywordLiteral(c), nil -} - -CPPINCLUDE = comments:ReservedComments t:CPPINCLUDEToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &CPPIncludeKeyword{Keyword: kw}, nil -} -CPPINCLUDEToken = "cpp_include" { - return NewKeywordLiteral(c), nil -} - -NAMESPACE = comments:ReservedComments t:NAMESPACEToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &NamespaceKeyword{Keyword: kw}, nil -} -NAMESPACEToken = "namespace" { - return NewKeywordLiteral(c), nil -} - - -CPPTYPE = comments:ReservedComments t:CPPTYPEToken !LetterOrDigit Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &CppTypeKeyword{Keyword: kw}, nil -} -CPPTYPEToken = "cpp_type" { - return NewKeywordLiteral(c), nil -} - - -LBRK = comments:ReservedComments t:LBRKToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &LBrkKeyword{Keyword: kw}, nil -} -LBRKToken = "[" { - return NewKeywordLiteral(c), nil -} - -RBRK = comments:ReservedComments t:RBRKToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &RBrkKeyword{Keyword: kw}, nil -} -RBRKToken = "]" { - return NewKeywordLiteral(c), nil -} - -LCUR = comments:ReservedComments t:LCURToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &LCurKeyword{Keyword: kw}, nil -} -RCUR = comments:ReservedComments t:RCURToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &RCurKeyword{Keyword: kw}, nil -} -LCURToken = "{" { - return NewKeywordLiteral(c), nil -} -RCURToken = "}" { - return NewKeywordLiteral(c), nil -} / %{errRCUR} - - -EQUAL = comments:ReservedComments t:EQUALToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &EqualKeyword{Keyword: kw}, nil -} -EQUALToken = "=" { - return NewKeywordLiteral(c), nil -} - -LPOINT = comments:ReservedComments t:LPOINTToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &LPointKeyword{Keyword: kw}, nil -} -LPOINTToken = "<" { - return NewKeywordLiteral(c), nil -} - -RPOINT = comments:ReservedComments t:RPOINTToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &RPointKeyword{Keyword: kw}, nil -} -RPOINTToken = ">" { - return NewKeywordLiteral(c), nil -} - -COMMA = comments:ReservedComments t:COMMAToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &CommaKeyword{Keyword: kw}, nil -} -COMMAToken = "," { - return NewKeywordLiteral(c), nil -} - -LPAR = comments:ReservedComments t:LPARToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &LParKeyword{Keyword: kw}, nil -} -LPARToken = "(" { - return NewKeywordLiteral(c), nil -} - -RPAR = comments:ReservedComments t:RPARToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &RParKeyword{Keyword: kw}, nil -} -RPARToken = ")" { - return NewKeywordLiteral(c), nil -} - -COLON = comments:ReservedComments t:COLONToken Indent* { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ColonKeyword{Keyword: kw}, nil -} -COLONToken = ":" { - return NewKeywordLiteral(c), nil -} - -DefinitionStart = STRUCT / UNION / EXCEPTION / ENUM / SERVICE / CONST / TYPEDEF - -ErrFieldIndex = #{ - return InvalidFieldIndexError -} (!([:\r\n]) .)* { // 消费到冒号或本行结束 - return NewBadFieldIndex(NewLocationFromCurrent(c)), nil -} - -// struct - -ErrStructField = #{ - return InvalidStructFieldError -} ( !(Field / (ReservedComments '}' Indent*) / DefinitionStart ) . )+ { - return NewBadField(NewLocationFromCurrent(c)), nil -} - -ErrStructIdentifier = #{ - return InvalidStructIdentifierError -} ( !'{' .)* { // struct identifier 异常,consume 掉异常字符直到出现 '{' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} // 构造 identifier 的 badnode - -ErrStructRCUR = #{ - return InvalidStructBlockRCURError -} ( !DefinitionStart .)* { // 缺少 '}',消费异常字符直到出现下一个 definition。TODO(jpf): 后面会加入 typedef 等定义,需要扩展补充 - return NewBadKeywordLiteral(c), nil -} - -ErrStructLCUR = #{ - return InvalidStructBlockLCURError -} ( !'}' .)* { // 缺少 '{',消费异常字符直到出现下一个 Field 或者 '}' - return NewBadKeywordLiteral(c), nil -} - -// union - -ErrUnionIdentifier = #{ - return InvalidUnionIdentifierError -} ( !'{' .)* { // identifier 异常,consume 掉异常字符直到出现 '{' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -ErrUnionRCUR = #{ - return InvalidUnionBlockRCURError -} ( !DefinitionStart .)* { - return NewBadKeywordLiteral(c), nil -} - -ErrUnionLCUR = #{ - return InvalidUnionBlockLCURError -} ( !'}' .)* { // 缺少 '{',消费异常字符直到出现下一个 Field 或者 '}' - return "?", nil -} - -ErrUnionField = #{ - return InvalidUnionFieldError -} ( !(Field / (ReservedComments '}' Indent*) / DefinitionStart ) . )+ { - return NewBadField(NewLocationFromCurrent(c)), nil -} - -// exception - -ErrExceptionIdentifier = #{ - return InvalidExceptionIdentifierError -} ( !'{' .)* { // identifier 异常,consume 掉异常字符直到出现 '{' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -ErrExceptionRCUR = #{ - return InvalidExceptionBlockRCURError -} ( !DefinitionStart .)* { - return NewBadKeywordLiteral(c), nil -} - -ErrExceptionLCUR = #{ - return InvalidExceptionBlockLCURError -} ( !'}' .)* { // 缺少 '{',消费异常字符直到出现下一个 Field 或者 '}' - return NewBadKeywordLiteral(c), nil -} - -ErrExceptionField = #{ - return InvalidExceptionFieldError -} ( !(Field / (ReservedComments '}' Indent*) / DefinitionStart ) . )+ { - return NewBadField(NewLocationFromCurrent(c)), nil -} - -// enum - -ErrEnumIdentifier = #{ - return InvalidEnumIdentifierError -} ( !'{' .)* { // enum identifier 异常,consume 掉异常字符直到出现 '{' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -ErrEnumRCUR = #{ - return InvalidEnumBlockRCURError -} ( !DefinitionStart .)* { - return NewBadKeywordLiteral(c), nil -} - -ErrEnumLCUR = #{ - return InvalidEnumBlockLCURError -} ( !'}' .)* { // 缺少 '{',消费异常字符直到出现下一个 Field 或者 '}' - return NewBadKeywordLiteral(c), nil -} - -ErrEnumValue = #{ - return InvalidEnumValueError -} ( !(Field / (ReservedComments '}' Indent*) / DefinitionStart ) . )* { - return NewBadEnumValue(NewLocationFromCurrent(c)), nil -} - -ErrEnumValueIntConstant = #{ - return InvalidEnumValueIntConstantError -} ( !(CarriageReturnLineFeed ) . )* { - return NewBadIntConstValue(NewLocationFromCurrent(c)), nil -} - -// typedef - -ErrTypedefIdentifier = #{ - return InvalidTypedefIdentifierError -} ( !(CarriageReturnLineFeed) .)* { - t := NewBadIdentifier(NewLocationFromCurrent(c)) - return t, nil -} - -// const - -ErrConstIdentifier = #{ - return InvalidConstIdentifierError -} (!EQUAL .)* { - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -ErrConstMissingValue = #{ - return InvalidConstMissingValueError -} (![\r\n] .)* { - return NewBadConstValue(NewLocationFromCurrent(c)), nil -} - -ErrConstConstValue = #{ - return InvalidConstConstValueError -} (![\r\n] .)* { - return NewBadConstValue(NewLocationFromCurrent(c)), nil -} - -// service - -ErrServiceIdentifier = #{ - return InvalidServiceIdentifierError -} ( !'{' .)* { // identifier 异常,consume 掉异常字符直到出现 '{' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -ErrServiceRCUR = #{ - return InvalidServiceBlockRCURError -} ( !DefinitionStart .)* { - return NewBadKeywordLiteral(c), nil -} - -ErrServiceFunction = #{ - return InvalidServiceFunctionError -} ( ![\r\n] .)* { - return NewBadFunction(NewLocationFromCurrent(c)), nil -} - -// function -ErrFunctionIdentifier = #{ - return InvalidFunctionIdentifierError -} ( !'(' .)* { // identifier 异常,consume 掉异常字符直到出现 '(' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} // 构造 identifier 的 badnode - -ErrFunctionArgument = #{ - return InvalidFunctionArgumentError -} ( ![,;)\r\n] . )* { - return NewBadField(NewLocationFromCurrent(c)), nil -} - - -// common error - -ErrLiteral1MissingRight = #{ - return InvalidLiteral1MissingRightError -} ( ![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadLiteral(NewLocationFromCurrent(c)), nil -} - -ErrLiteral1 = #{ - return InvalidLiteral1Error -} ( ![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadLiteral(NewLocationFromCurrent(c)), nil -} - -ErrLiteral2MissingRight = #{ - return InvalidLiteral2MissingRightError -} ( ![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadLiteral(NewLocationFromCurrent(c)), nil -} - -ErrLiteral2 = #{ - return InvalidLiteral2Error -} ( ![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadLiteral(NewLocationFromCurrent(c)), nil -} - -ErrConst = #{ - return InvalidConstError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadConst(NewLocationFromCurrent(c)), nil -} - -ErrTypedef = #{ - return InvalidTypedefError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadTypedef(NewLocationFromCurrent(c)), nil -} - -ErrEnum = #{ - return InvalidEnumError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadEnum(NewLocationFromCurrent(c)), nil -} - -ErrService = #{ - return InvalidServiceError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadService(NewLocationFromCurrent(c)), nil -} - -ErrStruct = #{ - return InvalidStructError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadStruct(NewLocationFromCurrent(c)), nil -} - -ErrUnion = #{ - return InvalidUnionError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadUnion(NewLocationFromCurrent(c)), nil -} - -ErrException = #{ - return InvalidExceptionError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadException(NewLocationFromCurrent(c)), nil -} - -ErrDefinition = #{ - return InvalidDefinitionError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadDefinition(NewLocationFromCurrent(c)), nil -} - -ErrInclude = #{ - return InvalidIncludeError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadInclude(NewLocationFromCurrent(c)), nil -} - -ErrCppInclude = #{ - return InvalidCppIncludeError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadCPPInclude(NewLocationFromCurrent(c)), nil -} - -ErrNamespace = #{ - return InvalidNamespaceError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadNamespace(NewLocationFromCurrent(c)), nil -} - -ErrHeader = #{ - return InvalidHeaderError -} (![\r\n] .)* { // 消费异常字符直到这行结束 - return NewBadHeader(NewLocationFromCurrent(c)), nil -} diff --git a/parser/thrift.peg.go b/parser/thrift.peg.go deleted file mode 100644 index 0a12904..0000000 --- a/parser/thrift.peg.go +++ /dev/null @@ -1,12941 +0,0 @@ -// Code generated by pigeon; DO NOT EDIT. - -package parser - -import ( - "bytes" - "errors" - "fmt" - "io" - "math" - "os" - "sort" - "strconv" - "strings" - "sync" - "unicode" - "unicode/utf8" -) - -func toStringSlice(strs any) []string { - if strs == nil { - return nil - } - - items := strs.([]any) - ret := make([]string, 0, len(items)) - for i := range items { - var item string - if _, ok := items[i].([]interface{}); ok { - data := items[i].([]interface{})[1] - if bs, ok := data.([]uint8); ok { - item = string(bs) - } else { - item = items[i].([]interface{})[1].(string) - } - } else { - item = items[i].(string) - } - - ret = append(ret, item) - } - - return ret -} - -func toFieldSlice(fields any) []*Field { - if fields == nil { - return nil - } - - items := fields.([]any) - ret := make([]*Field, 0, len(items)) - for i := range items { - item := items[i].(*Field) - ret = append(ret, item) - } - return ret -} - -func toConstValueSlice(values any) []*ConstValue { - if values == nil { - return nil - } - - items := values.([]any) - ret := make([]*ConstValue, 0, len(items)) - for i := range items { - item := items[i].(*ConstValue) - ret = append(ret, item) - } - return ret -} - -func toString(text any) string { - if text == nil { - return "" - } - - data := text.([]interface{}) - - ret := bytes.NewBuffer(nil) - for i := range data { - ret.WriteString(data[i].(string)) - } - - return ret.String() -} - -func toAnnotationSlice(annos any) []*Annotation { - if annos == nil { - return nil - } - items := annos.([]any) - ret := make([]*Annotation, 0, len(items)) - for i := range items { - ret = append(ret, items[i].(*Annotation)) - } - - return ret -} - -func toHeaderSlice(headers any) []Header { - if headers == nil { - return nil - } - items := headers.([]any) - ret := make([]Header, 0, len(items)) - for i := range items { - ret = append(ret, items[i].(Header)) - } - return ret -} - -func toDefinitionSlice(defs any) []Definition { - if defs == nil { - return nil - } - items := defs.([]any) - ret := make([]Definition, 0, len(items)) - for i := range items { - ret = append(ret, items[i].(Definition)) - } - return ret -} - -func toEnumValueSlice(v any) []*EnumValue { - if v == nil { - return nil - } - values := v.([]any) - - ret := make([]*EnumValue, 0, len(values)) - value := int64(0) - for i := range values { - enumV := values[i].(*EnumValue) - if enumV.ValueNode == nil { - enumV.Value = value - } else { - value = enumV.Value - } - value++ - ret = append(ret, enumV) - } - return ret -} - -func toFunctionSlice(fns any) []*Function { - if fns == nil { - return nil - } - items := fns.([]any) - ret := make([]*Function, 0, len(items)) - for i := range items { - ret = append(ret, items[i].(*Function)) - } - return ret -} - -func toCommentSlice(comments any) []*Comment { - if comments == nil { - return nil - } - items := comments.([]any) - ret := make([]*Comment, 0, len(items)) - for i := range items { - item := items[i] - commentItem, ok := item.(*Comment) - if ok { - ret = append(ret, commentItem) - } - } - - if len(ret) == 0 { - return nil - } - - return ret -} - -func toAnnotations(annos any) *Annotations { - if annos == nil { - return nil - } - - return annos.(*Annotations) -} - -func toListSeparatorKeyword(sep any) *ListSeparatorKeyword { - if sep == nil { - return nil - } - - return sep.(*ListSeparatorKeyword) -} - -var g = &grammar{ - rules: []*rule{ - { - name: "Document", - pos: position{line: 182, col: 1, offset: 3075}, - expr: &recoveryExpr{ - pos: position{line: 182, col: 12, offset: 3086}, - expr: &recoveryExpr{ - pos: position{line: 182, col: 12, offset: 3086}, - expr: &actionExpr{ - pos: position{line: 182, col: 12, offset: 3086}, - run: (*parser).callonDocument3, - expr: &seqExpr{ - pos: position{line: 182, col: 12, offset: 3086}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 182, col: 12, offset: 3086}, - label: "headers", - expr: &zeroOrMoreExpr{ - pos: position{line: 182, col: 20, offset: 3094}, - expr: &ruleRefExpr{ - pos: position{line: 182, col: 20, offset: 3094}, - name: "Header", - }, - }, - }, - &labeledExpr{ - pos: position{line: 182, col: 29, offset: 3103}, - label: "defs", - expr: &zeroOrMoreExpr{ - pos: position{line: 182, col: 34, offset: 3108}, - expr: &ruleRefExpr{ - pos: position{line: 182, col: 34, offset: 3108}, - name: "Definition", - }, - }, - }, - &labeledExpr{ - pos: position{line: 182, col: 46, offset: 3120}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 182, col: 55, offset: 3129}, - name: "ReservedComments", - }, - }, - ¬Expr{ - pos: position{line: 182, col: 72, offset: 3146}, - expr: &anyMatcher{ - line: 182, col: 73, offset: 3147, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 184, col: 17, offset: 3291}, - name: "ErrHeader", - }, - failureLabel: []string{ - "errHeader", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 184, col: 45, offset: 3319}, - name: "ErrDefinition", - }, - failureLabel: []string{ - "errDefinition", - }, - }, - }, - { - name: "Header", - pos: position{line: 186, col: 1, offset: 3334}, - expr: &recoveryExpr{ - pos: position{line: 186, col: 10, offset: 3343}, - expr: &recoveryExpr{ - pos: position{line: 186, col: 10, offset: 3343}, - expr: &recoveryExpr{ - pos: position{line: 186, col: 10, offset: 3343}, - expr: &choiceExpr{ - pos: position{line: 186, col: 10, offset: 3343}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 186, col: 10, offset: 3343}, - run: (*parser).callonHeader5, - expr: &seqExpr{ - pos: position{line: 186, col: 10, offset: 3343}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 186, col: 10, offset: 3343}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 186, col: 19, offset: 3352}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 186, col: 36, offset: 3369}, - label: "v", - expr: &choiceExpr{ - pos: position{line: 186, col: 39, offset: 3372}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 186, col: 39, offset: 3372}, - name: "Include", - }, - &ruleRefExpr{ - pos: position{line: 186, col: 49, offset: 3382}, - name: "CppInclude", - }, - &ruleRefExpr{ - pos: position{line: 186, col: 62, offset: 3395}, - name: "Namespace", - }, - }, - }, - }, - &labeledExpr{ - pos: position{line: 186, col: 73, offset: 3406}, - label: "endLineComments", - expr: &ruleRefExpr{ - pos: position{line: 186, col: 89, offset: 3422}, - name: "ReservedEndLineComments", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 190, col: 5, offset: 3579}, - run: (*parser).callonHeader16, - expr: &labeledExpr{ - pos: position{line: 190, col: 5, offset: 3579}, - label: "x", - expr: &seqExpr{ - pos: position{line: 190, col: 8, offset: 3582}, - exprs: []any{ - ¬Expr{ - pos: position{line: 190, col: 8, offset: 3582}, - expr: &ruleRefExpr{ - pos: position{line: 190, col: 10, offset: 3584}, - name: "Definition", - }, - }, - &ruleRefExpr{ - pos: position{line: 190, col: 22, offset: 3596}, - name: "ReservedComments", - }, - &andExpr{ - pos: position{line: 190, col: 39, offset: 3613}, - expr: &oneOrMoreExpr{ - pos: position{line: 190, col: 41, offset: 3615}, - expr: &anyMatcher{ - line: 190, col: 41, offset: 3615, - }, - }, - }, - &andCodeExpr{ - pos: position{line: 190, col: 45, offset: 3619}, - run: (*parser).callonHeader25, - }, - &throwExpr{ - pos: position{line: 196, col: 3, offset: 3820}, - label: "errHeader", - }, - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 200, col: 18, offset: 3985}, - name: "ErrInclude", - }, - failureLabel: []string{ - "errInclude", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 200, col: 47, offset: 4014}, - name: "ErrorCppInclude", - }, - failureLabel: []string{ - "errCppInclude", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 200, col: 80, offset: 4047}, - name: "ErrorNamespace", - }, - failureLabel: []string{ - "errNamespace", - }, - }, - }, - { - name: "Include", - pos: position{line: 202, col: 1, offset: 4063}, - expr: &choiceExpr{ - pos: position{line: 202, col: 11, offset: 4073}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 202, col: 11, offset: 4073}, - run: (*parser).callonInclude2, - expr: &seqExpr{ - pos: position{line: 202, col: 11, offset: 4073}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 202, col: 11, offset: 4073}, - label: "includeKeyword", - expr: &ruleRefExpr{ - pos: position{line: 202, col: 26, offset: 4088}, - name: "INCLUDE", - }, - }, - &labeledExpr{ - pos: position{line: 202, col: 34, offset: 4096}, - label: "include", - expr: &ruleRefExpr{ - pos: position{line: 202, col: 42, offset: 4104}, - name: "Literal", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 208, col: 5, offset: 4313}, - run: (*parser).callonInclude8, - expr: &labeledExpr{ - pos: position{line: 208, col: 5, offset: 4313}, - label: "x", - expr: &seqExpr{ - pos: position{line: 208, col: 8, offset: 4316}, - exprs: []any{ - &andExpr{ - pos: position{line: 208, col: 8, offset: 4316}, - expr: &seqExpr{ - pos: position{line: 208, col: 10, offset: 4318}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 208, col: 10, offset: 4318}, - name: "INCLUDE", - }, - &zeroOrMoreExpr{ - pos: position{line: 208, col: 18, offset: 4326}, - expr: &anyMatcher{ - line: 208, col: 18, offset: 4326, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 208, col: 22, offset: 4330}, - label: "errInclude", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "CppInclude", - pos: position{line: 213, col: 1, offset: 4377}, - expr: &choiceExpr{ - pos: position{line: 213, col: 15, offset: 4391}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 213, col: 15, offset: 4391}, - run: (*parser).callonCppInclude2, - expr: &seqExpr{ - pos: position{line: 213, col: 15, offset: 4391}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 213, col: 15, offset: 4391}, - label: "cppIncludeKeyword", - expr: &ruleRefExpr{ - pos: position{line: 213, col: 33, offset: 4409}, - name: "CPPINCLUDE", - }, - }, - &labeledExpr{ - pos: position{line: 213, col: 44, offset: 4420}, - label: "include", - expr: &ruleRefExpr{ - pos: position{line: 213, col: 52, offset: 4428}, - name: "Literal", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 219, col: 5, offset: 4646}, - run: (*parser).callonCppInclude8, - expr: &labeledExpr{ - pos: position{line: 219, col: 5, offset: 4646}, - label: "x", - expr: &seqExpr{ - pos: position{line: 219, col: 8, offset: 4649}, - exprs: []any{ - &andExpr{ - pos: position{line: 219, col: 8, offset: 4649}, - expr: &seqExpr{ - pos: position{line: 219, col: 10, offset: 4651}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 219, col: 10, offset: 4651}, - name: "CPPINCLUDE", - }, - &zeroOrMoreExpr{ - pos: position{line: 219, col: 21, offset: 4662}, - expr: &anyMatcher{ - line: 219, col: 21, offset: 4662, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 219, col: 25, offset: 4666}, - label: "errCppInclude", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "Namespace", - pos: position{line: 224, col: 1, offset: 4716}, - expr: &choiceExpr{ - pos: position{line: 224, col: 14, offset: 4729}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 224, col: 14, offset: 4729}, - run: (*parser).callonNamespace2, - expr: &seqExpr{ - pos: position{line: 224, col: 14, offset: 4729}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 224, col: 14, offset: 4729}, - label: "namespaceKeyword", - expr: &ruleRefExpr{ - pos: position{line: 224, col: 31, offset: 4746}, - name: "NAMESPACE", - }, - }, - &labeledExpr{ - pos: position{line: 224, col: 41, offset: 4756}, - label: "language", - expr: &ruleRefExpr{ - pos: position{line: 224, col: 50, offset: 4765}, - name: "NamespaceScope", - }, - }, - &labeledExpr{ - pos: position{line: 224, col: 65, offset: 4780}, - label: "name", - expr: &ruleRefExpr{ - pos: position{line: 224, col: 70, offset: 4785}, - name: "Identifier", - }, - }, - &labeledExpr{ - pos: position{line: 224, col: 81, offset: 4796}, - label: "annotations", - expr: &zeroOrOneExpr{ - pos: position{line: 224, col: 93, offset: 4808}, - expr: &ruleRefExpr{ - pos: position{line: 224, col: 93, offset: 4808}, - name: "Annotations", - }, - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 226, col: 5, offset: 4994}, - run: (*parser).callonNamespace13, - expr: &labeledExpr{ - pos: position{line: 226, col: 5, offset: 4994}, - label: "x", - expr: &seqExpr{ - pos: position{line: 226, col: 8, offset: 4997}, - exprs: []any{ - &andExpr{ - pos: position{line: 226, col: 8, offset: 4997}, - expr: &seqExpr{ - pos: position{line: 226, col: 10, offset: 4999}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 226, col: 10, offset: 4999}, - name: "NAMESPACE", - }, - &zeroOrMoreExpr{ - pos: position{line: 226, col: 20, offset: 5009}, - expr: &anyMatcher{ - line: 226, col: 20, offset: 5009, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 226, col: 24, offset: 5013}, - label: "errNamespace", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "NamespaceScope", - pos: position{line: 230, col: 1, offset: 5061}, - expr: &actionExpr{ - pos: position{line: 230, col: 19, offset: 5079}, - run: (*parser).callonNamespaceScope1, - expr: &labeledExpr{ - pos: position{line: 230, col: 19, offset: 5079}, - label: "v", - expr: &choiceExpr{ - pos: position{line: 230, col: 22, offset: 5082}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 230, col: 22, offset: 5082}, - name: "NamespaceScopeAny", - }, - &ruleRefExpr{ - pos: position{line: 230, col: 42, offset: 5102}, - name: "Identifier", - }, - }, - }, - }, - }, - }, - { - name: "NamespaceScopeAny", - pos: position{line: 239, col: 1, offset: 5207}, - expr: &actionExpr{ - pos: position{line: 239, col: 21, offset: 5227}, - run: (*parser).callonNamespaceScopeAny1, - expr: &seqExpr{ - pos: position{line: 239, col: 21, offset: 5227}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 239, col: 21, offset: 5227}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 239, col: 30, offset: 5236}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 239, col: 47, offset: 5253}, - label: "idName", - expr: &ruleRefExpr{ - pos: position{line: 239, col: 54, offset: 5260}, - name: "NamespaceScopeAnyToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 239, col: 77, offset: 5283}, - expr: &ruleRefExpr{ - pos: position{line: 239, col: 77, offset: 5283}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "NamespaceScopeAnyToken", - pos: position{line: 243, col: 1, offset: 5399}, - expr: &actionExpr{ - pos: position{line: 243, col: 26, offset: 5424}, - run: (*parser).callonNamespaceScopeAnyToken1, - expr: &litMatcher{ - pos: position{line: 243, col: 26, offset: 5424}, - val: "*", - ignoreCase: false, - want: "\"*\"", - }, - }, - }, - { - name: "Definition", - pos: position{line: 247, col: 1, offset: 5496}, - expr: &recoveryExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - expr: &recoveryExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - expr: &recoveryExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - expr: &recoveryExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - expr: &recoveryExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - expr: &recoveryExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - expr: &recoveryExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - expr: &choiceExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - run: (*parser).callonDefinition9, - expr: &seqExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 247, col: 14, offset: 5509}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 247, col: 23, offset: 5518}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 247, col: 40, offset: 5535}, - label: "v", - expr: &choiceExpr{ - pos: position{line: 247, col: 43, offset: 5538}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 247, col: 43, offset: 5538}, - name: "Const", - }, - &ruleRefExpr{ - pos: position{line: 247, col: 51, offset: 5546}, - name: "Typedef", - }, - &ruleRefExpr{ - pos: position{line: 247, col: 61, offset: 5556}, - name: "Enum", - }, - &ruleRefExpr{ - pos: position{line: 247, col: 68, offset: 5563}, - name: "Service", - }, - &ruleRefExpr{ - pos: position{line: 247, col: 78, offset: 5573}, - name: "Struct", - }, - &ruleRefExpr{ - pos: position{line: 247, col: 87, offset: 5582}, - name: "Union", - }, - &ruleRefExpr{ - pos: position{line: 247, col: 95, offset: 5590}, - name: "Exception", - }, - }, - }, - }, - &labeledExpr{ - pos: position{line: 247, col: 106, offset: 5601}, - label: "annos", - expr: &zeroOrOneExpr{ - pos: position{line: 247, col: 112, offset: 5607}, - expr: &ruleRefExpr{ - pos: position{line: 247, col: 112, offset: 5607}, - name: "Annotations", - }, - }, - }, - &labeledExpr{ - pos: position{line: 247, col: 125, offset: 5620}, - label: "endLineComments", - expr: &ruleRefExpr{ - pos: position{line: 247, col: 141, offset: 5636}, - name: "ReservedEndLineComments", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 254, col: 5, offset: 5901}, - run: (*parser).callonDefinition27, - expr: &labeledExpr{ - pos: position{line: 254, col: 5, offset: 5901}, - label: "x", - expr: &seqExpr{ - pos: position{line: 254, col: 8, offset: 5904}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 254, col: 8, offset: 5904}, - name: "ReservedComments", - }, - &andExpr{ - pos: position{line: 254, col: 25, offset: 5921}, - expr: &oneOrMoreExpr{ - pos: position{line: 254, col: 27, offset: 5923}, - expr: &anyMatcher{ - line: 254, col: 27, offset: 5923, - }, - }, - }, - &andCodeExpr{ - pos: position{line: 254, col: 31, offset: 5927}, - run: (*parser).callonDefinition34, - }, - &throwExpr{ - pos: position{line: 260, col: 3, offset: 6127}, - label: "errDefinition", - }, - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 263, col: 16, offset: 6261}, - name: "ErrConst", - }, - failureLabel: []string{ - "errConst", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 263, col: 40, offset: 6285}, - name: "ErrTypedef", - }, - failureLabel: []string{ - "errTypedef", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 263, col: 63, offset: 6308}, - name: "ErrEnum", - }, - failureLabel: []string{ - "errEnum", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 263, col: 86, offset: 6331}, - name: "ErrService", - }, - failureLabel: []string{ - "errService", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 263, col: 111, offset: 6356}, - name: "ErrStruct", - }, - failureLabel: []string{ - "errStruct", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 263, col: 134, offset: 6379}, - name: "ErrUnion", - }, - failureLabel: []string{ - "errUnion", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 263, col: 160, offset: 6405}, - name: "ErrException", - }, - failureLabel: []string{ - "errException", - }, - }, - }, - { - name: "Const", - pos: position{line: 265, col: 1, offset: 6419}, - expr: &recoveryExpr{ - pos: position{line: 265, col: 9, offset: 6427}, - expr: &recoveryExpr{ - pos: position{line: 265, col: 9, offset: 6427}, - expr: &recoveryExpr{ - pos: position{line: 265, col: 9, offset: 6427}, - expr: &choiceExpr{ - pos: position{line: 265, col: 9, offset: 6427}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 265, col: 9, offset: 6427}, - run: (*parser).callonConst5, - expr: &seqExpr{ - pos: position{line: 265, col: 9, offset: 6427}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 265, col: 9, offset: 6427}, - label: "constKeyword", - expr: &ruleRefExpr{ - pos: position{line: 265, col: 22, offset: 6440}, - name: "CONST", - }, - }, - &labeledExpr{ - pos: position{line: 265, col: 28, offset: 6446}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 265, col: 30, offset: 6448}, - name: "FieldType", - }, - }, - &labeledExpr{ - pos: position{line: 265, col: 40, offset: 6458}, - label: "name", - expr: &ruleRefExpr{ - pos: position{line: 265, col: 45, offset: 6463}, - name: "DefinitionIdentifier", - }, - }, - &labeledExpr{ - pos: position{line: 265, col: 66, offset: 6484}, - label: "v", - expr: &ruleRefExpr{ - pos: position{line: 265, col: 68, offset: 6486}, - name: "ConstEqualValue", - }, - }, - &labeledExpr{ - pos: position{line: 265, col: 84, offset: 6502}, - label: "sep", - expr: &zeroOrOneExpr{ - pos: position{line: 265, col: 88, offset: 6506}, - expr: &ruleRefExpr{ - pos: position{line: 265, col: 88, offset: 6506}, - name: "ListSeparator", - }, - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 268, col: 5, offset: 6765}, - run: (*parser).callonConst18, - expr: &labeledExpr{ - pos: position{line: 268, col: 5, offset: 6765}, - label: "x", - expr: &seqExpr{ - pos: position{line: 268, col: 8, offset: 6768}, - exprs: []any{ - &andExpr{ - pos: position{line: 268, col: 8, offset: 6768}, - expr: &seqExpr{ - pos: position{line: 268, col: 10, offset: 6770}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 268, col: 10, offset: 6770}, - name: "CONST", - }, - &zeroOrMoreExpr{ - pos: position{line: 268, col: 16, offset: 6776}, - expr: &anyMatcher{ - line: 268, col: 16, offset: 6776, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 268, col: 20, offset: 6780}, - label: "errConst", - }, - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 270, col: 21, offset: 6841}, - name: "ErrConstIdentifier", - }, - failureLabel: []string{ - "errIdentifier", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 270, col: 65, offset: 6885}, - name: "ErrConstMissingValue", - }, - failureLabel: []string{ - "errConstMissingValue", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 270, col: 109, offset: 6929}, - name: "ErrConstConstValue", - }, - failureLabel: []string{ - "errConstConstValue", - }, - }, - }, - { - name: "ConstEqualValue", - pos: position{line: 272, col: 1, offset: 6949}, - expr: &choiceExpr{ - pos: position{line: 272, col: 19, offset: 6967}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 272, col: 19, offset: 6967}, - run: (*parser).callonConstEqualValue2, - expr: &labeledExpr{ - pos: position{line: 272, col: 19, offset: 6967}, - label: "v", - expr: &seqExpr{ - pos: position{line: 272, col: 22, offset: 6970}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 272, col: 22, offset: 6970}, - name: "EQUAL", - }, - &ruleRefExpr{ - pos: position{line: 272, col: 28, offset: 6976}, - name: "ConstValue", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 274, col: 5, offset: 7009}, - run: (*parser).callonConstEqualValue7, - expr: &labeledExpr{ - pos: position{line: 274, col: 5, offset: 7009}, - label: "x", - expr: &seqExpr{ - pos: position{line: 274, col: 8, offset: 7012}, - exprs: []any{ - ¬Expr{ - pos: position{line: 274, col: 8, offset: 7012}, - expr: &ruleRefExpr{ - pos: position{line: 274, col: 9, offset: 7013}, - name: "EQUAL", - }, - }, - &throwExpr{ - pos: position{line: 274, col: 15, offset: 7019}, - label: "errConstMissingValue", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 276, col: 5, offset: 7105}, - run: (*parser).callonConstEqualValue13, - expr: &labeledExpr{ - pos: position{line: 276, col: 5, offset: 7105}, - label: "x", - expr: &seqExpr{ - pos: position{line: 276, col: 8, offset: 7108}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 276, col: 8, offset: 7108}, - name: "EQUAL", - }, - &throwExpr{ - pos: position{line: 276, col: 14, offset: 7114}, - label: "errConstConstValue", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "Typedef", - pos: position{line: 280, col: 1, offset: 7157}, - expr: &recoveryExpr{ - pos: position{line: 280, col: 11, offset: 7167}, - expr: &choiceExpr{ - pos: position{line: 280, col: 11, offset: 7167}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 280, col: 11, offset: 7167}, - run: (*parser).callonTypedef3, - expr: &seqExpr{ - pos: position{line: 280, col: 11, offset: 7167}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 280, col: 11, offset: 7167}, - label: "typedefKeyword", - expr: &ruleRefExpr{ - pos: position{line: 280, col: 26, offset: 7182}, - name: "TYPEDEF", - }, - }, - &labeledExpr{ - pos: position{line: 280, col: 34, offset: 7190}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 280, col: 36, offset: 7192}, - name: "FieldType", - }, - }, - &labeledExpr{ - pos: position{line: 280, col: 46, offset: 7202}, - label: "alias", - expr: &ruleRefExpr{ - pos: position{line: 280, col: 52, offset: 7208}, - name: "DefinitionIdentifier", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 282, col: 5, offset: 7357}, - run: (*parser).callonTypedef11, - expr: &labeledExpr{ - pos: position{line: 282, col: 5, offset: 7357}, - label: "x", - expr: &seqExpr{ - pos: position{line: 282, col: 8, offset: 7360}, - exprs: []any{ - &andExpr{ - pos: position{line: 282, col: 8, offset: 7360}, - expr: &seqExpr{ - pos: position{line: 282, col: 10, offset: 7362}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 282, col: 10, offset: 7362}, - name: "TYPEDEF", - }, - &zeroOrMoreExpr{ - pos: position{line: 282, col: 18, offset: 7370}, - expr: &anyMatcher{ - line: 282, col: 18, offset: 7370, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 282, col: 22, offset: 7374}, - label: "errTypedef", - }, - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 284, col: 21, offset: 7437}, - name: "ErrTypedefIdentifier", - }, - failureLabel: []string{ - "errIdentifier", - }, - }, - }, - { - name: "Enum", - pos: position{line: 286, col: 1, offset: 7459}, - expr: &recoveryExpr{ - pos: position{line: 286, col: 8, offset: 7466}, - expr: &recoveryExpr{ - pos: position{line: 286, col: 8, offset: 7466}, - expr: &recoveryExpr{ - pos: position{line: 286, col: 8, offset: 7466}, - expr: &choiceExpr{ - pos: position{line: 286, col: 8, offset: 7466}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 286, col: 8, offset: 7466}, - run: (*parser).callonEnum5, - expr: &seqExpr{ - pos: position{line: 286, col: 8, offset: 7466}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 286, col: 8, offset: 7466}, - label: "enum", - expr: &ruleRefExpr{ - pos: position{line: 286, col: 13, offset: 7471}, - name: "ENUM", - }, - }, - &labeledExpr{ - pos: position{line: 286, col: 18, offset: 7476}, - label: "name", - expr: &ruleRefExpr{ - pos: position{line: 286, col: 23, offset: 7481}, - name: "DefinitionIdentifier", - }, - }, - &labeledExpr{ - pos: position{line: 286, col: 44, offset: 7502}, - label: "lcur", - expr: &ruleRefExpr{ - pos: position{line: 286, col: 49, offset: 7507}, - name: "LCUR", - }, - }, - &labeledExpr{ - pos: position{line: 286, col: 54, offset: 7512}, - label: "v", - expr: &zeroOrMoreExpr{ - pos: position{line: 286, col: 56, offset: 7514}, - expr: &ruleRefExpr{ - pos: position{line: 286, col: 56, offset: 7514}, - name: "EnumValueLine", - }, - }, - }, - &labeledExpr{ - pos: position{line: 286, col: 71, offset: 7529}, - label: "rcur", - expr: &ruleRefExpr{ - pos: position{line: 286, col: 76, offset: 7534}, - name: "RCUR", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 289, col: 5, offset: 7715}, - run: (*parser).callonEnum18, - expr: &labeledExpr{ - pos: position{line: 289, col: 5, offset: 7715}, - label: "x", - expr: &seqExpr{ - pos: position{line: 289, col: 8, offset: 7718}, - exprs: []any{ - &andExpr{ - pos: position{line: 289, col: 8, offset: 7718}, - expr: &seqExpr{ - pos: position{line: 289, col: 10, offset: 7720}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 289, col: 10, offset: 7720}, - name: "ENUM", - }, - &zeroOrMoreExpr{ - pos: position{line: 289, col: 15, offset: 7725}, - expr: &anyMatcher{ - line: 289, col: 15, offset: 7725, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 289, col: 19, offset: 7729}, - label: "errEnum", - }, - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 291, col: 21, offset: 7789}, - name: "ErrEnumIdentifier", - }, - failureLabel: []string{ - "errIdentifier", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 291, col: 51, offset: 7819}, - name: "ErrEnumRCUR", - }, - failureLabel: []string{ - "errRCUR", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 291, col: 80, offset: 7848}, - name: "ErrEnumValue", - }, - failureLabel: []string{ - "errEnumValue", - }, - }, - }, - { - name: "EnumValueLine", - pos: position{line: 293, col: 1, offset: 7862}, - expr: &actionExpr{ - pos: position{line: 293, col: 17, offset: 7878}, - run: (*parser).callonEnumValueLine1, - expr: &seqExpr{ - pos: position{line: 293, col: 17, offset: 7878}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 293, col: 17, offset: 7878}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 293, col: 26, offset: 7887}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 293, col: 43, offset: 7904}, - label: "v", - expr: &ruleRefExpr{ - pos: position{line: 293, col: 45, offset: 7906}, - name: "EnumValue", - }, - }, - &labeledExpr{ - pos: position{line: 293, col: 55, offset: 7916}, - label: "endLineComments", - expr: &ruleRefExpr{ - pos: position{line: 293, col: 71, offset: 7932}, - name: "ReservedEndLineComments", - }, - }, - }, - }, - }, - }, - { - name: "EnumValue", - pos: position{line: 298, col: 1, offset: 8064}, - expr: &recoveryExpr{ - pos: position{line: 298, col: 14, offset: 8077}, - expr: &actionExpr{ - pos: position{line: 298, col: 14, offset: 8077}, - run: (*parser).callonEnumValue2, - expr: &seqExpr{ - pos: position{line: 298, col: 14, offset: 8077}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 298, col: 14, offset: 8077}, - label: "name", - expr: &ruleRefExpr{ - pos: position{line: 298, col: 19, offset: 8082}, - name: "Identifier", - }, - }, - &labeledExpr{ - pos: position{line: 298, col: 30, offset: 8093}, - label: "value", - expr: &zeroOrOneExpr{ - pos: position{line: 298, col: 36, offset: 8099}, - expr: &ruleRefExpr{ - pos: position{line: 298, col: 37, offset: 8100}, - name: "EnumValueIntConstant", - }, - }, - }, - &labeledExpr{ - pos: position{line: 298, col: 60, offset: 8123}, - label: "annos", - expr: &zeroOrOneExpr{ - pos: position{line: 298, col: 66, offset: 8129}, - expr: &ruleRefExpr{ - pos: position{line: 298, col: 66, offset: 8129}, - name: "Annotations", - }, - }, - }, - &labeledExpr{ - pos: position{line: 298, col: 79, offset: 8142}, - label: "sep", - expr: &zeroOrOneExpr{ - pos: position{line: 298, col: 83, offset: 8146}, - expr: &ruleRefExpr{ - pos: position{line: 298, col: 83, offset: 8146}, - name: "ListSeparator", - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 310, col: 22, offset: 8603}, - name: "ErrEnumValueIntConstant", - }, - failureLabel: []string{ - "errIntConstant", - }, - }, - }, - { - name: "Service", - pos: position{line: 312, col: 1, offset: 8628}, - expr: &recoveryExpr{ - pos: position{line: 312, col: 11, offset: 8638}, - expr: &recoveryExpr{ - pos: position{line: 312, col: 11, offset: 8638}, - expr: &recoveryExpr{ - pos: position{line: 312, col: 11, offset: 8638}, - expr: &choiceExpr{ - pos: position{line: 312, col: 11, offset: 8638}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 312, col: 11, offset: 8638}, - run: (*parser).callonService5, - expr: &seqExpr{ - pos: position{line: 312, col: 11, offset: 8638}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 312, col: 11, offset: 8638}, - label: "svc", - expr: &ruleRefExpr{ - pos: position{line: 312, col: 15, offset: 8642}, - name: "SERVICE", - }, - }, - &labeledExpr{ - pos: position{line: 312, col: 23, offset: 8650}, - label: "name", - expr: &ruleRefExpr{ - pos: position{line: 312, col: 28, offset: 8655}, - name: "DefinitionIdentifier", - }, - }, - &labeledExpr{ - pos: position{line: 312, col: 49, offset: 8676}, - label: "extends", - expr: &zeroOrOneExpr{ - pos: position{line: 312, col: 57, offset: 8684}, - expr: &seqExpr{ - pos: position{line: 312, col: 59, offset: 8686}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 312, col: 59, offset: 8686}, - name: "EXTENDS", - }, - &ruleRefExpr{ - pos: position{line: 312, col: 67, offset: 8694}, - name: "Identifier", - }, - }, - }, - }, - }, - &labeledExpr{ - pos: position{line: 312, col: 81, offset: 8708}, - label: "lcur", - expr: &ruleRefExpr{ - pos: position{line: 312, col: 86, offset: 8713}, - name: "LCUR", - }, - }, - &labeledExpr{ - pos: position{line: 312, col: 91, offset: 8718}, - label: "fns", - expr: &zeroOrMoreExpr{ - pos: position{line: 312, col: 95, offset: 8722}, - expr: &ruleRefExpr{ - pos: position{line: 312, col: 95, offset: 8722}, - name: "Function", - }, - }, - }, - &labeledExpr{ - pos: position{line: 312, col: 105, offset: 8732}, - label: "rcur", - expr: &ruleRefExpr{ - pos: position{line: 312, col: 110, offset: 8737}, - name: "RCUR", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 321, col: 5, offset: 9144}, - run: (*parser).callonService23, - expr: &labeledExpr{ - pos: position{line: 321, col: 5, offset: 9144}, - label: "x", - expr: &seqExpr{ - pos: position{line: 321, col: 8, offset: 9147}, - exprs: []any{ - &andExpr{ - pos: position{line: 321, col: 8, offset: 9147}, - expr: &seqExpr{ - pos: position{line: 321, col: 10, offset: 9149}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 321, col: 10, offset: 9149}, - name: "SERVICE", - }, - &zeroOrMoreExpr{ - pos: position{line: 321, col: 18, offset: 9157}, - expr: &anyMatcher{ - line: 321, col: 18, offset: 9157, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 321, col: 22, offset: 9161}, - label: "errService", - }, - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 323, col: 21, offset: 9224}, - name: "ErrServiceIdentifier", - }, - failureLabel: []string{ - "errIdentifier", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 323, col: 54, offset: 9257}, - name: "ErrServiceRCUR", - }, - failureLabel: []string{ - "errRCUR", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 323, col: 85, offset: 9288}, - name: "ErrServiceFunction", - }, - failureLabel: []string{ - "errFunction", - }, - }, - }, - { - name: "Struct", - pos: position{line: 325, col: 1, offset: 9309}, - expr: &recoveryExpr{ - pos: position{line: 325, col: 10, offset: 9318}, - expr: &recoveryExpr{ - pos: position{line: 325, col: 10, offset: 9318}, - expr: &recoveryExpr{ - pos: position{line: 325, col: 10, offset: 9318}, - expr: &choiceExpr{ - pos: position{line: 325, col: 10, offset: 9318}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 325, col: 10, offset: 9318}, - run: (*parser).callonStruct5, - expr: &seqExpr{ - pos: position{line: 325, col: 10, offset: 9318}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 325, col: 10, offset: 9318}, - label: "st", - expr: &ruleRefExpr{ - pos: position{line: 325, col: 13, offset: 9321}, - name: "STRUCT", - }, - }, - &labeledExpr{ - pos: position{line: 325, col: 20, offset: 9328}, - label: "id", - expr: &ruleRefExpr{ - pos: position{line: 325, col: 23, offset: 9331}, - name: "DefinitionIdentifier", - }, - }, - &labeledExpr{ - pos: position{line: 325, col: 44, offset: 9352}, - label: "lcur", - expr: &ruleRefExpr{ - pos: position{line: 325, col: 49, offset: 9357}, - name: "LCUR", - }, - }, - &labeledExpr{ - pos: position{line: 325, col: 54, offset: 9362}, - label: "fields", - expr: &zeroOrMoreExpr{ - pos: position{line: 325, col: 61, offset: 9369}, - expr: &ruleRefExpr{ - pos: position{line: 325, col: 61, offset: 9369}, - name: "FieldWithThrow", - }, - }, - }, - &labeledExpr{ - pos: position{line: 325, col: 77, offset: 9385}, - label: "rcur", - expr: &ruleRefExpr{ - pos: position{line: 325, col: 82, offset: 9390}, - name: "RCUR", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 327, col: 5, offset: 9554}, - run: (*parser).callonStruct18, - expr: &labeledExpr{ - pos: position{line: 327, col: 5, offset: 9554}, - label: "x", - expr: &seqExpr{ - pos: position{line: 327, col: 8, offset: 9557}, - exprs: []any{ - &andExpr{ - pos: position{line: 327, col: 8, offset: 9557}, - expr: &seqExpr{ - pos: position{line: 327, col: 10, offset: 9559}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 327, col: 10, offset: 9559}, - name: "STRUCT", - }, - &zeroOrMoreExpr{ - pos: position{line: 327, col: 17, offset: 9566}, - expr: &anyMatcher{ - line: 327, col: 17, offset: 9566, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 327, col: 21, offset: 9570}, - label: "errStruct", - }, - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 329, col: 21, offset: 9632}, - name: "ErrStructIdentifier", - }, - failureLabel: []string{ - "errIdentifier", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 329, col: 53, offset: 9664}, - name: "ErrStructRCUR", - }, - failureLabel: []string{ - "errRCUR", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 329, col: 81, offset: 9692}, - name: "ErrStructField", - }, - failureLabel: []string{ - "errField", - }, - }, - }, - { - name: "Union", - pos: position{line: 331, col: 1, offset: 9708}, - expr: &recoveryExpr{ - pos: position{line: 331, col: 9, offset: 9716}, - expr: &recoveryExpr{ - pos: position{line: 331, col: 9, offset: 9716}, - expr: &recoveryExpr{ - pos: position{line: 331, col: 9, offset: 9716}, - expr: &choiceExpr{ - pos: position{line: 331, col: 9, offset: 9716}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 331, col: 9, offset: 9716}, - run: (*parser).callonUnion5, - expr: &seqExpr{ - pos: position{line: 331, col: 9, offset: 9716}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 331, col: 9, offset: 9716}, - label: "union", - expr: &ruleRefExpr{ - pos: position{line: 331, col: 15, offset: 9722}, - name: "UNION", - }, - }, - &labeledExpr{ - pos: position{line: 331, col: 21, offset: 9728}, - label: "name", - expr: &ruleRefExpr{ - pos: position{line: 331, col: 26, offset: 9733}, - name: "DefinitionIdentifier", - }, - }, - &labeledExpr{ - pos: position{line: 331, col: 47, offset: 9754}, - label: "lcur", - expr: &ruleRefExpr{ - pos: position{line: 331, col: 52, offset: 9759}, - name: "LCUR", - }, - }, - &labeledExpr{ - pos: position{line: 331, col: 57, offset: 9764}, - label: "fields", - expr: &zeroOrMoreExpr{ - pos: position{line: 331, col: 64, offset: 9771}, - expr: &ruleRefExpr{ - pos: position{line: 331, col: 64, offset: 9771}, - name: "FieldWithThrow", - }, - }, - }, - &labeledExpr{ - pos: position{line: 331, col: 80, offset: 9787}, - label: "rcur", - expr: &ruleRefExpr{ - pos: position{line: 331, col: 85, offset: 9792}, - name: "RCUR", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 333, col: 5, offset: 9959}, - run: (*parser).callonUnion18, - expr: &labeledExpr{ - pos: position{line: 333, col: 5, offset: 9959}, - label: "x", - expr: &seqExpr{ - pos: position{line: 333, col: 8, offset: 9962}, - exprs: []any{ - &andExpr{ - pos: position{line: 333, col: 8, offset: 9962}, - expr: &seqExpr{ - pos: position{line: 333, col: 10, offset: 9964}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 333, col: 10, offset: 9964}, - name: "UNION", - }, - &zeroOrMoreExpr{ - pos: position{line: 333, col: 16, offset: 9970}, - expr: &anyMatcher{ - line: 333, col: 16, offset: 9970, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 333, col: 20, offset: 9974}, - label: "errUnion", - }, - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 335, col: 21, offset: 10035}, - name: "ErrUnionIdentifier", - }, - failureLabel: []string{ - "errIdentifier", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 335, col: 52, offset: 10066}, - name: "ErrUnionRCUR", - }, - failureLabel: []string{ - "errRCUR", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 335, col: 78, offset: 10092}, - name: "ErrUnionField", - }, - failureLabel: []string{ - "errField", - }, - }, - }, - { - name: "Exception", - pos: position{line: 338, col: 1, offset: 10108}, - expr: &recoveryExpr{ - pos: position{line: 338, col: 14, offset: 10121}, - expr: &recoveryExpr{ - pos: position{line: 338, col: 14, offset: 10121}, - expr: &recoveryExpr{ - pos: position{line: 338, col: 14, offset: 10121}, - expr: &choiceExpr{ - pos: position{line: 338, col: 14, offset: 10121}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 338, col: 14, offset: 10121}, - run: (*parser).callonException5, - expr: &seqExpr{ - pos: position{line: 338, col: 14, offset: 10121}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 338, col: 14, offset: 10121}, - label: "excep", - expr: &ruleRefExpr{ - pos: position{line: 338, col: 20, offset: 10127}, - name: "EXCEPTION", - }, - }, - &labeledExpr{ - pos: position{line: 338, col: 30, offset: 10137}, - label: "name", - expr: &ruleRefExpr{ - pos: position{line: 338, col: 35, offset: 10142}, - name: "DefinitionIdentifier", - }, - }, - &labeledExpr{ - pos: position{line: 338, col: 56, offset: 10163}, - label: "lcur", - expr: &ruleRefExpr{ - pos: position{line: 338, col: 61, offset: 10168}, - name: "LCUR", - }, - }, - &labeledExpr{ - pos: position{line: 338, col: 66, offset: 10173}, - label: "fields", - expr: &zeroOrMoreExpr{ - pos: position{line: 338, col: 73, offset: 10180}, - expr: &ruleRefExpr{ - pos: position{line: 338, col: 73, offset: 10180}, - name: "FieldWithThrow", - }, - }, - }, - &labeledExpr{ - pos: position{line: 338, col: 89, offset: 10196}, - label: "rcur", - expr: &ruleRefExpr{ - pos: position{line: 338, col: 94, offset: 10201}, - name: "RCUR", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 340, col: 5, offset: 10376}, - run: (*parser).callonException18, - expr: &labeledExpr{ - pos: position{line: 340, col: 5, offset: 10376}, - label: "x", - expr: &seqExpr{ - pos: position{line: 340, col: 8, offset: 10379}, - exprs: []any{ - &andExpr{ - pos: position{line: 340, col: 8, offset: 10379}, - expr: &seqExpr{ - pos: position{line: 340, col: 10, offset: 10381}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 340, col: 10, offset: 10381}, - name: "EXCEPTION", - }, - &zeroOrMoreExpr{ - pos: position{line: 340, col: 20, offset: 10391}, - expr: &anyMatcher{ - line: 340, col: 20, offset: 10391, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 340, col: 24, offset: 10395}, - label: "errException", - }, - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 342, col: 21, offset: 10460}, - name: "ErrExceptionIdentifier", - }, - failureLabel: []string{ - "errIdentifier", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 342, col: 56, offset: 10495}, - name: "ErrExceptionRCUR", - }, - failureLabel: []string{ - "errRCUR", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 342, col: 86, offset: 10525}, - name: "ErrExceptionField", - }, - failureLabel: []string{ - "errField", - }, - }, - }, - { - name: "FieldWithThrow", - pos: position{line: 345, col: 1, offset: 10545}, - expr: &choiceExpr{ - pos: position{line: 345, col: 18, offset: 10562}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 345, col: 18, offset: 10562}, - name: "Field", - }, - &actionExpr{ - pos: position{line: 345, col: 26, offset: 10570}, - run: (*parser).callonFieldWithThrow3, - expr: &labeledExpr{ - pos: position{line: 345, col: 26, offset: 10570}, - label: "x", - expr: &seqExpr{ - pos: position{line: 345, col: 30, offset: 10574}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 345, col: 30, offset: 10574}, - name: "ReservedComments", - }, - ¬Expr{ - pos: position{line: 345, col: 47, offset: 10591}, - expr: &choiceExpr{ - pos: position{line: 345, col: 49, offset: 10593}, - alternatives: []any{ - &seqExpr{ - pos: position{line: 345, col: 51, offset: 10595}, - exprs: []any{ - &litMatcher{ - pos: position{line: 345, col: 51, offset: 10595}, - val: "}", - ignoreCase: false, - want: "\"}\"", - }, - &zeroOrMoreExpr{ - pos: position{line: 345, col: 55, offset: 10599}, - expr: &ruleRefExpr{ - pos: position{line: 345, col: 55, offset: 10599}, - name: "Indent", - }, - }, - }, - }, - &ruleRefExpr{ - pos: position{line: 345, col: 66, offset: 10610}, - name: "DefinitionStart", - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 345, col: 84, offset: 10628}, - label: "errField", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "Field", - pos: position{line: 349, col: 1, offset: 10673}, - expr: &actionExpr{ - pos: position{line: 349, col: 9, offset: 10681}, - run: (*parser).callonField1, - expr: &seqExpr{ - pos: position{line: 349, col: 9, offset: 10681}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 349, col: 9, offset: 10681}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 349, col: 18, offset: 10690}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 349, col: 35, offset: 10707}, - label: "index", - expr: &ruleRefExpr{ - pos: position{line: 349, col: 41, offset: 10713}, - name: "FieldId", - }, - }, - &labeledExpr{ - pos: position{line: 349, col: 49, offset: 10721}, - label: "required", - expr: &zeroOrOneExpr{ - pos: position{line: 349, col: 58, offset: 10730}, - expr: &ruleRefExpr{ - pos: position{line: 349, col: 58, offset: 10730}, - name: "FieldReq", - }, - }, - }, - &labeledExpr{ - pos: position{line: 349, col: 68, offset: 10740}, - label: "fieldType", - expr: &ruleRefExpr{ - pos: position{line: 349, col: 78, offset: 10750}, - name: "FieldType", - }, - }, - &labeledExpr{ - pos: position{line: 349, col: 88, offset: 10760}, - label: "id", - expr: &ruleRefExpr{ - pos: position{line: 349, col: 91, offset: 10763}, - name: "Identifier", - }, - }, - &labeledExpr{ - pos: position{line: 349, col: 102, offset: 10774}, - label: "value", - expr: &zeroOrOneExpr{ - pos: position{line: 349, col: 108, offset: 10780}, - expr: &seqExpr{ - pos: position{line: 349, col: 109, offset: 10781}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 349, col: 109, offset: 10781}, - name: "EQUAL", - }, - &ruleRefExpr{ - pos: position{line: 349, col: 115, offset: 10787}, - name: "ConstValue", - }, - }, - }, - }, - }, - &labeledExpr{ - pos: position{line: 349, col: 128, offset: 10800}, - label: "annos", - expr: &zeroOrOneExpr{ - pos: position{line: 349, col: 134, offset: 10806}, - expr: &ruleRefExpr{ - pos: position{line: 349, col: 134, offset: 10806}, - name: "Annotations", - }, - }, - }, - &labeledExpr{ - pos: position{line: 349, col: 147, offset: 10819}, - label: "sep", - expr: &zeroOrOneExpr{ - pos: position{line: 349, col: 151, offset: 10823}, - expr: &ruleRefExpr{ - pos: position{line: 349, col: 151, offset: 10823}, - name: "ListSeparator", - }, - }, - }, - &labeledExpr{ - pos: position{line: 349, col: 166, offset: 10838}, - label: "lineComments", - expr: &ruleRefExpr{ - pos: position{line: 349, col: 179, offset: 10851}, - name: "ReservedEndLineComments", - }, - }, - }, - }, - }, - }, - { - name: "FieldId", - pos: position{line: 365, col: 1, offset: 11416}, - expr: &recoveryExpr{ - pos: position{line: 365, col: 11, offset: 11426}, - expr: &actionExpr{ - pos: position{line: 365, col: 11, offset: 11426}, - run: (*parser).callonFieldId2, - expr: &seqExpr{ - pos: position{line: 365, col: 11, offset: 11426}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 365, col: 11, offset: 11426}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 365, col: 20, offset: 11435}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 365, col: 37, offset: 11452}, - label: "i", - expr: &ruleRefExpr{ - pos: position{line: 365, col: 39, offset: 11454}, - name: "FieldIndex", - }, - }, - &labeledExpr{ - pos: position{line: 365, col: 50, offset: 11465}, - label: "colon", - expr: &ruleRefExpr{ - pos: position{line: 365, col: 56, offset: 11471}, - name: "COLON", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 365, col: 62, offset: 11477}, - expr: &ruleRefExpr{ - pos: position{line: 365, col: 62, offset: 11477}, - name: "Indent", - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 370, col: 21, offset: 11653}, - name: "ErrFieldIndex", - }, - failureLabel: []string{ - "errFieldIndex", - }, - }, - }, - { - name: "FieldReq", - pos: position{line: 372, col: 1, offset: 11668}, - expr: &actionExpr{ - pos: position{line: 372, col: 12, offset: 11679}, - run: (*parser).callonFieldReq1, - expr: &seqExpr{ - pos: position{line: 372, col: 12, offset: 11679}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 372, col: 12, offset: 11679}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 372, col: 21, offset: 11688}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 372, col: 38, offset: 11705}, - label: "r", - expr: &ruleRefExpr{ - pos: position{line: 372, col: 40, offset: 11707}, - name: "IsRequired", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 372, col: 51, offset: 11718}, - expr: &ruleRefExpr{ - pos: position{line: 372, col: 51, offset: 11718}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "IsRequired", - pos: position{line: 377, col: 1, offset: 11863}, - expr: &actionExpr{ - pos: position{line: 377, col: 14, offset: 11876}, - run: (*parser).callonIsRequired1, - expr: &labeledExpr{ - pos: position{line: 377, col: 14, offset: 11876}, - label: "v", - expr: &choiceExpr{ - pos: position{line: 377, col: 17, offset: 11879}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 377, col: 17, offset: 11879}, - name: "RequiredToken", - }, - &ruleRefExpr{ - pos: position{line: 377, col: 33, offset: 11895}, - name: "OptionalToken", - }, - }, - }, - }, - }, - }, - { - name: "RequiredToken", - pos: position{line: 381, col: 1, offset: 11930}, - expr: &actionExpr{ - pos: position{line: 381, col: 17, offset: 11946}, - run: (*parser).callonRequiredToken1, - expr: &litMatcher{ - pos: position{line: 381, col: 17, offset: 11946}, - val: "required", - ignoreCase: false, - want: "\"required\"", - }, - }, - }, - { - name: "OptionalToken", - pos: position{line: 385, col: 1, offset: 11996}, - expr: &actionExpr{ - pos: position{line: 385, col: 17, offset: 12012}, - run: (*parser).callonOptionalToken1, - expr: &litMatcher{ - pos: position{line: 385, col: 17, offset: 12012}, - val: "optional", - ignoreCase: false, - want: "\"optional\"", - }, - }, - }, - { - name: "Function", - pos: position{line: 389, col: 1, offset: 12062}, - expr: &recoveryExpr{ - pos: position{line: 389, col: 12, offset: 12073}, - expr: &recoveryExpr{ - pos: position{line: 389, col: 12, offset: 12073}, - expr: &choiceExpr{ - pos: position{line: 389, col: 12, offset: 12073}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 389, col: 12, offset: 12073}, - run: (*parser).callonFunction4, - expr: &seqExpr{ - pos: position{line: 389, col: 12, offset: 12073}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 389, col: 12, offset: 12073}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 389, col: 21, offset: 12082}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 389, col: 38, offset: 12099}, - label: "oneway", - expr: &zeroOrOneExpr{ - pos: position{line: 389, col: 45, offset: 12106}, - expr: &ruleRefExpr{ - pos: position{line: 389, col: 45, offset: 12106}, - name: "ONEWAY", - }, - }, - }, - &labeledExpr{ - pos: position{line: 389, col: 53, offset: 12114}, - label: "ft", - expr: &ruleRefExpr{ - pos: position{line: 389, col: 56, offset: 12117}, - name: "FunctionType", - }, - }, - &labeledExpr{ - pos: position{line: 389, col: 69, offset: 12130}, - label: "name", - expr: &ruleRefExpr{ - pos: position{line: 389, col: 74, offset: 12135}, - name: "DefinitionIdentifier", - }, - }, - &labeledExpr{ - pos: position{line: 389, col: 95, offset: 12156}, - label: "lpar", - expr: &ruleRefExpr{ - pos: position{line: 389, col: 100, offset: 12161}, - name: "LPAR", - }, - }, - &labeledExpr{ - pos: position{line: 389, col: 105, offset: 12166}, - label: "args", - expr: &zeroOrMoreExpr{ - pos: position{line: 389, col: 110, offset: 12171}, - expr: &ruleRefExpr{ - pos: position{line: 389, col: 110, offset: 12171}, - name: "FunctionFieldWithThrow", - }, - }, - }, - &labeledExpr{ - pos: position{line: 389, col: 134, offset: 12195}, - label: "rpar", - expr: &ruleRefExpr{ - pos: position{line: 389, col: 139, offset: 12200}, - name: "RPAR", - }, - }, - &labeledExpr{ - pos: position{line: 389, col: 144, offset: 12205}, - label: "throws", - expr: &zeroOrOneExpr{ - pos: position{line: 389, col: 151, offset: 12212}, - expr: &ruleRefExpr{ - pos: position{line: 389, col: 151, offset: 12212}, - name: "Throws", - }, - }, - }, - &labeledExpr{ - pos: position{line: 389, col: 159, offset: 12220}, - label: "annos", - expr: &zeroOrOneExpr{ - pos: position{line: 389, col: 165, offset: 12226}, - expr: &ruleRefExpr{ - pos: position{line: 389, col: 165, offset: 12226}, - name: "Annotations", - }, - }, - }, - &labeledExpr{ - pos: position{line: 389, col: 178, offset: 12239}, - label: "sep", - expr: &zeroOrOneExpr{ - pos: position{line: 389, col: 182, offset: 12243}, - expr: &ruleRefExpr{ - pos: position{line: 389, col: 182, offset: 12243}, - name: "ListSeparator", - }, - }, - }, - &labeledExpr{ - pos: position{line: 389, col: 197, offset: 12258}, - label: "endLineComments", - expr: &ruleRefExpr{ - pos: position{line: 389, col: 213, offset: 12274}, - name: "ReservedEndLineComments", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 409, col: 5, offset: 12924}, - run: (*parser).callonFunction33, - expr: &labeledExpr{ - pos: position{line: 409, col: 5, offset: 12924}, - label: "x", - expr: &seqExpr{ - pos: position{line: 409, col: 8, offset: 12927}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 409, col: 8, offset: 12927}, - name: "ReservedComments", - }, - &andExpr{ - pos: position{line: 409, col: 25, offset: 12944}, - expr: &seqExpr{ - pos: position{line: 409, col: 27, offset: 12946}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 409, col: 27, offset: 12946}, - label: "oneway", - expr: &zeroOrOneExpr{ - pos: position{line: 409, col: 34, offset: 12953}, - expr: &ruleRefExpr{ - pos: position{line: 409, col: 34, offset: 12953}, - name: "ONEWAY", - }, - }, - }, - &labeledExpr{ - pos: position{line: 409, col: 42, offset: 12961}, - label: "ft", - expr: &ruleRefExpr{ - pos: position{line: 409, col: 45, offset: 12964}, - name: "FunctionType", - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 409, col: 59, offset: 12978}, - label: "errFunction", - }, - }, - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 411, col: 21, offset: 13042}, - name: "ErrFunctionIdentifier", - }, - failureLabel: []string{ - "errIdentifier", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 411, col: 56, offset: 13077}, - name: "ErrFunctionArgument", - }, - failureLabel: []string{ - "errField", - }, - }, - }, - { - name: "FunctionFieldWithThrow", - pos: position{line: 413, col: 1, offset: 13098}, - expr: &choiceExpr{ - pos: position{line: 413, col: 26, offset: 13123}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 413, col: 26, offset: 13123}, - run: (*parser).callonFunctionFieldWithThrow2, - expr: &labeledExpr{ - pos: position{line: 413, col: 26, offset: 13123}, - label: "v", - expr: &ruleRefExpr{ - pos: position{line: 413, col: 28, offset: 13125}, - name: "Field", - }, - }, - }, - &actionExpr{ - pos: position{line: 415, col: 6, offset: 13153}, - run: (*parser).callonFunctionFieldWithThrow5, - expr: &labeledExpr{ - pos: position{line: 415, col: 6, offset: 13153}, - label: "x", - expr: &seqExpr{ - pos: position{line: 415, col: 9, offset: 13156}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 415, col: 9, offset: 13156}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 415, col: 18, offset: 13165}, - name: "ReservedComments", - }, - }, - &andExpr{ - pos: position{line: 415, col: 35, offset: 13182}, - expr: &seqExpr{ - pos: position{line: 415, col: 37, offset: 13184}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 415, col: 37, offset: 13184}, - label: "index", - expr: &ruleRefExpr{ - pos: position{line: 415, col: 43, offset: 13190}, - name: "FieldId", - }, - }, - &labeledExpr{ - pos: position{line: 415, col: 51, offset: 13198}, - label: "required", - expr: &zeroOrOneExpr{ - pos: position{line: 415, col: 60, offset: 13207}, - expr: &ruleRefExpr{ - pos: position{line: 415, col: 60, offset: 13207}, - name: "FieldReq", - }, - }, - }, - &labeledExpr{ - pos: position{line: 415, col: 70, offset: 13217}, - label: "fieldType", - expr: &ruleRefExpr{ - pos: position{line: 415, col: 80, offset: 13227}, - name: "FieldType", - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 415, col: 91, offset: 13238}, - label: "errField", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "FunctionType", - pos: position{line: 420, col: 1, offset: 13284}, - expr: &choiceExpr{ - pos: position{line: 420, col: 18, offset: 13301}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 420, col: 18, offset: 13301}, - name: "VOID", - }, - &ruleRefExpr{ - pos: position{line: 420, col: 25, offset: 13308}, - name: "FieldType", - }, - }, - }, - }, - { - name: "Throws", - pos: position{line: 422, col: 1, offset: 13319}, - expr: &actionExpr{ - pos: position{line: 422, col: 11, offset: 13329}, - run: (*parser).callonThrows1, - expr: &seqExpr{ - pos: position{line: 422, col: 11, offset: 13329}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 422, col: 11, offset: 13329}, - label: "throws", - expr: &ruleRefExpr{ - pos: position{line: 422, col: 18, offset: 13336}, - name: "THROWS", - }, - }, - &labeledExpr{ - pos: position{line: 422, col: 25, offset: 13343}, - label: "lpar", - expr: &ruleRefExpr{ - pos: position{line: 422, col: 30, offset: 13348}, - name: "LPAR", - }, - }, - &labeledExpr{ - pos: position{line: 422, col: 35, offset: 13353}, - label: "fields", - expr: &zeroOrMoreExpr{ - pos: position{line: 422, col: 42, offset: 13360}, - expr: &ruleRefExpr{ - pos: position{line: 422, col: 42, offset: 13360}, - name: "Field", - }, - }, - }, - &labeledExpr{ - pos: position{line: 422, col: 49, offset: 13367}, - label: "rpar", - expr: &ruleRefExpr{ - pos: position{line: 422, col: 54, offset: 13372}, - name: "RPAR", - }, - }, - }, - }, - }, - }, - { - name: "FieldType", - pos: position{line: 426, col: 1, offset: 13521}, - expr: &actionExpr{ - pos: position{line: 426, col: 13, offset: 13533}, - run: (*parser).callonFieldType1, - expr: &seqExpr{ - pos: position{line: 426, col: 13, offset: 13533}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 426, col: 13, offset: 13533}, - label: "v", - expr: &choiceExpr{ - pos: position{line: 426, col: 16, offset: 13536}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 426, col: 16, offset: 13536}, - name: "ContainerType", - }, - &ruleRefExpr{ - pos: position{line: 426, col: 32, offset: 13552}, - name: "BaseType", - }, - &ruleRefExpr{ - pos: position{line: 426, col: 43, offset: 13563}, - name: "IdentifierType", - }, - }, - }, - }, - &labeledExpr{ - pos: position{line: 426, col: 59, offset: 13579}, - label: "annos", - expr: &zeroOrOneExpr{ - pos: position{line: 426, col: 65, offset: 13585}, - expr: &ruleRefExpr{ - pos: position{line: 426, col: 65, offset: 13585}, - name: "Annotations", - }, - }, - }, - }, - }, - }, - }, - { - name: "IdentifierType", - pos: position{line: 433, col: 1, offset: 13681}, - expr: &actionExpr{ - pos: position{line: 433, col: 18, offset: 13698}, - run: (*parser).callonIdentifierType1, - expr: &labeledExpr{ - pos: position{line: 433, col: 18, offset: 13698}, - label: "v", - expr: &ruleRefExpr{ - pos: position{line: 433, col: 20, offset: 13700}, - name: "Identifier", - }, - }, - }, - }, - { - name: "BaseType", - pos: position{line: 437, col: 1, offset: 13759}, - expr: &actionExpr{ - pos: position{line: 437, col: 12, offset: 13770}, - run: (*parser).callonBaseType1, - expr: &labeledExpr{ - pos: position{line: 437, col: 12, offset: 13770}, - label: "v", - expr: &choiceExpr{ - pos: position{line: 437, col: 15, offset: 13773}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 437, col: 15, offset: 13773}, - name: "BOOL", - }, - &ruleRefExpr{ - pos: position{line: 437, col: 22, offset: 13780}, - name: "BYTE", - }, - &ruleRefExpr{ - pos: position{line: 437, col: 29, offset: 13787}, - name: "I8", - }, - &ruleRefExpr{ - pos: position{line: 437, col: 34, offset: 13792}, - name: "I16", - }, - &ruleRefExpr{ - pos: position{line: 437, col: 40, offset: 13798}, - name: "I32", - }, - &ruleRefExpr{ - pos: position{line: 437, col: 46, offset: 13804}, - name: "I64", - }, - &ruleRefExpr{ - pos: position{line: 437, col: 52, offset: 13810}, - name: "DOUBLE", - }, - &ruleRefExpr{ - pos: position{line: 437, col: 61, offset: 13819}, - name: "STRING", - }, - &ruleRefExpr{ - pos: position{line: 437, col: 70, offset: 13828}, - name: "BINARY", - }, - &ruleRefExpr{ - pos: position{line: 437, col: 79, offset: 13837}, - name: "UUID", - }, - }, - }, - }, - }, - }, - { - name: "ContainerType", - pos: position{line: 441, col: 1, offset: 13946}, - expr: &actionExpr{ - pos: position{line: 441, col: 17, offset: 13962}, - run: (*parser).callonContainerType1, - expr: &labeledExpr{ - pos: position{line: 441, col: 17, offset: 13962}, - label: "v", - expr: &choiceExpr{ - pos: position{line: 441, col: 20, offset: 13965}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 441, col: 20, offset: 13965}, - name: "MapType", - }, - &ruleRefExpr{ - pos: position{line: 441, col: 30, offset: 13975}, - name: "SetType", - }, - &ruleRefExpr{ - pos: position{line: 441, col: 40, offset: 13985}, - name: "ListType", - }, - }, - }, - }, - }, - }, - { - name: "MapType", - pos: position{line: 445, col: 1, offset: 14028}, - expr: &actionExpr{ - pos: position{line: 445, col: 12, offset: 14039}, - run: (*parser).callonMapType1, - expr: &seqExpr{ - pos: position{line: 445, col: 12, offset: 14039}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 445, col: 12, offset: 14039}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 445, col: 14, offset: 14041}, - name: "MAP", - }, - }, - &labeledExpr{ - pos: position{line: 445, col: 18, offset: 14045}, - label: "cpp", - expr: &zeroOrOneExpr{ - pos: position{line: 445, col: 22, offset: 14049}, - expr: &ruleRefExpr{ - pos: position{line: 445, col: 22, offset: 14049}, - name: "CppType", - }, - }, - }, - &labeledExpr{ - pos: position{line: 445, col: 31, offset: 14058}, - label: "lp", - expr: &ruleRefExpr{ - pos: position{line: 445, col: 34, offset: 14061}, - name: "LPOINT", - }, - }, - &labeledExpr{ - pos: position{line: 445, col: 41, offset: 14068}, - label: "key", - expr: &ruleRefExpr{ - pos: position{line: 445, col: 45, offset: 14072}, - name: "FieldType", - }, - }, - &labeledExpr{ - pos: position{line: 445, col: 55, offset: 14082}, - label: "comma", - expr: &ruleRefExpr{ - pos: position{line: 445, col: 61, offset: 14088}, - name: "COMMA", - }, - }, - &labeledExpr{ - pos: position{line: 445, col: 67, offset: 14094}, - label: "value", - expr: &ruleRefExpr{ - pos: position{line: 445, col: 73, offset: 14100}, - name: "FieldType", - }, - }, - &labeledExpr{ - pos: position{line: 445, col: 83, offset: 14110}, - label: "rp", - expr: &ruleRefExpr{ - pos: position{line: 445, col: 86, offset: 14113}, - name: "RPOINT", - }, - }, - }, - }, - }, - }, - { - name: "SetType", - pos: position{line: 455, col: 1, offset: 14377}, - expr: &actionExpr{ - pos: position{line: 455, col: 11, offset: 14387}, - run: (*parser).callonSetType1, - expr: &seqExpr{ - pos: position{line: 455, col: 11, offset: 14387}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 455, col: 11, offset: 14387}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 455, col: 13, offset: 14389}, - name: "SET", - }, - }, - &labeledExpr{ - pos: position{line: 455, col: 17, offset: 14393}, - label: "cpp", - expr: &zeroOrOneExpr{ - pos: position{line: 455, col: 21, offset: 14397}, - expr: &ruleRefExpr{ - pos: position{line: 455, col: 21, offset: 14397}, - name: "CppType", - }, - }, - }, - &labeledExpr{ - pos: position{line: 455, col: 30, offset: 14406}, - label: "lp", - expr: &ruleRefExpr{ - pos: position{line: 455, col: 33, offset: 14409}, - name: "LPOINT", - }, - }, - &labeledExpr{ - pos: position{line: 455, col: 40, offset: 14416}, - label: "key", - expr: &ruleRefExpr{ - pos: position{line: 455, col: 44, offset: 14420}, - name: "FieldType", - }, - }, - &labeledExpr{ - pos: position{line: 455, col: 54, offset: 14430}, - label: "rp", - expr: &ruleRefExpr{ - pos: position{line: 455, col: 57, offset: 14433}, - name: "RPOINT", - }, - }, - }, - }, - }, - }, - { - name: "ListType", - pos: position{line: 464, col: 1, offset: 14662}, - expr: &actionExpr{ - pos: position{line: 464, col: 12, offset: 14673}, - run: (*parser).callonListType1, - expr: &seqExpr{ - pos: position{line: 464, col: 12, offset: 14673}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 464, col: 12, offset: 14673}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 464, col: 14, offset: 14675}, - name: "LIST", - }, - }, - &labeledExpr{ - pos: position{line: 464, col: 19, offset: 14680}, - label: "lp", - expr: &ruleRefExpr{ - pos: position{line: 464, col: 22, offset: 14683}, - name: "LPOINT", - }, - }, - &labeledExpr{ - pos: position{line: 464, col: 29, offset: 14690}, - label: "key", - expr: &ruleRefExpr{ - pos: position{line: 464, col: 33, offset: 14694}, - name: "FieldType", - }, - }, - &labeledExpr{ - pos: position{line: 464, col: 43, offset: 14704}, - label: "rp", - expr: &ruleRefExpr{ - pos: position{line: 464, col: 46, offset: 14707}, - name: "RPOINT", - }, - }, - &labeledExpr{ - pos: position{line: 464, col: 53, offset: 14714}, - label: "cpp", - expr: &zeroOrOneExpr{ - pos: position{line: 464, col: 57, offset: 14718}, - expr: &ruleRefExpr{ - pos: position{line: 464, col: 57, offset: 14718}, - name: "CppType", - }, - }, - }, - }, - }, - }, - }, - { - name: "CppType", - pos: position{line: 473, col: 1, offset: 14949}, - expr: &actionExpr{ - pos: position{line: 473, col: 11, offset: 14959}, - run: (*parser).callonCppType1, - expr: &seqExpr{ - pos: position{line: 473, col: 11, offset: 14959}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 473, col: 11, offset: 14959}, - label: "cpp", - expr: &ruleRefExpr{ - pos: position{line: 473, col: 15, offset: 14963}, - name: "CPPTYPE", - }, - }, - &labeledExpr{ - pos: position{line: 473, col: 23, offset: 14971}, - label: "l", - expr: &ruleRefExpr{ - pos: position{line: 473, col: 25, offset: 14973}, - name: "Literal", - }, - }, - }, - }, - }, - }, - { - name: "ConstValue", - pos: position{line: 477, col: 1, offset: 15074}, - expr: &actionExpr{ - pos: position{line: 477, col: 14, offset: 15087}, - run: (*parser).callonConstValue1, - expr: &labeledExpr{ - pos: position{line: 477, col: 14, offset: 15087}, - label: "v", - expr: &choiceExpr{ - pos: position{line: 477, col: 17, offset: 15090}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 477, col: 17, offset: 15090}, - name: "DoubleConstant", - }, - &ruleRefExpr{ - pos: position{line: 477, col: 34, offset: 15107}, - name: "IntConstant", - }, - &ruleRefExpr{ - pos: position{line: 477, col: 48, offset: 15121}, - name: "Literal", - }, - &ruleRefExpr{ - pos: position{line: 477, col: 58, offset: 15131}, - name: "IdentifierConst", - }, - &ruleRefExpr{ - pos: position{line: 477, col: 76, offset: 15149}, - name: "ConstMap", - }, - &ruleRefExpr{ - pos: position{line: 477, col: 87, offset: 15160}, - name: "ConstList", - }, - }, - }, - }, - }, - }, - { - name: "IdentifierConst", - pos: position{line: 484, col: 1, offset: 15321}, - expr: &actionExpr{ - pos: position{line: 484, col: 19, offset: 15339}, - run: (*parser).callonIdentifierConst1, - expr: &seqExpr{ - pos: position{line: 484, col: 19, offset: 15339}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 484, col: 19, offset: 15339}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 484, col: 28, offset: 15348}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 484, col: 45, offset: 15365}, - label: "cv", - expr: &ruleRefExpr{ - pos: position{line: 484, col: 48, offset: 15368}, - name: "IdentifierConstValue", - }, - }, - }, - }, - }, - }, - { - name: "IdentifierConstValue", - pos: position{line: 490, col: 1, offset: 15480}, - expr: &actionExpr{ - pos: position{line: 490, col: 24, offset: 15503}, - run: (*parser).callonIdentifierConstValue1, - expr: &labeledExpr{ - pos: position{line: 490, col: 24, offset: 15503}, - label: "id", - expr: &ruleRefExpr{ - pos: position{line: 490, col: 27, offset: 15506}, - name: "Identifier", - }, - }, - }, - }, - { - name: "EnumValueIntConstant", - pos: position{line: 495, col: 1, offset: 15628}, - expr: &choiceExpr{ - pos: position{line: 495, col: 24, offset: 15651}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 495, col: 24, offset: 15651}, - run: (*parser).callonEnumValueIntConstant2, - expr: &labeledExpr{ - pos: position{line: 495, col: 24, offset: 15651}, - label: "v", - expr: &seqExpr{ - pos: position{line: 495, col: 27, offset: 15654}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 495, col: 27, offset: 15654}, - name: "EQUAL", - }, - &ruleRefExpr{ - pos: position{line: 495, col: 33, offset: 15660}, - name: "IntConstant", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 497, col: 5, offset: 15694}, - run: (*parser).callonEnumValueIntConstant7, - expr: &labeledExpr{ - pos: position{line: 497, col: 5, offset: 15694}, - label: "x", - expr: &seqExpr{ - pos: position{line: 497, col: 8, offset: 15697}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 497, col: 8, offset: 15697}, - name: "EQUAL", - }, - &ruleRefExpr{ - pos: position{line: 497, col: 14, offset: 15703}, - name: "ReservedComments", - }, - &throwExpr{ - pos: position{line: 497, col: 31, offset: 15720}, - label: "errIntConstant", - }, - &zeroOrMoreExpr{ - pos: position{line: 497, col: 49, offset: 15738}, - expr: &ruleRefExpr{ - pos: position{line: 497, col: 49, offset: 15738}, - name: "Indent", - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "IntConstant", - pos: position{line: 501, col: 1, offset: 15799}, - expr: &choiceExpr{ - pos: position{line: 501, col: 15, offset: 15813}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 501, col: 15, offset: 15813}, - run: (*parser).callonIntConstant2, - expr: &seqExpr{ - pos: position{line: 501, col: 15, offset: 15813}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 501, col: 15, offset: 15813}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 501, col: 24, offset: 15822}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 501, col: 42, offset: 15840}, - label: "v", - expr: &choiceExpr{ - pos: position{line: 501, col: 45, offset: 15843}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 501, col: 45, offset: 15843}, - name: "HexIntConstant", - }, - &ruleRefExpr{ - pos: position{line: 501, col: 62, offset: 15860}, - name: "OctIntConstant", - }, - &ruleRefExpr{ - pos: position{line: 501, col: 79, offset: 15877}, - name: "NormalIntConstant", - }, - }, - }, - }, - ¬Expr{ - pos: position{line: 501, col: 98, offset: 15896}, - expr: &charClassMatcher{ - pos: position{line: 501, col: 99, offset: 15897}, - val: "[a-zA-Z]", - ranges: []rune{'a', 'z', 'A', 'Z'}, - ignoreCase: false, - inverted: false, - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 501, col: 109, offset: 15907}, - expr: &ruleRefExpr{ - pos: position{line: 501, col: 109, offset: 15907}, - name: "Indent", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 506, col: 5, offset: 16000}, - run: (*parser).callonIntConstant15, - expr: &labeledExpr{ - pos: position{line: 506, col: 5, offset: 16000}, - label: "x", - expr: &seqExpr{ - pos: position{line: 506, col: 8, offset: 16003}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 506, col: 8, offset: 16003}, - name: "ReservedComments", - }, - &andExpr{ - pos: position{line: 506, col: 25, offset: 16020}, - expr: &choiceExpr{ - pos: position{line: 506, col: 27, offset: 16022}, - alternatives: []any{ - &litMatcher{ - pos: position{line: 506, col: 27, offset: 16022}, - val: "0x", - ignoreCase: false, - want: "\"0x\"", - }, - &litMatcher{ - pos: position{line: 506, col: 34, offset: 16029}, - val: "0o", - ignoreCase: false, - want: "\"0o\"", - }, - &seqExpr{ - pos: position{line: 506, col: 42, offset: 16037}, - exprs: []any{ - &zeroOrOneExpr{ - pos: position{line: 506, col: 42, offset: 16037}, - expr: &choiceExpr{ - pos: position{line: 506, col: 43, offset: 16038}, - alternatives: []any{ - &litMatcher{ - pos: position{line: 506, col: 43, offset: 16038}, - val: "+", - ignoreCase: false, - want: "\"+\"", - }, - &litMatcher{ - pos: position{line: 506, col: 49, offset: 16044}, - val: "-", - ignoreCase: false, - want: "\"-\"", - }, - }, - }, - }, - &ruleRefExpr{ - pos: position{line: 506, col: 55, offset: 16050}, - name: "Digit", - }, - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 506, col: 63, offset: 16058}, - label: "errIntConstant", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "HexIntConstant", - pos: position{line: 510, col: 1, offset: 16108}, - expr: &actionExpr{ - pos: position{line: 510, col: 18, offset: 16125}, - run: (*parser).callonHexIntConstant1, - expr: &seqExpr{ - pos: position{line: 510, col: 18, offset: 16125}, - exprs: []any{ - &litMatcher{ - pos: position{line: 510, col: 18, offset: 16125}, - val: "0x", - ignoreCase: false, - want: "\"0x\"", - }, - &oneOrMoreExpr{ - pos: position{line: 510, col: 23, offset: 16130}, - expr: &choiceExpr{ - pos: position{line: 510, col: 24, offset: 16131}, - alternatives: []any{ - &charClassMatcher{ - pos: position{line: 510, col: 24, offset: 16131}, - val: "[0-9]", - ranges: []rune{'0', '9'}, - ignoreCase: false, - inverted: false, - }, - &charClassMatcher{ - pos: position{line: 510, col: 32, offset: 16139}, - val: "[A-Z]", - ranges: []rune{'A', 'Z'}, - ignoreCase: false, - inverted: false, - }, - &charClassMatcher{ - pos: position{line: 510, col: 40, offset: 16147}, - val: "[a-z]", - ranges: []rune{'a', 'z'}, - ignoreCase: false, - inverted: false, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "OctIntConstant", - pos: position{line: 522, col: 1, offset: 16392}, - expr: &actionExpr{ - pos: position{line: 522, col: 18, offset: 16409}, - run: (*parser).callonOctIntConstant1, - expr: &seqExpr{ - pos: position{line: 522, col: 18, offset: 16409}, - exprs: []any{ - &litMatcher{ - pos: position{line: 522, col: 18, offset: 16409}, - val: "0o", - ignoreCase: false, - want: "\"0o\"", - }, - &oneOrMoreExpr{ - pos: position{line: 522, col: 23, offset: 16414}, - expr: &ruleRefExpr{ - pos: position{line: 522, col: 23, offset: 16414}, - name: "Digit", - }, - }, - }, - }, - }, - }, - { - name: "NormalIntConstant", - pos: position{line: 533, col: 1, offset: 16649}, - expr: &actionExpr{ - pos: position{line: 533, col: 21, offset: 16669}, - run: (*parser).callonNormalIntConstant1, - expr: &seqExpr{ - pos: position{line: 533, col: 21, offset: 16669}, - exprs: []any{ - &zeroOrOneExpr{ - pos: position{line: 533, col: 21, offset: 16669}, - expr: &choiceExpr{ - pos: position{line: 533, col: 22, offset: 16670}, - alternatives: []any{ - &litMatcher{ - pos: position{line: 533, col: 22, offset: 16670}, - val: "+", - ignoreCase: false, - want: "\"+\"", - }, - &litMatcher{ - pos: position{line: 533, col: 28, offset: 16676}, - val: "-", - ignoreCase: false, - want: "\"-\"", - }, - }, - }, - }, - &oneOrMoreExpr{ - pos: position{line: 533, col: 34, offset: 16682}, - expr: &ruleRefExpr{ - pos: position{line: 533, col: 34, offset: 16682}, - name: "Digit", - }, - }, - }, - }, - }, - }, - { - name: "FieldIndex", - pos: position{line: 544, col: 1, offset: 16892}, - expr: &choiceExpr{ - pos: position{line: 544, col: 14, offset: 16905}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 544, col: 14, offset: 16905}, - run: (*parser).callonFieldIndex2, - expr: &oneOrMoreExpr{ - pos: position{line: 544, col: 14, offset: 16905}, - expr: &ruleRefExpr{ - pos: position{line: 544, col: 14, offset: 16905}, - name: "Digit", - }, - }, - }, - &actionExpr{ - pos: position{line: 550, col: 5, offset: 17080}, - run: (*parser).callonFieldIndex5, - expr: &labeledExpr{ - pos: position{line: 550, col: 5, offset: 17080}, - label: "x", - expr: &seqExpr{ - pos: position{line: 550, col: 8, offset: 17083}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 550, col: 8, offset: 17083}, - name: "ReservedComments", - }, - &andExpr{ - pos: position{line: 550, col: 25, offset: 17100}, - expr: &seqExpr{ - pos: position{line: 550, col: 27, offset: 17102}, - exprs: []any{ - &oneOrMoreExpr{ - pos: position{line: 550, col: 27, offset: 17102}, - expr: &charClassMatcher{ - pos: position{line: 550, col: 27, offset: 17102}, - val: "[a-zA-Z]", - ranges: []rune{'a', 'z', 'A', 'Z'}, - ignoreCase: false, - inverted: false, - }, - }, - &ruleRefExpr{ - pos: position{line: 550, col: 37, offset: 17112}, - name: "COLON", - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 550, col: 44, offset: 17119}, - label: "errFieldIndex", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "DoubleConstant", - pos: position{line: 554, col: 1, offset: 17168}, - expr: &actionExpr{ - pos: position{line: 554, col: 19, offset: 17186}, - run: (*parser).callonDoubleConstant1, - expr: &seqExpr{ - pos: position{line: 554, col: 19, offset: 17186}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 554, col: 19, offset: 17186}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 554, col: 28, offset: 17195}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 554, col: 45, offset: 17212}, - label: "v", - expr: &ruleRefExpr{ - pos: position{line: 554, col: 47, offset: 17214}, - name: "DoubleConstantValue", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 554, col: 67, offset: 17234}, - expr: &ruleRefExpr{ - pos: position{line: 554, col: 67, offset: 17234}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "DoubleConstantValue", - pos: position{line: 561, col: 1, offset: 17326}, - expr: &actionExpr{ - pos: position{line: 561, col: 23, offset: 17348}, - run: (*parser).callonDoubleConstantValue1, - expr: &seqExpr{ - pos: position{line: 561, col: 23, offset: 17348}, - exprs: []any{ - &zeroOrOneExpr{ - pos: position{line: 561, col: 23, offset: 17348}, - expr: &choiceExpr{ - pos: position{line: 561, col: 24, offset: 17349}, - alternatives: []any{ - &litMatcher{ - pos: position{line: 561, col: 24, offset: 17349}, - val: "+", - ignoreCase: false, - want: "\"+\"", - }, - &litMatcher{ - pos: position{line: 561, col: 30, offset: 17355}, - val: "-", - ignoreCase: false, - want: "\"-\"", - }, - }, - }, - }, - &choiceExpr{ - pos: position{line: 561, col: 37, offset: 17362}, - alternatives: []any{ - &seqExpr{ - pos: position{line: 561, col: 37, offset: 17362}, - exprs: []any{ - &zeroOrMoreExpr{ - pos: position{line: 561, col: 37, offset: 17362}, - expr: &ruleRefExpr{ - pos: position{line: 561, col: 37, offset: 17362}, - name: "Digit", - }, - }, - &litMatcher{ - pos: position{line: 561, col: 44, offset: 17369}, - val: ".", - ignoreCase: false, - want: "\".\"", - }, - &oneOrMoreExpr{ - pos: position{line: 561, col: 48, offset: 17373}, - expr: &ruleRefExpr{ - pos: position{line: 561, col: 48, offset: 17373}, - name: "Digit", - }, - }, - &zeroOrOneExpr{ - pos: position{line: 561, col: 56, offset: 17381}, - expr: &ruleRefExpr{ - pos: position{line: 561, col: 56, offset: 17381}, - name: "Exponent", - }, - }, - }, - }, - &seqExpr{ - pos: position{line: 561, col: 68, offset: 17393}, - exprs: []any{ - &oneOrMoreExpr{ - pos: position{line: 561, col: 68, offset: 17393}, - expr: &ruleRefExpr{ - pos: position{line: 561, col: 68, offset: 17393}, - name: "Digit", - }, - }, - &ruleRefExpr{ - pos: position{line: 561, col: 75, offset: 17400}, - name: "Exponent", - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "Exponent", - pos: position{line: 572, col: 1, offset: 17622}, - expr: &seqExpr{ - pos: position{line: 572, col: 12, offset: 17633}, - exprs: []any{ - &choiceExpr{ - pos: position{line: 572, col: 13, offset: 17634}, - alternatives: []any{ - &litMatcher{ - pos: position{line: 572, col: 13, offset: 17634}, - val: "e", - ignoreCase: false, - want: "\"e\"", - }, - &litMatcher{ - pos: position{line: 572, col: 19, offset: 17640}, - val: "E", - ignoreCase: false, - want: "\"E\"", - }, - }, - }, - &ruleRefExpr{ - pos: position{line: 572, col: 24, offset: 17645}, - name: "IntConstant", - }, - }, - }, - }, - { - name: "Annotations", - pos: position{line: 574, col: 1, offset: 17658}, - expr: &actionExpr{ - pos: position{line: 574, col: 16, offset: 17673}, - run: (*parser).callonAnnotations1, - expr: &seqExpr{ - pos: position{line: 574, col: 16, offset: 17673}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 574, col: 16, offset: 17673}, - label: "lpar", - expr: &ruleRefExpr{ - pos: position{line: 574, col: 21, offset: 17678}, - name: "LPAR", - }, - }, - &labeledExpr{ - pos: position{line: 574, col: 26, offset: 17683}, - label: "annos", - expr: &oneOrMoreExpr{ - pos: position{line: 574, col: 32, offset: 17689}, - expr: &ruleRefExpr{ - pos: position{line: 574, col: 32, offset: 17689}, - name: "Annotation", - }, - }, - }, - &labeledExpr{ - pos: position{line: 574, col: 44, offset: 17701}, - label: "rpar", - expr: &ruleRefExpr{ - pos: position{line: 574, col: 49, offset: 17706}, - name: "RPAR", - }, - }, - }, - }, - }, - }, - { - name: "Annotation", - pos: position{line: 578, col: 1, offset: 17839}, - expr: &actionExpr{ - pos: position{line: 578, col: 15, offset: 17853}, - run: (*parser).callonAnnotation1, - expr: &seqExpr{ - pos: position{line: 578, col: 15, offset: 17853}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 578, col: 15, offset: 17853}, - label: "id", - expr: &ruleRefExpr{ - pos: position{line: 578, col: 18, offset: 17856}, - name: "Identifier", - }, - }, - &labeledExpr{ - pos: position{line: 578, col: 29, offset: 17867}, - label: "eq", - expr: &ruleRefExpr{ - pos: position{line: 578, col: 32, offset: 17870}, - name: "EQUAL", - }, - }, - &labeledExpr{ - pos: position{line: 578, col: 38, offset: 17876}, - label: "value", - expr: &ruleRefExpr{ - pos: position{line: 578, col: 44, offset: 17882}, - name: "Literal", - }, - }, - &labeledExpr{ - pos: position{line: 578, col: 52, offset: 17890}, - label: "sep", - expr: &zeroOrOneExpr{ - pos: position{line: 578, col: 56, offset: 17894}, - expr: &ruleRefExpr{ - pos: position{line: 578, col: 56, offset: 17894}, - name: "ListSeparator", - }, - }, - }, - }, - }, - }, - }, - { - name: "ConstList", - pos: position{line: 582, col: 1, offset: 18053}, - expr: &actionExpr{ - pos: position{line: 582, col: 14, offset: 18066}, - run: (*parser).callonConstList1, - expr: &seqExpr{ - pos: position{line: 582, col: 14, offset: 18066}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 582, col: 14, offset: 18066}, - label: "lbrk", - expr: &ruleRefExpr{ - pos: position{line: 582, col: 19, offset: 18071}, - name: "LBRK", - }, - }, - &labeledExpr{ - pos: position{line: 582, col: 24, offset: 18076}, - label: "v", - expr: &zeroOrMoreExpr{ - pos: position{line: 582, col: 26, offset: 18078}, - expr: &ruleRefExpr{ - pos: position{line: 582, col: 26, offset: 18078}, - name: "ConstListItem", - }, - }, - }, - &labeledExpr{ - pos: position{line: 582, col: 41, offset: 18093}, - label: "rbrk", - expr: &ruleRefExpr{ - pos: position{line: 582, col: 46, offset: 18098}, - name: "RBRK", - }, - }, - }, - }, - }, - }, - { - name: "ConstListItem", - pos: position{line: 591, col: 1, offset: 18280}, - expr: &actionExpr{ - pos: position{line: 591, col: 17, offset: 18296}, - run: (*parser).callonConstListItem1, - expr: &seqExpr{ - pos: position{line: 591, col: 17, offset: 18296}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 591, col: 17, offset: 18296}, - label: "v", - expr: &ruleRefExpr{ - pos: position{line: 591, col: 19, offset: 18298}, - name: "ConstValue", - }, - }, - &labeledExpr{ - pos: position{line: 591, col: 30, offset: 18309}, - label: "sep", - expr: &zeroOrOneExpr{ - pos: position{line: 591, col: 34, offset: 18313}, - expr: &ruleRefExpr{ - pos: position{line: 591, col: 34, offset: 18313}, - name: "ListSeparator", - }, - }, - }, - }, - }, - }, - }, - { - name: "ConstMap", - pos: position{line: 601, col: 1, offset: 18450}, - expr: &actionExpr{ - pos: position{line: 601, col: 13, offset: 18462}, - run: (*parser).callonConstMap1, - expr: &seqExpr{ - pos: position{line: 601, col: 13, offset: 18462}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 601, col: 13, offset: 18462}, - label: "lcur", - expr: &ruleRefExpr{ - pos: position{line: 601, col: 18, offset: 18467}, - name: "LCUR", - }, - }, - &labeledExpr{ - pos: position{line: 601, col: 23, offset: 18472}, - label: "v", - expr: &zeroOrMoreExpr{ - pos: position{line: 601, col: 25, offset: 18474}, - expr: &ruleRefExpr{ - pos: position{line: 601, col: 25, offset: 18474}, - name: "ConstMapItem", - }, - }, - }, - &labeledExpr{ - pos: position{line: 601, col: 39, offset: 18488}, - label: "rcur", - expr: &ruleRefExpr{ - pos: position{line: 601, col: 44, offset: 18493}, - name: "RCUR", - }, - }, - }, - }, - }, - }, - { - name: "ConstMapItem", - pos: position{line: 610, col: 1, offset: 18674}, - expr: &actionExpr{ - pos: position{line: 610, col: 16, offset: 18689}, - run: (*parser).callonConstMapItem1, - expr: &seqExpr{ - pos: position{line: 610, col: 16, offset: 18689}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 610, col: 16, offset: 18689}, - label: "key", - expr: &ruleRefExpr{ - pos: position{line: 610, col: 20, offset: 18693}, - name: "ConstValue", - }, - }, - &labeledExpr{ - pos: position{line: 610, col: 31, offset: 18704}, - label: "colon", - expr: &ruleRefExpr{ - pos: position{line: 610, col: 37, offset: 18710}, - name: "COLON", - }, - }, - &labeledExpr{ - pos: position{line: 610, col: 43, offset: 18716}, - label: "value", - expr: &ruleRefExpr{ - pos: position{line: 610, col: 49, offset: 18722}, - name: "ConstValue", - }, - }, - &labeledExpr{ - pos: position{line: 610, col: 60, offset: 18733}, - label: "sep", - expr: &zeroOrOneExpr{ - pos: position{line: 610, col: 64, offset: 18737}, - expr: &ruleRefExpr{ - pos: position{line: 610, col: 64, offset: 18737}, - name: "ListSeparator", - }, - }, - }, - }, - }, - }, - }, - { - name: "EscapeLiteralChar", - pos: position{line: 621, col: 1, offset: 18983}, - expr: &actionExpr{ - pos: position{line: 621, col: 21, offset: 19003}, - run: (*parser).callonEscapeLiteralChar1, - expr: &seqExpr{ - pos: position{line: 621, col: 21, offset: 19003}, - exprs: []any{ - &litMatcher{ - pos: position{line: 621, col: 21, offset: 19003}, - val: "\\", - ignoreCase: false, - want: "\"\\\\\"", - }, - &charClassMatcher{ - pos: position{line: 621, col: 26, offset: 19008}, - val: "[\"']", - chars: []rune{'"', '\''}, - ignoreCase: false, - inverted: false, - }, - }, - }, - }, - }, - { - name: "Literal", - pos: position{line: 625, col: 1, offset: 19046}, - expr: &recoveryExpr{ - pos: position{line: 625, col: 11, offset: 19056}, - expr: &recoveryExpr{ - pos: position{line: 625, col: 11, offset: 19056}, - expr: &recoveryExpr{ - pos: position{line: 625, col: 11, offset: 19056}, - expr: &recoveryExpr{ - pos: position{line: 625, col: 11, offset: 19056}, - expr: &actionExpr{ - pos: position{line: 625, col: 11, offset: 19056}, - run: (*parser).callonLiteral5, - expr: &labeledExpr{ - pos: position{line: 625, col: 11, offset: 19056}, - label: "l", - expr: &choiceExpr{ - pos: position{line: 625, col: 14, offset: 19059}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 625, col: 14, offset: 19059}, - name: "Literal1", - }, - &ruleRefExpr{ - pos: position{line: 625, col: 25, offset: 19070}, - name: "Literal2", - }, - }, - }, - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 627, col: 31, offset: 19127}, - name: "ErrLiteral1MissingRight", - }, - failureLabel: []string{ - "errLiteral1MissingRight", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 627, col: 71, offset: 19167}, - name: "ErrLiteral1", - }, - failureLabel: []string{ - "errLiteral1", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 627, col: 111, offset: 19207}, - name: "ErrLiteral2MissingRight", - }, - failureLabel: []string{ - "errLiteral2MissingRight", - }, - }, - recoverExpr: &ruleRefExpr{ - pos: position{line: 627, col: 151, offset: 19247}, - name: "ErrLiteral2", - }, - failureLabel: []string{ - "errLiteral2", - }, - }, - }, - { - name: "Literal1", - pos: position{line: 629, col: 1, offset: 19260}, - expr: &choiceExpr{ - pos: position{line: 629, col: 12, offset: 19271}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 629, col: 12, offset: 19271}, - run: (*parser).callonLiteral12, - expr: &seqExpr{ - pos: position{line: 629, col: 12, offset: 19271}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 629, col: 12, offset: 19271}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 629, col: 21, offset: 19280}, - name: "ReservedComments", - }, - }, - &litMatcher{ - pos: position{line: 629, col: 38, offset: 19297}, - val: "\"", - ignoreCase: false, - want: "\"\\\"\"", - }, - &labeledExpr{ - pos: position{line: 629, col: 42, offset: 19301}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 629, col: 44, offset: 19303}, - name: "Literal1Val", - }, - }, - &litMatcher{ - pos: position{line: 629, col: 56, offset: 19315}, - val: "\"", - ignoreCase: false, - want: "\"\\\"\"", - }, - &zeroOrMoreExpr{ - pos: position{line: 629, col: 60, offset: 19319}, - expr: &ruleRefExpr{ - pos: position{line: 629, col: 60, offset: 19319}, - name: "Indent", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 631, col: 5, offset: 19432}, - run: (*parser).callonLiteral112, - expr: &labeledExpr{ - pos: position{line: 631, col: 5, offset: 19432}, - label: "x", - expr: &seqExpr{ - pos: position{line: 631, col: 8, offset: 19435}, - exprs: []any{ - &andExpr{ - pos: position{line: 631, col: 8, offset: 19435}, - expr: &seqExpr{ - pos: position{line: 631, col: 10, offset: 19437}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 631, col: 10, offset: 19437}, - name: "ReservedComments", - }, - &litMatcher{ - pos: position{line: 631, col: 27, offset: 19454}, - val: "\"", - ignoreCase: false, - want: "\"\\\"\"", - }, - &labeledExpr{ - pos: position{line: 631, col: 31, offset: 19458}, - label: "t", - expr: &zeroOrMoreExpr{ - pos: position{line: 631, col: 33, offset: 19460}, - expr: &choiceExpr{ - pos: position{line: 631, col: 34, offset: 19461}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 631, col: 34, offset: 19461}, - name: "EscapeLiteralChar", - }, - &seqExpr{ - pos: position{line: 631, col: 54, offset: 19481}, - exprs: []any{ - ¬Expr{ - pos: position{line: 631, col: 54, offset: 19481}, - expr: &litMatcher{ - pos: position{line: 631, col: 55, offset: 19482}, - val: "\"", - ignoreCase: false, - want: "\"\\\"\"", - }, - }, - &anyMatcher{ - line: 631, col: 59, offset: 19486, - }, - }, - }, - }, - }, - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 631, col: 63, offset: 19490}, - expr: &ruleRefExpr{ - pos: position{line: 631, col: 63, offset: 19490}, - name: "Indent", - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 631, col: 72, offset: 19499}, - label: "errLiteral1MissingRight", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "Literal2", - pos: position{line: 635, col: 1, offset: 19560}, - expr: &choiceExpr{ - pos: position{line: 635, col: 12, offset: 19571}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 635, col: 12, offset: 19571}, - run: (*parser).callonLiteral22, - expr: &seqExpr{ - pos: position{line: 635, col: 12, offset: 19571}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 635, col: 12, offset: 19571}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 635, col: 21, offset: 19580}, - name: "ReservedComments", - }, - }, - &litMatcher{ - pos: position{line: 635, col: 38, offset: 19597}, - val: "'", - ignoreCase: false, - want: "\"'\"", - }, - &labeledExpr{ - pos: position{line: 635, col: 42, offset: 19601}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 635, col: 44, offset: 19603}, - name: "Literal2Val", - }, - }, - &litMatcher{ - pos: position{line: 635, col: 56, offset: 19615}, - val: "'", - ignoreCase: false, - want: "\"'\"", - }, - &zeroOrMoreExpr{ - pos: position{line: 635, col: 60, offset: 19619}, - expr: &ruleRefExpr{ - pos: position{line: 635, col: 60, offset: 19619}, - name: "Indent", - }, - }, - }, - }, - }, - &actionExpr{ - pos: position{line: 637, col: 5, offset: 19731}, - run: (*parser).callonLiteral212, - expr: &labeledExpr{ - pos: position{line: 637, col: 5, offset: 19731}, - label: "x", - expr: &seqExpr{ - pos: position{line: 637, col: 8, offset: 19734}, - exprs: []any{ - &andExpr{ - pos: position{line: 637, col: 8, offset: 19734}, - expr: &seqExpr{ - pos: position{line: 637, col: 10, offset: 19736}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 637, col: 10, offset: 19736}, - name: "ReservedComments", - }, - &litMatcher{ - pos: position{line: 637, col: 27, offset: 19753}, - val: "'", - ignoreCase: false, - want: "\"'\"", - }, - &labeledExpr{ - pos: position{line: 637, col: 31, offset: 19757}, - label: "t", - expr: &zeroOrMoreExpr{ - pos: position{line: 637, col: 33, offset: 19759}, - expr: &choiceExpr{ - pos: position{line: 637, col: 34, offset: 19760}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 637, col: 34, offset: 19760}, - name: "EscapeLiteralChar", - }, - &seqExpr{ - pos: position{line: 637, col: 54, offset: 19780}, - exprs: []any{ - ¬Expr{ - pos: position{line: 637, col: 54, offset: 19780}, - expr: &litMatcher{ - pos: position{line: 637, col: 55, offset: 19781}, - val: "'", - ignoreCase: false, - want: "\"'\"", - }, - }, - &anyMatcher{ - line: 637, col: 59, offset: 19785, - }, - }, - }, - }, - }, - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 637, col: 63, offset: 19789}, - expr: &ruleRefExpr{ - pos: position{line: 637, col: 63, offset: 19789}, - name: "Indent", - }, - }, - }, - }, - }, - &throwExpr{ - pos: position{line: 637, col: 72, offset: 19798}, - label: "errLiteral2MissingRight", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "Literal1Val", - pos: position{line: 641, col: 1, offset: 19859}, - expr: &actionExpr{ - pos: position{line: 641, col: 15, offset: 19873}, - run: (*parser).callonLiteral1Val1, - expr: &zeroOrMoreExpr{ - pos: position{line: 641, col: 15, offset: 19873}, - expr: &choiceExpr{ - pos: position{line: 641, col: 16, offset: 19874}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 641, col: 16, offset: 19874}, - name: "EscapeLiteralChar", - }, - &seqExpr{ - pos: position{line: 641, col: 36, offset: 19894}, - exprs: []any{ - ¬Expr{ - pos: position{line: 641, col: 36, offset: 19894}, - expr: &charClassMatcher{ - pos: position{line: 641, col: 37, offset: 19895}, - val: "[\"\\r\\n]", - chars: []rune{'"', '\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 641, col: 45, offset: 19903, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "Literal2Val", - pos: position{line: 645, col: 1, offset: 19984}, - expr: &actionExpr{ - pos: position{line: 645, col: 15, offset: 19998}, - run: (*parser).callonLiteral2Val1, - expr: &zeroOrMoreExpr{ - pos: position{line: 645, col: 15, offset: 19998}, - expr: &choiceExpr{ - pos: position{line: 645, col: 16, offset: 19999}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 645, col: 16, offset: 19999}, - name: "EscapeLiteralChar", - }, - &seqExpr{ - pos: position{line: 645, col: 36, offset: 20019}, - exprs: []any{ - ¬Expr{ - pos: position{line: 645, col: 36, offset: 20019}, - expr: &charClassMatcher{ - pos: position{line: 645, col: 37, offset: 20020}, - val: "['\\r\\n]", - chars: []rune{'\'', '\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 645, col: 45, offset: 20028, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "DefinitionIdentifier", - pos: position{line: 649, col: 1, offset: 20109}, - expr: &choiceExpr{ - pos: position{line: 649, col: 24, offset: 20132}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 649, col: 24, offset: 20132}, - run: (*parser).callonDefinitionIdentifier2, - expr: &labeledExpr{ - pos: position{line: 649, col: 24, offset: 20132}, - label: "id", - expr: &ruleRefExpr{ - pos: position{line: 649, col: 27, offset: 20135}, - name: "Identifier", - }, - }, - }, - &throwExpr{ - pos: position{line: 651, col: 5, offset: 20182}, - label: "errIdentifier", - }, - }, - }, - }, - { - name: "Identifier", - pos: position{line: 653, col: 1, offset: 20200}, - expr: &actionExpr{ - pos: position{line: 653, col: 14, offset: 20213}, - run: (*parser).callonIdentifier1, - expr: &seqExpr{ - pos: position{line: 653, col: 14, offset: 20213}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 653, col: 14, offset: 20213}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 653, col: 23, offset: 20222}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 653, col: 40, offset: 20239}, - label: "id", - expr: &ruleRefExpr{ - pos: position{line: 653, col: 43, offset: 20242}, - name: "IdentifierToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 653, col: 59, offset: 20258}, - expr: &ruleRefExpr{ - pos: position{line: 653, col: 59, offset: 20258}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "IdentifierToken", - pos: position{line: 659, col: 1, offset: 20389}, - expr: &actionExpr{ - pos: position{line: 659, col: 19, offset: 20407}, - run: (*parser).callonIdentifierToken1, - expr: &seqExpr{ - pos: position{line: 659, col: 19, offset: 20407}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 659, col: 19, offset: 20407}, - name: "Letter", - }, - &zeroOrMoreExpr{ - pos: position{line: 659, col: 26, offset: 20414}, - expr: &choiceExpr{ - pos: position{line: 659, col: 28, offset: 20416}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 659, col: 28, offset: 20416}, - name: "Letter", - }, - &ruleRefExpr{ - pos: position{line: 659, col: 37, offset: 20425}, - name: "Digit", - }, - &litMatcher{ - pos: position{line: 659, col: 45, offset: 20433}, - val: ".", - ignoreCase: false, - want: "\".\"", - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ListSeparator", - pos: position{line: 663, col: 1, offset: 20520}, - expr: &actionExpr{ - pos: position{line: 663, col: 17, offset: 20536}, - run: (*parser).callonListSeparator1, - expr: &seqExpr{ - pos: position{line: 663, col: 17, offset: 20536}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 663, col: 17, offset: 20536}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 663, col: 26, offset: 20545}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 663, col: 43, offset: 20562}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 663, col: 45, offset: 20564}, - name: "ListSeparatorToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 663, col: 64, offset: 20583}, - expr: &ruleRefExpr{ - pos: position{line: 663, col: 64, offset: 20583}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "ListSeparatorToken", - pos: position{line: 669, col: 1, offset: 20734}, - expr: &actionExpr{ - pos: position{line: 669, col: 22, offset: 20755}, - run: (*parser).callonListSeparatorToken1, - expr: &choiceExpr{ - pos: position{line: 669, col: 23, offset: 20756}, - alternatives: []any{ - &litMatcher{ - pos: position{line: 669, col: 23, offset: 20756}, - val: ",", - ignoreCase: false, - want: "\",\"", - }, - &litMatcher{ - pos: position{line: 669, col: 29, offset: 20762}, - val: ";", - ignoreCase: false, - want: "\";\"", - }, - }, - }, - }, - }, - { - name: "Letter", - pos: position{line: 673, col: 1, offset: 20806}, - expr: &choiceExpr{ - pos: position{line: 673, col: 10, offset: 20815}, - alternatives: []any{ - &charClassMatcher{ - pos: position{line: 673, col: 10, offset: 20815}, - val: "[A-Z]", - ranges: []rune{'A', 'Z'}, - ignoreCase: false, - inverted: false, - }, - &charClassMatcher{ - pos: position{line: 673, col: 18, offset: 20823}, - val: "[a-z]", - ranges: []rune{'a', 'z'}, - ignoreCase: false, - inverted: false, - }, - &actionExpr{ - pos: position{line: 673, col: 26, offset: 20831}, - run: (*parser).callonLetter4, - expr: &litMatcher{ - pos: position{line: 673, col: 26, offset: 20831}, - val: "_", - ignoreCase: false, - want: "\"_\"", - }, - }, - }, - }, - }, - { - name: "LetterOrDigit", - pos: position{line: 676, col: 1, offset: 20867}, - expr: &choiceExpr{ - pos: position{line: 676, col: 17, offset: 20883}, - alternatives: []any{ - &charClassMatcher{ - pos: position{line: 676, col: 17, offset: 20883}, - val: "[a-z]", - ranges: []rune{'a', 'z'}, - ignoreCase: false, - inverted: false, - }, - &charClassMatcher{ - pos: position{line: 676, col: 25, offset: 20891}, - val: "[A-Z]", - ranges: []rune{'A', 'Z'}, - ignoreCase: false, - inverted: false, - }, - &charClassMatcher{ - pos: position{line: 676, col: 33, offset: 20899}, - val: "[0-9]", - ranges: []rune{'0', '9'}, - ignoreCase: false, - inverted: false, - }, - &actionExpr{ - pos: position{line: 676, col: 41, offset: 20907}, - run: (*parser).callonLetterOrDigit5, - expr: &charClassMatcher{ - pos: position{line: 676, col: 41, offset: 20907}, - val: "[_$]", - chars: []rune{'_', '$'}, - ignoreCase: false, - inverted: false, - }, - }, - }, - }, - }, - { - name: "Digit", - pos: position{line: 680, col: 1, offset: 20945}, - expr: &actionExpr{ - pos: position{line: 680, col: 9, offset: 20953}, - run: (*parser).callonDigit1, - expr: &charClassMatcher{ - pos: position{line: 680, col: 9, offset: 20953}, - val: "[0-9]", - ranges: []rune{'0', '9'}, - ignoreCase: false, - inverted: false, - }, - }, - }, - { - name: "ReservedComments", - pos: position{line: 684, col: 1, offset: 20992}, - expr: &actionExpr{ - pos: position{line: 684, col: 20, offset: 21011}, - run: (*parser).callonReservedComments1, - expr: &labeledExpr{ - pos: position{line: 684, col: 20, offset: 21011}, - label: "comments", - expr: &zeroOrMoreExpr{ - pos: position{line: 684, col: 29, offset: 21020}, - expr: &choiceExpr{ - pos: position{line: 684, col: 30, offset: 21021}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 684, col: 30, offset: 21021}, - name: "Space", - }, - &ruleRefExpr{ - pos: position{line: 684, col: 38, offset: 21029}, - name: "Comment", - }, - }, - }, - }, - }, - }, - }, - { - name: "ReservedEndLineComments", - pos: position{line: 687, col: 1, offset: 21081}, - expr: &actionExpr{ - pos: position{line: 687, col: 27, offset: 21107}, - run: (*parser).callonReservedEndLineComments1, - expr: &labeledExpr{ - pos: position{line: 687, col: 27, offset: 21107}, - label: "comments", - expr: &zeroOrMoreExpr{ - pos: position{line: 687, col: 36, offset: 21116}, - expr: &choiceExpr{ - pos: position{line: 687, col: 37, offset: 21117}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 687, col: 37, offset: 21117}, - name: "Indent", - }, - &ruleRefExpr{ - pos: position{line: 687, col: 46, offset: 21126}, - name: "Comment", - }, - }, - }, - }, - }, - }, - }, - { - name: "Space", - pos: position{line: 691, col: 1, offset: 21179}, - expr: &actionExpr{ - pos: position{line: 691, col: 9, offset: 21187}, - run: (*parser).callonSpace1, - expr: &oneOrMoreExpr{ - pos: position{line: 691, col: 9, offset: 21187}, - expr: &choiceExpr{ - pos: position{line: 691, col: 10, offset: 21188}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 691, col: 10, offset: 21188}, - name: "Indent", - }, - &ruleRefExpr{ - pos: position{line: 691, col: 19, offset: 21197}, - name: "CarriageReturnLineFeed", - }, - }, - }, - }, - }, - }, - { - name: "Indent", - pos: position{line: 694, col: 1, offset: 21242}, - expr: &actionExpr{ - pos: position{line: 694, col: 10, offset: 21251}, - run: (*parser).callonIndent1, - expr: &charClassMatcher{ - pos: position{line: 694, col: 10, offset: 21251}, - val: "[ \\t\\v]", - chars: []rune{' ', '\t', '\v'}, - ignoreCase: false, - inverted: false, - }, - }, - }, - { - name: "CarriageReturnLineFeed", - pos: position{line: 697, col: 1, offset: 21279}, - expr: &charClassMatcher{ - pos: position{line: 697, col: 26, offset: 21304}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - { - name: "Comment", - pos: position{line: 699, col: 1, offset: 21312}, - expr: &actionExpr{ - pos: position{line: 699, col: 11, offset: 21322}, - run: (*parser).callonComment1, - expr: &labeledExpr{ - pos: position{line: 699, col: 11, offset: 21322}, - label: "v", - expr: &choiceExpr{ - pos: position{line: 699, col: 14, offset: 21325}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 699, col: 14, offset: 21325}, - name: "LongComment", - }, - &ruleRefExpr{ - pos: position{line: 699, col: 28, offset: 21339}, - name: "LineComment", - }, - &ruleRefExpr{ - pos: position{line: 699, col: 42, offset: 21353}, - name: "UnixComment", - }, - }, - }, - }, - }, - }, - { - name: "LongComment", - pos: position{line: 702, col: 1, offset: 21396}, - expr: &actionExpr{ - pos: position{line: 702, col: 15, offset: 21410}, - run: (*parser).callonLongComment1, - expr: &seqExpr{ - pos: position{line: 702, col: 15, offset: 21410}, - exprs: []any{ - &litMatcher{ - pos: position{line: 702, col: 15, offset: 21410}, - val: "/*", - ignoreCase: false, - want: "\"/*\"", - }, - &ruleRefExpr{ - pos: position{line: 702, col: 20, offset: 21415}, - name: "LongCommentMatch", - }, - &litMatcher{ - pos: position{line: 702, col: 37, offset: 21432}, - val: "*/", - ignoreCase: false, - want: "\"*/\"", - }, - }, - }, - }, - }, - { - name: "LongCommentMatch", - pos: position{line: 705, col: 1, offset: 21531}, - expr: &actionExpr{ - pos: position{line: 705, col: 20, offset: 21550}, - run: (*parser).callonLongCommentMatch1, - expr: &zeroOrMoreExpr{ - pos: position{line: 705, col: 20, offset: 21550}, - expr: &seqExpr{ - pos: position{line: 705, col: 21, offset: 21551}, - exprs: []any{ - ¬Expr{ - pos: position{line: 705, col: 21, offset: 21551}, - expr: &litMatcher{ - pos: position{line: 705, col: 22, offset: 21552}, - val: "*/", - ignoreCase: false, - want: "\"*/\"", - }, - }, - &anyMatcher{ - line: 705, col: 27, offset: 21557, - }, - }, - }, - }, - }, - }, - { - name: "LineComment", - pos: position{line: 709, col: 1, offset: 21594}, - expr: &actionExpr{ - pos: position{line: 709, col: 15, offset: 21608}, - run: (*parser).callonLineComment1, - expr: &seqExpr{ - pos: position{line: 709, col: 15, offset: 21608}, - exprs: []any{ - &litMatcher{ - pos: position{line: 709, col: 15, offset: 21608}, - val: "//", - ignoreCase: false, - want: "\"//\"", - }, - &ruleRefExpr{ - pos: position{line: 709, col: 20, offset: 21613}, - name: "LineCommentMatch", - }, - }, - }, - }, - }, - { - name: "LineCommentMatch", - pos: position{line: 712, col: 1, offset: 21725}, - expr: &actionExpr{ - pos: position{line: 712, col: 20, offset: 21744}, - run: (*parser).callonLineCommentMatch1, - expr: &zeroOrMoreExpr{ - pos: position{line: 712, col: 20, offset: 21744}, - expr: &seqExpr{ - pos: position{line: 712, col: 21, offset: 21745}, - exprs: []any{ - ¬Expr{ - pos: position{line: 712, col: 21, offset: 21745}, - expr: &charClassMatcher{ - pos: position{line: 712, col: 22, offset: 21746}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 712, col: 29, offset: 21753, - }, - }, - }, - }, - }, - }, - { - name: "UnixComment", - pos: position{line: 716, col: 1, offset: 21790}, - expr: &actionExpr{ - pos: position{line: 716, col: 15, offset: 21804}, - run: (*parser).callonUnixComment1, - expr: &seqExpr{ - pos: position{line: 716, col: 15, offset: 21804}, - exprs: []any{ - &litMatcher{ - pos: position{line: 716, col: 15, offset: 21804}, - val: "#", - ignoreCase: false, - want: "\"#\"", - }, - &ruleRefExpr{ - pos: position{line: 716, col: 19, offset: 21808}, - name: "UnixCommentMatch", - }, - }, - }, - }, - }, - { - name: "UnixCommentMatch", - pos: position{line: 719, col: 1, offset: 21915}, - expr: &actionExpr{ - pos: position{line: 719, col: 20, offset: 21934}, - run: (*parser).callonUnixCommentMatch1, - expr: &zeroOrMoreExpr{ - pos: position{line: 719, col: 20, offset: 21934}, - expr: &seqExpr{ - pos: position{line: 719, col: 21, offset: 21935}, - exprs: []any{ - ¬Expr{ - pos: position{line: 719, col: 21, offset: 21935}, - expr: &charClassMatcher{ - pos: position{line: 719, col: 22, offset: 21936}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 719, col: 29, offset: 21943, - }, - }, - }, - }, - }, - }, - { - name: "BOOL", - pos: position{line: 723, col: 1, offset: 21981}, - expr: &actionExpr{ - pos: position{line: 723, col: 8, offset: 21988}, - run: (*parser).callonBOOL1, - expr: &seqExpr{ - pos: position{line: 723, col: 8, offset: 21988}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 723, col: 8, offset: 21988}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 723, col: 17, offset: 21997}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 723, col: 34, offset: 22014}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 723, col: 36, offset: 22016}, - name: "BOOLToken", - }, - }, - ¬Expr{ - pos: position{line: 723, col: 53, offset: 22033}, - expr: &ruleRefExpr{ - pos: position{line: 723, col: 54, offset: 22034}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 723, col: 69, offset: 22049}, - expr: &ruleRefExpr{ - pos: position{line: 723, col: 69, offset: 22049}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "BOOLToken", - pos: position{line: 729, col: 1, offset: 22136}, - expr: &actionExpr{ - pos: position{line: 729, col: 14, offset: 22149}, - run: (*parser).callonBOOLToken1, - expr: &litMatcher{ - pos: position{line: 729, col: 14, offset: 22149}, - val: "bool", - ignoreCase: false, - want: "\"bool\"", - }, - }, - }, - { - name: "BYTE", - pos: position{line: 733, col: 1, offset: 22209}, - expr: &actionExpr{ - pos: position{line: 733, col: 8, offset: 22216}, - run: (*parser).callonBYTE1, - expr: &seqExpr{ - pos: position{line: 733, col: 8, offset: 22216}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 733, col: 8, offset: 22216}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 733, col: 17, offset: 22225}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 733, col: 34, offset: 22242}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 733, col: 36, offset: 22244}, - name: "BYTEToken", - }, - }, - ¬Expr{ - pos: position{line: 733, col: 53, offset: 22261}, - expr: &ruleRefExpr{ - pos: position{line: 733, col: 54, offset: 22262}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 733, col: 69, offset: 22277}, - expr: &ruleRefExpr{ - pos: position{line: 733, col: 69, offset: 22277}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "BYTEToken", - pos: position{line: 739, col: 1, offset: 22364}, - expr: &actionExpr{ - pos: position{line: 739, col: 13, offset: 22376}, - run: (*parser).callonBYTEToken1, - expr: &litMatcher{ - pos: position{line: 739, col: 13, offset: 22376}, - val: "byte", - ignoreCase: false, - want: "\"byte\"", - }, - }, - }, - { - name: "I8", - pos: position{line: 743, col: 1, offset: 22436}, - expr: &actionExpr{ - pos: position{line: 743, col: 6, offset: 22441}, - run: (*parser).callonI81, - expr: &seqExpr{ - pos: position{line: 743, col: 6, offset: 22441}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 743, col: 6, offset: 22441}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 743, col: 15, offset: 22450}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 743, col: 32, offset: 22467}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 743, col: 34, offset: 22469}, - name: "I8Token", - }, - }, - ¬Expr{ - pos: position{line: 743, col: 51, offset: 22486}, - expr: &ruleRefExpr{ - pos: position{line: 743, col: 52, offset: 22487}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 743, col: 67, offset: 22502}, - expr: &ruleRefExpr{ - pos: position{line: 743, col: 67, offset: 22502}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "I8Token", - pos: position{line: 749, col: 1, offset: 22589}, - expr: &actionExpr{ - pos: position{line: 749, col: 11, offset: 22599}, - run: (*parser).callonI8Token1, - expr: &litMatcher{ - pos: position{line: 749, col: 11, offset: 22599}, - val: "i8", - ignoreCase: false, - want: "\"i8\"", - }, - }, - }, - { - name: "I16", - pos: position{line: 754, col: 1, offset: 22658}, - expr: &actionExpr{ - pos: position{line: 754, col: 7, offset: 22664}, - run: (*parser).callonI161, - expr: &seqExpr{ - pos: position{line: 754, col: 7, offset: 22664}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 754, col: 7, offset: 22664}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 754, col: 16, offset: 22673}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 754, col: 33, offset: 22690}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 754, col: 35, offset: 22692}, - name: "I16Token", - }, - }, - ¬Expr{ - pos: position{line: 754, col: 52, offset: 22709}, - expr: &ruleRefExpr{ - pos: position{line: 754, col: 53, offset: 22710}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 754, col: 68, offset: 22725}, - expr: &ruleRefExpr{ - pos: position{line: 754, col: 68, offset: 22725}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "I16Token", - pos: position{line: 760, col: 1, offset: 22812}, - expr: &actionExpr{ - pos: position{line: 760, col: 12, offset: 22823}, - run: (*parser).callonI16Token1, - expr: &litMatcher{ - pos: position{line: 760, col: 12, offset: 22823}, - val: "i16", - ignoreCase: false, - want: "\"i16\"", - }, - }, - }, - { - name: "I32", - pos: position{line: 764, col: 1, offset: 22882}, - expr: &actionExpr{ - pos: position{line: 764, col: 7, offset: 22888}, - run: (*parser).callonI321, - expr: &seqExpr{ - pos: position{line: 764, col: 7, offset: 22888}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 764, col: 7, offset: 22888}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 764, col: 16, offset: 22897}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 764, col: 33, offset: 22914}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 764, col: 35, offset: 22916}, - name: "I32Token", - }, - }, - ¬Expr{ - pos: position{line: 764, col: 52, offset: 22933}, - expr: &ruleRefExpr{ - pos: position{line: 764, col: 53, offset: 22934}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 764, col: 68, offset: 22949}, - expr: &ruleRefExpr{ - pos: position{line: 764, col: 68, offset: 22949}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "I32Token", - pos: position{line: 770, col: 1, offset: 23036}, - expr: &actionExpr{ - pos: position{line: 770, col: 12, offset: 23047}, - run: (*parser).callonI32Token1, - expr: &litMatcher{ - pos: position{line: 770, col: 12, offset: 23047}, - val: "i32", - ignoreCase: false, - want: "\"i32\"", - }, - }, - }, - { - name: "I64", - pos: position{line: 774, col: 1, offset: 23106}, - expr: &actionExpr{ - pos: position{line: 774, col: 7, offset: 23112}, - run: (*parser).callonI641, - expr: &seqExpr{ - pos: position{line: 774, col: 7, offset: 23112}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 774, col: 7, offset: 23112}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 774, col: 16, offset: 23121}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 774, col: 33, offset: 23138}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 774, col: 35, offset: 23140}, - name: "I64Token", - }, - }, - ¬Expr{ - pos: position{line: 774, col: 52, offset: 23157}, - expr: &ruleRefExpr{ - pos: position{line: 774, col: 53, offset: 23158}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 774, col: 68, offset: 23173}, - expr: &ruleRefExpr{ - pos: position{line: 774, col: 68, offset: 23173}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "I64Token", - pos: position{line: 780, col: 1, offset: 23260}, - expr: &actionExpr{ - pos: position{line: 780, col: 12, offset: 23271}, - run: (*parser).callonI64Token1, - expr: &litMatcher{ - pos: position{line: 780, col: 12, offset: 23271}, - val: "i64", - ignoreCase: false, - want: "\"i64\"", - }, - }, - }, - { - name: "DOUBLE", - pos: position{line: 784, col: 1, offset: 23330}, - expr: &actionExpr{ - pos: position{line: 784, col: 10, offset: 23339}, - run: (*parser).callonDOUBLE1, - expr: &seqExpr{ - pos: position{line: 784, col: 10, offset: 23339}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 784, col: 10, offset: 23339}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 784, col: 19, offset: 23348}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 784, col: 36, offset: 23365}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 784, col: 38, offset: 23367}, - name: "DOUBLEToken", - }, - }, - ¬Expr{ - pos: position{line: 784, col: 55, offset: 23384}, - expr: &ruleRefExpr{ - pos: position{line: 784, col: 56, offset: 23385}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 784, col: 71, offset: 23400}, - expr: &ruleRefExpr{ - pos: position{line: 784, col: 71, offset: 23400}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "DOUBLEToken", - pos: position{line: 790, col: 1, offset: 23487}, - expr: &actionExpr{ - pos: position{line: 790, col: 15, offset: 23501}, - run: (*parser).callonDOUBLEToken1, - expr: &litMatcher{ - pos: position{line: 790, col: 15, offset: 23501}, - val: "double", - ignoreCase: false, - want: "\"double\"", - }, - }, - }, - { - name: "STRING", - pos: position{line: 794, col: 1, offset: 23563}, - expr: &actionExpr{ - pos: position{line: 794, col: 10, offset: 23572}, - run: (*parser).callonSTRING1, - expr: &seqExpr{ - pos: position{line: 794, col: 10, offset: 23572}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 794, col: 10, offset: 23572}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 794, col: 19, offset: 23581}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 794, col: 36, offset: 23598}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 794, col: 38, offset: 23600}, - name: "STRINGToken", - }, - }, - ¬Expr{ - pos: position{line: 794, col: 55, offset: 23617}, - expr: &ruleRefExpr{ - pos: position{line: 794, col: 56, offset: 23618}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 794, col: 71, offset: 23633}, - expr: &ruleRefExpr{ - pos: position{line: 794, col: 71, offset: 23633}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "STRINGToken", - pos: position{line: 800, col: 1, offset: 23720}, - expr: &actionExpr{ - pos: position{line: 800, col: 15, offset: 23734}, - run: (*parser).callonSTRINGToken1, - expr: &litMatcher{ - pos: position{line: 800, col: 15, offset: 23734}, - val: "string", - ignoreCase: false, - want: "\"string\"", - }, - }, - }, - { - name: "BINARY", - pos: position{line: 804, col: 1, offset: 23796}, - expr: &actionExpr{ - pos: position{line: 804, col: 10, offset: 23805}, - run: (*parser).callonBINARY1, - expr: &seqExpr{ - pos: position{line: 804, col: 10, offset: 23805}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 804, col: 10, offset: 23805}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 804, col: 19, offset: 23814}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 804, col: 36, offset: 23831}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 804, col: 38, offset: 23833}, - name: "BINARYToken", - }, - }, - ¬Expr{ - pos: position{line: 804, col: 55, offset: 23850}, - expr: &ruleRefExpr{ - pos: position{line: 804, col: 56, offset: 23851}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 804, col: 71, offset: 23866}, - expr: &ruleRefExpr{ - pos: position{line: 804, col: 71, offset: 23866}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "BINARYToken", - pos: position{line: 810, col: 1, offset: 23953}, - expr: &actionExpr{ - pos: position{line: 810, col: 15, offset: 23967}, - run: (*parser).callonBINARYToken1, - expr: &litMatcher{ - pos: position{line: 810, col: 15, offset: 23967}, - val: "binary", - ignoreCase: false, - want: "\"binary\"", - }, - }, - }, - { - name: "UUID", - pos: position{line: 814, col: 1, offset: 24029}, - expr: &actionExpr{ - pos: position{line: 814, col: 8, offset: 24036}, - run: (*parser).callonUUID1, - expr: &seqExpr{ - pos: position{line: 814, col: 8, offset: 24036}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 814, col: 8, offset: 24036}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 814, col: 17, offset: 24045}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 814, col: 34, offset: 24062}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 814, col: 36, offset: 24064}, - name: "UUIDToken", - }, - }, - ¬Expr{ - pos: position{line: 814, col: 51, offset: 24079}, - expr: &ruleRefExpr{ - pos: position{line: 814, col: 52, offset: 24080}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 814, col: 67, offset: 24095}, - expr: &ruleRefExpr{ - pos: position{line: 814, col: 67, offset: 24095}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "UUIDToken", - pos: position{line: 820, col: 1, offset: 24182}, - expr: &actionExpr{ - pos: position{line: 820, col: 13, offset: 24194}, - run: (*parser).callonUUIDToken1, - expr: &litMatcher{ - pos: position{line: 820, col: 13, offset: 24194}, - val: "uuid", - ignoreCase: false, - want: "\"uuid\"", - }, - }, - }, - { - name: "MAP", - pos: position{line: 824, col: 1, offset: 24254}, - expr: &actionExpr{ - pos: position{line: 824, col: 7, offset: 24260}, - run: (*parser).callonMAP1, - expr: &seqExpr{ - pos: position{line: 824, col: 7, offset: 24260}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 824, col: 7, offset: 24260}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 824, col: 16, offset: 24269}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 824, col: 33, offset: 24286}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 824, col: 35, offset: 24288}, - name: "MAPToken", - }, - }, - ¬Expr{ - pos: position{line: 824, col: 54, offset: 24307}, - expr: &ruleRefExpr{ - pos: position{line: 824, col: 55, offset: 24308}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 824, col: 70, offset: 24323}, - expr: &ruleRefExpr{ - pos: position{line: 824, col: 70, offset: 24323}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "MAPToken", - pos: position{line: 830, col: 1, offset: 24410}, - expr: &actionExpr{ - pos: position{line: 830, col: 12, offset: 24421}, - run: (*parser).callonMAPToken1, - expr: &litMatcher{ - pos: position{line: 830, col: 12, offset: 24421}, - val: "map", - ignoreCase: false, - want: "\"map\"", - }, - }, - }, - { - name: "SET", - pos: position{line: 834, col: 1, offset: 24480}, - expr: &actionExpr{ - pos: position{line: 834, col: 7, offset: 24486}, - run: (*parser).callonSET1, - expr: &seqExpr{ - pos: position{line: 834, col: 7, offset: 24486}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 834, col: 7, offset: 24486}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 834, col: 16, offset: 24495}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 834, col: 33, offset: 24512}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 834, col: 35, offset: 24514}, - name: "SETToken", - }, - }, - ¬Expr{ - pos: position{line: 834, col: 54, offset: 24533}, - expr: &ruleRefExpr{ - pos: position{line: 834, col: 55, offset: 24534}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 834, col: 70, offset: 24549}, - expr: &ruleRefExpr{ - pos: position{line: 834, col: 70, offset: 24549}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "SETToken", - pos: position{line: 840, col: 1, offset: 24636}, - expr: &actionExpr{ - pos: position{line: 840, col: 12, offset: 24647}, - run: (*parser).callonSETToken1, - expr: &litMatcher{ - pos: position{line: 840, col: 12, offset: 24647}, - val: "set", - ignoreCase: false, - want: "\"set\"", - }, - }, - }, - { - name: "LIST", - pos: position{line: 844, col: 1, offset: 24706}, - expr: &actionExpr{ - pos: position{line: 844, col: 8, offset: 24713}, - run: (*parser).callonLIST1, - expr: &seqExpr{ - pos: position{line: 844, col: 8, offset: 24713}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 844, col: 8, offset: 24713}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 844, col: 17, offset: 24722}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 844, col: 34, offset: 24739}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 844, col: 36, offset: 24741}, - name: "ListToken", - }, - }, - ¬Expr{ - pos: position{line: 844, col: 55, offset: 24760}, - expr: &ruleRefExpr{ - pos: position{line: 844, col: 56, offset: 24761}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 844, col: 71, offset: 24776}, - expr: &ruleRefExpr{ - pos: position{line: 844, col: 71, offset: 24776}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "ListToken", - pos: position{line: 850, col: 1, offset: 24863}, - expr: &actionExpr{ - pos: position{line: 850, col: 13, offset: 24875}, - run: (*parser).callonListToken1, - expr: &litMatcher{ - pos: position{line: 850, col: 13, offset: 24875}, - val: "list", - ignoreCase: false, - want: "\"list\"", - }, - }, - }, - { - name: "CONST", - pos: position{line: 854, col: 1, offset: 24935}, - expr: &actionExpr{ - pos: position{line: 854, col: 9, offset: 24943}, - run: (*parser).callonCONST1, - expr: &seqExpr{ - pos: position{line: 854, col: 9, offset: 24943}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 854, col: 9, offset: 24943}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 854, col: 18, offset: 24952}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 854, col: 35, offset: 24969}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 854, col: 37, offset: 24971}, - name: "CONSTToken", - }, - }, - ¬Expr{ - pos: position{line: 854, col: 56, offset: 24990}, - expr: &ruleRefExpr{ - pos: position{line: 854, col: 57, offset: 24991}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 854, col: 72, offset: 25006}, - expr: &ruleRefExpr{ - pos: position{line: 854, col: 72, offset: 25006}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "CONSTToken", - pos: position{line: 860, col: 1, offset: 25149}, - expr: &actionExpr{ - pos: position{line: 860, col: 14, offset: 25162}, - run: (*parser).callonCONSTToken1, - expr: &litMatcher{ - pos: position{line: 860, col: 14, offset: 25162}, - val: "const", - ignoreCase: false, - want: "\"const\"", - }, - }, - }, - { - name: "ONEWAY", - pos: position{line: 864, col: 1, offset: 25209}, - expr: &actionExpr{ - pos: position{line: 864, col: 10, offset: 25218}, - run: (*parser).callonONEWAY1, - expr: &seqExpr{ - pos: position{line: 864, col: 10, offset: 25218}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 864, col: 10, offset: 25218}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 864, col: 19, offset: 25227}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 864, col: 36, offset: 25244}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 864, col: 38, offset: 25246}, - name: "ONEWAYToken", - }, - }, - ¬Expr{ - pos: position{line: 864, col: 57, offset: 25265}, - expr: &ruleRefExpr{ - pos: position{line: 864, col: 58, offset: 25266}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 864, col: 73, offset: 25281}, - expr: &ruleRefExpr{ - pos: position{line: 864, col: 73, offset: 25281}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "ONEWAYToken", - pos: position{line: 870, col: 1, offset: 25425}, - expr: &actionExpr{ - pos: position{line: 870, col: 15, offset: 25439}, - run: (*parser).callonONEWAYToken1, - expr: &litMatcher{ - pos: position{line: 870, col: 15, offset: 25439}, - val: "oneway", - ignoreCase: false, - want: "\"oneway\"", - }, - }, - }, - { - name: "TYPEDEF", - pos: position{line: 874, col: 1, offset: 25487}, - expr: &actionExpr{ - pos: position{line: 874, col: 11, offset: 25497}, - run: (*parser).callonTYPEDEF1, - expr: &seqExpr{ - pos: position{line: 874, col: 11, offset: 25497}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 874, col: 11, offset: 25497}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 874, col: 20, offset: 25506}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 874, col: 37, offset: 25523}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 874, col: 39, offset: 25525}, - name: "TYPEDEFToken", - }, - }, - ¬Expr{ - pos: position{line: 874, col: 56, offset: 25542}, - expr: &ruleRefExpr{ - pos: position{line: 874, col: 57, offset: 25543}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 874, col: 72, offset: 25558}, - expr: &ruleRefExpr{ - pos: position{line: 874, col: 72, offset: 25558}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "TYPEDEFToken", - pos: position{line: 880, col: 1, offset: 25703}, - expr: &actionExpr{ - pos: position{line: 880, col: 16, offset: 25718}, - run: (*parser).callonTYPEDEFToken1, - expr: &litMatcher{ - pos: position{line: 880, col: 16, offset: 25718}, - val: "typedef", - ignoreCase: false, - want: "\"typedef\"", - }, - }, - }, - { - name: "VOID", - pos: position{line: 885, col: 1, offset: 25768}, - expr: &actionExpr{ - pos: position{line: 885, col: 15, offset: 25782}, - run: (*parser).callonVOID1, - expr: &seqExpr{ - pos: position{line: 885, col: 15, offset: 25782}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 885, col: 15, offset: 25782}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 885, col: 24, offset: 25791}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 885, col: 41, offset: 25808}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 885, col: 43, offset: 25810}, - name: "VOIDToken", - }, - }, - ¬Expr{ - pos: position{line: 885, col: 61, offset: 25828}, - expr: &ruleRefExpr{ - pos: position{line: 885, col: 62, offset: 25829}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 885, col: 77, offset: 25844}, - expr: &ruleRefExpr{ - pos: position{line: 885, col: 77, offset: 25844}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "VOIDToken", - pos: position{line: 890, col: 1, offset: 25985}, - expr: &actionExpr{ - pos: position{line: 890, col: 13, offset: 25997}, - run: (*parser).callonVOIDToken1, - expr: &litMatcher{ - pos: position{line: 890, col: 13, offset: 25997}, - val: "void", - ignoreCase: false, - want: "\"void\"", - }, - }, - }, - { - name: "THROWS", - pos: position{line: 894, col: 1, offset: 26043}, - expr: &actionExpr{ - pos: position{line: 894, col: 15, offset: 26057}, - run: (*parser).callonTHROWS1, - expr: &seqExpr{ - pos: position{line: 894, col: 15, offset: 26057}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 894, col: 15, offset: 26057}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 894, col: 24, offset: 26066}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 894, col: 41, offset: 26083}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 894, col: 43, offset: 26085}, - name: "THROWSToken", - }, - }, - ¬Expr{ - pos: position{line: 894, col: 62, offset: 26104}, - expr: &ruleRefExpr{ - pos: position{line: 894, col: 63, offset: 26105}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 894, col: 78, offset: 26120}, - expr: &ruleRefExpr{ - pos: position{line: 894, col: 78, offset: 26120}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "THROWSToken", - pos: position{line: 899, col: 1, offset: 26263}, - expr: &actionExpr{ - pos: position{line: 899, col: 15, offset: 26277}, - run: (*parser).callonTHROWSToken1, - expr: &litMatcher{ - pos: position{line: 899, col: 15, offset: 26277}, - val: "throws", - ignoreCase: false, - want: "\"throws\"", - }, - }, - }, - { - name: "EXCEPTION", - pos: position{line: 903, col: 1, offset: 26325}, - expr: &actionExpr{ - pos: position{line: 903, col: 15, offset: 26339}, - run: (*parser).callonEXCEPTION1, - expr: &seqExpr{ - pos: position{line: 903, col: 15, offset: 26339}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 903, col: 15, offset: 26339}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 903, col: 24, offset: 26348}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 903, col: 41, offset: 26365}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 903, col: 43, offset: 26367}, - name: "EXCEPTIONToken", - }, - }, - ¬Expr{ - pos: position{line: 903, col: 62, offset: 26386}, - expr: &ruleRefExpr{ - pos: position{line: 903, col: 63, offset: 26387}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 903, col: 78, offset: 26402}, - expr: &ruleRefExpr{ - pos: position{line: 903, col: 78, offset: 26402}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "EXCEPTIONToken", - pos: position{line: 908, col: 1, offset: 26548}, - expr: &actionExpr{ - pos: position{line: 908, col: 18, offset: 26565}, - run: (*parser).callonEXCEPTIONToken1, - expr: &litMatcher{ - pos: position{line: 908, col: 18, offset: 26565}, - val: "exception", - ignoreCase: false, - want: "\"exception\"", - }, - }, - }, - { - name: "EXTENDS", - pos: position{line: 913, col: 1, offset: 26617}, - expr: &actionExpr{ - pos: position{line: 913, col: 15, offset: 26631}, - run: (*parser).callonEXTENDS1, - expr: &seqExpr{ - pos: position{line: 913, col: 15, offset: 26631}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 913, col: 15, offset: 26631}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 913, col: 24, offset: 26640}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 913, col: 41, offset: 26657}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 913, col: 43, offset: 26659}, - name: "EXTENDSToken", - }, - }, - ¬Expr{ - pos: position{line: 913, col: 62, offset: 26678}, - expr: &ruleRefExpr{ - pos: position{line: 913, col: 63, offset: 26679}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 913, col: 78, offset: 26694}, - expr: &ruleRefExpr{ - pos: position{line: 913, col: 78, offset: 26694}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "EXTENDSToken", - pos: position{line: 918, col: 1, offset: 26838}, - expr: &actionExpr{ - pos: position{line: 918, col: 16, offset: 26853}, - run: (*parser).callonEXTENDSToken1, - expr: &litMatcher{ - pos: position{line: 918, col: 16, offset: 26853}, - val: "extends", - ignoreCase: false, - want: "\"extends\"", - }, - }, - }, - { - name: "SERVICE", - pos: position{line: 922, col: 1, offset: 26902}, - expr: &actionExpr{ - pos: position{line: 922, col: 15, offset: 26916}, - run: (*parser).callonSERVICE1, - expr: &seqExpr{ - pos: position{line: 922, col: 15, offset: 26916}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 922, col: 15, offset: 26916}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 922, col: 24, offset: 26925}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 922, col: 41, offset: 26942}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 922, col: 43, offset: 26944}, - name: "SERVICEToken", - }, - }, - ¬Expr{ - pos: position{line: 922, col: 62, offset: 26963}, - expr: &ruleRefExpr{ - pos: position{line: 922, col: 63, offset: 26964}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 922, col: 78, offset: 26979}, - expr: &ruleRefExpr{ - pos: position{line: 922, col: 78, offset: 26979}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "SERVICEToken", - pos: position{line: 927, col: 1, offset: 27123}, - expr: &actionExpr{ - pos: position{line: 927, col: 16, offset: 27138}, - run: (*parser).callonSERVICEToken1, - expr: &litMatcher{ - pos: position{line: 927, col: 16, offset: 27138}, - val: "service", - ignoreCase: false, - want: "\"service\"", - }, - }, - }, - { - name: "STRUCT", - pos: position{line: 931, col: 1, offset: 27187}, - expr: &actionExpr{ - pos: position{line: 931, col: 15, offset: 27201}, - run: (*parser).callonSTRUCT1, - expr: &seqExpr{ - pos: position{line: 931, col: 15, offset: 27201}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 931, col: 15, offset: 27201}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 931, col: 24, offset: 27210}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 931, col: 41, offset: 27227}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 931, col: 43, offset: 27229}, - name: "STRUCTToken", - }, - }, - ¬Expr{ - pos: position{line: 931, col: 62, offset: 27248}, - expr: &ruleRefExpr{ - pos: position{line: 931, col: 63, offset: 27249}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 931, col: 78, offset: 27264}, - expr: &ruleRefExpr{ - pos: position{line: 931, col: 78, offset: 27264}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "STRUCTToken", - pos: position{line: 936, col: 1, offset: 27407}, - expr: &actionExpr{ - pos: position{line: 936, col: 15, offset: 27421}, - run: (*parser).callonSTRUCTToken1, - expr: &litMatcher{ - pos: position{line: 936, col: 15, offset: 27421}, - val: "struct", - ignoreCase: false, - want: "\"struct\"", - }, - }, - }, - { - name: "UNION", - pos: position{line: 940, col: 1, offset: 27469}, - expr: &actionExpr{ - pos: position{line: 940, col: 15, offset: 27483}, - run: (*parser).callonUNION1, - expr: &seqExpr{ - pos: position{line: 940, col: 15, offset: 27483}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 940, col: 15, offset: 27483}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 940, col: 24, offset: 27492}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 940, col: 41, offset: 27509}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 940, col: 43, offset: 27511}, - name: "UNIONToken", - }, - }, - ¬Expr{ - pos: position{line: 940, col: 61, offset: 27529}, - expr: &ruleRefExpr{ - pos: position{line: 940, col: 62, offset: 27530}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 940, col: 77, offset: 27545}, - expr: &ruleRefExpr{ - pos: position{line: 940, col: 77, offset: 27545}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "UNIONToken", - pos: position{line: 945, col: 1, offset: 27687}, - expr: &actionExpr{ - pos: position{line: 945, col: 14, offset: 27700}, - run: (*parser).callonUNIONToken1, - expr: &litMatcher{ - pos: position{line: 945, col: 14, offset: 27700}, - val: "union", - ignoreCase: false, - want: "\"union\"", - }, - }, - }, - { - name: "ENUM", - pos: position{line: 949, col: 1, offset: 27747}, - expr: &actionExpr{ - pos: position{line: 949, col: 15, offset: 27761}, - run: (*parser).callonENUM1, - expr: &seqExpr{ - pos: position{line: 949, col: 15, offset: 27761}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 949, col: 15, offset: 27761}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 949, col: 24, offset: 27770}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 949, col: 41, offset: 27787}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 949, col: 43, offset: 27789}, - name: "ENUMToken", - }, - }, - ¬Expr{ - pos: position{line: 949, col: 62, offset: 27808}, - expr: &ruleRefExpr{ - pos: position{line: 949, col: 63, offset: 27809}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 949, col: 78, offset: 27824}, - expr: &ruleRefExpr{ - pos: position{line: 949, col: 78, offset: 27824}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "ENUMToken", - pos: position{line: 954, col: 1, offset: 27965}, - expr: &actionExpr{ - pos: position{line: 954, col: 13, offset: 27977}, - run: (*parser).callonENUMToken1, - expr: &litMatcher{ - pos: position{line: 954, col: 13, offset: 27977}, - val: "enum", - ignoreCase: false, - want: "\"enum\"", - }, - }, - }, - { - name: "INCLUDE", - pos: position{line: 958, col: 1, offset: 28023}, - expr: &actionExpr{ - pos: position{line: 958, col: 15, offset: 28037}, - run: (*parser).callonINCLUDE1, - expr: &seqExpr{ - pos: position{line: 958, col: 15, offset: 28037}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 958, col: 15, offset: 28037}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 958, col: 24, offset: 28046}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 958, col: 41, offset: 28063}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 958, col: 43, offset: 28065}, - name: "INCLUDEToken", - }, - }, - ¬Expr{ - pos: position{line: 958, col: 62, offset: 28084}, - expr: &ruleRefExpr{ - pos: position{line: 958, col: 63, offset: 28085}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 958, col: 78, offset: 28100}, - expr: &ruleRefExpr{ - pos: position{line: 958, col: 78, offset: 28100}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "INCLUDEToken", - pos: position{line: 963, col: 1, offset: 28244}, - expr: &actionExpr{ - pos: position{line: 963, col: 16, offset: 28259}, - run: (*parser).callonINCLUDEToken1, - expr: &litMatcher{ - pos: position{line: 963, col: 16, offset: 28259}, - val: "include", - ignoreCase: false, - want: "\"include\"", - }, - }, - }, - { - name: "CPPINCLUDE", - pos: position{line: 967, col: 1, offset: 28308}, - expr: &actionExpr{ - pos: position{line: 967, col: 15, offset: 28322}, - run: (*parser).callonCPPINCLUDE1, - expr: &seqExpr{ - pos: position{line: 967, col: 15, offset: 28322}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 967, col: 15, offset: 28322}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 967, col: 24, offset: 28331}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 967, col: 41, offset: 28348}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 967, col: 43, offset: 28350}, - name: "CPPINCLUDEToken", - }, - }, - ¬Expr{ - pos: position{line: 967, col: 61, offset: 28368}, - expr: &ruleRefExpr{ - pos: position{line: 967, col: 62, offset: 28369}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 967, col: 77, offset: 28384}, - expr: &ruleRefExpr{ - pos: position{line: 967, col: 77, offset: 28384}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "CPPINCLUDEToken", - pos: position{line: 972, col: 1, offset: 28531}, - expr: &actionExpr{ - pos: position{line: 972, col: 19, offset: 28549}, - run: (*parser).callonCPPINCLUDEToken1, - expr: &litMatcher{ - pos: position{line: 972, col: 19, offset: 28549}, - val: "cpp_include", - ignoreCase: false, - want: "\"cpp_include\"", - }, - }, - }, - { - name: "NAMESPACE", - pos: position{line: 976, col: 1, offset: 28602}, - expr: &actionExpr{ - pos: position{line: 976, col: 15, offset: 28616}, - run: (*parser).callonNAMESPACE1, - expr: &seqExpr{ - pos: position{line: 976, col: 15, offset: 28616}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 976, col: 15, offset: 28616}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 976, col: 24, offset: 28625}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 976, col: 41, offset: 28642}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 976, col: 43, offset: 28644}, - name: "NAMESPACEToken", - }, - }, - ¬Expr{ - pos: position{line: 976, col: 62, offset: 28663}, - expr: &ruleRefExpr{ - pos: position{line: 976, col: 63, offset: 28664}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 976, col: 78, offset: 28679}, - expr: &ruleRefExpr{ - pos: position{line: 976, col: 78, offset: 28679}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "NAMESPACEToken", - pos: position{line: 981, col: 1, offset: 28825}, - expr: &actionExpr{ - pos: position{line: 981, col: 18, offset: 28842}, - run: (*parser).callonNAMESPACEToken1, - expr: &litMatcher{ - pos: position{line: 981, col: 18, offset: 28842}, - val: "namespace", - ignoreCase: false, - want: "\"namespace\"", - }, - }, - }, - { - name: "CPPTYPE", - pos: position{line: 986, col: 1, offset: 28894}, - expr: &actionExpr{ - pos: position{line: 986, col: 15, offset: 28908}, - run: (*parser).callonCPPTYPE1, - expr: &seqExpr{ - pos: position{line: 986, col: 15, offset: 28908}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 986, col: 15, offset: 28908}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 986, col: 24, offset: 28917}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 986, col: 41, offset: 28934}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 986, col: 43, offset: 28936}, - name: "CPPTYPEToken", - }, - }, - ¬Expr{ - pos: position{line: 986, col: 61, offset: 28954}, - expr: &ruleRefExpr{ - pos: position{line: 986, col: 62, offset: 28955}, - name: "LetterOrDigit", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 986, col: 77, offset: 28970}, - expr: &ruleRefExpr{ - pos: position{line: 986, col: 77, offset: 28970}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "CPPTYPEToken", - pos: position{line: 991, col: 1, offset: 29114}, - expr: &actionExpr{ - pos: position{line: 991, col: 16, offset: 29129}, - run: (*parser).callonCPPTYPEToken1, - expr: &litMatcher{ - pos: position{line: 991, col: 16, offset: 29129}, - val: "cpp_type", - ignoreCase: false, - want: "\"cpp_type\"", - }, - }, - }, - { - name: "LBRK", - pos: position{line: 996, col: 1, offset: 29180}, - expr: &actionExpr{ - pos: position{line: 996, col: 15, offset: 29194}, - run: (*parser).callonLBRK1, - expr: &seqExpr{ - pos: position{line: 996, col: 15, offset: 29194}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 996, col: 15, offset: 29194}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 996, col: 24, offset: 29203}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 996, col: 41, offset: 29220}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 996, col: 43, offset: 29222}, - name: "LBRKToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 996, col: 57, offset: 29236}, - expr: &ruleRefExpr{ - pos: position{line: 996, col: 57, offset: 29236}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "LBRKToken", - pos: position{line: 1001, col: 1, offset: 29377}, - expr: &actionExpr{ - pos: position{line: 1001, col: 13, offset: 29389}, - run: (*parser).callonLBRKToken1, - expr: &litMatcher{ - pos: position{line: 1001, col: 13, offset: 29389}, - val: "[", - ignoreCase: false, - want: "\"[\"", - }, - }, - }, - { - name: "RBRK", - pos: position{line: 1005, col: 1, offset: 29432}, - expr: &actionExpr{ - pos: position{line: 1005, col: 15, offset: 29446}, - run: (*parser).callonRBRK1, - expr: &seqExpr{ - pos: position{line: 1005, col: 15, offset: 29446}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 1005, col: 15, offset: 29446}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 1005, col: 24, offset: 29455}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 1005, col: 41, offset: 29472}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 1005, col: 43, offset: 29474}, - name: "RBRKToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 1005, col: 57, offset: 29488}, - expr: &ruleRefExpr{ - pos: position{line: 1005, col: 57, offset: 29488}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "RBRKToken", - pos: position{line: 1010, col: 1, offset: 29629}, - expr: &actionExpr{ - pos: position{line: 1010, col: 13, offset: 29641}, - run: (*parser).callonRBRKToken1, - expr: &litMatcher{ - pos: position{line: 1010, col: 13, offset: 29641}, - val: "]", - ignoreCase: false, - want: "\"]\"", - }, - }, - }, - { - name: "LCUR", - pos: position{line: 1014, col: 1, offset: 29684}, - expr: &actionExpr{ - pos: position{line: 1014, col: 14, offset: 29697}, - run: (*parser).callonLCUR1, - expr: &seqExpr{ - pos: position{line: 1014, col: 14, offset: 29697}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 1014, col: 14, offset: 29697}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 1014, col: 23, offset: 29706}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 1014, col: 40, offset: 29723}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 1014, col: 42, offset: 29725}, - name: "LCURToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 1014, col: 56, offset: 29739}, - expr: &ruleRefExpr{ - pos: position{line: 1014, col: 56, offset: 29739}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "RCUR", - pos: position{line: 1019, col: 1, offset: 29880}, - expr: &actionExpr{ - pos: position{line: 1019, col: 8, offset: 29887}, - run: (*parser).callonRCUR1, - expr: &seqExpr{ - pos: position{line: 1019, col: 8, offset: 29887}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 1019, col: 8, offset: 29887}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 1019, col: 17, offset: 29896}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 1019, col: 34, offset: 29913}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 1019, col: 36, offset: 29915}, - name: "RCURToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 1019, col: 50, offset: 29929}, - expr: &ruleRefExpr{ - pos: position{line: 1019, col: 50, offset: 29929}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "LCURToken", - pos: position{line: 1024, col: 1, offset: 30070}, - expr: &actionExpr{ - pos: position{line: 1024, col: 13, offset: 30082}, - run: (*parser).callonLCURToken1, - expr: &litMatcher{ - pos: position{line: 1024, col: 13, offset: 30082}, - val: "{", - ignoreCase: false, - want: "\"{\"", - }, - }, - }, - { - name: "RCURToken", - pos: position{line: 1027, col: 1, offset: 30124}, - expr: &choiceExpr{ - pos: position{line: 1027, col: 13, offset: 30136}, - alternatives: []any{ - &actionExpr{ - pos: position{line: 1027, col: 13, offset: 30136}, - run: (*parser).callonRCURToken2, - expr: &litMatcher{ - pos: position{line: 1027, col: 13, offset: 30136}, - val: "}", - ignoreCase: false, - want: "\"}\"", - }, - }, - &throwExpr{ - pos: position{line: 1029, col: 5, offset: 30180}, - label: "errRCUR", - }, - }, - }, - }, - { - name: "EQUAL", - pos: position{line: 1032, col: 1, offset: 30193}, - expr: &actionExpr{ - pos: position{line: 1032, col: 9, offset: 30201}, - run: (*parser).callonEQUAL1, - expr: &seqExpr{ - pos: position{line: 1032, col: 9, offset: 30201}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 1032, col: 9, offset: 30201}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 1032, col: 18, offset: 30210}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 1032, col: 35, offset: 30227}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 1032, col: 37, offset: 30229}, - name: "EQUALToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 1032, col: 52, offset: 30244}, - expr: &ruleRefExpr{ - pos: position{line: 1032, col: 52, offset: 30244}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "EQUALToken", - pos: position{line: 1037, col: 1, offset: 30386}, - expr: &actionExpr{ - pos: position{line: 1037, col: 14, offset: 30399}, - run: (*parser).callonEQUALToken1, - expr: &litMatcher{ - pos: position{line: 1037, col: 14, offset: 30399}, - val: "=", - ignoreCase: false, - want: "\"=\"", - }, - }, - }, - { - name: "LPOINT", - pos: position{line: 1041, col: 1, offset: 30442}, - expr: &actionExpr{ - pos: position{line: 1041, col: 15, offset: 30456}, - run: (*parser).callonLPOINT1, - expr: &seqExpr{ - pos: position{line: 1041, col: 15, offset: 30456}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 1041, col: 15, offset: 30456}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 1041, col: 24, offset: 30465}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 1041, col: 41, offset: 30482}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 1041, col: 43, offset: 30484}, - name: "LPOINTToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 1041, col: 59, offset: 30500}, - expr: &ruleRefExpr{ - pos: position{line: 1041, col: 59, offset: 30500}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "LPOINTToken", - pos: position{line: 1046, col: 1, offset: 30643}, - expr: &actionExpr{ - pos: position{line: 1046, col: 15, offset: 30657}, - run: (*parser).callonLPOINTToken1, - expr: &litMatcher{ - pos: position{line: 1046, col: 15, offset: 30657}, - val: "<", - ignoreCase: false, - want: "\"<\"", - }, - }, - }, - { - name: "RPOINT", - pos: position{line: 1050, col: 1, offset: 30700}, - expr: &actionExpr{ - pos: position{line: 1050, col: 15, offset: 30714}, - run: (*parser).callonRPOINT1, - expr: &seqExpr{ - pos: position{line: 1050, col: 15, offset: 30714}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 1050, col: 15, offset: 30714}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 1050, col: 24, offset: 30723}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 1050, col: 41, offset: 30740}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 1050, col: 43, offset: 30742}, - name: "RPOINTToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 1050, col: 58, offset: 30757}, - expr: &ruleRefExpr{ - pos: position{line: 1050, col: 58, offset: 30757}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "RPOINTToken", - pos: position{line: 1055, col: 1, offset: 30900}, - expr: &actionExpr{ - pos: position{line: 1055, col: 15, offset: 30914}, - run: (*parser).callonRPOINTToken1, - expr: &litMatcher{ - pos: position{line: 1055, col: 15, offset: 30914}, - val: ">", - ignoreCase: false, - want: "\">\"", - }, - }, - }, - { - name: "COMMA", - pos: position{line: 1059, col: 1, offset: 30957}, - expr: &actionExpr{ - pos: position{line: 1059, col: 15, offset: 30971}, - run: (*parser).callonCOMMA1, - expr: &seqExpr{ - pos: position{line: 1059, col: 15, offset: 30971}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 1059, col: 15, offset: 30971}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 1059, col: 24, offset: 30980}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 1059, col: 41, offset: 30997}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 1059, col: 43, offset: 30999}, - name: "COMMAToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 1059, col: 58, offset: 31014}, - expr: &ruleRefExpr{ - pos: position{line: 1059, col: 58, offset: 31014}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "COMMAToken", - pos: position{line: 1064, col: 1, offset: 31156}, - expr: &actionExpr{ - pos: position{line: 1064, col: 14, offset: 31169}, - run: (*parser).callonCOMMAToken1, - expr: &litMatcher{ - pos: position{line: 1064, col: 14, offset: 31169}, - val: ",", - ignoreCase: false, - want: "\",\"", - }, - }, - }, - { - name: "LPAR", - pos: position{line: 1068, col: 1, offset: 31212}, - expr: &actionExpr{ - pos: position{line: 1068, col: 15, offset: 31226}, - run: (*parser).callonLPAR1, - expr: &seqExpr{ - pos: position{line: 1068, col: 15, offset: 31226}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 1068, col: 15, offset: 31226}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 1068, col: 24, offset: 31235}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 1068, col: 41, offset: 31252}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 1068, col: 43, offset: 31254}, - name: "LPARToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 1068, col: 57, offset: 31268}, - expr: &ruleRefExpr{ - pos: position{line: 1068, col: 57, offset: 31268}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "LPARToken", - pos: position{line: 1073, col: 1, offset: 31409}, - expr: &actionExpr{ - pos: position{line: 1073, col: 13, offset: 31421}, - run: (*parser).callonLPARToken1, - expr: &litMatcher{ - pos: position{line: 1073, col: 13, offset: 31421}, - val: "(", - ignoreCase: false, - want: "\"(\"", - }, - }, - }, - { - name: "RPAR", - pos: position{line: 1077, col: 1, offset: 31464}, - expr: &actionExpr{ - pos: position{line: 1077, col: 15, offset: 31478}, - run: (*parser).callonRPAR1, - expr: &seqExpr{ - pos: position{line: 1077, col: 15, offset: 31478}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 1077, col: 15, offset: 31478}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 1077, col: 24, offset: 31487}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 1077, col: 41, offset: 31504}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 1077, col: 43, offset: 31506}, - name: "RPARToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 1077, col: 57, offset: 31520}, - expr: &ruleRefExpr{ - pos: position{line: 1077, col: 57, offset: 31520}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "RPARToken", - pos: position{line: 1082, col: 1, offset: 31661}, - expr: &actionExpr{ - pos: position{line: 1082, col: 13, offset: 31673}, - run: (*parser).callonRPARToken1, - expr: &litMatcher{ - pos: position{line: 1082, col: 13, offset: 31673}, - val: ")", - ignoreCase: false, - want: "\")\"", - }, - }, - }, - { - name: "COLON", - pos: position{line: 1086, col: 1, offset: 31716}, - expr: &actionExpr{ - pos: position{line: 1086, col: 15, offset: 31730}, - run: (*parser).callonCOLON1, - expr: &seqExpr{ - pos: position{line: 1086, col: 15, offset: 31730}, - exprs: []any{ - &labeledExpr{ - pos: position{line: 1086, col: 15, offset: 31730}, - label: "comments", - expr: &ruleRefExpr{ - pos: position{line: 1086, col: 24, offset: 31739}, - name: "ReservedComments", - }, - }, - &labeledExpr{ - pos: position{line: 1086, col: 41, offset: 31756}, - label: "t", - expr: &ruleRefExpr{ - pos: position{line: 1086, col: 43, offset: 31758}, - name: "COLONToken", - }, - }, - &zeroOrMoreExpr{ - pos: position{line: 1086, col: 58, offset: 31773}, - expr: &ruleRefExpr{ - pos: position{line: 1086, col: 58, offset: 31773}, - name: "Indent", - }, - }, - }, - }, - }, - }, - { - name: "COLONToken", - pos: position{line: 1091, col: 1, offset: 31915}, - expr: &actionExpr{ - pos: position{line: 1091, col: 14, offset: 31928}, - run: (*parser).callonCOLONToken1, - expr: &litMatcher{ - pos: position{line: 1091, col: 14, offset: 31928}, - val: ":", - ignoreCase: false, - want: "\":\"", - }, - }, - }, - { - name: "DefinitionStart", - pos: position{line: 1095, col: 1, offset: 31971}, - expr: &choiceExpr{ - pos: position{line: 1095, col: 19, offset: 31989}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 1095, col: 19, offset: 31989}, - name: "STRUCT", - }, - &ruleRefExpr{ - pos: position{line: 1095, col: 28, offset: 31998}, - name: "UNION", - }, - &ruleRefExpr{ - pos: position{line: 1095, col: 36, offset: 32006}, - name: "EXCEPTION", - }, - &ruleRefExpr{ - pos: position{line: 1095, col: 48, offset: 32018}, - name: "ENUM", - }, - &ruleRefExpr{ - pos: position{line: 1095, col: 55, offset: 32025}, - name: "SERVICE", - }, - &ruleRefExpr{ - pos: position{line: 1095, col: 65, offset: 32035}, - name: "CONST", - }, - &ruleRefExpr{ - pos: position{line: 1095, col: 73, offset: 32043}, - name: "TYPEDEF", - }, - }, - }, - }, - { - name: "ErrFieldIndex", - pos: position{line: 1097, col: 1, offset: 32052}, - expr: &actionExpr{ - pos: position{line: 1097, col: 17, offset: 32068}, - run: (*parser).callonErrFieldIndex1, - expr: &seqExpr{ - pos: position{line: 1097, col: 17, offset: 32068}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1097, col: 17, offset: 32068}, - run: (*parser).callonErrFieldIndex3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1099, col: 3, offset: 32104}, - expr: &seqExpr{ - pos: position{line: 1099, col: 4, offset: 32105}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1099, col: 4, offset: 32105}, - expr: &charClassMatcher{ - pos: position{line: 1099, col: 6, offset: 32107}, - val: "[:\\r\\n]", - chars: []rune{':', '\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1099, col: 15, offset: 32116, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrStructField", - pos: position{line: 1105, col: 1, offset: 32227}, - expr: &actionExpr{ - pos: position{line: 1105, col: 18, offset: 32244}, - run: (*parser).callonErrStructField1, - expr: &seqExpr{ - pos: position{line: 1105, col: 18, offset: 32244}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1105, col: 18, offset: 32244}, - run: (*parser).callonErrStructField3, - }, - &oneOrMoreExpr{ - pos: position{line: 1107, col: 3, offset: 32281}, - expr: &seqExpr{ - pos: position{line: 1107, col: 5, offset: 32283}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1107, col: 5, offset: 32283}, - expr: &choiceExpr{ - pos: position{line: 1107, col: 7, offset: 32285}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 1107, col: 7, offset: 32285}, - name: "Field", - }, - &seqExpr{ - pos: position{line: 1107, col: 16, offset: 32294}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 1107, col: 16, offset: 32294}, - name: "ReservedComments", - }, - &litMatcher{ - pos: position{line: 1107, col: 33, offset: 32311}, - val: "}", - ignoreCase: false, - want: "\"}\"", - }, - &zeroOrMoreExpr{ - pos: position{line: 1107, col: 37, offset: 32315}, - expr: &ruleRefExpr{ - pos: position{line: 1107, col: 37, offset: 32315}, - name: "Indent", - }, - }, - }, - }, - &ruleRefExpr{ - pos: position{line: 1107, col: 48, offset: 32326}, - name: "DefinitionStart", - }, - }, - }, - }, - &anyMatcher{ - line: 1107, col: 66, offset: 32344, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrStructIdentifier", - pos: position{line: 1111, col: 1, offset: 32406}, - expr: &actionExpr{ - pos: position{line: 1111, col: 23, offset: 32428}, - run: (*parser).callonErrStructIdentifier1, - expr: &seqExpr{ - pos: position{line: 1111, col: 23, offset: 32428}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1111, col: 23, offset: 32428}, - run: (*parser).callonErrStructIdentifier3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1113, col: 3, offset: 32470}, - expr: &seqExpr{ - pos: position{line: 1113, col: 5, offset: 32472}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1113, col: 5, offset: 32472}, - expr: &litMatcher{ - pos: position{line: 1113, col: 6, offset: 32473}, - val: "{", - ignoreCase: false, - want: "\"{\"", - }, - }, - &anyMatcher{ - line: 1113, col: 10, offset: 32477, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrStructRCUR", - pos: position{line: 1119, col: 1, offset: 32662}, - expr: &actionExpr{ - pos: position{line: 1119, col: 17, offset: 32678}, - run: (*parser).callonErrStructRCUR1, - expr: &seqExpr{ - pos: position{line: 1119, col: 17, offset: 32678}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1119, col: 17, offset: 32678}, - run: (*parser).callonErrStructRCUR3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1121, col: 3, offset: 32719}, - expr: &seqExpr{ - pos: position{line: 1121, col: 5, offset: 32721}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1121, col: 5, offset: 32721}, - expr: &ruleRefExpr{ - pos: position{line: 1121, col: 6, offset: 32722}, - name: "DefinitionStart", - }, - }, - &anyMatcher{ - line: 1121, col: 22, offset: 32738, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrStructLCUR", - pos: position{line: 1125, col: 1, offset: 32919}, - expr: &actionExpr{ - pos: position{line: 1125, col: 17, offset: 32935}, - run: (*parser).callonErrStructLCUR1, - expr: &seqExpr{ - pos: position{line: 1125, col: 17, offset: 32935}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1125, col: 17, offset: 32935}, - run: (*parser).callonErrStructLCUR3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1127, col: 3, offset: 32976}, - expr: &seqExpr{ - pos: position{line: 1127, col: 5, offset: 32978}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1127, col: 5, offset: 32978}, - expr: &litMatcher{ - pos: position{line: 1127, col: 6, offset: 32979}, - val: "}", - ignoreCase: false, - want: "\"}\"", - }, - }, - &anyMatcher{ - line: 1127, col: 10, offset: 32983, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrUnionIdentifier", - pos: position{line: 1133, col: 1, offset: 33112}, - expr: &actionExpr{ - pos: position{line: 1133, col: 22, offset: 33133}, - run: (*parser).callonErrUnionIdentifier1, - expr: &seqExpr{ - pos: position{line: 1133, col: 22, offset: 33133}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1133, col: 22, offset: 33133}, - run: (*parser).callonErrUnionIdentifier3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1135, col: 3, offset: 33174}, - expr: &seqExpr{ - pos: position{line: 1135, col: 5, offset: 33176}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1135, col: 5, offset: 33176}, - expr: &litMatcher{ - pos: position{line: 1135, col: 6, offset: 33177}, - val: "{", - ignoreCase: false, - want: "\"{\"", - }, - }, - &anyMatcher{ - line: 1135, col: 10, offset: 33181, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrUnionRCUR", - pos: position{line: 1141, col: 1, offset: 33326}, - expr: &actionExpr{ - pos: position{line: 1141, col: 16, offset: 33341}, - run: (*parser).callonErrUnionRCUR1, - expr: &seqExpr{ - pos: position{line: 1141, col: 16, offset: 33341}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1141, col: 16, offset: 33341}, - run: (*parser).callonErrUnionRCUR3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1143, col: 3, offset: 33381}, - expr: &seqExpr{ - pos: position{line: 1143, col: 5, offset: 33383}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1143, col: 5, offset: 33383}, - expr: &ruleRefExpr{ - pos: position{line: 1143, col: 6, offset: 33384}, - name: "DefinitionStart", - }, - }, - &anyMatcher{ - line: 1143, col: 22, offset: 33400, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrUnionLCUR", - pos: position{line: 1147, col: 1, offset: 33446}, - expr: &actionExpr{ - pos: position{line: 1147, col: 16, offset: 33461}, - run: (*parser).callonErrUnionLCUR1, - expr: &seqExpr{ - pos: position{line: 1147, col: 16, offset: 33461}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1147, col: 16, offset: 33461}, - run: (*parser).callonErrUnionLCUR3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1149, col: 3, offset: 33501}, - expr: &seqExpr{ - pos: position{line: 1149, col: 5, offset: 33503}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1149, col: 5, offset: 33503}, - expr: &litMatcher{ - pos: position{line: 1149, col: 6, offset: 33504}, - val: "}", - ignoreCase: false, - want: "\"}\"", - }, - }, - &anyMatcher{ - line: 1149, col: 10, offset: 33508, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrUnionField", - pos: position{line: 1153, col: 1, offset: 33607}, - expr: &actionExpr{ - pos: position{line: 1153, col: 17, offset: 33623}, - run: (*parser).callonErrUnionField1, - expr: &seqExpr{ - pos: position{line: 1153, col: 17, offset: 33623}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1153, col: 17, offset: 33623}, - run: (*parser).callonErrUnionField3, - }, - &oneOrMoreExpr{ - pos: position{line: 1155, col: 3, offset: 33659}, - expr: &seqExpr{ - pos: position{line: 1155, col: 5, offset: 33661}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1155, col: 5, offset: 33661}, - expr: &choiceExpr{ - pos: position{line: 1155, col: 7, offset: 33663}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 1155, col: 7, offset: 33663}, - name: "Field", - }, - &seqExpr{ - pos: position{line: 1155, col: 16, offset: 33672}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 1155, col: 16, offset: 33672}, - name: "ReservedComments", - }, - &litMatcher{ - pos: position{line: 1155, col: 33, offset: 33689}, - val: "}", - ignoreCase: false, - want: "\"}\"", - }, - &zeroOrMoreExpr{ - pos: position{line: 1155, col: 37, offset: 33693}, - expr: &ruleRefExpr{ - pos: position{line: 1155, col: 37, offset: 33693}, - name: "Indent", - }, - }, - }, - }, - &ruleRefExpr{ - pos: position{line: 1155, col: 48, offset: 33704}, - name: "DefinitionStart", - }, - }, - }, - }, - &anyMatcher{ - line: 1155, col: 66, offset: 33722, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrExceptionIdentifier", - pos: position{line: 1161, col: 1, offset: 33798}, - expr: &actionExpr{ - pos: position{line: 1161, col: 26, offset: 33823}, - run: (*parser).callonErrExceptionIdentifier1, - expr: &seqExpr{ - pos: position{line: 1161, col: 26, offset: 33823}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1161, col: 26, offset: 33823}, - run: (*parser).callonErrExceptionIdentifier3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1163, col: 3, offset: 33868}, - expr: &seqExpr{ - pos: position{line: 1163, col: 5, offset: 33870}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1163, col: 5, offset: 33870}, - expr: &litMatcher{ - pos: position{line: 1163, col: 6, offset: 33871}, - val: "{", - ignoreCase: false, - want: "\"{\"", - }, - }, - &anyMatcher{ - line: 1163, col: 10, offset: 33875, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrExceptionRCUR", - pos: position{line: 1169, col: 1, offset: 34020}, - expr: &actionExpr{ - pos: position{line: 1169, col: 20, offset: 34039}, - run: (*parser).callonErrExceptionRCUR1, - expr: &seqExpr{ - pos: position{line: 1169, col: 20, offset: 34039}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1169, col: 20, offset: 34039}, - run: (*parser).callonErrExceptionRCUR3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1171, col: 3, offset: 34083}, - expr: &seqExpr{ - pos: position{line: 1171, col: 5, offset: 34085}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1171, col: 5, offset: 34085}, - expr: &ruleRefExpr{ - pos: position{line: 1171, col: 6, offset: 34086}, - name: "DefinitionStart", - }, - }, - &anyMatcher{ - line: 1171, col: 22, offset: 34102, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrExceptionLCUR", - pos: position{line: 1175, col: 1, offset: 34148}, - expr: &actionExpr{ - pos: position{line: 1175, col: 20, offset: 34167}, - run: (*parser).callonErrExceptionLCUR1, - expr: &seqExpr{ - pos: position{line: 1175, col: 20, offset: 34167}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1175, col: 20, offset: 34167}, - run: (*parser).callonErrExceptionLCUR3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1177, col: 3, offset: 34211}, - expr: &seqExpr{ - pos: position{line: 1177, col: 5, offset: 34213}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1177, col: 5, offset: 34213}, - expr: &litMatcher{ - pos: position{line: 1177, col: 6, offset: 34214}, - val: "}", - ignoreCase: false, - want: "\"}\"", - }, - }, - &anyMatcher{ - line: 1177, col: 10, offset: 34218, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrExceptionField", - pos: position{line: 1181, col: 1, offset: 34337}, - expr: &actionExpr{ - pos: position{line: 1181, col: 21, offset: 34357}, - run: (*parser).callonErrExceptionField1, - expr: &seqExpr{ - pos: position{line: 1181, col: 21, offset: 34357}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1181, col: 21, offset: 34357}, - run: (*parser).callonErrExceptionField3, - }, - &oneOrMoreExpr{ - pos: position{line: 1183, col: 3, offset: 34397}, - expr: &seqExpr{ - pos: position{line: 1183, col: 5, offset: 34399}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1183, col: 5, offset: 34399}, - expr: &choiceExpr{ - pos: position{line: 1183, col: 7, offset: 34401}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 1183, col: 7, offset: 34401}, - name: "Field", - }, - &seqExpr{ - pos: position{line: 1183, col: 16, offset: 34410}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 1183, col: 16, offset: 34410}, - name: "ReservedComments", - }, - &litMatcher{ - pos: position{line: 1183, col: 33, offset: 34427}, - val: "}", - ignoreCase: false, - want: "\"}\"", - }, - &zeroOrMoreExpr{ - pos: position{line: 1183, col: 37, offset: 34431}, - expr: &ruleRefExpr{ - pos: position{line: 1183, col: 37, offset: 34431}, - name: "Indent", - }, - }, - }, - }, - &ruleRefExpr{ - pos: position{line: 1183, col: 48, offset: 34442}, - name: "DefinitionStart", - }, - }, - }, - }, - &anyMatcher{ - line: 1183, col: 66, offset: 34460, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrEnumIdentifier", - pos: position{line: 1189, col: 1, offset: 34531}, - expr: &actionExpr{ - pos: position{line: 1189, col: 21, offset: 34551}, - run: (*parser).callonErrEnumIdentifier1, - expr: &seqExpr{ - pos: position{line: 1189, col: 21, offset: 34551}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1189, col: 21, offset: 34551}, - run: (*parser).callonErrEnumIdentifier3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1191, col: 3, offset: 34591}, - expr: &seqExpr{ - pos: position{line: 1191, col: 5, offset: 34593}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1191, col: 5, offset: 34593}, - expr: &litMatcher{ - pos: position{line: 1191, col: 6, offset: 34594}, - val: "{", - ignoreCase: false, - want: "\"{\"", - }, - }, - &anyMatcher{ - line: 1191, col: 10, offset: 34598, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrEnumRCUR", - pos: position{line: 1197, col: 1, offset: 34748}, - expr: &actionExpr{ - pos: position{line: 1197, col: 15, offset: 34762}, - run: (*parser).callonErrEnumRCUR1, - expr: &seqExpr{ - pos: position{line: 1197, col: 15, offset: 34762}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1197, col: 15, offset: 34762}, - run: (*parser).callonErrEnumRCUR3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1199, col: 3, offset: 34801}, - expr: &seqExpr{ - pos: position{line: 1199, col: 5, offset: 34803}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1199, col: 5, offset: 34803}, - expr: &ruleRefExpr{ - pos: position{line: 1199, col: 6, offset: 34804}, - name: "DefinitionStart", - }, - }, - &anyMatcher{ - line: 1199, col: 22, offset: 34820, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrEnumLCUR", - pos: position{line: 1203, col: 1, offset: 34866}, - expr: &actionExpr{ - pos: position{line: 1203, col: 15, offset: 34880}, - run: (*parser).callonErrEnumLCUR1, - expr: &seqExpr{ - pos: position{line: 1203, col: 15, offset: 34880}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1203, col: 15, offset: 34880}, - run: (*parser).callonErrEnumLCUR3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1205, col: 3, offset: 34919}, - expr: &seqExpr{ - pos: position{line: 1205, col: 5, offset: 34921}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1205, col: 5, offset: 34921}, - expr: &litMatcher{ - pos: position{line: 1205, col: 6, offset: 34922}, - val: "}", - ignoreCase: false, - want: "\"}\"", - }, - }, - &anyMatcher{ - line: 1205, col: 10, offset: 34926, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrEnumValue", - pos: position{line: 1209, col: 1, offset: 35045}, - expr: &actionExpr{ - pos: position{line: 1209, col: 16, offset: 35060}, - run: (*parser).callonErrEnumValue1, - expr: &seqExpr{ - pos: position{line: 1209, col: 16, offset: 35060}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1209, col: 16, offset: 35060}, - run: (*parser).callonErrEnumValue3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1211, col: 3, offset: 35095}, - expr: &seqExpr{ - pos: position{line: 1211, col: 5, offset: 35097}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1211, col: 5, offset: 35097}, - expr: &choiceExpr{ - pos: position{line: 1211, col: 7, offset: 35099}, - alternatives: []any{ - &ruleRefExpr{ - pos: position{line: 1211, col: 7, offset: 35099}, - name: "Field", - }, - &seqExpr{ - pos: position{line: 1211, col: 16, offset: 35108}, - exprs: []any{ - &ruleRefExpr{ - pos: position{line: 1211, col: 16, offset: 35108}, - name: "ReservedComments", - }, - &litMatcher{ - pos: position{line: 1211, col: 33, offset: 35125}, - val: "}", - ignoreCase: false, - want: "\"}\"", - }, - &zeroOrMoreExpr{ - pos: position{line: 1211, col: 37, offset: 35129}, - expr: &ruleRefExpr{ - pos: position{line: 1211, col: 37, offset: 35129}, - name: "Indent", - }, - }, - }, - }, - &ruleRefExpr{ - pos: position{line: 1211, col: 48, offset: 35140}, - name: "DefinitionStart", - }, - }, - }, - }, - &anyMatcher{ - line: 1211, col: 66, offset: 35158, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrEnumValueIntConstant", - pos: position{line: 1215, col: 1, offset: 35224}, - expr: &actionExpr{ - pos: position{line: 1215, col: 27, offset: 35250}, - run: (*parser).callonErrEnumValueIntConstant1, - expr: &seqExpr{ - pos: position{line: 1215, col: 27, offset: 35250}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1215, col: 27, offset: 35250}, - run: (*parser).callonErrEnumValueIntConstant3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1217, col: 3, offset: 35296}, - expr: &seqExpr{ - pos: position{line: 1217, col: 5, offset: 35298}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1217, col: 5, offset: 35298}, - expr: &ruleRefExpr{ - pos: position{line: 1217, col: 7, offset: 35300}, - name: "CarriageReturnLineFeed", - }, - }, - &anyMatcher{ - line: 1217, col: 32, offset: 35325, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrTypedefIdentifier", - pos: position{line: 1223, col: 1, offset: 35408}, - expr: &actionExpr{ - pos: position{line: 1223, col: 24, offset: 35431}, - run: (*parser).callonErrTypedefIdentifier1, - expr: &seqExpr{ - pos: position{line: 1223, col: 24, offset: 35431}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1223, col: 24, offset: 35431}, - run: (*parser).callonErrTypedefIdentifier3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1225, col: 3, offset: 35474}, - expr: &seqExpr{ - pos: position{line: 1225, col: 5, offset: 35476}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1225, col: 5, offset: 35476}, - expr: &ruleRefExpr{ - pos: position{line: 1225, col: 7, offset: 35478}, - name: "CarriageReturnLineFeed", - }, - }, - &anyMatcher{ - line: 1225, col: 31, offset: 35502, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrConstIdentifier", - pos: position{line: 1232, col: 1, offset: 35586}, - expr: &actionExpr{ - pos: position{line: 1232, col: 22, offset: 35607}, - run: (*parser).callonErrConstIdentifier1, - expr: &seqExpr{ - pos: position{line: 1232, col: 22, offset: 35607}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1232, col: 22, offset: 35607}, - run: (*parser).callonErrConstIdentifier3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1234, col: 3, offset: 35648}, - expr: &seqExpr{ - pos: position{line: 1234, col: 4, offset: 35649}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1234, col: 4, offset: 35649}, - expr: &ruleRefExpr{ - pos: position{line: 1234, col: 5, offset: 35650}, - name: "EQUAL", - }, - }, - &anyMatcher{ - line: 1234, col: 11, offset: 35656, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrConstMissingValue", - pos: position{line: 1240, col: 1, offset: 35731}, - expr: &actionExpr{ - pos: position{line: 1240, col: 24, offset: 35754}, - run: (*parser).callonErrConstMissingValue1, - expr: &seqExpr{ - pos: position{line: 1240, col: 24, offset: 35754}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1240, col: 24, offset: 35754}, - run: (*parser).callonErrConstMissingValue3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1242, col: 3, offset: 35797}, - expr: &seqExpr{ - pos: position{line: 1242, col: 4, offset: 35798}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1242, col: 4, offset: 35798}, - expr: &charClassMatcher{ - pos: position{line: 1242, col: 5, offset: 35799}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1242, col: 12, offset: 35806, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrConstConstValue", - pos: position{line: 1246, col: 1, offset: 35872}, - expr: &actionExpr{ - pos: position{line: 1246, col: 22, offset: 35893}, - run: (*parser).callonErrConstConstValue1, - expr: &seqExpr{ - pos: position{line: 1246, col: 22, offset: 35893}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1246, col: 22, offset: 35893}, - run: (*parser).callonErrConstConstValue3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1248, col: 3, offset: 35934}, - expr: &seqExpr{ - pos: position{line: 1248, col: 4, offset: 35935}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1248, col: 4, offset: 35935}, - expr: &charClassMatcher{ - pos: position{line: 1248, col: 5, offset: 35936}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1248, col: 12, offset: 35943, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrServiceIdentifier", - pos: position{line: 1254, col: 1, offset: 36021}, - expr: &actionExpr{ - pos: position{line: 1254, col: 24, offset: 36044}, - run: (*parser).callonErrServiceIdentifier1, - expr: &seqExpr{ - pos: position{line: 1254, col: 24, offset: 36044}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1254, col: 24, offset: 36044}, - run: (*parser).callonErrServiceIdentifier3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1256, col: 3, offset: 36087}, - expr: &seqExpr{ - pos: position{line: 1256, col: 5, offset: 36089}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1256, col: 5, offset: 36089}, - expr: &litMatcher{ - pos: position{line: 1256, col: 6, offset: 36090}, - val: "{", - ignoreCase: false, - want: "\"{\"", - }, - }, - &anyMatcher{ - line: 1256, col: 10, offset: 36094, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrServiceRCUR", - pos: position{line: 1262, col: 1, offset: 36239}, - expr: &actionExpr{ - pos: position{line: 1262, col: 18, offset: 36256}, - run: (*parser).callonErrServiceRCUR1, - expr: &seqExpr{ - pos: position{line: 1262, col: 18, offset: 36256}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1262, col: 18, offset: 36256}, - run: (*parser).callonErrServiceRCUR3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1264, col: 3, offset: 36298}, - expr: &seqExpr{ - pos: position{line: 1264, col: 5, offset: 36300}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1264, col: 5, offset: 36300}, - expr: &ruleRefExpr{ - pos: position{line: 1264, col: 6, offset: 36301}, - name: "DefinitionStart", - }, - }, - &anyMatcher{ - line: 1264, col: 22, offset: 36317, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrServiceFunction", - pos: position{line: 1268, col: 1, offset: 36363}, - expr: &actionExpr{ - pos: position{line: 1268, col: 23, offset: 36385}, - run: (*parser).callonErrServiceFunction1, - expr: &seqExpr{ - pos: position{line: 1268, col: 23, offset: 36385}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1268, col: 23, offset: 36385}, - run: (*parser).callonErrServiceFunction3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1270, col: 3, offset: 36426}, - expr: &seqExpr{ - pos: position{line: 1270, col: 5, offset: 36428}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1270, col: 5, offset: 36428}, - expr: &charClassMatcher{ - pos: position{line: 1270, col: 6, offset: 36429}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1270, col: 13, offset: 36436, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrFunctionIdentifier", - pos: position{line: 1275, col: 1, offset: 36512}, - expr: &actionExpr{ - pos: position{line: 1275, col: 25, offset: 36536}, - run: (*parser).callonErrFunctionIdentifier1, - expr: &seqExpr{ - pos: position{line: 1275, col: 25, offset: 36536}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1275, col: 25, offset: 36536}, - run: (*parser).callonErrFunctionIdentifier3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1277, col: 3, offset: 36580}, - expr: &seqExpr{ - pos: position{line: 1277, col: 5, offset: 36582}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1277, col: 5, offset: 36582}, - expr: &litMatcher{ - pos: position{line: 1277, col: 6, offset: 36583}, - val: "(", - ignoreCase: false, - want: "\"(\"", - }, - }, - &anyMatcher{ - line: 1277, col: 10, offset: 36587, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrFunctionArgument", - pos: position{line: 1283, col: 1, offset: 36765}, - expr: &actionExpr{ - pos: position{line: 1283, col: 23, offset: 36787}, - run: (*parser).callonErrFunctionArgument1, - expr: &seqExpr{ - pos: position{line: 1283, col: 23, offset: 36787}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1283, col: 23, offset: 36787}, - run: (*parser).callonErrFunctionArgument3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1285, col: 3, offset: 36829}, - expr: &seqExpr{ - pos: position{line: 1285, col: 5, offset: 36831}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1285, col: 5, offset: 36831}, - expr: &charClassMatcher{ - pos: position{line: 1285, col: 6, offset: 36832}, - val: "[,;)\\r\\n]", - chars: []rune{',', ';', ')', '\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1285, col: 16, offset: 36842, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrLiteral1MissingRight", - pos: position{line: 1292, col: 1, offset: 36922}, - expr: &actionExpr{ - pos: position{line: 1292, col: 27, offset: 36948}, - run: (*parser).callonErrLiteral1MissingRight1, - expr: &seqExpr{ - pos: position{line: 1292, col: 27, offset: 36948}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1292, col: 27, offset: 36948}, - run: (*parser).callonErrLiteral1MissingRight3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1294, col: 3, offset: 36994}, - expr: &seqExpr{ - pos: position{line: 1294, col: 5, offset: 36996}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1294, col: 5, offset: 36996}, - expr: &charClassMatcher{ - pos: position{line: 1294, col: 6, offset: 36997}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1294, col: 13, offset: 37004, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrLiteral1", - pos: position{line: 1298, col: 1, offset: 37107}, - expr: &actionExpr{ - pos: position{line: 1298, col: 15, offset: 37121}, - run: (*parser).callonErrLiteral11, - expr: &seqExpr{ - pos: position{line: 1298, col: 15, offset: 37121}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1298, col: 15, offset: 37121}, - run: (*parser).callonErrLiteral13, - }, - &zeroOrMoreExpr{ - pos: position{line: 1300, col: 3, offset: 37155}, - expr: &seqExpr{ - pos: position{line: 1300, col: 5, offset: 37157}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1300, col: 5, offset: 37157}, - expr: &charClassMatcher{ - pos: position{line: 1300, col: 6, offset: 37158}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1300, col: 13, offset: 37165, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrLiteral2MissingRight", - pos: position{line: 1304, col: 1, offset: 37268}, - expr: &actionExpr{ - pos: position{line: 1304, col: 27, offset: 37294}, - run: (*parser).callonErrLiteral2MissingRight1, - expr: &seqExpr{ - pos: position{line: 1304, col: 27, offset: 37294}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1304, col: 27, offset: 37294}, - run: (*parser).callonErrLiteral2MissingRight3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1306, col: 3, offset: 37340}, - expr: &seqExpr{ - pos: position{line: 1306, col: 5, offset: 37342}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1306, col: 5, offset: 37342}, - expr: &charClassMatcher{ - pos: position{line: 1306, col: 6, offset: 37343}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1306, col: 13, offset: 37350, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrLiteral2", - pos: position{line: 1310, col: 1, offset: 37453}, - expr: &actionExpr{ - pos: position{line: 1310, col: 15, offset: 37467}, - run: (*parser).callonErrLiteral21, - expr: &seqExpr{ - pos: position{line: 1310, col: 15, offset: 37467}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1310, col: 15, offset: 37467}, - run: (*parser).callonErrLiteral23, - }, - &zeroOrMoreExpr{ - pos: position{line: 1312, col: 3, offset: 37501}, - expr: &seqExpr{ - pos: position{line: 1312, col: 5, offset: 37503}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1312, col: 5, offset: 37503}, - expr: &charClassMatcher{ - pos: position{line: 1312, col: 6, offset: 37504}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1312, col: 13, offset: 37511, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrConst", - pos: position{line: 1316, col: 1, offset: 37614}, - expr: &actionExpr{ - pos: position{line: 1316, col: 12, offset: 37625}, - run: (*parser).callonErrConst1, - expr: &seqExpr{ - pos: position{line: 1316, col: 12, offset: 37625}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1316, col: 12, offset: 37625}, - run: (*parser).callonErrConst3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1318, col: 3, offset: 37656}, - expr: &seqExpr{ - pos: position{line: 1318, col: 4, offset: 37657}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1318, col: 4, offset: 37657}, - expr: &charClassMatcher{ - pos: position{line: 1318, col: 5, offset: 37658}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1318, col: 12, offset: 37665, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrTypedef", - pos: position{line: 1322, col: 1, offset: 37766}, - expr: &actionExpr{ - pos: position{line: 1322, col: 14, offset: 37779}, - run: (*parser).callonErrTypedef1, - expr: &seqExpr{ - pos: position{line: 1322, col: 14, offset: 37779}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1322, col: 14, offset: 37779}, - run: (*parser).callonErrTypedef3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1324, col: 3, offset: 37812}, - expr: &seqExpr{ - pos: position{line: 1324, col: 4, offset: 37813}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1324, col: 4, offset: 37813}, - expr: &charClassMatcher{ - pos: position{line: 1324, col: 5, offset: 37814}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1324, col: 12, offset: 37821, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrEnum", - pos: position{line: 1328, col: 1, offset: 37925}, - expr: &actionExpr{ - pos: position{line: 1328, col: 11, offset: 37935}, - run: (*parser).callonErrEnum1, - expr: &seqExpr{ - pos: position{line: 1328, col: 11, offset: 37935}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1328, col: 11, offset: 37935}, - run: (*parser).callonErrEnum3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1330, col: 3, offset: 37965}, - expr: &seqExpr{ - pos: position{line: 1330, col: 4, offset: 37966}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1330, col: 4, offset: 37966}, - expr: &charClassMatcher{ - pos: position{line: 1330, col: 5, offset: 37967}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1330, col: 12, offset: 37974, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrService", - pos: position{line: 1334, col: 1, offset: 38075}, - expr: &actionExpr{ - pos: position{line: 1334, col: 14, offset: 38088}, - run: (*parser).callonErrService1, - expr: &seqExpr{ - pos: position{line: 1334, col: 14, offset: 38088}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1334, col: 14, offset: 38088}, - run: (*parser).callonErrService3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1336, col: 3, offset: 38121}, - expr: &seqExpr{ - pos: position{line: 1336, col: 4, offset: 38122}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1336, col: 4, offset: 38122}, - expr: &charClassMatcher{ - pos: position{line: 1336, col: 5, offset: 38123}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1336, col: 12, offset: 38130, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrStruct", - pos: position{line: 1340, col: 1, offset: 38234}, - expr: &actionExpr{ - pos: position{line: 1340, col: 13, offset: 38246}, - run: (*parser).callonErrStruct1, - expr: &seqExpr{ - pos: position{line: 1340, col: 13, offset: 38246}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1340, col: 13, offset: 38246}, - run: (*parser).callonErrStruct3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1342, col: 3, offset: 38278}, - expr: &seqExpr{ - pos: position{line: 1342, col: 4, offset: 38279}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1342, col: 4, offset: 38279}, - expr: &charClassMatcher{ - pos: position{line: 1342, col: 5, offset: 38280}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1342, col: 12, offset: 38287, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrUnion", - pos: position{line: 1346, col: 1, offset: 38390}, - expr: &actionExpr{ - pos: position{line: 1346, col: 12, offset: 38401}, - run: (*parser).callonErrUnion1, - expr: &seqExpr{ - pos: position{line: 1346, col: 12, offset: 38401}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1346, col: 12, offset: 38401}, - run: (*parser).callonErrUnion3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1348, col: 3, offset: 38432}, - expr: &seqExpr{ - pos: position{line: 1348, col: 4, offset: 38433}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1348, col: 4, offset: 38433}, - expr: &charClassMatcher{ - pos: position{line: 1348, col: 5, offset: 38434}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1348, col: 12, offset: 38441, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrException", - pos: position{line: 1352, col: 1, offset: 38543}, - expr: &actionExpr{ - pos: position{line: 1352, col: 16, offset: 38558}, - run: (*parser).callonErrException1, - expr: &seqExpr{ - pos: position{line: 1352, col: 16, offset: 38558}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1352, col: 16, offset: 38558}, - run: (*parser).callonErrException3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1354, col: 3, offset: 38593}, - expr: &seqExpr{ - pos: position{line: 1354, col: 4, offset: 38594}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1354, col: 4, offset: 38594}, - expr: &charClassMatcher{ - pos: position{line: 1354, col: 5, offset: 38595}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1354, col: 12, offset: 38602, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrDefinition", - pos: position{line: 1358, col: 1, offset: 38708}, - expr: &actionExpr{ - pos: position{line: 1358, col: 17, offset: 38724}, - run: (*parser).callonErrDefinition1, - expr: &seqExpr{ - pos: position{line: 1358, col: 17, offset: 38724}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1358, col: 17, offset: 38724}, - run: (*parser).callonErrDefinition3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1360, col: 3, offset: 38760}, - expr: &seqExpr{ - pos: position{line: 1360, col: 4, offset: 38761}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1360, col: 4, offset: 38761}, - expr: &charClassMatcher{ - pos: position{line: 1360, col: 5, offset: 38762}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1360, col: 12, offset: 38769, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrInclude", - pos: position{line: 1364, col: 1, offset: 38875}, - expr: &actionExpr{ - pos: position{line: 1364, col: 14, offset: 38888}, - run: (*parser).callonErrInclude1, - expr: &seqExpr{ - pos: position{line: 1364, col: 14, offset: 38888}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1364, col: 14, offset: 38888}, - run: (*parser).callonErrInclude3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1366, col: 3, offset: 38921}, - expr: &seqExpr{ - pos: position{line: 1366, col: 4, offset: 38922}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1366, col: 4, offset: 38922}, - expr: &charClassMatcher{ - pos: position{line: 1366, col: 5, offset: 38923}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1366, col: 12, offset: 38930, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrCppInclude", - pos: position{line: 1370, col: 1, offset: 39033}, - expr: &actionExpr{ - pos: position{line: 1370, col: 17, offset: 39049}, - run: (*parser).callonErrCppInclude1, - expr: &seqExpr{ - pos: position{line: 1370, col: 17, offset: 39049}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1370, col: 17, offset: 39049}, - run: (*parser).callonErrCppInclude3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1372, col: 3, offset: 39085}, - expr: &seqExpr{ - pos: position{line: 1372, col: 4, offset: 39086}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1372, col: 4, offset: 39086}, - expr: &charClassMatcher{ - pos: position{line: 1372, col: 5, offset: 39087}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1372, col: 12, offset: 39094, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrNamespace", - pos: position{line: 1376, col: 1, offset: 39200}, - expr: &actionExpr{ - pos: position{line: 1376, col: 16, offset: 39215}, - run: (*parser).callonErrNamespace1, - expr: &seqExpr{ - pos: position{line: 1376, col: 16, offset: 39215}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1376, col: 16, offset: 39215}, - run: (*parser).callonErrNamespace3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1378, col: 3, offset: 39250}, - expr: &seqExpr{ - pos: position{line: 1378, col: 4, offset: 39251}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1378, col: 4, offset: 39251}, - expr: &charClassMatcher{ - pos: position{line: 1378, col: 5, offset: 39252}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1378, col: 12, offset: 39259, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "ErrHeader", - pos: position{line: 1382, col: 1, offset: 39364}, - expr: &actionExpr{ - pos: position{line: 1382, col: 13, offset: 39376}, - run: (*parser).callonErrHeader1, - expr: &seqExpr{ - pos: position{line: 1382, col: 13, offset: 39376}, - exprs: []any{ - &stateCodeExpr{ - pos: position{line: 1382, col: 13, offset: 39376}, - run: (*parser).callonErrHeader3, - }, - &zeroOrMoreExpr{ - pos: position{line: 1384, col: 3, offset: 39408}, - expr: &seqExpr{ - pos: position{line: 1384, col: 4, offset: 39409}, - exprs: []any{ - ¬Expr{ - pos: position{line: 1384, col: 4, offset: 39409}, - expr: &charClassMatcher{ - pos: position{line: 1384, col: 5, offset: 39410}, - val: "[\\r\\n]", - chars: []rune{'\r', '\n'}, - ignoreCase: false, - inverted: false, - }, - }, - &anyMatcher{ - line: 1384, col: 12, offset: 39417, - }, - }, - }, - }, - }, - }, - }, - }, - }, -} - -func (c *current) onDocument3(headers, defs, comments any) (any, error) { - return NewDocument(toHeaderSlice(headers), toDefinitionSlice(defs), comments.([]*Comment), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonDocument3() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onDocument3(stack["headers"], stack["defs"], stack["comments"]) -} - -func (c *current) onHeader5(comments, v, endLineComments any) (any, error) { - c.globalStore["parse"] = "header" - v.(Header).SetComments(comments.([]*Comment), endLineComments.([]*Comment)) - return v, nil -} - -func (p *parser) callonHeader5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onHeader5(stack["comments"], stack["v"], stack["endLineComments"]) -} - -func (c *current) onHeader25() (bool, error) { - /* fmt.Println("header predict:", c.pos, "text:", string(c.text)) */ - if state, ok := c.globalStore["parse"]; !ok || ok && state.(string) == "header" { - return true, nil - } - return false, nil -} - -func (p *parser) callonHeader25() (bool, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onHeader25() -} - -func (c *current) onHeader16(x any) (any, error) { - /* fmt.Println("header return:", c.pos, "text:", string(c.text)) */ - badHeader := x.([]any)[4].(*BadHeader) - return badHeader, nil -} - -func (p *parser) callonHeader16() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onHeader16(stack["x"]) -} - -func (c *current) onInclude2(includeKeyword, include any) (any, error) { - includeV, ok := include.(*Literal) - if !ok { - includeV = include.([]interface{})[0].(*Literal) - } - return NewInclude(includeKeyword.(*IncludeKeyword), includeV, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonInclude2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onInclude2(stack["includeKeyword"], stack["include"]) -} - -func (c *current) onInclude8(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonInclude8() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onInclude8(stack["x"]) -} - -func (c *current) onCppInclude2(cppIncludeKeyword, include any) (any, error) { - includeV, ok := include.(*Literal) - if !ok { - includeV = include.([]interface{})[0].(*Literal) - } - return NewCPPInclude(cppIncludeKeyword.(*CPPIncludeKeyword), includeV, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonCppInclude2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCppInclude2(stack["cppIncludeKeyword"], stack["include"]) -} - -func (c *current) onCppInclude8(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonCppInclude8() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCppInclude8(stack["x"]) -} - -func (c *current) onNamespace2(namespaceKeyword, language, name, annotations any) (any, error) { - return NewNamespace(namespaceKeyword.(*NamespaceKeyword), language.(*NamespaceScope), name.(*Identifier), toAnnotations(annotations), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonNamespace2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onNamespace2(stack["namespaceKeyword"], stack["language"], stack["name"], stack["annotations"]) -} - -func (c *current) onNamespace13(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonNamespace13() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onNamespace13(stack["x"]) -} - -func (c *current) onNamespaceScope1(v any) (any, error) { - id := v.(*Identifier) - res := &NamespaceScope{ - Identifier: *id, - } - - return res, nil -} - -func (p *parser) callonNamespaceScope1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onNamespaceScope1(stack["v"]) -} - -func (c *current) onNamespaceScopeAny1(comments, idName any) (any, error) { - return NewIdentifier(idName.(*IdentifierName), comments.([]*Comment), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonNamespaceScopeAny1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onNamespaceScopeAny1(stack["comments"], stack["idName"]) -} - -func (c *current) onNamespaceScopeAnyToken1() (any, error) { - return NewIdentifierName("*", NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonNamespaceScopeAnyToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onNamespaceScopeAnyToken1() -} - -func (c *current) onDefinition9(comments, v, annos, endLineComments any) (any, error) { - c.globalStore["parse"] = "definition" - def := v.(Definition) - def.SetComments(comments.([]*Comment), endLineComments.([]*Comment)) - def.SetAnnotations(toAnnotations(annos)) - def.SetLocation(NewLocationFromCurrent(c)) - return def, nil -} - -func (p *parser) callonDefinition9() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onDefinition9(stack["comments"], stack["v"], stack["annos"], stack["endLineComments"]) -} - -func (c *current) onDefinition34() (bool, error) { - /* fmt.Println("definition predict:", c.pos, "text:", string(c.text)) */ - if state, ok := c.globalStore["parse"]; ok && state.(string) == "definition" { - return true, nil - } - return false, nil -} - -func (p *parser) callonDefinition34() (bool, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onDefinition34() -} - -func (c *current) onDefinition27(x any) (any, error) { - /* fmt.Println("definition return:", c.pos, "text:", string(c.text)) */ - return x.([]any)[3], nil -} - -func (p *parser) callonDefinition27() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onDefinition27(stack["x"]) -} - -func (c *current) onConst5(constKeyword, t, name, v, sep any) (any, error) { - equalAndValue := v.([]any) - return NewConst(constKeyword.(*ConstKeyword), equalAndValue[0].(*EqualKeyword), toListSeparatorKeyword(sep), name.(*Identifier), t.(*FieldType), equalAndValue[1].(*ConstValue), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonConst5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onConst5(stack["constKeyword"], stack["t"], stack["name"], stack["v"], stack["sep"]) -} - -func (c *current) onConst18(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonConst18() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onConst18(stack["x"]) -} - -func (c *current) onConstEqualValue2(v any) (any, error) { - return v, nil -} - -func (p *parser) callonConstEqualValue2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onConstEqualValue2(stack["v"]) -} - -func (c *current) onConstEqualValue7(x any) (any, error) { - return []any{NewBadEqualKeyword(), x.([]any)[1]}, nil -} - -func (p *parser) callonConstEqualValue7() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onConstEqualValue7(stack["x"]) -} - -func (c *current) onConstEqualValue13(x any) (any, error) { - return x, nil -} - -func (p *parser) callonConstEqualValue13() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onConstEqualValue13(stack["x"]) -} - -func (c *current) onTypedef3(typedefKeyword, t, alias any) (any, error) { - return NewTypedef(typedefKeyword.(*TypedefKeyword), t.(*FieldType), alias.(*Identifier), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonTypedef3() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onTypedef3(stack["typedefKeyword"], stack["t"], stack["alias"]) -} - -func (c *current) onTypedef11(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonTypedef11() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onTypedef11(stack["x"]) -} - -func (c *current) onEnum5(enum, name, lcur, v, rcur any) (any, error) { - values := toEnumValueSlice(v) - return NewEnum(enum.(*EnumKeyword), lcur.(*LCurKeyword), rcur.(*RCurKeyword), name.(*Identifier), values, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonEnum5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEnum5(stack["enum"], stack["name"], stack["lcur"], stack["v"], stack["rcur"]) -} - -func (c *current) onEnum18(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonEnum18() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEnum18(stack["x"]) -} - -func (c *current) onEnumValueLine1(comments, v, endLineComments any) (any, error) { - v.(*EnumValue).SetComments(comments.([]*Comment), endLineComments.([]*Comment)) - return v, nil -} - -func (p *parser) callonEnumValueLine1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEnumValueLine1(stack["comments"], stack["v"], stack["endLineComments"]) -} - -func (c *current) onEnumValue2(name, value, annos, sep any) (any, error) { - var valueNode *ConstValue - var equalNode *EqualKeyword - intV := int64(-1) - if value != nil { - //fmt.Println(value.(Node).Type(), value) - equalNode = value.([]any)[0].(*EqualKeyword) - valueNode = value.([]any)[1].(*ConstValue) - intV = valueNode.Value.(int64) - } - - return NewEnumValue(toListSeparatorKeyword(sep), equalNode, name.(*Identifier), valueNode, intV, toAnnotations(annos), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonEnumValue2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEnumValue2(stack["name"], stack["value"], stack["annos"], stack["sep"]) -} - -func (c *current) onService5(svc, name, extends, lcur, fns, rcur any) (any, error) { - var extendsVal *Identifier - var extendsKeyword *ExtendsKeyword - if extends != nil { - extendsKeyword = extends.([]any)[0].(*ExtendsKeyword) - extendsVal = extends.([]any)[1].(*Identifier) - } - fnsVal := toFunctionSlice(fns) - return NewService(svc.(*ServiceKeyword), extendsKeyword, lcur.(*LCurKeyword), rcur.(*RCurKeyword), name.(*Identifier), extendsVal, fnsVal, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonService5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onService5(stack["svc"], stack["name"], stack["extends"], stack["lcur"], stack["fns"], stack["rcur"]) -} - -func (c *current) onService23(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonService23() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onService23(stack["x"]) -} - -func (c *current) onStruct5(st, id, lcur, fields, rcur any) (any, error) { - return NewStruct(st.(*StructKeyword), lcur.(*LCurKeyword), rcur.(*RCurKeyword), id.(*Identifier), toFieldSlice(fields), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonStruct5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onStruct5(stack["st"], stack["id"], stack["lcur"], stack["fields"], stack["rcur"]) -} - -func (c *current) onStruct18(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonStruct18() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onStruct18(stack["x"]) -} - -func (c *current) onUnion5(union, name, lcur, fields, rcur any) (any, error) { - return NewUnion(union.(*UnionKeyword), lcur.(*LCurKeyword), rcur.(*RCurKeyword), name.(*Identifier), toFieldSlice(fields), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonUnion5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onUnion5(stack["union"], stack["name"], stack["lcur"], stack["fields"], stack["rcur"]) -} - -func (c *current) onUnion18(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonUnion18() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onUnion18(stack["x"]) -} - -func (c *current) onException5(excep, name, lcur, fields, rcur any) (any, error) { - return NewException(excep.(*ExceptionKeyword), lcur.(*LCurKeyword), rcur.(*RCurKeyword), name.(*Identifier), toFieldSlice(fields), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonException5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onException5(stack["excep"], stack["name"], stack["lcur"], stack["fields"], stack["rcur"]) -} - -func (c *current) onException18(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonException18() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onException18(stack["x"]) -} - -func (c *current) onFieldWithThrow3(x any) (any, error) { - return x.([]any)[2], nil -} - -func (p *parser) callonFieldWithThrow3() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onFieldWithThrow3(stack["x"]) -} - -func (c *current) onField1(comments, index, required, fieldType, id, value, annos, sep, lineComments any) (any, error) { - var constV *ConstValue - var equalKeyword *EqualKeyword - if value != nil { - equalKeyword = value.([]any)[0].(*EqualKeyword) - constV = value.([]any)[1].(*ConstValue) - } - var requiredV *RequiredKeyword - if required != nil { - requiredV = required.(*RequiredKeyword) - } - - return NewField(equalKeyword, toListSeparatorKeyword(sep), comments.([]*Comment), lineComments.([]*Comment), toAnnotations(annos), index.(*FieldIndex), requiredV, fieldType.(*FieldType), id.(*Identifier), constV, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonField1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onField1(stack["comments"], stack["index"], stack["required"], stack["fieldType"], stack["id"], stack["value"], stack["annos"], stack["sep"], stack["lineComments"]) -} - -func (c *current) onFieldId2(comments, i, colon any) (any, error) { - fieldIndex := i.(*FieldIndex) - - return NewFieldIndex(colon.(*ColonKeyword), fieldIndex.Value, comments.([]*Comment), fieldIndex.Location), nil -} - -func (p *parser) callonFieldId2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onFieldId2(stack["comments"], stack["i"], stack["colon"]) -} - -func (c *current) onFieldReq1(comments, r any) (any, error) { - kw := NewKeyword(comments.([]*Comment), r.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &RequiredKeyword{Keyword: kw}, nil -} - -func (p *parser) callonFieldReq1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onFieldReq1(stack["comments"], stack["r"]) -} - -func (c *current) onIsRequired1(v any) (any, error) { - return v, nil -} - -func (p *parser) callonIsRequired1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onIsRequired1(stack["v"]) -} - -func (c *current) onRequiredToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonRequiredToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onRequiredToken1() -} - -func (c *current) onOptionalToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonOptionalToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onOptionalToken1() -} - -func (c *current) onFunction4(comments, oneway, ft, name, lpar, args, rpar, throws, annos, sep, endLineComments any) (any, error) { - var ftype *FieldType - var voidKeyword *VoidKeyword - if _, ok := ft.(*VoidKeyword); ok { - voidKeyword = ft.(*VoidKeyword) - } else { - ftype = ft.(*FieldType) - } - - var throwsV *Throws - if throws != nil { - throwsV = throws.(*Throws) - } - - var onewayKeyword *OnewayKeyword - if oneway != nil { - onewayKeyword = oneway.(*OnewayKeyword) - } - - return NewFunction(lpar.(*LParKeyword), rpar.(*RParKeyword), toListSeparatorKeyword(sep), name.(*Identifier), onewayKeyword, voidKeyword, ftype, toFieldSlice(args), throwsV, comments.([]*Comment), endLineComments.([]*Comment), toAnnotations(annos), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonFunction4() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onFunction4(stack["comments"], stack["oneway"], stack["ft"], stack["name"], stack["lpar"], stack["args"], stack["rpar"], stack["throws"], stack["annos"], stack["sep"], stack["endLineComments"]) -} - -func (c *current) onFunction33(x any) (any, error) { - return x.([]any)[2], nil -} - -func (p *parser) callonFunction33() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onFunction33(stack["x"]) -} - -func (c *current) onFunctionFieldWithThrow2(v any) (any, error) { - return v, nil -} - -func (p *parser) callonFunctionFieldWithThrow2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onFunctionFieldWithThrow2(stack["v"]) -} - -func (c *current) onFunctionFieldWithThrow5(x any) (any, error) { - return x.([]any)[2], nil -} - -func (p *parser) callonFunctionFieldWithThrow5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onFunctionFieldWithThrow5(stack["x"]) -} - -func (c *current) onThrows1(throws, lpar, fields, rpar any) (any, error) { - return NewThrows(throws.(*ThrowsKeyword), lpar.(*LParKeyword), rpar.(*RParKeyword), toFieldSlice(fields), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonThrows1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onThrows1(stack["throws"], stack["lpar"], stack["fields"], stack["rpar"]) -} - -func (c *current) onFieldType1(v, annos any) (any, error) { - ft := v.(*FieldType) - ft.Annotations = toAnnotations(annos) - - return ft, nil -} - -func (p *parser) callonFieldType1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onFieldType1(stack["v"], stack["annos"]) -} - -func (c *current) onIdentifierType1(v any) (any, error) { - return v.(*Identifier).ToFieldType(), nil -} - -func (p *parser) callonIdentifierType1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onIdentifierType1(stack["v"]) -} - -func (c *current) onBaseType1(v any) (any, error) { - return NewFieldType(nil, nil, nil, nil, v.(*TypeName), nil, nil, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonBaseType1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onBaseType1(stack["v"]) -} - -func (c *current) onContainerType1(v any) (any, error) { - return v.(*FieldType), nil -} - -func (p *parser) callonContainerType1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onContainerType1(stack["v"]) -} - -func (c *current) onMapType1(t, cpp, lp, key, comma, value, rp any) (any, error) { - var cppType *CppType - if cpp != nil { - cppType = cpp.(*CppType) - } - - return NewFieldType(lp.(*LPointKeyword), rp.(*RPointKeyword), comma.(*CommaKeyword), cppType, t.(*TypeName), key.(*FieldType), value.(*FieldType), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonMapType1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onMapType1(stack["t"], stack["cpp"], stack["lp"], stack["key"], stack["comma"], stack["value"], stack["rp"]) -} - -func (c *current) onSetType1(t, cpp, lp, key, rp any) (any, error) { - var cppType *CppType - if cpp != nil { - cppType = cpp.(*CppType) - } - - return NewFieldType(lp.(*LPointKeyword), rp.(*RPointKeyword), nil, cppType, t.(*TypeName), key.(*FieldType), nil, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonSetType1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onSetType1(stack["t"], stack["cpp"], stack["lp"], stack["key"], stack["rp"]) -} - -func (c *current) onListType1(t, lp, key, rp, cpp any) (any, error) { - var cppType *CppType - if cpp != nil { - cppType = cpp.(*CppType) - } - - return NewFieldType(lp.(*LPointKeyword), rp.(*RPointKeyword), nil, cppType, t.(*TypeName), key.(*FieldType), nil, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonListType1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onListType1(stack["t"], stack["lp"], stack["key"], stack["rp"], stack["cpp"]) -} - -func (c *current) onCppType1(cpp, l any) (any, error) { - return NewCppType(cpp.(*CppTypeKeyword), l.(*Literal), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonCppType1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCppType1(stack["cpp"], stack["l"]) -} - -func (c *current) onConstValue1(v any) (any, error) { - if literal, ok := v.(*Literal); ok { - return NewConstValue("string", literal, NewLocationFromCurrent(c)), nil - } - return v.(*ConstValue), nil -} - -func (p *parser) callonConstValue1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onConstValue1(stack["v"]) -} - -func (c *current) onIdentifierConst1(comments, cv any) (any, error) { - node := cv.(*ConstValue) - node.SetComments(comments.([]*Comment)) - return node, nil -} - -func (p *parser) callonIdentifierConst1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onIdentifierConst1(stack["comments"], stack["cv"]) -} - -func (c *current) onIdentifierConstValue1(id any) (any, error) { - cv := NewConstValue("identifier", id.(*Identifier).Name.Text, NewLocationFromCurrent(c)) - return cv, nil -} - -func (p *parser) callonIdentifierConstValue1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onIdentifierConstValue1(stack["id"]) -} - -func (c *current) onEnumValueIntConstant2(v any) (any, error) { - return v, nil -} - -func (p *parser) callonEnumValueIntConstant2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEnumValueIntConstant2(stack["v"]) -} - -func (c *current) onEnumValueIntConstant7(x any) (any, error) { - return []any{x.([]any)[0], x.([]any)[2]}, nil -} - -func (p *parser) callonEnumValueIntConstant7() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEnumValueIntConstant7(stack["x"]) -} - -func (c *current) onIntConstant2(comments, v any) (any, error) { - cv := v.(*ConstValue) - cv.SetComments(comments.([]*Comment)) - - return cv, nil -} - -func (p *parser) callonIntConstant2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onIntConstant2(stack["comments"], stack["v"]) -} - -func (c *current) onIntConstant15(x any) (any, error) { - return x.([]any)[2], nil -} - -func (p *parser) callonIntConstant15() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onIntConstant15(stack["x"]) -} - -func (c *current) onHexIntConstant1() (any, error) { - v, err := strconv.ParseInt(strings.TrimPrefix(string(c.text), "0x"), 16, 64) - if err != nil { - return nil, err - } - - cv := NewConstValue("i64", v, NewLocationFromCurrent(c)) - cv.ValueInText = string(c.text) - - return cv, nil -} - -func (p *parser) callonHexIntConstant1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onHexIntConstant1() -} - -func (c *current) onOctIntConstant1() (any, error) { - v, err := strconv.ParseInt(strings.TrimPrefix(string(c.text), "0o"), 8, 64) - if err != nil { - return nil, err - } - cv := NewConstValue("i64", v, NewLocationFromCurrent(c)) - cv.ValueInText = string(c.text) - - return cv, nil -} - -func (p *parser) callonOctIntConstant1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onOctIntConstant1() -} - -func (c *current) onNormalIntConstant1() (any, error) { - v, err := strconv.ParseInt(string(c.text), 10, 64) - if err != nil { - return nil, err - } - cv := NewConstValue("i64", v, NewLocationFromCurrent(c)) - cv.ValueInText = string(c.text) - - return cv, nil -} - -func (p *parser) callonNormalIntConstant1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onNormalIntConstant1() -} - -func (c *current) onFieldIndex2() (any, error) { - v, err := strconv.ParseInt(string(c.text), 10, 64) - if err != nil { - return nil, err - } - return NewFieldIndex(nil, int(v), nil, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonFieldIndex2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onFieldIndex2() -} - -func (c *current) onFieldIndex5(x any) (any, error) { - return x.([]any)[2], nil -} - -func (p *parser) callonFieldIndex5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onFieldIndex5(stack["x"]) -} - -func (c *current) onDoubleConstant1(comments, v any) (any, error) { - cv := v.(*ConstValue) - cv.SetComments(comments.([]*Comment)) - - return cv, nil -} - -func (p *parser) callonDoubleConstant1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onDoubleConstant1(stack["comments"], stack["v"]) -} - -func (c *current) onDoubleConstantValue1() (any, error) { - v, err := strconv.ParseFloat(string(c.text), 64) - if err != nil { - return nil, err - } - cv := NewConstValue("double", v, NewLocationFromCurrent(c)) - cv.ValueInText = string(c.text) - - return cv, nil -} - -func (p *parser) callonDoubleConstantValue1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onDoubleConstantValue1() -} - -func (c *current) onAnnotations1(lpar, annos, rpar any) (any, error) { - return NewAnnotations(lpar.(*LParKeyword), rpar.(*RParKeyword), toAnnotationSlice(annos), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonAnnotations1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onAnnotations1(stack["lpar"], stack["annos"], stack["rpar"]) -} - -func (c *current) onAnnotation1(id, eq, value, sep any) (any, error) { - return NewAnnotation(eq.(*EqualKeyword), toListSeparatorKeyword(sep), id.(*Identifier), value.(*Literal), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonAnnotation1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onAnnotation1(stack["id"], stack["eq"], stack["value"], stack["sep"]) -} - -func (c *current) onConstList1(lbrk, v, rbrk any) (any, error) { - cv := NewConstValue("list", toConstValueSlice(v), NewLocationFromCurrent(c)) - - cv.LBrkKeyword = lbrk.(*LBrkKeyword) - cv.RBrkKeyword = rbrk.(*RBrkKeyword) - - return cv, nil -} - -func (p *parser) callonConstList1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onConstList1(stack["lbrk"], stack["v"], stack["rbrk"]) -} - -func (c *current) onConstListItem1(v, sep any) (any, error) { - cv := v.(*ConstValue) - - if sep != nil { - cv.ListSeparatorKeyword = sep.(*ListSeparatorKeyword) - } - - return cv, nil -} - -func (p *parser) callonConstListItem1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onConstListItem1(stack["v"], stack["sep"]) -} - -func (c *current) onConstMap1(lcur, v, rcur any) (any, error) { - cv := NewConstValue("map", toConstValueSlice(v), NewLocationFromCurrent(c)) - - cv.LCurKeyword = lcur.(*LCurKeyword) - cv.RCurKeyword = rcur.(*RCurKeyword) - - return cv, nil -} - -func (p *parser) callonConstMap1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onConstMap1(stack["lcur"], stack["v"], stack["rcur"]) -} - -func (c *current) onConstMapItem1(key, colon, value, sep any) (any, error) { - cv := NewMapConstValue(key.(*ConstValue), value.(*ConstValue), NewLocationFromCurrent(c)) - - cv.ColonKeyword = colon.(*ColonKeyword) - if sep != nil { - cv.ListSeparatorKeyword = sep.(*ListSeparatorKeyword) - } - - return cv, nil -} - -func (p *parser) callonConstMapItem1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onConstMapItem1(stack["key"], stack["colon"], stack["value"], stack["sep"]) -} - -func (c *current) onEscapeLiteralChar1() (any, error) { - return string(c.text), nil -} - -func (p *parser) callonEscapeLiteralChar1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEscapeLiteralChar1() -} - -func (c *current) onLiteral5(l any) (any, error) { - return l, nil -} - -func (p *parser) callonLiteral5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLiteral5(stack["l"]) -} - -func (c *current) onLiteral12(comments, t any) (any, error) { - return NewLiteral(comments.([]*Comment), t.(*LiteralValue), "\"", NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonLiteral12() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLiteral12(stack["comments"], stack["t"]) -} - -func (c *current) onLiteral112(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonLiteral112() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLiteral112(stack["x"]) -} - -func (c *current) onLiteral22(comments, t any) (any, error) { - return NewLiteral(comments.([]*Comment), t.(*LiteralValue), "'", NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonLiteral22() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLiteral22(stack["comments"], stack["t"]) -} - -func (c *current) onLiteral212(x any) (any, error) { - return x.([]any)[1], nil -} - -func (p *parser) callonLiteral212() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLiteral212(stack["x"]) -} - -func (c *current) onLiteral1Val1() (any, error) { - return NewLiteralValue(string(c.text), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonLiteral1Val1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLiteral1Val1() -} - -func (c *current) onLiteral2Val1() (any, error) { - return NewLiteralValue(string(c.text), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonLiteral2Val1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLiteral2Val1() -} - -func (c *current) onDefinitionIdentifier2(id any) (any, error) { - return id.(*Identifier), nil -} - -func (p *parser) callonDefinitionIdentifier2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onDefinitionIdentifier2(stack["id"]) -} - -func (c *current) onIdentifier1(comments, id any) (any, error) { - idName := id.(*IdentifierName) - - return NewIdentifier(idName, comments.([]*Comment), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonIdentifier1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onIdentifier1(stack["comments"], stack["id"]) -} - -func (c *current) onIdentifierToken1() (any, error) { - return NewIdentifierName(string(c.text), NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonIdentifierToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onIdentifierToken1() -} - -func (c *current) onListSeparator1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ListSeparatorKeyword{Keyword: kw}, nil -} - -func (p *parser) callonListSeparator1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onListSeparator1(stack["comments"], stack["t"]) -} - -func (c *current) onListSeparatorToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonListSeparatorToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onListSeparatorToken1() -} - -func (c *current) onLetter4() (any, error) { - return string(c.text), nil -} - -func (p *parser) callonLetter4() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLetter4() -} - -func (c *current) onLetterOrDigit5() (any, error) { - return string(c.text), nil -} - -func (p *parser) callonLetterOrDigit5() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLetterOrDigit5() -} - -func (c *current) onDigit1() (any, error) { - return string(c.text), nil -} - -func (p *parser) callonDigit1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onDigit1() -} - -func (c *current) onReservedComments1(comments any) (any, error) { - return toCommentSlice(comments), nil -} - -func (p *parser) callonReservedComments1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onReservedComments1(stack["comments"]) -} - -func (c *current) onReservedEndLineComments1(comments any) (any, error) { - return toCommentSlice(comments), nil -} - -func (p *parser) callonReservedEndLineComments1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onReservedEndLineComments1(stack["comments"]) -} - -func (c *current) onSpace1() (any, error) { - return "", nil -} - -func (p *parser) callonSpace1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onSpace1() -} - -func (c *current) onIndent1() (any, error) { - return "", nil -} - -func (p *parser) callonIndent1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onIndent1() -} - -func (c *current) onComment1(v any) (any, error) { - return v.(*Comment), nil -} - -func (p *parser) callonComment1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onComment1(stack["v"]) -} - -func (c *current) onLongComment1() (any, error) { - return NewComment(string(c.text), CommentStyleMultiLine, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonLongComment1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLongComment1() -} - -func (c *current) onLongCommentMatch1() (any, error) { - return string(c.text), nil -} - -func (p *parser) callonLongCommentMatch1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLongCommentMatch1() -} - -func (c *current) onLineComment1() (any, error) { - return NewComment(string(c.text), CommentStyleSingleLine, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonLineComment1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLineComment1() -} - -func (c *current) onLineCommentMatch1() (any, error) { - return string(c.text), nil -} - -func (p *parser) callonLineCommentMatch1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLineCommentMatch1() -} - -func (c *current) onUnixComment1() (any, error) { - return NewComment(string(c.text), CommentStyleShell, NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonUnixComment1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onUnixComment1() -} - -func (c *current) onUnixCommentMatch1() (any, error) { - return string(c.text), nil -} - -func (p *parser) callonUnixCommentMatch1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onUnixCommentMatch1() -} - -func (c *current) onBOOL1(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonBOOL1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onBOOL1(stack["comments"], stack["t"]) -} - -func (c *current) onBOOLToken1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonBOOLToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onBOOLToken1() -} - -func (c *current) onBYTE1(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonBYTE1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onBYTE1(stack["comments"], stack["t"]) -} - -func (c *current) onBYTEToken1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonBYTEToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onBYTEToken1() -} - -func (c *current) onI81(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonI81() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onI81(stack["comments"], stack["t"]) -} - -func (c *current) onI8Token1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonI8Token1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onI8Token1() -} - -func (c *current) onI161(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonI161() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onI161(stack["comments"], stack["t"]) -} - -func (c *current) onI16Token1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonI16Token1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onI16Token1() -} - -func (c *current) onI321(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonI321() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onI321(stack["comments"], stack["t"]) -} - -func (c *current) onI32Token1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonI32Token1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onI32Token1() -} - -func (c *current) onI641(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonI641() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onI641(stack["comments"], stack["t"]) -} - -func (c *current) onI64Token1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonI64Token1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onI64Token1() -} - -func (c *current) onDOUBLE1(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonDOUBLE1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onDOUBLE1(stack["comments"], stack["t"]) -} - -func (c *current) onDOUBLEToken1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonDOUBLEToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onDOUBLEToken1() -} - -func (c *current) onSTRING1(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonSTRING1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onSTRING1(stack["comments"], stack["t"]) -} - -func (c *current) onSTRINGToken1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonSTRINGToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onSTRINGToken1() -} - -func (c *current) onBINARY1(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonBINARY1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onBINARY1(stack["comments"], stack["t"]) -} - -func (c *current) onBINARYToken1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonBINARYToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onBINARYToken1() -} - -func (c *current) onUUID1(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonUUID1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onUUID1(stack["comments"], stack["t"]) -} - -func (c *current) onUUIDToken1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonUUIDToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onUUIDToken1() -} - -func (c *current) onMAP1(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonMAP1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onMAP1(stack["comments"], stack["t"]) -} - -func (c *current) onMAPToken1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonMAPToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onMAPToken1() -} - -func (c *current) onSET1(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonSET1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onSET1(stack["comments"], stack["t"]) -} - -func (c *current) onSETToken1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonSETToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onSETToken1() -} - -func (c *current) onLIST1(comments, t any) (any, error) { - tn := t.(*TypeName) - tn.Comments = comments.([]*Comment) - - return tn, nil -} - -func (p *parser) callonLIST1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLIST1(stack["comments"], stack["t"]) -} - -func (c *current) onListToken1() (any, error) { - return NewTypeName(string(c.text), c.pos), nil -} - -func (p *parser) callonListToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onListToken1() -} - -func (c *current) onCONST1(comments, t any) (any, error) { - - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ConstKeyword{Keyword: kw}, nil -} - -func (p *parser) callonCONST1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCONST1(stack["comments"], stack["t"]) -} - -func (c *current) onCONSTToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonCONSTToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCONSTToken1() -} - -func (c *current) onONEWAY1(comments, t any) (any, error) { - - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &OnewayKeyword{Keyword: kw}, nil -} - -func (p *parser) callonONEWAY1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onONEWAY1(stack["comments"], stack["t"]) -} - -func (c *current) onONEWAYToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonONEWAYToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onONEWAYToken1() -} - -func (c *current) onTYPEDEF1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &TypedefKeyword{Keyword: kw}, nil -} - -func (p *parser) callonTYPEDEF1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onTYPEDEF1(stack["comments"], stack["t"]) -} - -func (c *current) onTYPEDEFToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonTYPEDEFToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onTYPEDEFToken1() -} - -func (c *current) onVOID1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &VoidKeyword{Keyword: kw}, nil -} - -func (p *parser) callonVOID1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onVOID1(stack["comments"], stack["t"]) -} - -func (c *current) onVOIDToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonVOIDToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onVOIDToken1() -} - -func (c *current) onTHROWS1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ThrowsKeyword{Keyword: kw}, nil -} - -func (p *parser) callonTHROWS1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onTHROWS1(stack["comments"], stack["t"]) -} - -func (c *current) onTHROWSToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonTHROWSToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onTHROWSToken1() -} - -func (c *current) onEXCEPTION1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ExceptionKeyword{Keyword: kw}, nil -} - -func (p *parser) callonEXCEPTION1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEXCEPTION1(stack["comments"], stack["t"]) -} - -func (c *current) onEXCEPTIONToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonEXCEPTIONToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEXCEPTIONToken1() -} - -func (c *current) onEXTENDS1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ExtendsKeyword{Keyword: kw}, nil -} - -func (p *parser) callonEXTENDS1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEXTENDS1(stack["comments"], stack["t"]) -} - -func (c *current) onEXTENDSToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonEXTENDSToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEXTENDSToken1() -} - -func (c *current) onSERVICE1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ServiceKeyword{Keyword: kw}, nil -} - -func (p *parser) callonSERVICE1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onSERVICE1(stack["comments"], stack["t"]) -} - -func (c *current) onSERVICEToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonSERVICEToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onSERVICEToken1() -} - -func (c *current) onSTRUCT1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &StructKeyword{Keyword: kw}, nil -} - -func (p *parser) callonSTRUCT1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onSTRUCT1(stack["comments"], stack["t"]) -} - -func (c *current) onSTRUCTToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonSTRUCTToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onSTRUCTToken1() -} - -func (c *current) onUNION1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &UnionKeyword{Keyword: kw}, nil -} - -func (p *parser) callonUNION1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onUNION1(stack["comments"], stack["t"]) -} - -func (c *current) onUNIONToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonUNIONToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onUNIONToken1() -} - -func (c *current) onENUM1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &EnumKeyword{Keyword: kw}, nil -} - -func (p *parser) callonENUM1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onENUM1(stack["comments"], stack["t"]) -} - -func (c *current) onENUMToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonENUMToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onENUMToken1() -} - -func (c *current) onINCLUDE1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &IncludeKeyword{Keyword: kw}, nil -} - -func (p *parser) callonINCLUDE1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onINCLUDE1(stack["comments"], stack["t"]) -} - -func (c *current) onINCLUDEToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonINCLUDEToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onINCLUDEToken1() -} - -func (c *current) onCPPINCLUDE1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &CPPIncludeKeyword{Keyword: kw}, nil -} - -func (p *parser) callonCPPINCLUDE1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCPPINCLUDE1(stack["comments"], stack["t"]) -} - -func (c *current) onCPPINCLUDEToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonCPPINCLUDEToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCPPINCLUDEToken1() -} - -func (c *current) onNAMESPACE1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &NamespaceKeyword{Keyword: kw}, nil -} - -func (p *parser) callonNAMESPACE1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onNAMESPACE1(stack["comments"], stack["t"]) -} - -func (c *current) onNAMESPACEToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonNAMESPACEToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onNAMESPACEToken1() -} - -func (c *current) onCPPTYPE1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &CppTypeKeyword{Keyword: kw}, nil -} - -func (p *parser) callonCPPTYPE1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCPPTYPE1(stack["comments"], stack["t"]) -} - -func (c *current) onCPPTYPEToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonCPPTYPEToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCPPTYPEToken1() -} - -func (c *current) onLBRK1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &LBrkKeyword{Keyword: kw}, nil -} - -func (p *parser) callonLBRK1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLBRK1(stack["comments"], stack["t"]) -} - -func (c *current) onLBRKToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonLBRKToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLBRKToken1() -} - -func (c *current) onRBRK1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &RBrkKeyword{Keyword: kw}, nil -} - -func (p *parser) callonRBRK1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onRBRK1(stack["comments"], stack["t"]) -} - -func (c *current) onRBRKToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonRBRKToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onRBRKToken1() -} - -func (c *current) onLCUR1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &LCurKeyword{Keyword: kw}, nil -} - -func (p *parser) callonLCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLCUR1(stack["comments"], stack["t"]) -} - -func (c *current) onRCUR1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &RCurKeyword{Keyword: kw}, nil -} - -func (p *parser) callonRCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onRCUR1(stack["comments"], stack["t"]) -} - -func (c *current) onLCURToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonLCURToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLCURToken1() -} - -func (c *current) onRCURToken2() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonRCURToken2() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onRCURToken2() -} - -func (c *current) onEQUAL1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &EqualKeyword{Keyword: kw}, nil -} - -func (p *parser) callonEQUAL1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEQUAL1(stack["comments"], stack["t"]) -} - -func (c *current) onEQUALToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonEQUALToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onEQUALToken1() -} - -func (c *current) onLPOINT1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &LPointKeyword{Keyword: kw}, nil -} - -func (p *parser) callonLPOINT1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLPOINT1(stack["comments"], stack["t"]) -} - -func (c *current) onLPOINTToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonLPOINTToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLPOINTToken1() -} - -func (c *current) onRPOINT1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &RPointKeyword{Keyword: kw}, nil -} - -func (p *parser) callonRPOINT1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onRPOINT1(stack["comments"], stack["t"]) -} - -func (c *current) onRPOINTToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonRPOINTToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onRPOINTToken1() -} - -func (c *current) onCOMMA1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &CommaKeyword{Keyword: kw}, nil -} - -func (p *parser) callonCOMMA1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCOMMA1(stack["comments"], stack["t"]) -} - -func (c *current) onCOMMAToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonCOMMAToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCOMMAToken1() -} - -func (c *current) onLPAR1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &LParKeyword{Keyword: kw}, nil -} - -func (p *parser) callonLPAR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLPAR1(stack["comments"], stack["t"]) -} - -func (c *current) onLPARToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonLPARToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onLPARToken1() -} - -func (c *current) onRPAR1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &RParKeyword{Keyword: kw}, nil -} - -func (p *parser) callonRPAR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onRPAR1(stack["comments"], stack["t"]) -} - -func (c *current) onRPARToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonRPARToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onRPARToken1() -} - -func (c *current) onCOLON1(comments, t any) (any, error) { - kw := NewKeyword(comments.([]*Comment), t.(*KeywordLiteral), NewLocationFromCurrent(c)) - - return &ColonKeyword{Keyword: kw}, nil -} - -func (p *parser) callonCOLON1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCOLON1(stack["comments"], stack["t"]) -} - -func (c *current) onCOLONToken1() (any, error) { - return NewKeywordLiteral(c), nil -} - -func (p *parser) callonCOLONToken1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onCOLONToken1() -} - -func (c *current) onErrFieldIndex3() error { - return InvalidFieldIndexError -} - -func (p *parser) callonErrFieldIndex3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrFieldIndex3() -} - -func (c *current) onErrFieldIndex1() (any, error) { - // 消费到冒号或本行结束 - return NewBadFieldIndex(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrFieldIndex1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrFieldIndex1() -} - -func (c *current) onErrStructField3() error { - return InvalidStructFieldError -} - -func (p *parser) callonErrStructField3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrStructField3() -} - -func (c *current) onErrStructField1() (any, error) { - return NewBadField(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrStructField1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrStructField1() -} - -func (c *current) onErrStructIdentifier3() error { - return InvalidStructIdentifierError -} - -func (p *parser) callonErrStructIdentifier3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrStructIdentifier3() -} - -func (c *current) onErrStructIdentifier1() (any, error) { - // struct identifier 异常,consume 掉异常字符直到出现 '{' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -func (p *parser) callonErrStructIdentifier1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrStructIdentifier1() -} - -func (c *current) onErrStructRCUR3() error { - return InvalidStructBlockRCURError -} - -func (p *parser) callonErrStructRCUR3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrStructRCUR3() -} - -func (c *current) onErrStructRCUR1() (any, error) { - // 缺少 '}',消费异常字符直到出现下一个 definition。TODO(jpf): 后面会加入 typedef 等定义,需要扩展补充 - return NewBadKeywordLiteral(c), nil -} - -func (p *parser) callonErrStructRCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrStructRCUR1() -} - -func (c *current) onErrStructLCUR3() error { - return InvalidStructBlockLCURError -} - -func (p *parser) callonErrStructLCUR3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrStructLCUR3() -} - -func (c *current) onErrStructLCUR1() (any, error) { - // 缺少 '{',消费异常字符直到出现下一个 Field 或者 '}' - return NewBadKeywordLiteral(c), nil -} - -func (p *parser) callonErrStructLCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrStructLCUR1() -} - -func (c *current) onErrUnionIdentifier3() error { - return InvalidUnionIdentifierError -} - -func (p *parser) callonErrUnionIdentifier3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrUnionIdentifier3() -} - -func (c *current) onErrUnionIdentifier1() (any, error) { - // identifier 异常,consume 掉异常字符直到出现 '{' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -func (p *parser) callonErrUnionIdentifier1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrUnionIdentifier1() -} - -func (c *current) onErrUnionRCUR3() error { - return InvalidUnionBlockRCURError -} - -func (p *parser) callonErrUnionRCUR3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrUnionRCUR3() -} - -func (c *current) onErrUnionRCUR1() (any, error) { - return NewBadKeywordLiteral(c), nil -} - -func (p *parser) callonErrUnionRCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrUnionRCUR1() -} - -func (c *current) onErrUnionLCUR3() error { - return InvalidUnionBlockLCURError -} - -func (p *parser) callonErrUnionLCUR3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrUnionLCUR3() -} - -func (c *current) onErrUnionLCUR1() (any, error) { - // 缺少 '{',消费异常字符直到出现下一个 Field 或者 '}' - return "?", nil -} - -func (p *parser) callonErrUnionLCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrUnionLCUR1() -} - -func (c *current) onErrUnionField3() error { - return InvalidUnionFieldError -} - -func (p *parser) callonErrUnionField3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrUnionField3() -} - -func (c *current) onErrUnionField1() (any, error) { - return NewBadField(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrUnionField1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrUnionField1() -} - -func (c *current) onErrExceptionIdentifier3() error { - return InvalidExceptionIdentifierError -} - -func (p *parser) callonErrExceptionIdentifier3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrExceptionIdentifier3() -} - -func (c *current) onErrExceptionIdentifier1() (any, error) { - // identifier 异常,consume 掉异常字符直到出现 '{' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -func (p *parser) callonErrExceptionIdentifier1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrExceptionIdentifier1() -} - -func (c *current) onErrExceptionRCUR3() error { - return InvalidExceptionBlockRCURError -} - -func (p *parser) callonErrExceptionRCUR3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrExceptionRCUR3() -} - -func (c *current) onErrExceptionRCUR1() (any, error) { - return NewBadKeywordLiteral(c), nil -} - -func (p *parser) callonErrExceptionRCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrExceptionRCUR1() -} - -func (c *current) onErrExceptionLCUR3() error { - return InvalidExceptionBlockLCURError -} - -func (p *parser) callonErrExceptionLCUR3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrExceptionLCUR3() -} - -func (c *current) onErrExceptionLCUR1() (any, error) { - // 缺少 '{',消费异常字符直到出现下一个 Field 或者 '}' - return NewBadKeywordLiteral(c), nil -} - -func (p *parser) callonErrExceptionLCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrExceptionLCUR1() -} - -func (c *current) onErrExceptionField3() error { - return InvalidExceptionFieldError -} - -func (p *parser) callonErrExceptionField3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrExceptionField3() -} - -func (c *current) onErrExceptionField1() (any, error) { - return NewBadField(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrExceptionField1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrExceptionField1() -} - -func (c *current) onErrEnumIdentifier3() error { - return InvalidEnumIdentifierError -} - -func (p *parser) callonErrEnumIdentifier3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnumIdentifier3() -} - -func (c *current) onErrEnumIdentifier1() (any, error) { - // enum identifier 异常,consume 掉异常字符直到出现 '{' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -func (p *parser) callonErrEnumIdentifier1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnumIdentifier1() -} - -func (c *current) onErrEnumRCUR3() error { - return InvalidEnumBlockRCURError -} - -func (p *parser) callonErrEnumRCUR3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnumRCUR3() -} - -func (c *current) onErrEnumRCUR1() (any, error) { - return NewBadKeywordLiteral(c), nil -} - -func (p *parser) callonErrEnumRCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnumRCUR1() -} - -func (c *current) onErrEnumLCUR3() error { - return InvalidEnumBlockLCURError -} - -func (p *parser) callonErrEnumLCUR3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnumLCUR3() -} - -func (c *current) onErrEnumLCUR1() (any, error) { - // 缺少 '{',消费异常字符直到出现下一个 Field 或者 '}' - return NewBadKeywordLiteral(c), nil -} - -func (p *parser) callonErrEnumLCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnumLCUR1() -} - -func (c *current) onErrEnumValue3() error { - return InvalidEnumValueError -} - -func (p *parser) callonErrEnumValue3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnumValue3() -} - -func (c *current) onErrEnumValue1() (any, error) { - return NewBadEnumValue(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrEnumValue1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnumValue1() -} - -func (c *current) onErrEnumValueIntConstant3() error { - return InvalidEnumValueIntConstantError -} - -func (p *parser) callonErrEnumValueIntConstant3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnumValueIntConstant3() -} - -func (c *current) onErrEnumValueIntConstant1() (any, error) { - return NewBadIntConstValue(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrEnumValueIntConstant1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnumValueIntConstant1() -} - -func (c *current) onErrTypedefIdentifier3() error { - return InvalidTypedefIdentifierError -} - -func (p *parser) callonErrTypedefIdentifier3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrTypedefIdentifier3() -} - -func (c *current) onErrTypedefIdentifier1() (any, error) { - t := NewBadIdentifier(NewLocationFromCurrent(c)) - return t, nil -} - -func (p *parser) callonErrTypedefIdentifier1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrTypedefIdentifier1() -} - -func (c *current) onErrConstIdentifier3() error { - return InvalidConstIdentifierError -} - -func (p *parser) callonErrConstIdentifier3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrConstIdentifier3() -} - -func (c *current) onErrConstIdentifier1() (any, error) { - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -func (p *parser) callonErrConstIdentifier1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrConstIdentifier1() -} - -func (c *current) onErrConstMissingValue3() error { - return InvalidConstMissingValueError -} - -func (p *parser) callonErrConstMissingValue3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrConstMissingValue3() -} - -func (c *current) onErrConstMissingValue1() (any, error) { - return NewBadConstValue(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrConstMissingValue1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrConstMissingValue1() -} - -func (c *current) onErrConstConstValue3() error { - return InvalidConstConstValueError -} - -func (p *parser) callonErrConstConstValue3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrConstConstValue3() -} - -func (c *current) onErrConstConstValue1() (any, error) { - return NewBadConstValue(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrConstConstValue1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrConstConstValue1() -} - -func (c *current) onErrServiceIdentifier3() error { - return InvalidServiceIdentifierError -} - -func (p *parser) callonErrServiceIdentifier3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrServiceIdentifier3() -} - -func (c *current) onErrServiceIdentifier1() (any, error) { - // identifier 异常,consume 掉异常字符直到出现 '{' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -func (p *parser) callonErrServiceIdentifier1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrServiceIdentifier1() -} - -func (c *current) onErrServiceRCUR3() error { - return InvalidServiceBlockRCURError -} - -func (p *parser) callonErrServiceRCUR3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrServiceRCUR3() -} - -func (c *current) onErrServiceRCUR1() (any, error) { - return NewBadKeywordLiteral(c), nil -} - -func (p *parser) callonErrServiceRCUR1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrServiceRCUR1() -} - -func (c *current) onErrServiceFunction3() error { - return InvalidServiceFunctionError -} - -func (p *parser) callonErrServiceFunction3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrServiceFunction3() -} - -func (c *current) onErrServiceFunction1() (any, error) { - return NewBadFunction(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrServiceFunction1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrServiceFunction1() -} - -func (c *current) onErrFunctionIdentifier3() error { - return InvalidFunctionIdentifierError -} - -func (p *parser) callonErrFunctionIdentifier3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrFunctionIdentifier3() -} - -func (c *current) onErrFunctionIdentifier1() (any, error) { - // identifier 异常,consume 掉异常字符直到出现 '(' 为止 - t := NewBadIdentifier(NewLocationFromCurrent(c)) - - return t, nil -} - -func (p *parser) callonErrFunctionIdentifier1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrFunctionIdentifier1() -} - -func (c *current) onErrFunctionArgument3() error { - return InvalidFunctionArgumentError -} - -func (p *parser) callonErrFunctionArgument3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrFunctionArgument3() -} - -func (c *current) onErrFunctionArgument1() (any, error) { - return NewBadField(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrFunctionArgument1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrFunctionArgument1() -} - -func (c *current) onErrLiteral1MissingRight3() error { - return InvalidLiteral1MissingRightError -} - -func (p *parser) callonErrLiteral1MissingRight3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrLiteral1MissingRight3() -} - -func (c *current) onErrLiteral1MissingRight1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadLiteral(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrLiteral1MissingRight1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrLiteral1MissingRight1() -} - -func (c *current) onErrLiteral13() error { - return InvalidLiteral1Error -} - -func (p *parser) callonErrLiteral13() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrLiteral13() -} - -func (c *current) onErrLiteral11() (any, error) { - // 消费异常字符直到这行结束 - return NewBadLiteral(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrLiteral11() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrLiteral11() -} - -func (c *current) onErrLiteral2MissingRight3() error { - return InvalidLiteral2MissingRightError -} - -func (p *parser) callonErrLiteral2MissingRight3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrLiteral2MissingRight3() -} - -func (c *current) onErrLiteral2MissingRight1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadLiteral(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrLiteral2MissingRight1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrLiteral2MissingRight1() -} - -func (c *current) onErrLiteral23() error { - return InvalidLiteral2Error -} - -func (p *parser) callonErrLiteral23() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrLiteral23() -} - -func (c *current) onErrLiteral21() (any, error) { - // 消费异常字符直到这行结束 - return NewBadLiteral(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrLiteral21() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrLiteral21() -} - -func (c *current) onErrConst3() error { - return InvalidConstError -} - -func (p *parser) callonErrConst3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrConst3() -} - -func (c *current) onErrConst1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadConst(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrConst1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrConst1() -} - -func (c *current) onErrTypedef3() error { - return InvalidTypedefError -} - -func (p *parser) callonErrTypedef3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrTypedef3() -} - -func (c *current) onErrTypedef1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadTypedef(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrTypedef1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrTypedef1() -} - -func (c *current) onErrEnum3() error { - return InvalidEnumError -} - -func (p *parser) callonErrEnum3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnum3() -} - -func (c *current) onErrEnum1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadEnum(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrEnum1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrEnum1() -} - -func (c *current) onErrService3() error { - return InvalidServiceError -} - -func (p *parser) callonErrService3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrService3() -} - -func (c *current) onErrService1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadService(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrService1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrService1() -} - -func (c *current) onErrStruct3() error { - return InvalidStructError -} - -func (p *parser) callonErrStruct3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrStruct3() -} - -func (c *current) onErrStruct1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadStruct(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrStruct1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrStruct1() -} - -func (c *current) onErrUnion3() error { - return InvalidUnionError -} - -func (p *parser) callonErrUnion3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrUnion3() -} - -func (c *current) onErrUnion1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadUnion(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrUnion1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrUnion1() -} - -func (c *current) onErrException3() error { - return InvalidExceptionError -} - -func (p *parser) callonErrException3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrException3() -} - -func (c *current) onErrException1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadException(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrException1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrException1() -} - -func (c *current) onErrDefinition3() error { - return InvalidDefinitionError -} - -func (p *parser) callonErrDefinition3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrDefinition3() -} - -func (c *current) onErrDefinition1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadDefinition(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrDefinition1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrDefinition1() -} - -func (c *current) onErrInclude3() error { - return InvalidIncludeError -} - -func (p *parser) callonErrInclude3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrInclude3() -} - -func (c *current) onErrInclude1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadInclude(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrInclude1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrInclude1() -} - -func (c *current) onErrCppInclude3() error { - return InvalidCppIncludeError -} - -func (p *parser) callonErrCppInclude3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrCppInclude3() -} - -func (c *current) onErrCppInclude1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadCPPInclude(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrCppInclude1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrCppInclude1() -} - -func (c *current) onErrNamespace3() error { - return InvalidNamespaceError -} - -func (p *parser) callonErrNamespace3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrNamespace3() -} - -func (c *current) onErrNamespace1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadNamespace(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrNamespace1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrNamespace1() -} - -func (c *current) onErrHeader3() error { - return InvalidHeaderError -} - -func (p *parser) callonErrHeader3() error { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrHeader3() -} - -func (c *current) onErrHeader1() (any, error) { - // 消费异常字符直到这行结束 - return NewBadHeader(NewLocationFromCurrent(c)), nil -} - -func (p *parser) callonErrHeader1() (any, error) { - stack := p.vstack[len(p.vstack)-1] - _ = stack - return p.cur.onErrHeader1() -} - -var ( - // errNoRule is returned when the grammar to parse has no rule. - errNoRule = errors.New("grammar has no rule") - - // errInvalidEntrypoint is returned when the specified entrypoint rule - // does not exit. - errInvalidEntrypoint = errors.New("invalid entrypoint") - - // errInvalidEncoding is returned when the source is not properly - // utf8-encoded. - errInvalidEncoding = errors.New("invalid encoding") - - // errMaxExprCnt is used to signal that the maximum number of - // expressions have been parsed. - errMaxExprCnt = errors.New("max number of expressions parsed") -) - -// Option is a function that can set an option on the parser. It returns -// the previous setting as an Option. -type Option func(*parser) Option - -// MaxExpressions creates an Option to stop parsing after the provided -// number of expressions have been parsed, if the value is 0 then the parser will -// parse for as many steps as needed (possibly an infinite number). -// -// The default for maxExprCnt is 0. -func MaxExpressions(maxExprCnt uint64) Option { - return func(p *parser) Option { - oldMaxExprCnt := p.maxExprCnt - p.maxExprCnt = maxExprCnt - return MaxExpressions(oldMaxExprCnt) - } -} - -// Entrypoint creates an Option to set the rule name to use as entrypoint. -// The rule name must have been specified in the -alternate-entrypoints -// if generating the parser with the -optimize-grammar flag, otherwise -// it may have been optimized out. Passing an empty string sets the -// entrypoint to the first rule in the grammar. -// -// The default is to start parsing at the first rule in the grammar. -func Entrypoint(ruleName string) Option { - return func(p *parser) Option { - oldEntrypoint := p.entrypoint - p.entrypoint = ruleName - if ruleName == "" { - p.entrypoint = g.rules[0].name - } - return Entrypoint(oldEntrypoint) - } -} - -// Statistics adds a user provided Stats struct to the parser to allow -// the user to process the results after the parsing has finished. -// Also the key for the "no match" counter is set. -// -// Example usage: -// -// input := "input" -// stats := Stats{} -// _, err := Parse("input-file", []byte(input), Statistics(&stats, "no match")) -// if err != nil { -// log.Panicln(err) -// } -// b, err := json.MarshalIndent(stats.ChoiceAltCnt, "", " ") -// if err != nil { -// log.Panicln(err) -// } -// fmt.Println(string(b)) -func Statistics(stats *Stats, choiceNoMatch string) Option { - return func(p *parser) Option { - oldStats := p.Stats - p.Stats = stats - oldChoiceNoMatch := p.choiceNoMatch - p.choiceNoMatch = choiceNoMatch - if p.Stats.ChoiceAltCnt == nil { - p.Stats.ChoiceAltCnt = make(map[string]map[string]int) - } - return Statistics(oldStats, oldChoiceNoMatch) - } -} - -// Debug creates an Option to set the debug flag to b. When set to true, -// debugging information is printed to stdout while parsing. -// -// The default is false. -func Debug(b bool) Option { - return func(p *parser) Option { - old := p.debug - p.debug = b - return Debug(old) - } -} - -// Memoize creates an Option to set the memoize flag to b. When set to true, -// the parser will cache all results so each expression is evaluated only -// once. This guarantees linear parsing time even for pathological cases, -// at the expense of more memory and slower times for typical cases. -// -// The default is false. -func Memoize(b bool) Option { - return func(p *parser) Option { - old := p.memoize - p.memoize = b - return Memoize(old) - } -} - -// AllowInvalidUTF8 creates an Option to allow invalid UTF-8 bytes. -// Every invalid UTF-8 byte is treated as a utf8.RuneError (U+FFFD) -// by character class matchers and is matched by the any matcher. -// The returned matched value, c.text and c.offset are NOT affected. -// -// The default is false. -func AllowInvalidUTF8(b bool) Option { - return func(p *parser) Option { - old := p.allowInvalidUTF8 - p.allowInvalidUTF8 = b - return AllowInvalidUTF8(old) - } -} - -// Recover creates an Option to set the recover flag to b. When set to -// true, this causes the parser to recover from panics and convert it -// to an error. Setting it to false can be useful while debugging to -// access the full stack trace. -// -// The default is true. -func Recover(b bool) Option { - return func(p *parser) Option { - old := p.recover - p.recover = b - return Recover(old) - } -} - -// GlobalStore creates an Option to set a key to a certain value in -// the globalStore. -func GlobalStore(key string, value any) Option { - return func(p *parser) Option { - old := p.cur.globalStore[key] - p.cur.globalStore[key] = value - return GlobalStore(key, old) - } -} - -// InitState creates an Option to set a key to a certain value in -// the global "state" store. -func InitState(key string, value any) Option { - return func(p *parser) Option { - old := p.cur.state[key] - p.cur.state[key] = value - return InitState(key, old) - } -} - -// ParseFile parses the file identified by filename. -func ParseFile(filename string, opts ...Option) (i any, err error) { - f, err := os.Open(filename) - if err != nil { - return nil, err - } - defer func() { - if closeErr := f.Close(); closeErr != nil { - err = closeErr - } - }() - return ParseReader(filename, f, opts...) -} - -// ParseReader parses the data from r using filename as information in the -// error messages. -func ParseReader(filename string, r io.Reader, opts ...Option) (any, error) { - b, err := io.ReadAll(r) - if err != nil { - return nil, err - } - - return Parse(filename, b, opts...) -} - -// Parse parses the data from b using filename as information in the -// error messages. -func Parse(filename string, b []byte, opts ...Option) (any, error) { - return newParser(filename, b, opts...).parse(g) -} - -// position records a position in the text. -type position struct { - line, col, offset int -} - -func (p position) String() string { - return strconv.Itoa(p.line) + ":" + strconv.Itoa(p.col) + " [" + strconv.Itoa(p.offset) + "]" -} - -// savepoint stores all state required to go back to this point in the -// parser. -type savepoint struct { - position - rn rune - w int -} - -type current struct { - pos position // start position of the match - text []byte // raw text of the match - - // state is a store for arbitrary key,value pairs that the user wants to be - // tied to the backtracking of the parser. - // This is always rolled back if a parsing rule fails. - state storeDict - - // globalStore is a general store for the user to store arbitrary key-value - // pairs that they need to manage and that they do not want tied to the - // backtracking of the parser. This is only modified by the user and never - // rolled back by the parser. It is always up to the user to keep this in a - // consistent state. - globalStore storeDict -} - -type storeDict map[string]any - -// the AST types... - -type grammar struct { - pos position - rules []*rule -} - -type rule struct { - pos position - name string - displayName string - expr any -} - -type choiceExpr struct { - pos position - alternatives []any -} - -type actionExpr struct { - pos position - expr any - run func(*parser) (any, error) -} - -type recoveryExpr struct { - pos position - expr any - recoverExpr any - failureLabel []string -} - -type seqExpr struct { - pos position - exprs []any -} - -type throwExpr struct { - pos position - label string -} - -type labeledExpr struct { - pos position - label string - expr any -} - -type expr struct { - pos position - expr any -} - -type ( - andExpr expr - notExpr expr - zeroOrOneExpr expr - zeroOrMoreExpr expr - oneOrMoreExpr expr -) - -type ruleRefExpr struct { - pos position - name string -} - -type stateCodeExpr struct { - pos position - run func(*parser) error -} - -type andCodeExpr struct { - pos position - run func(*parser) (bool, error) -} - -type notCodeExpr struct { - pos position - run func(*parser) (bool, error) -} - -type litMatcher struct { - pos position - val string - ignoreCase bool - want string -} - -type charClassMatcher struct { - pos position - val string - basicLatinChars [128]bool - chars []rune - ranges []rune - classes []*unicode.RangeTable - ignoreCase bool - inverted bool -} - -type anyMatcher position - -// errList cumulates the errors found by the parser. -type errList []error - -func (e *errList) add(err error) { - *e = append(*e, err) -} - -func (e errList) err() error { - if len(e) == 0 { - return nil - } - e.dedupe() - return e -} - -func (e *errList) dedupe() { - var cleaned []error - set := make(map[string]bool) - for _, err := range *e { - if msg := err.Error(); !set[msg] { - set[msg] = true - cleaned = append(cleaned, err) - } - } - *e = cleaned -} - -func (e errList) Error() string { - switch len(e) { - case 0: - return "" - case 1: - return e[0].Error() - default: - var buf bytes.Buffer - - for i, err := range e { - if i > 0 { - buf.WriteRune('\n') - } - buf.WriteString(err.Error()) - } - return buf.String() - } -} - -// parserError wraps an error with a prefix indicating the rule in which -// the error occurred. The original error is stored in the Inner field. -type parserError struct { - Inner error - pos position - prefix string - expected []string -} - -// Error returns the error message. -func (p *parserError) Error() string { - return p.prefix + ": " + p.Inner.Error() -} - -// newParser creates a parser with the specified input source and options. -func newParser(filename string, b []byte, opts ...Option) *parser { - stats := Stats{ - ChoiceAltCnt: make(map[string]map[string]int), - } - - p := &parser{ - filename: filename, - errs: new(errList), - data: b, - pt: savepoint{position: position{line: 1}}, - recover: true, - cur: current{ - state: make(storeDict), - globalStore: make(storeDict), - }, - maxFailPos: position{col: 1, line: 1}, - maxFailExpected: make([]string, 0, 20), - Stats: &stats, - // start rule is rule [0] unless an alternate entrypoint is specified - entrypoint: g.rules[0].name, - } - p.setOptions(opts) - - if p.maxExprCnt == 0 { - p.maxExprCnt = math.MaxUint64 - } - - return p -} - -// setOptions applies the options to the parser. -func (p *parser) setOptions(opts []Option) { - for _, opt := range opts { - opt(p) - } -} - -type resultTuple struct { - v any - b bool - end savepoint -} - -const choiceNoMatch = -1 - -// Stats stores some statistics, gathered during parsing -type Stats struct { - // ExprCnt counts the number of expressions processed during parsing - // This value is compared to the maximum number of expressions allowed - // (set by the MaxExpressions option). - ExprCnt uint64 - - // ChoiceAltCnt is used to count for each ordered choice expression, - // which alternative is used how may times. - // These numbers allow to optimize the order of the ordered choice expression - // to increase the performance of the parser - // - // The outer key of ChoiceAltCnt is composed of the name of the rule as well - // as the line and the column of the ordered choice. - // The inner key of ChoiceAltCnt is the number (one-based) of the matching alternative. - // For each alternative the number of matches are counted. If an ordered choice does not - // match, a special counter is incremented. The name of this counter is set with - // the parser option Statistics. - // For an alternative to be included in ChoiceAltCnt, it has to match at least once. - ChoiceAltCnt map[string]map[string]int -} - -type parser struct { - filename string - pt savepoint - cur current - - data []byte - errs *errList - - depth int - recover bool - debug bool - - memoize bool - // memoization table for the packrat algorithm: - // map[offset in source] map[expression or rule] {value, match} - memo map[int]map[any]resultTuple - - // rules table, maps the rule identifier to the rule node - rules map[string]*rule - // variables stack, map of label to value - vstack []map[string]any - // rule stack, allows identification of the current rule in errors - rstack []*rule - - // parse fail - maxFailPos position - maxFailExpected []string - maxFailInvertExpected bool - - // max number of expressions to be parsed - maxExprCnt uint64 - // entrypoint for the parser - entrypoint string - - allowInvalidUTF8 bool - - *Stats - - choiceNoMatch string - // recovery expression stack, keeps track of the currently available recovery expression, these are traversed in reverse - recoveryStack []map[string]any -} - -// push a variable set on the vstack. -func (p *parser) pushV() { - if cap(p.vstack) == len(p.vstack) { - // create new empty slot in the stack - p.vstack = append(p.vstack, nil) - } else { - // slice to 1 more - p.vstack = p.vstack[:len(p.vstack)+1] - } - - // get the last args set - m := p.vstack[len(p.vstack)-1] - if m != nil && len(m) == 0 { - // empty map, all good - return - } - - m = make(map[string]any) - p.vstack[len(p.vstack)-1] = m -} - -// pop a variable set from the vstack. -func (p *parser) popV() { - // if the map is not empty, clear it - m := p.vstack[len(p.vstack)-1] - if len(m) > 0 { - // GC that map - p.vstack[len(p.vstack)-1] = nil - } - p.vstack = p.vstack[:len(p.vstack)-1] -} - -// push a recovery expression with its labels to the recoveryStack -func (p *parser) pushRecovery(labels []string, expr any) { - if cap(p.recoveryStack) == len(p.recoveryStack) { - // create new empty slot in the stack - p.recoveryStack = append(p.recoveryStack, nil) - } else { - // slice to 1 more - p.recoveryStack = p.recoveryStack[:len(p.recoveryStack)+1] - } - - m := make(map[string]any, len(labels)) - for _, fl := range labels { - m[fl] = expr - } - p.recoveryStack[len(p.recoveryStack)-1] = m -} - -// pop a recovery expression from the recoveryStack -func (p *parser) popRecovery() { - // GC that map - p.recoveryStack[len(p.recoveryStack)-1] = nil - - p.recoveryStack = p.recoveryStack[:len(p.recoveryStack)-1] -} - -func (p *parser) print(prefix, s string) string { - if !p.debug { - return s - } - - fmt.Printf("%s %d:%d:%d: %s [%#U]\n", - prefix, p.pt.line, p.pt.col, p.pt.offset, s, p.pt.rn) - return s -} - -func (p *parser) printIndent(mark string, s string) string { - return p.print(strings.Repeat(" ", p.depth)+mark, s) -} - -func (p *parser) in(s string) string { - res := p.printIndent(">", s) - p.depth++ - return res -} - -func (p *parser) out(s string) string { - p.depth-- - return p.printIndent("<", s) -} - -func (p *parser) addErr(err error) { - p.addErrAt(err, p.pt.position, []string{}) -} - -func (p *parser) addErrAt(err error, pos position, expected []string) { - var buf bytes.Buffer - if p.filename != "" { - buf.WriteString(p.filename) - } - if buf.Len() > 0 { - buf.WriteString(":") - } - buf.WriteString(fmt.Sprintf("%d:%d (%d)", pos.line, pos.col, pos.offset)) - if len(p.rstack) > 0 { - if buf.Len() > 0 { - buf.WriteString(": ") - } - rule := p.rstack[len(p.rstack)-1] - if rule.displayName != "" { - buf.WriteString("rule " + rule.displayName) - } else { - buf.WriteString("rule " + rule.name) - } - } - pe := &parserError{Inner: err, pos: pos, prefix: buf.String(), expected: expected} - p.errs.add(pe) -} - -func (p *parser) failAt(fail bool, pos position, want string) { - // process fail if parsing fails and not inverted or parsing succeeds and invert is set - if fail == p.maxFailInvertExpected { - if pos.offset < p.maxFailPos.offset { - return - } - - if pos.offset > p.maxFailPos.offset { - p.maxFailPos = pos - p.maxFailExpected = p.maxFailExpected[:0] - } - - if p.maxFailInvertExpected { - want = "!" + want - } - p.maxFailExpected = append(p.maxFailExpected, want) - } -} - -// read advances the parser to the next rune. -func (p *parser) read() { - p.pt.offset += p.pt.w - rn, n := utf8.DecodeRune(p.data[p.pt.offset:]) - p.pt.rn = rn - p.pt.w = n - p.pt.col++ - if rn == '\n' { - p.pt.line++ - p.pt.col = 0 - } - - if rn == utf8.RuneError && n == 1 { // see utf8.DecodeRune - if !p.allowInvalidUTF8 { - p.addErr(errInvalidEncoding) - } - } -} - -// restore parser position to the savepoint pt. -func (p *parser) restore(pt savepoint) { - if p.debug { - defer p.out(p.in("restore")) - } - if pt.offset == p.pt.offset { - return - } - p.pt = pt -} - -// Cloner is implemented by any value that has a Clone method, which returns a -// copy of the value. This is mainly used for types which are not passed by -// value (e.g map, slice, chan) or structs that contain such types. -// -// This is used in conjunction with the global state feature to create proper -// copies of the state to allow the parser to properly restore the state in -// the case of backtracking. -type Cloner interface { - Clone() any -} - -var statePool = &sync.Pool{ - New: func() any { return make(storeDict) }, -} - -func (sd storeDict) Discard() { - for k := range sd { - delete(sd, k) - } - statePool.Put(sd) -} - -// clone and return parser current state. -func (p *parser) cloneState() storeDict { - if p.debug { - defer p.out(p.in("cloneState")) - } - - state := statePool.Get().(storeDict) - for k, v := range p.cur.state { - if c, ok := v.(Cloner); ok { - state[k] = c.Clone() - } else { - state[k] = v - } - } - return state -} - -// restore parser current state to the state storeDict. -// every restoreState should applied only one time for every cloned state -func (p *parser) restoreState(state storeDict) { - if p.debug { - defer p.out(p.in("restoreState")) - } - p.cur.state.Discard() - p.cur.state = state -} - -// get the slice of bytes from the savepoint start to the current position. -func (p *parser) sliceFrom(start savepoint) []byte { - return p.data[start.position.offset:p.pt.position.offset] -} - -func (p *parser) getMemoized(node any) (resultTuple, bool) { - if len(p.memo) == 0 { - return resultTuple{}, false - } - m := p.memo[p.pt.offset] - if len(m) == 0 { - return resultTuple{}, false - } - res, ok := m[node] - return res, ok -} - -func (p *parser) setMemoized(pt savepoint, node any, tuple resultTuple) { - if p.memo == nil { - p.memo = make(map[int]map[any]resultTuple) - } - m := p.memo[pt.offset] - if m == nil { - m = make(map[any]resultTuple) - p.memo[pt.offset] = m - } - m[node] = tuple -} - -func (p *parser) buildRulesTable(g *grammar) { - p.rules = make(map[string]*rule, len(g.rules)) - for _, r := range g.rules { - p.rules[r.name] = r - } -} - -func (p *parser) parse(g *grammar) (val any, err error) { - if len(g.rules) == 0 { - p.addErr(errNoRule) - return nil, p.errs.err() - } - - // TODO : not super critical but this could be generated - p.buildRulesTable(g) - - if p.recover { - // panic can be used in action code to stop parsing immediately - // and return the panic as an error. - defer func() { - if e := recover(); e != nil { - if p.debug { - defer p.out(p.in("panic handler")) - } - val = nil - switch e := e.(type) { - case error: - p.addErr(e) - default: - p.addErr(fmt.Errorf("%v", e)) - } - err = p.errs.err() - } - }() - } - - startRule, ok := p.rules[p.entrypoint] - if !ok { - p.addErr(errInvalidEntrypoint) - return nil, p.errs.err() - } - - p.read() // advance to first rune - val, ok = p.parseRuleWrap(startRule) - if !ok { - if len(*p.errs) == 0 { - // If parsing fails, but no errors have been recorded, the expected values - // for the farthest parser position are returned as error. - maxFailExpectedMap := make(map[string]struct{}, len(p.maxFailExpected)) - for _, v := range p.maxFailExpected { - maxFailExpectedMap[v] = struct{}{} - } - expected := make([]string, 0, len(maxFailExpectedMap)) - eof := false - if _, ok := maxFailExpectedMap["!."]; ok { - delete(maxFailExpectedMap, "!.") - eof = true - } - for k := range maxFailExpectedMap { - expected = append(expected, k) - } - sort.Strings(expected) - if eof { - expected = append(expected, "EOF") - } - p.addErrAt(errors.New("no match found, expected: "+listJoin(expected, ", ", "or")), p.maxFailPos, expected) - } - - return nil, p.errs.err() - } - return val, p.errs.err() -} - -func listJoin(list []string, sep string, lastSep string) string { - switch len(list) { - case 0: - return "" - case 1: - return list[0] - default: - return strings.Join(list[:len(list)-1], sep) + " " + lastSep + " " + list[len(list)-1] - } -} - -func (p *parser) parseRuleMemoize(rule *rule) (any, bool) { - res, ok := p.getMemoized(rule) - if ok { - p.restore(res.end) - return res.v, res.b - } - - startMark := p.pt - val, ok := p.parseRule(rule) - p.setMemoized(startMark, rule, resultTuple{val, ok, p.pt}) - - return val, ok -} - -func (p *parser) parseRuleWrap(rule *rule) (any, bool) { - if p.debug { - defer p.out(p.in("parseRule " + rule.name)) - } - var ( - val any - ok bool - startMark = p.pt - ) - - if p.memoize { - val, ok = p.parseRuleMemoize(rule) - } else { - val, ok = p.parseRule(rule) - } - - if ok && p.debug { - p.printIndent("MATCH", string(p.sliceFrom(startMark))) - } - return val, ok -} - -func (p *parser) parseRule(rule *rule) (any, bool) { - p.rstack = append(p.rstack, rule) - p.pushV() - val, ok := p.parseExprWrap(rule.expr) - p.popV() - p.rstack = p.rstack[:len(p.rstack)-1] - return val, ok -} - -func (p *parser) parseExprWrap(expr any) (any, bool) { - var pt savepoint - - if p.memoize { - res, ok := p.getMemoized(expr) - if ok { - p.restore(res.end) - return res.v, res.b - } - pt = p.pt - } - - val, ok := p.parseExpr(expr) - - if p.memoize { - p.setMemoized(pt, expr, resultTuple{val, ok, p.pt}) - } - return val, ok -} - -func (p *parser) parseExpr(expr any) (any, bool) { - p.ExprCnt++ - if p.ExprCnt > p.maxExprCnt { - panic(errMaxExprCnt) - } - - var val any - var ok bool - switch expr := expr.(type) { - case *actionExpr: - val, ok = p.parseActionExpr(expr) - case *andCodeExpr: - val, ok = p.parseAndCodeExpr(expr) - case *andExpr: - val, ok = p.parseAndExpr(expr) - case *anyMatcher: - val, ok = p.parseAnyMatcher(expr) - case *charClassMatcher: - val, ok = p.parseCharClassMatcher(expr) - case *choiceExpr: - val, ok = p.parseChoiceExpr(expr) - case *labeledExpr: - val, ok = p.parseLabeledExpr(expr) - case *litMatcher: - val, ok = p.parseLitMatcher(expr) - case *notCodeExpr: - val, ok = p.parseNotCodeExpr(expr) - case *notExpr: - val, ok = p.parseNotExpr(expr) - case *oneOrMoreExpr: - val, ok = p.parseOneOrMoreExpr(expr) - case *recoveryExpr: - val, ok = p.parseRecoveryExpr(expr) - case *ruleRefExpr: - val, ok = p.parseRuleRefExpr(expr) - case *seqExpr: - val, ok = p.parseSeqExpr(expr) - case *stateCodeExpr: - val, ok = p.parseStateCodeExpr(expr) - case *throwExpr: - val, ok = p.parseThrowExpr(expr) - case *zeroOrMoreExpr: - val, ok = p.parseZeroOrMoreExpr(expr) - case *zeroOrOneExpr: - val, ok = p.parseZeroOrOneExpr(expr) - default: - panic(fmt.Sprintf("unknown expression type %T", expr)) - } - return val, ok -} - -func (p *parser) parseActionExpr(act *actionExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseActionExpr")) - } - - start := p.pt - val, ok := p.parseExprWrap(act.expr) - if ok { - p.cur.pos = start.position - p.cur.text = p.sliceFrom(start) - state := p.cloneState() - actVal, err := act.run(p) - if err != nil { - p.addErrAt(err, start.position, []string{}) - } - p.restoreState(state) - - val = actVal - } - if ok && p.debug { - p.printIndent("MATCH", string(p.sliceFrom(start))) - } - return val, ok -} - -func (p *parser) parseAndCodeExpr(and *andCodeExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseAndCodeExpr")) - } - - state := p.cloneState() - - ok, err := and.run(p) - if err != nil { - p.addErr(err) - } - p.restoreState(state) - - return nil, ok -} - -func (p *parser) parseAndExpr(and *andExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseAndExpr")) - } - - pt := p.pt - state := p.cloneState() - p.pushV() - _, ok := p.parseExprWrap(and.expr) - p.popV() - p.restoreState(state) - p.restore(pt) - - return nil, ok -} - -func (p *parser) parseAnyMatcher(any *anyMatcher) (any, bool) { - if p.debug { - defer p.out(p.in("parseAnyMatcher")) - } - - if p.pt.rn == utf8.RuneError && p.pt.w == 0 { - // EOF - see utf8.DecodeRune - p.failAt(false, p.pt.position, ".") - return nil, false - } - start := p.pt - p.read() - p.failAt(true, start.position, ".") - return p.sliceFrom(start), true -} - -func (p *parser) parseCharClassMatcher(chr *charClassMatcher) (any, bool) { - if p.debug { - defer p.out(p.in("parseCharClassMatcher")) - } - - cur := p.pt.rn - start := p.pt - - // can't match EOF - if cur == utf8.RuneError && p.pt.w == 0 { // see utf8.DecodeRune - p.failAt(false, start.position, chr.val) - return nil, false - } - - if chr.ignoreCase { - cur = unicode.ToLower(cur) - } - - // try to match in the list of available chars - for _, rn := range chr.chars { - if rn == cur { - if chr.inverted { - p.failAt(false, start.position, chr.val) - return nil, false - } - p.read() - p.failAt(true, start.position, chr.val) - return p.sliceFrom(start), true - } - } - - // try to match in the list of ranges - for i := 0; i < len(chr.ranges); i += 2 { - if cur >= chr.ranges[i] && cur <= chr.ranges[i+1] { - if chr.inverted { - p.failAt(false, start.position, chr.val) - return nil, false - } - p.read() - p.failAt(true, start.position, chr.val) - return p.sliceFrom(start), true - } - } - - // try to match in the list of Unicode classes - for _, cl := range chr.classes { - if unicode.Is(cl, cur) { - if chr.inverted { - p.failAt(false, start.position, chr.val) - return nil, false - } - p.read() - p.failAt(true, start.position, chr.val) - return p.sliceFrom(start), true - } - } - - if chr.inverted { - p.read() - p.failAt(true, start.position, chr.val) - return p.sliceFrom(start), true - } - p.failAt(false, start.position, chr.val) - return nil, false -} - -func (p *parser) incChoiceAltCnt(ch *choiceExpr, altI int) { - choiceIdent := fmt.Sprintf("%s %d:%d", p.rstack[len(p.rstack)-1].name, ch.pos.line, ch.pos.col) - m := p.ChoiceAltCnt[choiceIdent] - if m == nil { - m = make(map[string]int) - p.ChoiceAltCnt[choiceIdent] = m - } - // We increment altI by 1, so the keys do not start at 0 - alt := strconv.Itoa(altI + 1) - if altI == choiceNoMatch { - alt = p.choiceNoMatch - } - m[alt]++ -} - -func (p *parser) parseChoiceExpr(ch *choiceExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseChoiceExpr")) - } - - for altI, alt := range ch.alternatives { - // dummy assignment to prevent compile error if optimized - _ = altI - - state := p.cloneState() - - p.pushV() - val, ok := p.parseExprWrap(alt) - p.popV() - if ok { - p.incChoiceAltCnt(ch, altI) - return val, ok - } - p.restoreState(state) - } - p.incChoiceAltCnt(ch, choiceNoMatch) - return nil, false -} - -func (p *parser) parseLabeledExpr(lab *labeledExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseLabeledExpr")) - } - - p.pushV() - val, ok := p.parseExprWrap(lab.expr) - p.popV() - if ok && lab.label != "" { - m := p.vstack[len(p.vstack)-1] - m[lab.label] = val - } - return val, ok -} - -func (p *parser) parseLitMatcher(lit *litMatcher) (any, bool) { - if p.debug { - defer p.out(p.in("parseLitMatcher")) - } - - start := p.pt - for _, want := range lit.val { - cur := p.pt.rn - if lit.ignoreCase { - cur = unicode.ToLower(cur) - } - if cur != want { - p.failAt(false, start.position, lit.want) - p.restore(start) - return nil, false - } - p.read() - } - p.failAt(true, start.position, lit.want) - return p.sliceFrom(start), true -} - -func (p *parser) parseNotCodeExpr(not *notCodeExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseNotCodeExpr")) - } - - state := p.cloneState() - - ok, err := not.run(p) - if err != nil { - p.addErr(err) - } - p.restoreState(state) - - return nil, !ok -} - -func (p *parser) parseNotExpr(not *notExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseNotExpr")) - } - - pt := p.pt - state := p.cloneState() - p.pushV() - p.maxFailInvertExpected = !p.maxFailInvertExpected - _, ok := p.parseExprWrap(not.expr) - p.maxFailInvertExpected = !p.maxFailInvertExpected - p.popV() - p.restoreState(state) - p.restore(pt) - - return nil, !ok -} - -func (p *parser) parseOneOrMoreExpr(expr *oneOrMoreExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseOneOrMoreExpr")) - } - - var vals []any - - for { - p.pushV() - val, ok := p.parseExprWrap(expr.expr) - p.popV() - if !ok { - if len(vals) == 0 { - // did not match once, no match - return nil, false - } - return vals, true - } - vals = append(vals, val) - } -} - -func (p *parser) parseRecoveryExpr(recover *recoveryExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseRecoveryExpr (" + strings.Join(recover.failureLabel, ",") + ")")) - } - - p.pushRecovery(recover.failureLabel, recover.recoverExpr) - val, ok := p.parseExprWrap(recover.expr) - p.popRecovery() - - return val, ok -} - -func (p *parser) parseRuleRefExpr(ref *ruleRefExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseRuleRefExpr " + ref.name)) - } - - if ref.name == "" { - panic(fmt.Sprintf("%s: invalid rule: missing name", ref.pos)) - } - - rule := p.rules[ref.name] - if rule == nil { - p.addErr(fmt.Errorf("undefined rule: %s", ref.name)) - return nil, false - } - return p.parseRuleWrap(rule) -} - -func (p *parser) parseSeqExpr(seq *seqExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseSeqExpr")) - } - - vals := make([]any, 0, len(seq.exprs)) - - pt := p.pt - state := p.cloneState() - for _, expr := range seq.exprs { - val, ok := p.parseExprWrap(expr) - if !ok { - p.restoreState(state) - p.restore(pt) - return nil, false - } - vals = append(vals, val) - } - return vals, true -} - -func (p *parser) parseStateCodeExpr(state *stateCodeExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseStateCodeExpr")) - } - - err := state.run(p) - if err != nil { - p.addErr(err) - } - return nil, true -} - -func (p *parser) parseThrowExpr(expr *throwExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseThrowExpr")) - } - - for i := len(p.recoveryStack) - 1; i >= 0; i-- { - if recoverExpr, ok := p.recoveryStack[i][expr.label]; ok { - if val, ok := p.parseExprWrap(recoverExpr); ok { - return val, ok - } - } - } - - return nil, false -} - -func (p *parser) parseZeroOrMoreExpr(expr *zeroOrMoreExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseZeroOrMoreExpr")) - } - - var vals []any - - for { - p.pushV() - val, ok := p.parseExprWrap(expr.expr) - p.popV() - if !ok { - return vals, true - } - vals = append(vals, val) - } -} - -func (p *parser) parseZeroOrOneExpr(expr *zeroOrOneExpr) (any, bool) { - if p.debug { - defer p.out(p.in("parseZeroOrOneExpr")) - } - - p.pushV() - val, _ := p.parseExprWrap(expr.expr) - p.popV() - // whether it matched or not, consider it a match - return val, true -} diff --git a/parser/util.go b/parser/util.go deleted file mode 100644 index d8ef563..0000000 --- a/parser/util.go +++ /dev/null @@ -1,5 +0,0 @@ -package parser - -func StringPointer(s string) *string { - return &s -} diff --git a/resolver/integration_test.go b/resolver/integration_test.go index 1be51ce..89b39df 100644 --- a/resolver/integration_test.go +++ b/resolver/integration_test.go @@ -1,11 +1,13 @@ package resolver import ( + "fmt" "os" "path/filepath" + "strings" "testing" - "github.com/joyme123/thrift-ls/parser" + "github.com/karitham/thrift-ls/syntax" ) func TestResolver_Integration_NestedIncludes(t *testing.T) { @@ -13,7 +15,7 @@ func TestResolver_Integration_NestedIncludes(t *testing.T) { tmpDir := t.TempDir() includeDir := filepath.Join(tmpDir, "includes") - if err := os.MkdirAll(includeDir, 0755); err != nil { + if err := os.MkdirAll(includeDir, 0o755); err != nil { t.Fatal(err) } @@ -24,7 +26,7 @@ func TestResolver_Integration_NestedIncludes(t *testing.T) { struct BaseID { 1: string value }` - if err := os.WriteFile(baseFile, []byte(baseContent), 0644); err != nil { + if err := os.WriteFile(baseFile, []byte(baseContent), 0o644); err != nil { t.Fatal(err) } @@ -38,7 +40,7 @@ struct UserID { 1: BaseID id 2: string name }` - if err := os.WriteFile(middleFile, []byte(middleContent), 0644); err != nil { + if err := os.WriteFile(middleFile, []byte(middleContent), 0o644); err != nil { t.Fatal(err) } @@ -51,53 +53,49 @@ namespace * main struct User { 1: UserID user }` - if err := os.WriteFile(mainFile, []byte(mainContent), 0644); err != nil { + if err := os.WriteFile(mainFile, []byte(mainContent), 0o644); err != nil { t.Fatal(err) } // Create resolver with include path r := New([]string{includeDir}) - // Test that IncludeCall resolves nested includes - includeCall := r.IncludeCall(mainFile) - - // First level: resolve middle.thrift - filename, content, err := includeCall("middle.thrift") - if err != nil { - t.Fatalf("failed to resolve middle.thrift: %v", err) - } + // Resolve middle.thrift from main.thrift's perspective + filename := r.Resolve(mainFile, "middle.thrift") if filename != middleFile { t.Errorf("expected %q, got %q", middleFile, filename) } - - // Parse middle.thrift to get its includes - psr := parser.PEGParser{} - middleDoc, parseErrs := psr.Parse(filename, content) - if len(parseErrs) > 0 { - t.Fatalf("failed to parse middle.thrift: %v", parseErrs) + content, err := os.ReadFile(filename) + if err != nil { + t.Fatalf("failed to read middle.thrift: %v", err) } - // Check that middle.thrift has the expected include - doc := middleDoc - if len(doc.Includes) == 0 { + // Parse middle.thrift to find its includes + middleDoc, errs := syntax.Parse(content) + for _, e := range errs { + if e.Severity == syntax.SeverityError { + t.Fatalf("failed to parse middle.thrift: %v", errs) + } + } + if len(middleDoc.Includes()) == 0 { t.Fatal("expected middle.thrift to have includes") } - if doc.Includes[0].Path.Value.Text != "base.thrift" { - t.Errorf("expected include path 'base.thrift', got %q", doc.Includes[0].Path.Value.Text) + includePath := middleDoc.Includes()[0].Path.Text + if strings.Trim(includePath, "\"'") != "base.thrift" { + t.Errorf("expected include path 'base.thrift', got %q", includePath) } - // Second level: resolve base.thrift from middle.thrift's perspective - // Create a new include call from middle file's perspective - includeCallFromMiddle := r.IncludeCall(middleFile) - filename2, content2, err := includeCallFromMiddle("base.thrift") - if err != nil { - t.Fatalf("failed to resolve base.thrift: %v", err) - } + // Resolve base.thrift from middle.thrift's perspective + filename2 := r.Resolve(middleFile, strings.Trim(includePath, "\"'")) if filename2 != baseFile { t.Errorf("expected %q, got %q", baseFile, filename2) } // Verify content + content2, err := os.ReadFile(filename2) + if err != nil { + t.Fatalf("failed to read base.thrift: %v", err) + } if string(content2) != baseContent { t.Errorf("base.thrift content mismatch") } @@ -109,10 +107,10 @@ func TestResolver_Integration_ResolutionOrder(t *testing.T) { includeDir := filepath.Join(tmpDir, "includes") srcDir := filepath.Join(tmpDir, "src") - if err := os.MkdirAll(includeDir, 0755); err != nil { + if err := os.MkdirAll(includeDir, 0o755); err != nil { t.Fatal(err) } - if err := os.MkdirAll(srcDir, 0755); err != nil { + if err := os.MkdirAll(srcDir, 0o755); err != nil { t.Fatal(err) } @@ -123,7 +121,7 @@ func TestResolver_Integration_ResolutionOrder(t *testing.T) { struct SharedInInclude { 1: string value }` - if err := os.WriteFile(includeVersion, []byte(includeContent), 0644); err != nil { + if err := os.WriteFile(includeVersion, []byte(includeContent), 0o644); err != nil { t.Fatal(err) } @@ -134,7 +132,7 @@ struct SharedInInclude { struct SharedInSrc { 1: string name }` - if err := os.WriteFile(srcVersion, []byte(srcContent), 0644); err != nil { + if err := os.WriteFile(srcVersion, []byte(srcContent), 0o644); err != nil { t.Fatal(err) } @@ -147,7 +145,7 @@ namespace * test struct Data { 1: string field }` - if err := os.WriteFile(mainFile, []byte(mainContent), 0644); err != nil { + if err := os.WriteFile(mainFile, []byte(mainContent), 0o644); err != nil { t.Fatal(err) } @@ -181,7 +179,7 @@ func TestResolver_Integration_RelativeFallback(t *testing.T) { struct LocalData { 1: string value }` - if err := os.WriteFile(localFile, []byte(localContent), 0644); err != nil { + if err := os.WriteFile(localFile, []byte(localContent), 0o644); err != nil { t.Fatal(err) } @@ -194,7 +192,7 @@ namespace * main struct Container { 1: LocalData data }` - if err := os.WriteFile(mainFile, []byte(mainContent), 0644); err != nil { + if err := os.WriteFile(mainFile, []byte(mainContent), 0o644); err != nil { t.Fatal(err) } @@ -222,10 +220,10 @@ func TestResolver_Integration_MultipleIncludePaths(t *testing.T) { includeDir1 := filepath.Join(tmpDir, "includes1") includeDir2 := filepath.Join(tmpDir, "includes2") - if err := os.MkdirAll(includeDir1, 0755); err != nil { + if err := os.MkdirAll(includeDir1, 0o755); err != nil { t.Fatal(err) } - if err := os.MkdirAll(includeDir2, 0755); err != nil { + if err := os.MkdirAll(includeDir2, 0o755); err != nil { t.Fatal(err) } @@ -236,7 +234,7 @@ func TestResolver_Integration_MultipleIncludePaths(t *testing.T) { struct UniqueInDir2 { 1: string value }` - if err := os.WriteFile(file2, []byte(content2), 0644); err != nil { + if err := os.WriteFile(file2, []byte(content2), 0o644); err != nil { t.Fatal(err) } @@ -247,7 +245,7 @@ struct UniqueInDir2 { struct BothFromDir1 { 1: string value }` - if err := os.WriteFile(fileBoth1, []byte(contentBoth1), 0644); err != nil { + if err := os.WriteFile(fileBoth1, []byte(contentBoth1), 0o644); err != nil { t.Fatal(err) } @@ -257,14 +255,14 @@ struct BothFromDir1 { struct BothFromDir2 { 1: string name }` - if err := os.WriteFile(fileBoth2, []byte(contentBoth2), 0644); err != nil { + if err := os.WriteFile(fileBoth2, []byte(contentBoth2), 0o644); err != nil { t.Fatal(err) } mainFile := filepath.Join(tmpDir, "main.thrift") mainContent := `include "unique.thrift" include "both.thrift"` - if err := os.WriteFile(mainFile, []byte(mainContent), 0644); err != nil { + if err := os.WriteFile(mainFile, []byte(mainContent), 0o644); err != nil { t.Fatal(err) } @@ -291,69 +289,12 @@ include "both.thrift"` } } -func TestResolver_Integration_ParseRecursively(t *testing.T) { - // Test using resolver with parser's ParseRecursively - tmpDir := t.TempDir() - includeDir := filepath.Join(tmpDir, "includes") - - if err := os.MkdirAll(includeDir, 0755); err != nil { - t.Fatal(err) - } - - // Create types.thrift - typesFile := filepath.Join(includeDir, "types.thrift") - typesContent := `namespace * types - -struct Address { - 1: string street - 2: string city -}` - if err := os.WriteFile(typesFile, []byte(typesContent), 0644); err != nil { - t.Fatal(err) - } - - // Create user.thrift that includes types - userFile := filepath.Join(tmpDir, "user.thrift") - userContent := `include "types.thrift" - -namespace * user - -struct User { - 1: string name - 2: Address address -}` - if err := os.WriteFile(userFile, []byte(userContent), 0644); err != nil { - t.Fatal(err) - } - - // Parse recursively using resolver - r := New([]string{includeDir}) - psr := parser.PEGParser{} - - results := psr.ParseRecursively(userFile, []byte(userContent), 0, r.IncludeCall(userFile)) - - if len(results) == 0 || results[0].Doc == nil { - t.Fatal("expected parsed document") - } - - // Verify the include was resolved - doc := results[0].Doc - if len(doc.Includes) == 0 { - t.Fatal("expected includes in parsed document") - } - - // Check that nested document was loaded (results[1:] contains included documents) - if len(results) < 2 { - t.Fatal("expected included documents to be loaded in results") - } -} - func TestResolver_Integration_DeeplyNestedIncludes(t *testing.T) { // Test deeply nested include chain: a -> b -> c -> d tmpDir := t.TempDir() includeDir := filepath.Join(tmpDir, "includes") - if err := os.MkdirAll(includeDir, 0755); err != nil { + if err := os.MkdirAll(includeDir, 0o755); err != nil { t.Fatal(err) } @@ -364,7 +305,7 @@ func TestResolver_Integration_DeeplyNestedIncludes(t *testing.T) { struct D { 1: string value }` - if err := os.WriteFile(dFile, []byte(dContent), 0644); err != nil { + if err := os.WriteFile(dFile, []byte(dContent), 0o644); err != nil { t.Fatal(err) } @@ -377,7 +318,7 @@ namespace * c struct C { 1: D d_field }` - if err := os.WriteFile(cFile, []byte(cContent), 0644); err != nil { + if err := os.WriteFile(cFile, []byte(cContent), 0o644); err != nil { t.Fatal(err) } @@ -390,7 +331,7 @@ namespace * b struct B { 1: C c_field }` - if err := os.WriteFile(bFile, []byte(bContent), 0644); err != nil { + if err := os.WriteFile(bFile, []byte(bContent), 0o644); err != nil { t.Fatal(err) } @@ -403,23 +344,44 @@ namespace * a struct A { 1: B b_field }` - if err := os.WriteFile(aFile, []byte(aContent), 0644); err != nil { + if err := os.WriteFile(aFile, []byte(aContent), 0o644); err != nil { t.Fatal(err) } - // Parse recursively with include path + // Walk the include chain: a -> b -> c -> d, resolving each include via + // the resolver and parsing with the syntax package. r := New([]string{includeDir}) - psr := parser.PEGParser{} - - results := psr.ParseRecursively(aFile, []byte(aContent), 0, r.IncludeCall(aFile)) - - if len(results) == 0 || results[0].Doc == nil { - t.Fatal("expected parsed document") - } - // Verify all nested includes were loaded (results includes main + included docs) - // Should have at least 4 results: a.thrift (main) + b.thrift + c.thrift + d.thrift - if len(results) < 4 { - t.Errorf("expected at least 4 results (main + 3 includes), got %d", len(results)) + parseIncludes := func(file string) ([]string, error) { + content, err := os.ReadFile(file) + if err != nil { + return nil, err + } + doc, errs := syntax.Parse(content) + for _, e := range errs { + if e.Severity == syntax.SeverityError { + return nil, fmt.Errorf("parse %s: %v", file, errs) + } + } + var paths []string + for _, inc := range doc.Includes() { + paths = append(paths, strings.Trim(inc.Path.Text, "\"'")) + } + return paths, nil + } + + chain := []string{aFile, bFile, cFile, dFile} + for i := 0; i < len(chain)-1; i++ { + includes, err := parseIncludes(chain[i]) + if err != nil { + t.Fatal(err) + } + if len(includes) != 1 { + t.Fatalf("%s: expected one include, got %v", chain[i], includes) + } + next := r.Resolve(chain[i], includes[0]) + if next != chain[i+1] { + t.Errorf("expected %q, got %q", chain[i+1], next) + } } } diff --git a/resolver/resolver.go b/resolver/resolver.go index 8bfd1d3..2f75ec8 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -1,23 +1,48 @@ package resolver import ( + "io/fs" "os" "path/filepath" - - "github.com/joyme123/thrift-ls/parser" + "strings" ) +// absFS adapts an fs.FS so absolute names work: os.DirFS rejects paths +// starting with "/" (fs.ValidPath), but the resolver works with absolute +// paths. +type absFS struct{ fsys fs.FS } + +func (a absFS) Open(name string) (fs.File, error) { + return a.fsys.Open(strings.TrimPrefix(name, "/")) +} + +func (a absFS) Stat(name string) (fs.FileInfo, error) { + return fs.Stat(a.fsys, strings.TrimPrefix(name, "/")) +} + +// FS returns an fs.FS over the real filesystem that accepts absolute paths. +func FS() fs.FS { return absFS{os.DirFS("/")} } + // Resolver resolves include paths for Thrift files. // It provides a centralized, pure implementation that can be used // by both CLI and LSP components. type Resolver struct { includePaths []string + fsys fs.FS } -// New creates a new Resolver with the given include paths. +// New creates a Resolver over the real filesystem. func New(includePaths []string) *Resolver { + return NewWithFS(includePaths, FS()) +} + +// NewWithFS creates a Resolver that checks file existence through fsys. +// Absolute paths are looked up as-is; os.DirFS("/") reproduces the default +// filesystem behavior, and fstest.MapFS makes tests hermetic. +func NewWithFS(includePaths []string, fsys fs.FS) *Resolver { return &Resolver{ includePaths: includePaths, + fsys: fsys, } } @@ -31,14 +56,14 @@ func (r *Resolver) Resolve(currentFile, includePath string) string { resolvedPath := filepath.Join(basePath, includePath) // Check if file exists - if _, err := os.Stat(resolvedPath); err == nil { + if r.exists(resolvedPath) { return resolvedPath } // Try each configured include path for _, ip := range r.includePaths { candidatePath := filepath.Join(ip, includePath) - if _, err := os.Stat(candidatePath); err == nil { + if r.exists(candidatePath) { return candidatePath } } @@ -47,33 +72,37 @@ func (r *Resolver) Resolve(currentFile, includePath string) string { return resolvedPath } -// ResolveContent resolves and reads the file content for an include path. -// This signature matches parser.IncludeCall and can be used directly -// with parser.PEGParser.ParseRecursively. +// exists reports whether the file exists on the resolver's filesystem. +func (r *Resolver) exists(path string) bool { + _, err := fs.Stat(r.fsys, path) + return err == nil +} + +// IncludeCall is a function that resolves and reads an include file. +type IncludeCall func(include string) (filename string, content []byte, err error) + +// ResolveContent resolves an include path and reads the file content. func (r *Resolver) ResolveContent(currentFile, includePath string) (filename string, content []byte, err error) { filename = r.Resolve(currentFile, includePath) - content, err = os.ReadFile(filename) + content, err = fs.ReadFile(r.fsys, filename) if err != nil { return filename, nil, err } return filename, content, nil } -// IncludeCall creates a parser.IncludeCall function for use with ParseRecursively. -// The returned function resolves includes using include paths first, then falls back -// to relative resolution from the initialFile. -func (r *Resolver) IncludeCall(initialFile string) parser.IncludeCall { +// IncludeCall creates an IncludeCall function for the given file. The +// returned function resolves includes using include paths first, then falls +// back to relative resolution from initialFile. +func (r *Resolver) IncludeCall(initialFile string) IncludeCall { return func(include string) (filename string, content []byte, err error) { - // Try include paths first for _, ip := range r.includePaths { candidatePath := filepath.Join(ip, include) - if _, statErr := os.Stat(candidatePath); statErr == nil { - content, err = os.ReadFile(candidatePath) + if r.exists(candidatePath) { + content, err = fs.ReadFile(r.fsys, candidatePath) return candidatePath, content, err } } - - // Fall back to relative resolution from initial file return r.ResolveContent(initialFile, include) } } diff --git a/resolver/resolver_test.go b/resolver/resolver_test.go index f6d1161..7042517 100644 --- a/resolver/resolver_test.go +++ b/resolver/resolver_test.go @@ -1,235 +1,132 @@ package resolver import ( + "bytes" + "io/fs" "os" "path/filepath" "testing" -) - -func TestResolve_Relative(t *testing.T) { - // Create temp dir and files - tmpDir := t.TempDir() + "testing/fstest" + "time" - // Create files - mainFile := filepath.Join(tmpDir, "main.thrift") - includeFile := filepath.Join(tmpDir, "types.thrift") + "github.com/karitham/thrift-ls/options" +) - if err := os.WriteFile(mainFile, []byte("// main"), 0644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(includeFile, []byte("// types"), 0644); err != nil { +// TestConfigRelativeIncludePaths verifies that include paths in a config are +// resolved relative to the config file, so they work regardless of the +// process working directory. +func TestConfigRelativeIncludePaths(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "thriftls.json") + if err := os.WriteFile(cfgPath, []byte(`{"includePaths": ["project/base"]}`), 0o644); err != nil { t.Fatal(err) } - r := New([]string{}) - - // Test resolving relative to main file - resolved := r.Resolve(mainFile, "types.thrift") - if resolved != includeFile { - t.Errorf("expected %q, got %q", includeFile, resolved) - } -} - -func TestResolve_IncludePath(t *testing.T) { - // Create temp dirs - tmpDir := t.TempDir() - includeDir := filepath.Join(tmpDir, "includes") - mainDir := filepath.Join(tmpDir, "src") - - if err := os.MkdirAll(includeDir, 0755); err != nil { - t.Fatal(err) + cfg, err := options.Load(cfgPath) + if err != nil { + t.Fatalf("Load: %v", err) } - if err := os.MkdirAll(mainDir, 0755); err != nil { - t.Fatal(err) + if cfg.IncludePaths == nil || len(*cfg.IncludePaths) != 1 { + t.Fatalf("includePaths = %v", cfg.IncludePaths) } - - // Create files - mainFile := filepath.Join(mainDir, "main.thrift") - includeFile := filepath.Join(includeDir, "shared.thrift") - - if err := os.WriteFile(mainFile, []byte("// main"), 0644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(includeFile, []byte("// shared"), 0644); err != nil { - t.Fatal(err) + want := filepath.Join(dir, "project", "base") + if got := (*cfg.IncludePaths)[0]; got != want { + t.Errorf("includePaths[0] = %q, want %q", got, want) } - // Resolver with include path - r := New([]string{includeDir}) - - // Resolve should find file in include path - resolved := r.Resolve(mainFile, "shared.thrift") - if resolved != includeFile { - t.Errorf("expected %q, got %q", includeFile, resolved) + // Resolution works from any CWD, against a hermetic in-memory fs. The + // config-relative include path is absolute, so the map keys are too + // (fstest.MapFS only supports relative keys). + baseFile := filepath.Join(dir, "project", "base", "types.thrift") + fsys := absMapFS{baseFile: []byte("struct T {}")} + r := NewWithFS(*cfg.IncludePaths, fsys) + cur := filepath.Join(dir, "project", "app.thrift") + if got := r.Resolve(cur, "types.thrift"); got != baseFile { + t.Errorf("Resolve = %q, want %q", got, baseFile) } } -func TestResolve_Fallback(t *testing.T) { - // Create temp dir - tmpDir := t.TempDir() - mainFile := filepath.Join(tmpDir, "main.thrift") +// absMapFS is an in-memory fs.FS keyed by absolute paths, for hermetic tests +// of absolute include-path resolution. +type absMapFS map[string][]byte - if err := os.WriteFile(mainFile, []byte("// main"), 0644); err != nil { - t.Fatal(err) - } - - r := New([]string{}) - - // Non-existent file should return relative path as fallback - resolved := r.Resolve(mainFile, "missing.thrift") - expected := filepath.Join(tmpDir, "missing.thrift") - if resolved != expected { - t.Errorf("expected %q, got %q", expected, resolved) +func (m absMapFS) Stat(name string) (fs.FileInfo, error) { + if _, ok := m[name]; !ok { + return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrNotExist} } + return absMapFileInfo{name: name}, nil } -func TestResolveContent(t *testing.T) { - // Create temp files - tmpDir := t.TempDir() - mainFile := filepath.Join(tmpDir, "main.thrift") - includeFile := filepath.Join(tmpDir, "types.thrift") - - content := []byte("struct User { 1: string Name }") - if err := os.WriteFile(mainFile, []byte("// main"), 0644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(includeFile, content, 0644); err != nil { - t.Fatal(err) - } - - r := New([]string{}) - - filename, data, err := r.ResolveContent(mainFile, "types.thrift") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if filename != includeFile { - t.Errorf("expected filename %q, got %q", includeFile, filename) - } - if string(data) != string(content) { - t.Errorf("expected content %q, got %q", content, data) +func (m absMapFS) Open(name string) (fs.File, error) { + data, ok := m[name] + if !ok { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} } + return &absMapFile{Reader: bytes.NewReader(data), info: absMapFileInfo{name: name}}, nil } -func TestResolveContent_NotFound(t *testing.T) { - // Create temp file but not the include - tmpDir := t.TempDir() - mainFile := filepath.Join(tmpDir, "main.thrift") +type absMapFileInfo struct{ name string } - if err := os.WriteFile(mainFile, []byte("// main"), 0644); err != nil { - t.Fatal(err) - } +func (absMapFileInfo) Name() string { return "" } +func (absMapFileInfo) Size() int64 { return 0 } +func (absMapFileInfo) Mode() fs.FileMode { return 0 } +func (absMapFileInfo) ModTime() time.Time { return time.Time{} } +func (absMapFileInfo) IsDir() bool { return false } +func (absMapFileInfo) Sys() any { return nil } - r := New([]string{}) - - filename, data, err := r.ResolveContent(mainFile, "missing.thrift") - if data != nil { - t.Error("expected nil content for missing file") - } - if err == nil { - t.Error("expected error for missing file") - } - // Should still return the attempted path - expected := filepath.Join(tmpDir, "missing.thrift") - if filename != expected { - t.Errorf("expected filename %q, got %q", expected, filename) - } -} - -func TestIncludeCall_IncludePath(t *testing.T) { - // Create temp dirs with separate include path - tmpDir := t.TempDir() - includeDir := filepath.Join(tmpDir, "includes") - mainDir := filepath.Join(tmpDir, "src") - - if err := os.MkdirAll(includeDir, 0755); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(mainDir, 0755); err != nil { - t.Fatal(err) - } - - // Create files - mainFile := filepath.Join(mainDir, "main.thrift") - includeFile := filepath.Join(includeDir, "shared.thrift") - - content := []byte("namespace * test\nstruct User { 1: string Name }") - if err := os.WriteFile(mainFile, []byte("// main"), 0644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(includeFile, content, 0644); err != nil { - t.Fatal(err) - } - - r := New([]string{includeDir}) - includeCall := r.IncludeCall(mainFile) - - filename, data, err := includeCall("shared.thrift") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if filename != includeFile { - t.Errorf("expected filename %q, got %q", includeFile, filename) - } - if string(data) != string(content) { - t.Errorf("expected content %q, got %q", content, data) - } +type absMapFile struct { + *bytes.Reader + info fs.FileInfo } -func TestIncludeCall_Fallback(t *testing.T) { - // Test fallback to relative resolution when not in include paths - tmpDir := t.TempDir() - mainFile := filepath.Join(tmpDir, "main.thrift") - localFile := filepath.Join(tmpDir, "local.thrift") +func (f *absMapFile) Stat() (fs.FileInfo, error) { return f.info, nil } +func (f *absMapFile) Close() error { return nil } - if err := os.WriteFile(mainFile, []byte("// main"), 0644); err != nil { - t.Fatal(err) - } - content := []byte("// local file") - if err := os.WriteFile(localFile, content, 0644); err != nil { +// TestConfigAbsoluteIncludePaths keeps absolute paths as-is. +func TestConfigAbsoluteIncludePaths(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "thriftls.json") + abs := filepath.Join(dir, "elsewhere") + if err := os.WriteFile(cfgPath, []byte(`{"includePaths": ["`+abs+`"]}`), 0o644); err != nil { t.Fatal(err) } - - r := New([]string{}) - includeCall := r.IncludeCall(mainFile) - - filename, data, err := includeCall("local.thrift") + cfg, err := options.Load(cfgPath) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf("Load: %v", err) } - if filename != localFile { - t.Errorf("expected filename %q, got %q", localFile, filename) - } - if string(data) != string(content) { - t.Errorf("expected content %q, got %q", content, data) + if got := (*cfg.IncludePaths)[0]; got != abs { + t.Errorf("includePaths[0] = %q, want %q", got, abs) } } -func TestIncludeCall_NilIncludePaths(t *testing.T) { - // Test with nil include paths - tmpDir := t.TempDir() - mainFile := filepath.Join(tmpDir, "main.thrift") - localFile := filepath.Join(tmpDir, "local.thrift") - - if err := os.WriteFile(mainFile, []byte("// main"), 0644); err != nil { - t.Fatal(err) - } - content := []byte("// local") - if err := os.WriteFile(localFile, content, 0644); err != nil { - t.Fatal(err) - } - - r := New(nil) - includeCall := r.IncludeCall(mainFile) - - filename, data, err := includeCall("local.thrift") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if filename != localFile { - t.Errorf("expected filename %q, got %q", localFile, filename) - } - if string(data) != string(content) { - t.Errorf("expected content %q, got %q", content, data) +// TestResolveOrder verifies resolution order against a hermetic fs: +// relative to the current file first, then each include path. +func TestResolveOrder(t *testing.T) { + fsys := fstest.MapFS{ + "proj/service/types.thrift": &fstest.MapFile{Data: []byte("local")}, + "proj/base/types.thrift": &fstest.MapFile{Data: []byte("base")}, + "proj/base/other.thrift": &fstest.MapFile{Data: []byte("base")}, + "proj/vendor/types.thrift": &fstest.MapFile{Data: []byte("vendor")}, + "proj/vendor/deep/nested.thrift": &fstest.MapFile{Data: []byte("nested")}, + } + r := NewWithFS([]string{"proj/base", "proj/vendor"}, fsys) + cur := "proj/service/order.thrift" + + tests := []struct { + name string + includePath string + want string + }{ + {"local file wins", "types.thrift", "proj/service/types.thrift"}, + {"first include path", "other.thrift", "proj/base/other.thrift"}, + {"second include path", "deep/nested.thrift", "proj/vendor/deep/nested.thrift"}, + {"missing falls back to relative", "missing.thrift", "proj/service/missing.thrift"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := r.Resolve(cur, tt.includePath); got != tt.want { + t.Errorf("Resolve(%q) = %q, want %q", tt.includePath, got, tt.want) + } + }) } } diff --git a/syntax/accessors.go b/syntax/accessors.go new file mode 100644 index 0000000..ec46b03 --- /dev/null +++ b/syntax/accessors.go @@ -0,0 +1,115 @@ +package syntax + +// Typed accessors over the document's ordered node list. The LSP consumes +// these heavily; the lists are rebuilt on each call, which is cheap for +// typical thrift files. + +// Includes returns the thrift include headers in source order. +func (d *Document) Includes() []*Include { + var out []*Include + for _, n := range d.Nodes { + if v, ok := n.(*Include); ok { + out = append(out, v) + } + } + return out +} + +// CPPIncludes returns the cpp_include headers in source order. +func (d *Document) CPPIncludes() []*CPPInclude { + var out []*CPPInclude + for _, n := range d.Nodes { + if v, ok := n.(*CPPInclude); ok { + out = append(out, v) + } + } + return out +} + +// Namespaces returns the namespace headers in source order. +func (d *Document) Namespaces() []*Namespace { + var out []*Namespace + for _, n := range d.Nodes { + if v, ok := n.(*Namespace); ok { + out = append(out, v) + } + } + return out +} + +// Structs returns the struct declarations in source order. +func (d *Document) Structs() []*Struct { + var out []*Struct + for _, n := range d.Nodes { + if v, ok := n.(*Struct); ok && v.Kind == StructDecl { + out = append(out, v) + } + } + return out +} + +// Unions returns the union declarations in source order. +func (d *Document) Unions() []*Struct { + var out []*Struct + for _, n := range d.Nodes { + if v, ok := n.(*Struct); ok && v.Kind == UnionDecl { + out = append(out, v) + } + } + return out +} + +// Exceptions returns the exception declarations in source order. +func (d *Document) Exceptions() []*Struct { + var out []*Struct + for _, n := range d.Nodes { + if v, ok := n.(*Struct); ok && v.Kind == ExceptionDecl { + out = append(out, v) + } + } + return out +} + +// Enums returns the enum declarations in source order. +func (d *Document) Enums() []*Enum { + var out []*Enum + for _, n := range d.Nodes { + if v, ok := n.(*Enum); ok { + out = append(out, v) + } + } + return out +} + +// Services returns the service declarations in source order. +func (d *Document) Services() []*Service { + var out []*Service + for _, n := range d.Nodes { + if v, ok := n.(*Service); ok { + out = append(out, v) + } + } + return out +} + +// Consts returns the const declarations in source order. +func (d *Document) Consts() []*Const { + var out []*Const + for _, n := range d.Nodes { + if v, ok := n.(*Const); ok { + out = append(out, v) + } + } + return out +} + +// Typedefs returns the typedef declarations in source order. +func (d *Document) Typedefs() []*Typedef { + var out []*Typedef + for _, n := range d.Nodes { + if v, ok := n.(*Typedef); ok { + out = append(out, v) + } + } + return out +} diff --git a/syntax/ast.go b/syntax/ast.go new file mode 100644 index 0000000..bb4ca82 --- /dev/null +++ b/syntax/ast.go @@ -0,0 +1,238 @@ +package syntax + +// This file defines the abstract syntax tree produced by the parser. +// +// Every node spans a contiguous range of tokens in the Document's token +// stream: TokStart/TokEnd are inclusive indices into Document.Tokens. +// Comments and blank-line information live on those tokens as trivia, so +// the tree itself stays free of formatting concerns. + +// Node is any AST node. All nodes embed nodeBase, which provides the token +// range and seals the interface to this package. +type Node interface { + TokStart() int + TokEnd() int + isNode() +} + +type nodeBase struct { + first, last int // inclusive token indices +} + +func (b nodeBase) TokStart() int { return b.first } +func (b nodeBase) TokEnd() int { return b.last } +func (nodeBase) isNode() {} + +// Identifier is a name: a plain identifier token. +type Identifier struct { + nodeBase + Text string +} + +// FieldTypeKind discriminates the forms a type reference can take. +type FieldTypeKind uint8 + +const ( + TypeBase FieldTypeKind = iota + TypeIdent + TypeMap + TypeList + TypeSet +) + +// FieldType is a type reference: a base type keyword, a named identifier, or +// a container (map/list/set). Base and container types may carry +// annotations; identifier types may not, matching the compiler. +type FieldType struct { + nodeBase + Kind FieldTypeKind + + Base TokenKind // TypeBase: the base type keyword + Ident *Identifier + KeyType *FieldType // TypeMap: key type + ValueType *FieldType // TypeMap/TypeList/TypeSet: value or element type + CPPType *Token // optional cpp_type "..." literal on containers + + Annotations *Annotations +} + +// ConstValueKind discriminates the forms a constant value can take. +type ConstValueKind uint8 + +const ( + ValueInt ConstValueKind = iota + ValueDouble + ValueString + ValueIdent + ValueList + ValueMap +) + +// ConstMapEntry is one key/value pair of a map constant. Order is preserved. +type ConstMapEntry struct { + Key *ConstValue + Value *ConstValue +} + +// ConstValue is a constant value: a scalar (int, double, string, identifier, +// true/false), a list, or a map. Scalar values keep their raw source text in +// Text (e.g. "0x1F", "-1.5e-3") so formatting is lossless. +type ConstValue struct { + nodeBase + Kind ConstValueKind + Text string // raw source text of scalar values + + List []*ConstValue + Map []ConstMapEntry +} + +// Include is a thrift include: include "path". +type Include struct { + nodeBase + Path *Token // the path string literal +} + +// CPPInclude is a C++ include: cpp_include "path". +type CPPInclude struct { + nodeBase + Path *Token // the path string literal +} + +// Namespace declares a namespace: namespace . +type Namespace struct { + nodeBase + Scope *Token // scope identifier or the '*' token + Name *Identifier + + Annotations *Annotations +} + +// Const declares a constant: const = . +type Const struct { + nodeBase + Type *FieldType + Name *Identifier + Value *ConstValue + Sep TokenKind // trailing , or ; (0 if none) +} + +// Typedef declares a type alias: typedef . +type Typedef struct { + nodeBase + Type *FieldType + Name *Identifier + Sep TokenKind + + Annotations *Annotations +} + +// EnumValue is one enum member: [= ]. +type EnumValue struct { + nodeBase + Name *Identifier + Value *Token // the optional int token; nil means auto-incremented + Sep TokenKind + + Annotations *Annotations +} + +// Enum declares an enum: enum { }. +type Enum struct { + nodeBase + Name *Identifier + Values []*EnumValue + + Annotations *Annotations +} + +// StructKind distinguishes struct, union, and exception declarations. +// It is the token kind of the leading keyword. +type StructKind = TokenKind + +const ( + StructDecl StructKind = TokenStruct + UnionDecl StructKind = TokenUnion + ExceptionDecl StructKind = TokenException +) + +// Struct is a struct, union, or exception: { }. +type Struct struct { + nodeBase + Kind StructKind + Name *Identifier + Fields []*Field + + Annotations *Annotations +} + +// Throws is a function's throws clause: throws ( ). +type Throws struct { + nodeBase + Fields []*Field +} + +// Function is a service method: +// [oneway] ( ) [throws ( )]. +type Function struct { + nodeBase + Oneway *Token // oneway/async keyword, nil for synchronous functions + Type *FieldType + Void *Token // the void keyword; Type is nil when Void is set + Name *Identifier + Args []*Field + Throws *Throws + Sep TokenKind + + Annotations *Annotations +} + +// Service declares a service: service [extends ] { }. +type Service struct { + nodeBase + Name *Identifier + Extends *Identifier // optional base service + Functions []*Function + + Annotations *Annotations +} + +// Field is a struct field, function argument, or throws entry: +// [:] [required|optional] [&] [= ]. +type Field struct { + nodeBase + FieldID *Token // the optional id int token; nil means implicit + Req TokenKind + Type *FieldType + Reference bool // & prefix (field reference) + Name *Identifier + Value *ConstValue + Sep TokenKind + + Annotations *Annotations +} + +// Annotation is one entry of an annotation group: [= "value"]. +// A bare is legal and means an implicit value of "1". +type Annotation struct { + nodeBase + Name *Identifier + Value *Token // the optional string literal; nil means bare + Sep TokenKind +} + +// Annotations is a parenthesized annotation group: ( ). +type Annotations struct { + nodeBase + Items []*Annotation +} + +// Document is a parsed thrift file: its token stream and the top-level nodes +// in source order. It implements Node with a range spanning the whole file. +type Document struct { + Tokens []Token + Nodes []Node +} + +func (d *Document) TokStart() int { return 0 } +func (d *Document) TokEnd() int { return len(d.Tokens) - 1 } +func (*Document) isNode() {} diff --git a/syntax/ast_visit.go b/syntax/ast_visit.go new file mode 100644 index 0000000..30af45e --- /dev/null +++ b/syntax/ast_visit.go @@ -0,0 +1,108 @@ +package syntax + +// SearchNodePathByPosition returns the path of nodes containing pos, from +// the document root to the innermost node. The LSP uses the deepest node to +// decide what the cursor is on and its ancestors for context. +func (d *Document) SearchNodePathByPosition(pos Position) []Node { + var path []Node + d.searchNodePath(d, pos, &path) + return path +} + +func (d *Document) searchNodePath(root Node, pos Position, path *[]Node) { + if !d.Contains(root, pos) { + return + } + *path = append(*path, root) + for _, child := range nodeChildren(root) { + d.searchNodePath(child, pos, path) + } +} + +// nodeChildren enumerates the child nodes of n. Identifiers appear as leaf +// nodes, so the deepest node on a name is the identifier itself; the parent +// path element identifies its role (a field name, a type reference, a +// definition name, and so on). +func nodeChildren(n Node) []Node { + switch v := n.(type) { + case *Document: + return v.Nodes + case *Const: + return []Node{v.Type, v.Name, v.Value} + case *Typedef: + return []Node{v.Type, v.Name} + case *Enum: + out := []Node{v.Name} + for _, value := range v.Values { + out = append(out, value) + } + return out + case *EnumValue: + return []Node{v.Name} + case *Struct: + out := []Node{v.Name} + for _, field := range v.Fields { + out = append(out, field) + } + return out + case *Service: + out := []Node{v.Name} + if v.Extends != nil { + out = append(out, v.Extends) + } + for _, fn := range v.Functions { + out = append(out, fn) + } + return out + case *Function: + out := []Node{v.Name} + if v.Type != nil { + out = append(out, v.Type) + } + for _, arg := range v.Args { + out = append(out, arg) + } + if v.Throws != nil { + out = append(out, v.Throws) + } + return out + case *Throws: + out := make([]Node, 0, len(v.Fields)) + for _, field := range v.Fields { + out = append(out, field) + } + return out + case *Field: + out := []Node{v.Type, v.Name} + if v.Value != nil { + out = append(out, v.Value) + } + return out + case *FieldType: + var out []Node + if v.Ident != nil { + out = append(out, v.Ident) + } + if v.KeyType != nil { + out = append(out, v.KeyType) + } + if v.ValueType != nil { + out = append(out, v.ValueType) + } + return out + case *ConstValue: + var out []Node + for _, item := range v.List { + out = append(out, item) + } + for _, entry := range v.Map { + out = append(out, entry.Key, entry.Value) + } + return out + case *Namespace: + return []Node{v.Name} + case *Include, *CPPInclude, *Identifier: + return nil + } + return nil +} diff --git a/syntax/lexer.go b/syntax/lexer.go new file mode 100644 index 0000000..6b0c09b --- /dev/null +++ b/syntax/lexer.go @@ -0,0 +1,718 @@ +// Package syntax provides a lossless lexer, AST, and recursive-descent +// parser for the Apache Thrift IDL. +// +// Grammar reference: the Apache Thrift compiler +// (compiler/cpp/src/thrift/thriftl.ll and thrifty.yy). +// +// Losslessness: comments are preserved as trivia attached to tokens in +// source order. Whitespace itself is not preserved; instead each token +// records how many blank lines preceded it, which is the only layout +// information a formatter is allowed to act on. +package syntax + +import ( + "fmt" + "unicode/utf8" +) + +// TokenKind identifies the lexical class of a Token. +type TokenKind uint8 + +const ( + TokenInvalid TokenKind = iota + TokenEOF + + TokenIdentifier + TokenIntConstant + TokenDoubleConstant + TokenStringLiteral + + // Keywords. + TokenInclude + TokenCPPInclude + TokenCPPType + TokenNamespace + TokenStruct + TokenUnion + TokenException + TokenService + TokenEnum + TokenConst + TokenTypedef + TokenOneway + TokenAsync // deprecated alias for oneway + TokenThrows + TokenExtends + TokenRequired + TokenOptional + TokenVoid + TokenBool + TokenByte // deprecated, accepted with a warning + TokenI8 + TokenI16 + TokenI32 + TokenI64 + TokenDouble + TokenString + TokenBinary + TokenSlist // no longer supported by the compiler, accepted for old files + TokenUUID + TokenMap + TokenList + TokenSet + TokenTrue + TokenFalse + + // Punctuation. + TokenLBrace + TokenRBrace + TokenLParen + TokenRParen + TokenLBracket + TokenRBracket + TokenLt + TokenGt + TokenComma + TokenSemicolon + TokenColon + TokenEqual + TokenStar + TokenAmp +) + +var keywordKinds = map[string]TokenKind{ + "include": TokenInclude, + "cpp_include": TokenCPPInclude, + "cpp_type": TokenCPPType, + "namespace": TokenNamespace, + "struct": TokenStruct, + "union": TokenUnion, + "exception": TokenException, + "service": TokenService, + "enum": TokenEnum, + "const": TokenConst, + "typedef": TokenTypedef, + "oneway": TokenOneway, + "async": TokenAsync, + "throws": TokenThrows, + "extends": TokenExtends, + "required": TokenRequired, + "optional": TokenOptional, + "void": TokenVoid, + "bool": TokenBool, + "byte": TokenByte, + "i8": TokenI8, + "i16": TokenI16, + "i32": TokenI32, + "i64": TokenI64, + "double": TokenDouble, + "string": TokenString, + "binary": TokenBinary, + "slist": TokenSlist, + "uuid": TokenUUID, + "map": TokenMap, + "list": TokenList, + "set": TokenSet, + "true": TokenTrue, + "false": TokenFalse, +} + +var keywordNames = func() map[TokenKind]string { + m := make(map[TokenKind]string, len(keywordKinds)) + for text, kind := range keywordKinds { + m[kind] = text + } + return m +}() + +// isKeyword reports whether the token kind is one of the reserved words. +func isKeyword(k TokenKind) bool { + _, ok := keywordNames[k] + return ok +} + +var tokenKindNames = map[TokenKind]string{ + TokenInvalid: "invalid", TokenEOF: "eof", + TokenIdentifier: "identifier", TokenIntConstant: "int", + TokenDoubleConstant: "double", TokenStringLiteral: "string", + TokenLBrace: "{", TokenRBrace: "}", TokenLParen: "(", TokenRParen: ")", + TokenLBracket: "[", TokenRBracket: "]", TokenLt: "<", TokenGt: ">", + TokenComma: ",", TokenSemicolon: ";", TokenColon: ":", TokenEqual: "=", + TokenStar: "*", TokenAmp: "&", +} + +func (k TokenKind) String() string { + if name, ok := keywordNames[k]; ok { + return name + } + if name, ok := tokenKindNames[k]; ok { + return name + } + return fmt.Sprintf("TokenKind(%d)", uint8(k)) +} + +// TriviaKind identifies the kind of a comment trivia. +type TriviaKind uint8 + +const ( + TriviaLineComment TriviaKind = iota // // or # + TriviaBlockComment // /* */ + TriviaDocComment // /** */ +) + +func (k TriviaKind) String() string { + switch k { + case TriviaLineComment: + return "line comment" + case TriviaBlockComment: + return "block comment" + case TriviaDocComment: + return "doc comment" + } + return fmt.Sprintf("TriviaKind(%d)", uint8(k)) +} + +// Trivia is a comment preserved in source order. It is attached to a Token +// either as leading (before the token) or trailing (after the previous token +// on the same line). +type Trivia struct { + Kind TriviaKind + Text string // exact source text, including comment delimiters + Offset int // byte offset of the first character + Line int // 1-based line of the first character + Col int // 1-based rune column of the first character +} + +// Token is a single lexical token with its attached comment trivia. +type Token struct { + Kind TokenKind + Text string // exact source text + + Offset int // byte offset of the first character + Line int // 1-based line of the first character + Col int // 1-based rune column of the first character + + // Leading holds comments that appear between the previous token and + // this one, on their own lines. + Leading []Trivia + // Trailing holds comments that appear after this token on the same + // line. + Trailing []Trivia + // BlankLinesBefore is the number of empty lines between the previous + // token and this one (0 means no blank line). + BlankLinesBefore int +} + +// Severity classifies a syntax error or warning. +type Severity uint8 + +const ( + SeverityError Severity = iota + SeverityWarning +) + +// Error is a lexical or parse error with a source position. +type Error struct { + Message string + Offset int + Line int + Col int + Severity Severity +} + +func (e Error) Error() string { + return fmt.Sprintf("%d:%d: %s", e.Line, e.Col, e.Message) +} + +// Lex tokenizes src into a flat token stream. The final token is always +// TokenEOF. Lexical errors are collected and returned alongside the tokens; +// lexing continues past errors so the parser can still recover. +func Lex(src []byte) ([]Token, []Error) { + l := &lexer{src: string(src)} + return l.run() +} + +type lexer struct { + src string + off int // byte offset of the next unread character + line int // 1-based line of the next unread character + col int // 1-based rune column of the next unread character + errs []Error +} + +type srcPos struct { + offset, line, col int +} + +func (l *lexer) run() ([]Token, []Error) { + l.line, l.col = 1, 1 + var tokens []Token + + for { + prevLine := -1 + if n := len(tokens); n > 0 { + prevLine = tokens[n-1].Line + } + leading, trailing, blankLines := l.scanTrivia(prevLine) + + tok := l.scanToken() + tok.Leading = leading + tok.BlankLinesBefore = blankLines + tokens = append(tokens, tok) + + if len(tokens) > 1 { + tokens[len(tokens)-2].Trailing = trailing + } + + if tok.Kind == TokenEOF { + return tokens, l.errs + } + } +} + +// scanTrivia consumes whitespace and comments between the previous token and +// the next one. Comments starting on the previous token's line become its +// trailing trivia; everything else becomes leading trivia of the next token. +// The returned blankLines count empty lines in the gap. +func (l *lexer) scanTrivia(prevLine int) (leading, trailing []Trivia, blankLines int) { + for l.off < len(l.src) { + switch c := l.src[l.off]; { + case isWhitespace(c): + blankLines += l.scanWhitespace() + case c == '/' && l.peekByte(1) == '/': + leading, trailing = l.appendComment(leading, trailing, prevLine, l.scanLineComment()) + case c == '/' && l.peekByte(1) == '*': + leading, trailing = l.appendComment(leading, trailing, prevLine, l.scanBlockComment()) + case c == '#': + leading, trailing = l.appendComment(leading, trailing, prevLine, l.scanLineComment()) + default: + return leading, trailing, blankLines + } + } + return leading, trailing, blankLines +} + +func (l *lexer) appendComment(leading, trailing []Trivia, prevLine int, t Trivia) ([]Trivia, []Trivia) { + if t.Line == prevLine { + return leading, append(trailing, t) + } + return append(leading, t), trailing +} + +// scanWhitespace consumes a whitespace run and returns the number of empty +// lines it contains (a blank line is a line containing only whitespace). +func (l *lexer) scanWhitespace() int { + newlines := 0 + for l.off < len(l.src) { + switch l.src[l.off] { + case '\n': + newlines++ + l.advanceByte() + case '\r': + newlines++ + l.advanceByte() + if l.off < len(l.src) && l.src[l.off] == '\n' { + l.advanceByte() + } + case ' ', '\t': + l.advanceByte() + default: + if newlines > 0 { + return newlines - 1 + } + return 0 + } + } + if newlines > 0 { + return newlines - 1 + } + return 0 +} + +func (l *lexer) scanLineComment() Trivia { + start := l.pos() + for l.off < len(l.src) && l.src[l.off] != '\n' && l.src[l.off] != '\r' { + l.advanceRune() + } + return l.finishTrivia(TriviaLineComment, start) +} + +// scanBlockComment scans a /* */ or /** */ comment. /** ... */ yields a doc +// comment trivia; everything else a block comment trivia. An unterminated +// comment consumes the rest of the input and reports an error. +func (l *lexer) scanBlockComment() Trivia { + start := l.pos() + doc := l.peekByte(2) == '*' + l.advanceByte() // / + l.advanceByte() // * + + for { + if l.off >= len(l.src) { + l.errorfAt(start, "unterminated comment") + break + } + if l.src[l.off] == '*' && l.peekByte(1) == '/' { + l.advanceByte() + l.advanceByte() + kind := TriviaBlockComment + if doc { + kind = TriviaDocComment + } + return l.finishTrivia(kind, start) + } + // An empty doc comment /**/ ends right after the opening /**. + if doc && l.off == start.offset+3 && l.src[l.off] == '/' { + l.advanceByte() + return l.finishTrivia(TriviaDocComment, start) + } + l.advanceRune() + } + + return l.finishTrivia(TriviaBlockComment, start) +} + +func (l *lexer) pos() srcPos { + return srcPos{l.off, l.line, l.col} +} + +func (l *lexer) finishTrivia(kind TriviaKind, start srcPos) Trivia { + return Trivia{Kind: kind, Text: l.src[start.offset:l.off], Offset: start.offset, Line: start.line, Col: start.col} +} + +// scanToken scans the next real token, skipping over invalid characters with +// errors. It always returns a token; TokenEOF at end of input. +func (l *lexer) scanToken() Token { + for { + if l.off >= len(l.src) { + return Token{Kind: TokenEOF, Offset: l.off, Line: l.line, Col: l.col} + } + + c := l.src[l.off] + + switch { + case isIdentStart(c): + return l.scanIdentifier() + case isDigit(c) || c == '+' || c == '-' || (c == '.' && isDigit(l.peekByte(1))): + if tok, ok := l.scanNumber(); ok { + return tok + } + l.errorf("unexpected character %q", c) + l.advanceRune() + case c == '\'' || c == '"': + return l.scanString() + case c == '&': + return l.symbolToken(TokenAmp) + case c == '*': + return l.symbolToken(TokenStar) + case isWhitespace(c): + l.advanceByte() + case c == '/' && l.peekByte(1) == '/': + l.scanLineComment() // discarded: appears after an invalid character + case c == '/' && l.peekByte(1) == '*': + l.scanBlockComment() // discarded: appears after an invalid character + case c == '#': + l.scanLineComment() // discarded: appears after an invalid character + default: + if kind, ok := symbolKinds[c]; ok { + return l.symbolToken(kind) + } + l.errorf("unexpected character %q", c) + l.advanceRune() + } + } +} + +var symbolKinds = map[byte]TokenKind{ + '{': TokenLBrace, '}': TokenRBrace, + '(': TokenLParen, ')': TokenRParen, + '[': TokenLBracket, ']': TokenRBracket, + '<': TokenLt, '>': TokenGt, + ',': TokenComma, ';': TokenSemicolon, + ':': TokenColon, '=': TokenEqual, +} + +func (l *lexer) symbolToken(kind TokenKind) Token { + start := l.pos() + l.advanceByte() + return Token{Kind: kind, Text: l.src[start.offset:l.off], Offset: start.offset, Line: start.line, Col: start.col} +} + +// scanIdentifier scans an identifier or keyword. Keywords are only matched +// when the identifier has no dot: "include.foo" is a single identifier, +// matching the compiler's longest-match lexing. +func (l *lexer) scanIdentifier() Token { + start := l.pos() + dotted := false + l.advanceByte() // first character is [a-zA-Z_] + for l.off < len(l.src) { + c := l.src[l.off] + if isIdentPart(c) { + l.advanceByte() + continue + } + if c == '.' && isIdentStart(l.peekByte(1)) { + dotted = true + l.advanceByte() // dot + l.advanceByte() // first identifier character after dot + continue + } + break + } + text := l.src[start.offset:l.off] + kind := TokenIdentifier + if !dotted { + if k, ok := keywordKinds[text]; ok { + kind = k + } + } + return Token{Kind: kind, Text: text, Offset: start.offset, Line: start.line, Col: start.col} +} + +// scanNumber scans int, hex, or double constants, mirroring the compiler's +// lexer: identifiers are scanned first ("e10" is an identifier), the longest +// of hex/int/double matches wins, and int wins ties against double ("-1" is +// an int, "-1.5" is a double, ".5" is a double). +func (l *lexer) scanNumber() (Token, bool) { + start := l.pos() + rest := l.src[start.offset:] + + hexLen := matchHex(rest) + intLen := matchInt(rest) + dubLen := matchDouble(rest) + + length := 0 + kind := TokenIntConstant + switch { + case hexLen > 0 && hexLen >= intLen && hexLen >= dubLen: + length = hexLen + case intLen > 0 && intLen >= dubLen: + length = intLen + case dubLen > 0: + length = dubLen + kind = TokenDoubleConstant + default: + return Token{}, false + } + + for i := 0; i < length; i++ { + l.advanceByte() + } + return Token{Kind: kind, Text: rest[:length], Offset: start.offset, Line: start.line, Col: start.col}, true +} + +// matchHex returns the length of a hex constant at the start of s, or 0. +// A hex constant is [+-]?"0x" followed by at least one hex digit. +func matchHex(s string) int { + i := 0 + if i < len(s) && (s[i] == '+' || s[i] == '-') { + i++ + } + if i+2 > len(s) || s[i] != '0' || (s[i+1] != 'x' && s[i+1] != 'X') { + return 0 + } + i += 2 + digits := 0 + for i < len(s) && isHexDigit(s[i]) { + i++ + digits++ + } + if digits == 0 { + return 0 + } + return i +} + +// matchInt returns the length of an int constant at the start of s, or 0. +// An int constant is [+-]? followed by at least one digit. +func matchInt(s string) int { + i := 0 + if i < len(s) && (s[i] == '+' || s[i] == '-') { + i++ + } + start := i + for i < len(s) && isDigit(s[i]) { + i++ + } + if i == start { + return 0 + } + return i +} + +// matchDouble returns the length of a double constant at the start of s, or 0. +// A double constant is [+-]?[0-9]*(\.[0-9]+)?([eE][+-]?[0-9]+)? with at least +// one digit somewhere: ".5", "+.5", and "1e10" are valid, "1." and "+." are not. +func matchDouble(s string) int { + i := 0 + if i < len(s) && (s[i] == '+' || s[i] == '-') { + i++ + } + sawDigit := false + for i < len(s) && isDigit(s[i]) { + i++ + sawDigit = true + } + if i < len(s) && s[i] == '.' { + digits := 0 + for j := i + 1; j < len(s) && isDigit(s[j]); j++ { + digits++ + } + if digits == 0 { + return 0 + } + sawDigit = true + i += 1 + digits + } + if i < len(s) && (s[i] == 'e' || s[i] == 'E') { + j := i + 1 + if j < len(s) && (s[j] == '+' || s[j] == '-') { + j++ + } + digits := 0 + for j < len(s) && isDigit(s[j]) { + j++ + digits++ + } + if digits == 0 { + return 0 + } + i = j + } + if !sawDigit { + return 0 + } + return i +} + +// scanString scans a quoted string literal, which may use single or double +// quotes and the escapes \r \n \t \" \' \\, matching the compiler's lexer. +// It returns the raw literal (including quotes) as the token text. A string +// ending at a newline or EOF is reported as an error and returned as-is so +// lexing can continue. +func (l *lexer) scanString() Token { + start := l.pos() + quote := l.src[l.off] + l.advanceByte() + + for { + if l.off >= len(l.src) { + l.errorfAt(start, "unterminated string literal") + break + } + c := l.src[l.off] + switch c { + case quote: + l.advanceByte() + return Token{Kind: TokenStringLiteral, Text: l.src[start.offset:l.off], Offset: start.offset, Line: start.line, Col: start.col} + case '\n', '\r': + l.errorfAt(start, "newline in string literal") + return Token{Kind: TokenStringLiteral, Text: l.src[start.offset:l.off], Offset: start.offset, Line: start.line, Col: start.col} + case '\\': + if l.off+1 >= len(l.src) { + l.advanceByte() // consume the backslash with the string + l.errorfAt(start, "unterminated string literal") + return Token{Kind: TokenStringLiteral, Text: l.src[start.offset:l.off], Offset: start.offset, Line: start.line, Col: start.col} + } + esc := l.peekByte(1) + switch esc { + case 'r', 'n', 't', '"', '\'', '\\': + l.advanceByte() + l.advanceByte() + default: + l.errorf("invalid escape sequence %q", "\\"+string(rune(esc))) + l.advanceByte() + // The escaped byte may be part of a multibyte rune; decode it + // as a rune so column counting stays consistent. + l.advanceRune() + } + default: + l.advanceRune() + } + } + return Token{Kind: TokenStringLiteral, Text: l.src[start.offset:l.off], Offset: start.offset, Line: start.line, Col: start.col} +} + +func (l *lexer) peekByte(ahead int) byte { + if l.off+ahead >= len(l.src) { + return 0 + } + return l.src[l.off+ahead] +} + +// advanceByte consumes one byte, tracking line and column. It is only called +// for ASCII characters. +func (l *lexer) advanceByte() { + switch l.src[l.off] { + case '\n': + l.line++ + l.col = 1 + case '\r': + // \r\n counts as a single newline; the column resets on \n. + if l.off+1 >= len(l.src) || l.src[l.off+1] != '\n' { + l.line++ + l.col = 1 + } + default: + l.col++ + } + l.off++ +} + +// advanceRune consumes one UTF-8 rune. Columns are counted in runes, and +// newlines reset the line like advanceByte does. +func (l *lexer) advanceRune() { + r, size := utf8.DecodeRuneInString(l.src[l.off:]) + switch r { + case '\n': + l.line++ + l.col = 1 + case '\r': + // \r\n counts as a single newline; the column resets on \n. + if l.off+size >= len(l.src) || l.src[l.off+size] != '\n' { + l.line++ + l.col = 1 + } + default: + l.col++ + } + l.off += size +} + +// errorf records a lexical error at the current position. +func (l *lexer) errorf(format string, args ...any) { + l.errs = append(l.errs, Error{ + Message: fmt.Sprintf(format, args...), + Offset: l.off, + Line: l.line, + Col: l.col, + }) +} + +func (l *lexer) errorfAt(p srcPos, format string, args ...any) { + l.errs = append(l.errs, Error{ + Message: fmt.Sprintf(format, args...), + Offset: p.offset, + Line: p.line, + Col: p.col, + }) +} + +func isWhitespace(c byte) bool { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' +} + +func isDigit(c byte) bool { + return c >= '0' && c <= '9' +} + +func isHexDigit(c byte) bool { + return isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') +} + +func isIdentStart(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' +} + +func isIdentPart(c byte) bool { + return isIdentStart(c) || isDigit(c) +} diff --git a/syntax/lexer_fuzz_test.go b/syntax/lexer_fuzz_test.go new file mode 100644 index 0000000..03d6d86 --- /dev/null +++ b/syntax/lexer_fuzz_test.go @@ -0,0 +1,146 @@ +package syntax + +import ( + "reflect" + "testing" + "unicode/utf8" +) + +// FuzzLex checks lexer invariants that hold for every input: +// +// - lexing always terminates with an EOF token (no infinite loops) +// - lexing never panics, even on truncated, binary, or invalid UTF-8 input +// - lexing is deterministic +// - token and trivia texts are exact slices of the source +// - reported line/col positions match the source bytes +// - trailing trivia always starts on its token's line +func FuzzLex(f *testing.F) { + for _, seed := range [][]byte{ + []byte(""), + []byte(" \n\t\r\n"), + []byte("struct S {\n 1: i32 id\n}"), + []byte("service Foo extends Bar {\n oneway void ping() throws (E e)\n}"), + []byte("const string s = \"héllo\"\nconst double d = -1.5e-3"), + []byte("// c\n# d\n/* e */\n/** doc */\n/**/"), + []byte("\"unterminated"), + []byte("\"trailing backslash\\"), + []byte("1. + - @ . : ; , < > [ ] { } ( ) = & *"), + []byte("\r\n\r\n"), + []byte("0x1F -0x10 .5 1e10 1. 0x e10"), + []byte("include \"foo.thrift\"\ncpp_include \"bar.h\""), + []byte("enum E { A = 1, B, C = 0x10 }"), + []byte("typedef map> M"), + []byte(`"a\qb"`), + []byte("/* unterminated"), + []byte("foo.bar baz1 _x"), + } { + f.Add(seed) + } + + f.Fuzz(func(t *testing.T, src []byte) { + toks, errs := Lex(src) + srcStr := string(src) + + if len(toks) == 0 || toks[len(toks)-1].Kind != TokenEOF { + t.Fatalf("lexing did not terminate with an EOF token: %d tokens", len(toks)) + } + // Every real token consumes at least one byte; only EOF is empty. + if len(toks) > len(src)+1 { + t.Fatalf("more tokens than input bytes: %d tokens for %d bytes", len(toks), len(src)) + } + + // Determinism. + toks2, errs2 := Lex(src) + if !reflect.DeepEqual(toks, toks2) || !reflect.DeepEqual(errs, errs2) { + t.Fatalf("lexing is not deterministic") + } + + // Errors are in bounds and positioned correctly. + for _, err := range errs { + if err.Offset < 0 || err.Offset > len(src) { + t.Fatalf("error offset %d out of range", err.Offset) + } + checkPos(t, srcStr, err.Offset, err.Line, err.Col, "error") + } + + // Token and trivia invariants. + prevEnd := 0 + for i, tok := range toks { + if tok.Kind == TokenInvalid { + t.Fatalf("token %d has invalid kind", i) + } + if tok.Offset < 0 || tok.Offset+len(tok.Text) > len(src) { + t.Fatalf("token %d (%s) spans outside the source", i, tok.Kind) + } + if got := src[tok.Offset : tok.Offset+len(tok.Text)]; string(got) != tok.Text { + t.Fatalf("token %d text %q does not match source %q", i, tok.Text, got) + } + checkPos(t, srcStr, tok.Offset, tok.Line, tok.Col, "token") + if tok.BlankLinesBefore < 0 { + t.Fatalf("token %d has negative BlankLinesBefore", i) + } + + // Leading trivia lies between the previous token and this one. + for _, tr := range tok.Leading { + if tr.Offset < prevEnd || tr.Offset+len(tr.Text) > tok.Offset || tr.Offset+len(tr.Text) > len(src) { + t.Fatalf("leading trivia %q of token %d outside the gap [%d, %d)", tr.Text, i, prevEnd, tok.Offset) + } + checkPos(t, srcStr, tr.Offset, tr.Line, tr.Col, "leading trivia") + } + + for _, tr := range tok.Trailing { + if tr.Offset < tok.Offset || tr.Offset+len(tr.Text) > len(src) { + t.Fatalf("trailing trivia %q of token %d outside the source", tr.Text, i) + } + checkPos(t, srcStr, tr.Offset, tr.Line, tr.Col, "trailing trivia") + if tr.Line != tok.Line { + t.Fatalf("trailing trivia %q of token %d starts on line %d, token is on line %d", + tr.Text, i, tr.Line, tok.Line) + } + } + + if tok.Kind != TokenEOF { + prevEnd = tok.Offset + len(tok.Text) + for _, tr := range tok.Trailing { + prevEnd += len(tr.Text) + } + } + } + }) +} + +// checkPos verifies that the lexer's line/col bookkeeping matches a naive +// scan of the source: \n (and lone \r, but not \r\n) start a new line, and +// columns count runes. +func checkPos(t *testing.T, src string, offset, line, col int, what string) { + t.Helper() + gotLine, gotCol := lineColAt(src, offset) + if gotLine != line || gotCol != col { + t.Fatalf("%s at offset %d: reported %d:%d, want %d:%d", what, offset, line, col, gotLine, gotCol) + } +} + +func lineColAt(src string, offset int) (line, col int) { + line, col = 1, 1 + for i := 0; i < offset; { + switch src[i] { + case '\n': + line++ + col = 1 + i++ + case '\r': + if i+1 < offset && src[i+1] == '\n' { + i++ // \r\n: the \n resets the line + continue + } + line++ + col = 1 + i++ + default: + _, size := utf8.DecodeRuneInString(src[i:offset]) + col++ + i += size + } + } + return line, col +} diff --git a/syntax/lexer_test.go b/syntax/lexer_test.go new file mode 100644 index 0000000..b405575 --- /dev/null +++ b/syntax/lexer_test.go @@ -0,0 +1,524 @@ +package syntax + +import ( + "reflect" + "strings" + "testing" +) + +type tokSpec struct { + kind TokenKind + text string +} + +func tokenSpecs(toks []Token) []tokSpec { + specs := make([]tokSpec, 0, len(toks)) + for _, tok := range toks { + specs = append(specs, tokSpec{tok.Kind, tok.Text}) + } + return specs +} + +func TestLexTokens(t *testing.T) { + eof := tokSpec{TokenEOF, ""} + tests := []struct { + name string + src string + want []tokSpec + }{ + {"empty", "", []tokSpec{eof}}, + {"whitespace only", " \n\t\r\n", []tokSpec{eof}}, + {"identifiers", "foo _bar baz1", []tokSpec{ + {TokenIdentifier, "foo"}, + {TokenIdentifier, "_bar"}, + {TokenIdentifier, "baz1"}, + eof, + }}, + {"dotted identifier", "foo.bar.baz", []tokSpec{ + {TokenIdentifier, "foo.bar.baz"}, + eof, + }}, + {"dotted keyword is identifier", "include.foo", []tokSpec{ + {TokenIdentifier, "include.foo"}, + eof, + }}, + {"ints", "0 42 -1 +7", []tokSpec{ + {TokenIntConstant, "0"}, + {TokenIntConstant, "42"}, + {TokenIntConstant, "-1"}, + {TokenIntConstant, "+7"}, + eof, + }}, + {"hex", "0x1F -0x10 +0xabcdef 0X2A", []tokSpec{ + {TokenIntConstant, "0x1F"}, + {TokenIntConstant, "-0x10"}, + {TokenIntConstant, "+0xabcdef"}, + {TokenIntConstant, "0X2A"}, + eof, + }}, + {"bare 0x splits into 0 and x", "0x", []tokSpec{ + {TokenIntConstant, "0"}, + {TokenIdentifier, "x"}, + eof, + }}, + {"doubles", "1.5 -1.5 .5 1e10 1.5e-3 +2.0 -0.25E+2", []tokSpec{ + {TokenDoubleConstant, "1.5"}, + {TokenDoubleConstant, "-1.5"}, + {TokenDoubleConstant, ".5"}, + {TokenDoubleConstant, "1e10"}, + {TokenDoubleConstant, "1.5e-3"}, + {TokenDoubleConstant, "+2.0"}, + {TokenDoubleConstant, "-0.25E+2"}, + eof, + }}, + {"int wins ties against double", "-1 42", []tokSpec{ + {TokenIntConstant, "-1"}, + {TokenIntConstant, "42"}, + eof, + }}, + {"e10 is an identifier", "e10", []tokSpec{ + {TokenIdentifier, "e10"}, + eof, + }}, + {"1e splits into int and identifier", "1e", []tokSpec{ + {TokenIntConstant, "1"}, + {TokenIdentifier, "e"}, + eof, + }}, + {"strings", `"foo" 'bar' "a\"b" 'it\'s'`, []tokSpec{ + {TokenStringLiteral, `"foo"`}, + {TokenStringLiteral, `'bar'`}, + {TokenStringLiteral, `"a\"b"`}, + {TokenStringLiteral, `'it\'s'`}, + eof, + }}, + {"escape sequences", `"\r\n\t\"\'\\"`, []tokSpec{ + {TokenStringLiteral, `"\r\n\t\"\'\\"`}, + eof, + }}, + {"empty strings", `"" ''`, []tokSpec{ + {TokenStringLiteral, `""`}, + {TokenStringLiteral, `''`}, + eof, + }}, + {"symbols", "{}()[]<>,;:=*&", []tokSpec{ + {TokenLBrace, "{"}, + {TokenRBrace, "}"}, + {TokenLParen, "("}, + {TokenRParen, ")"}, + {TokenLBracket, "["}, + {TokenRBracket, "]"}, + {TokenLt, "<"}, + {TokenGt, ">"}, + {TokenComma, ","}, + {TokenSemicolon, ";"}, + {TokenColon, ":"}, + {TokenEqual, "="}, + {TokenStar, "*"}, + {TokenAmp, "&"}, + eof, + }}, + {"all keywords", "include cpp_include cpp_type namespace struct union exception service enum const typedef oneway async throws extends required optional void bool byte i8 i16 i32 i64 double string binary slist uuid map list set true false", []tokSpec{ + {TokenInclude, "include"}, + {TokenCPPInclude, "cpp_include"}, + {TokenCPPType, "cpp_type"}, + {TokenNamespace, "namespace"}, + {TokenStruct, "struct"}, + {TokenUnion, "union"}, + {TokenException, "exception"}, + {TokenService, "service"}, + {TokenEnum, "enum"}, + {TokenConst, "const"}, + {TokenTypedef, "typedef"}, + {TokenOneway, "oneway"}, + {TokenAsync, "async"}, + {TokenThrows, "throws"}, + {TokenExtends, "extends"}, + {TokenRequired, "required"}, + {TokenOptional, "optional"}, + {TokenVoid, "void"}, + {TokenBool, "bool"}, + {TokenByte, "byte"}, + {TokenI8, "i8"}, + {TokenI16, "i16"}, + {TokenI32, "i32"}, + {TokenI64, "i64"}, + {TokenDouble, "double"}, + {TokenString, "string"}, + {TokenBinary, "binary"}, + {TokenSlist, "slist"}, + {TokenUUID, "uuid"}, + {TokenMap, "map"}, + {TokenList, "list"}, + {TokenSet, "set"}, + {TokenTrue, "true"}, + {TokenFalse, "false"}, + eof, + }}, + {"struct skeleton", "struct S {\n 1: i32 id\n}", []tokSpec{ + {TokenStruct, "struct"}, + {TokenIdentifier, "S"}, + {TokenLBrace, "{"}, + {TokenIntConstant, "1"}, + {TokenColon, ":"}, + {TokenI32, "i32"}, + {TokenIdentifier, "id"}, + {TokenRBrace, "}"}, + eof, + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + toks, errs := Lex([]byte(tt.src)) + if len(errs) > 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if got := tokenSpecs(toks); !reflect.DeepEqual(got, tt.want) { + t.Errorf("tokens mismatch\n got: %v\nwant: %v", got, tt.want) + } + }) + } +} + +type posSpec struct { + line, col, offset int +} + +func TestLexPositions(t *testing.T) { + tests := []struct { + name string + src string + want []posSpec // one per token, aligned with tokenSpecs order + }{ + { + "single line", + "struct S {}", + []posSpec{{1, 1, 0}, {1, 8, 7}, {1, 10, 9}, {1, 11, 10}, {1, 12, 11}}, + }, + { + "multiline", + "struct S {\n 1: i32 id\n}", + []posSpec{{1, 1, 0}, {1, 8, 7}, {1, 10, 9}, {2, 3, 13}, {2, 4, 14}, {2, 6, 16}, {2, 10, 20}, {3, 1, 23}, {3, 2, 24}}, + }, + { + "columns count runes", + "// é\nstruct S {}", + []posSpec{{2, 1, 6}, {2, 8, 13}, {2, 10, 15}, {2, 11, 16}, {2, 12, 17}}, + }, + { + "crlf line endings", + "struct S {\r\n}", + []posSpec{{1, 1, 0}, {1, 8, 7}, {1, 10, 9}, {2, 1, 12}, {2, 2, 13}}, + }, + { + "string with multibyte content", + `const string s = "héllo"`, + []posSpec{{1, 1, 0}, {1, 7, 6}, {1, 14, 13}, {1, 16, 15}, {1, 18, 17}, {1, 25, 25}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + toks, errs := Lex([]byte(tt.src)) + if len(errs) > 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if len(toks) != len(tt.want) { + t.Fatalf("got %d tokens (%v), want %d", len(toks), tokenSpecs(toks), len(tt.want)) + } + for i, want := range tt.want { + got := toks[i] + if got.Line != want.line || got.Col != want.col || got.Offset != want.offset { + t.Errorf("token %d (%s %q) at %d:%d (offset %d), want %d:%d (offset %d)", + i, got.Kind, got.Text, got.Line, got.Col, got.Offset, + want.line, want.col, want.offset) + } + } + }) + } +} + +type triviaCheck struct { + idx int + leading []string // comment texts, in order + trailing []string + blankLinesBefore int +} + +func TestLexTrivia(t *testing.T) { + tests := []struct { + name string + src string + checks []triviaCheck + }{ + { + "same-line comment is trailing", + "i32 x // c\ni32 y", + []triviaCheck{ + {idx: 1, trailing: []string{"// c"}}, + }, + }, + { + "own-line comment is leading", + "// c\ni32 x", + []triviaCheck{ + {idx: 0, leading: []string{"// c"}}, + }, + }, + { + "comment between tokens on own line is leading of next", + "i32 x\n// c\ni32 y", + []triviaCheck{ + {idx: 2, leading: []string{"// c"}}, + }, + }, + { + "block comment on token line is trailing", + "i32 x /* c */ i32 y", + []triviaCheck{ + {idx: 1, trailing: []string{"/* c */"}}, + }, + }, + { + "hash comment is a line comment", + "i32 x # c\ni32 y", + []triviaCheck{ + {idx: 1, trailing: []string{"# c"}}, + }, + }, + { + "multiple trailing comments keep order", + "i32 x /* a */ // b\ni32 y", + []triviaCheck{ + {idx: 1, trailing: []string{"/* a */", "// b"}}, + }, + }, + { + "doc comment kind", + "/** doc */\nstruct S {}", + []triviaCheck{ + {idx: 0, leading: []string{"/** doc */"}}, + }, + }, + { + "empty doc comment", + "/**/\nstruct S {}", + []triviaCheck{ + {idx: 0, leading: []string{"/**/"}}, + }, + }, + { + "comments at end of file attach to eof", + "i32 x\n// tail", + []triviaCheck{ + {idx: 2, leading: []string{"// tail"}}, + }, + }, + { + "trailing comment at end of file attaches to token", + "i32 x // tail", + []triviaCheck{ + {idx: 1, trailing: []string{"// tail"}}, + }, + }, + { + "blank lines counted", + "i32 x\n\n\ni32 y", + []triviaCheck{ + {idx: 2, blankLinesBefore: 2}, + }, + }, + { + "blank line before comment counts", + "i32 x\n\n// c\ni32 y", + []triviaCheck{ + {idx: 2, leading: []string{"// c"}, blankLinesBefore: 1}, + }, + }, + { + "blank line after trailing comment counts", + "i32 x // c\n\ni32 y", + []triviaCheck{ + {idx: 2, blankLinesBefore: 1}, + }, + }, + { + "no blank lines", + "i32 x\ni32 y", + []triviaCheck{ + {idx: 2, blankLinesBefore: 0}, + }, + }, + { + "multiline block comment spanning tokens", + "i32 x /*\n c\n*/ i32 y", + []triviaCheck{ + {idx: 1, trailing: []string{"/*\n c\n*/"}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + toks, errs := Lex([]byte(tt.src)) + if len(errs) > 0 { + t.Fatalf("unexpected errors: %v", errs) + } + for _, check := range tt.checks { + if check.idx >= len(toks) { + t.Fatalf("token index %d out of range (%d tokens)", check.idx, len(toks)) + } + tok := toks[check.idx] + if got := triviaTexts(tok.Leading); !reflect.DeepEqual(got, check.leading) { + t.Errorf("token %d leading: got %v, want %v", check.idx, got, check.leading) + } + if got := triviaTexts(tok.Trailing); !reflect.DeepEqual(got, check.trailing) { + t.Errorf("token %d trailing: got %v, want %v", check.idx, got, check.trailing) + } + if tok.BlankLinesBefore != check.blankLinesBefore { + t.Errorf("token %d blankLinesBefore: got %d, want %d", check.idx, tok.BlankLinesBefore, check.blankLinesBefore) + } + } + }) + } +} + +func triviaTexts(trivia []Trivia) []string { + if len(trivia) == 0 { + return nil + } + texts := make([]string, 0, len(trivia)) + for _, t := range trivia { + texts = append(texts, t.Text) + } + return texts +} + +func TestLexTriviaKinds(t *testing.T) { + tests := []struct { + name string + src string + want []TriviaKind + }{ + {"line", "// a\n# b\n", []TriviaKind{TriviaLineComment, TriviaLineComment}}, + {"block and doc", "/** d */\n/* b */\n", []TriviaKind{TriviaDocComment, TriviaBlockComment}}, + {"silly comment is doc", "/***/\n", []TriviaKind{TriviaDocComment}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + toks, errs := Lex([]byte(tt.src)) + if len(errs) > 0 { + t.Fatalf("unexpected errors: %v", errs) + } + eof := toks[len(toks)-1] + if eof.Kind != TokenEOF { + t.Fatalf("last token is %v, want eof", eof.Kind) + } + var got []TriviaKind + for _, trivia := range eof.Leading { + got = append(got, trivia.Kind) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("trivia kinds: got %v, want %v", got, tt.want) + } + }) + } +} + +func TestLexErrors(t *testing.T) { + tests := []struct { + name string + src string + wantErrs []string // substrings, one per expected error + wantKinds []TokenKind + }{ + { + name: "invalid escape", + src: `"a\qb"`, + wantErrs: []string{"invalid escape sequence"}, + wantKinds: []TokenKind{TokenStringLiteral, TokenEOF}, + }, + { + name: "unterminated string", + src: `"abc`, + wantErrs: []string{"unterminated string literal"}, + wantKinds: []TokenKind{TokenStringLiteral, TokenEOF}, + }, + { + name: "trailing backslash before eof", + src: `"abc\`, + wantErrs: []string{"unterminated string literal"}, + wantKinds: []TokenKind{TokenStringLiteral, TokenEOF}, + }, + { + name: "newline in string", + src: "\"a\nb\"", + wantErrs: []string{"newline in string literal", "unterminated string literal"}, + wantKinds: []TokenKind{TokenStringLiteral, TokenIdentifier, TokenStringLiteral, TokenEOF}, + }, + { + name: "lone dot", + src: "foo.", + wantErrs: []string{"unexpected character"}, + wantKinds: []TokenKind{TokenIdentifier, TokenEOF}, + }, + { + name: "dot before identifier", + src: "include .foo", + wantErrs: []string{"unexpected character"}, + wantKinds: []TokenKind{TokenInclude, TokenIdentifier, TokenEOF}, + }, + { + name: "trailing dot after int", + src: "1.", + wantErrs: []string{"unexpected character"}, + wantKinds: []TokenKind{TokenIntConstant, TokenEOF}, + }, + { + name: "lone plus", + src: "1 +", + wantErrs: []string{"unexpected character"}, + wantKinds: []TokenKind{TokenIntConstant, TokenEOF}, + }, + { + name: "unexpected character with recovery", + src: "foo @ bar", + wantErrs: []string{"unexpected character"}, + wantKinds: []TokenKind{TokenIdentifier, TokenIdentifier, TokenEOF}, + }, + { + name: "unterminated comment", + src: "/* x", + wantErrs: []string{"unterminated comment"}, + wantKinds: []TokenKind{TokenEOF}, + }, + { + name: "unterminated doc comment", + src: "/** x", + wantErrs: []string{"unterminated comment"}, + wantKinds: []TokenKind{TokenEOF}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + toks, errs := Lex([]byte(tt.src)) + if len(errs) != len(tt.wantErrs) { + t.Fatalf("got %d errors (%v), want %d", len(errs), errs, len(tt.wantErrs)) + } + for i, want := range tt.wantErrs { + if got := errs[i].Error(); !strings.Contains(got, want) { + t.Errorf("error %d: got %q, want substring %q", i, got, want) + } + } + got := make([]TokenKind, 0, len(toks)) + for _, tok := range toks { + got = append(got, tok.Kind) + } + if !reflect.DeepEqual(got, tt.wantKinds) { + t.Errorf("token kinds: got %v, want %v", got, tt.wantKinds) + } + }) + } +} diff --git a/syntax/parser.go b/syntax/parser.go new file mode 100644 index 0000000..e50d4f4 --- /dev/null +++ b/syntax/parser.go @@ -0,0 +1,770 @@ +package syntax + +import ( + "fmt" + "sort" +) + +// Parse lexes and parses src into a Document. It always returns a document +// (possibly partial) and every lexical and parse error that was found. +// Documents with errors must not be formatted. +func Parse(src []byte) (*Document, []Error) { + toks, lexErrs := Lex(src) + doc, parseErrs := ParseTokens(toks) + errs := append(lexErrs, parseErrs...) + sort.SliceStable(errs, func(i, j int) bool { return errs[i].Offset < errs[j].Offset }) + return doc, errs +} + +// ParseTokens parses an already-lexed token stream. The LSP can reuse this to +// reparse a file from its cached tokens without re-lexing. +func ParseTokens(toks []Token) (*Document, []Error) { + p := &parser{toks: toks} + return p.parseDocument(), p.errs +} + +type parser struct { + toks []Token + pos int + errs []Error +} + +// --- token helpers --------------------------------------------------------- + +func (p *parser) cur() Token { return p.toks[p.pos] } + +func (p *parser) at(k TokenKind) bool { return p.cur().Kind == k } + +func (p *parser) advance() *Token { + t := &p.toks[p.pos] + p.pos++ + return t +} + +// accept consumes and returns the current token when its kind matches. +func (p *parser) accept(k TokenKind) *Token { + if p.at(k) { + return p.advance() + } + return nil +} + +// expect consumes the current token when its kind matches and reports an +// error otherwise. +func (p *parser) expect(k TokenKind, what string) bool { + if p.at(k) { + p.advance() + return true + } + p.errorfCur("expected %s, got %q", what, p.cur().Text) + return false +} + +// synchronizeTo skips tokens until one of the given kinds or EOF. +func (p *parser) synchronizeTo(kinds ...TokenKind) { + for !p.at(TokenEOF) { + for _, k := range kinds { + if p.cur().Kind == k { + return + } + } + p.advance() + } +} + +func (p *parser) errorfCur(format string, args ...any) { + t := p.cur() + p.errs = append(p.errs, Error{ + Message: fmt.Sprintf(format, args...), + Offset: t.Offset, + Line: t.Line, + Col: t.Col, + Severity: SeverityError, + }) +} + +func (p *parser) warnf(t *Token, format string, args ...any) { + p.errs = append(p.errs, Error{ + Message: fmt.Sprintf(format, args...), + Offset: t.Offset, + Line: t.Line, + Col: t.Col, + Severity: SeverityWarning, + }) +} + +// --- document level -------------------------------------------------------- + +func (p *parser) parseDocument() *Document { + doc := &Document{Tokens: p.toks} + for { + switch p.cur().Kind { + case TokenEOF: + return doc + case TokenInclude: + if n := p.parseInclude(); n != nil { + doc.Nodes = append(doc.Nodes, n) + } + case TokenCPPInclude: + if n := p.parseCPPInclude(); n != nil { + doc.Nodes = append(doc.Nodes, n) + } + case TokenNamespace: + if n := p.parseNamespace(); n != nil { + doc.Nodes = append(doc.Nodes, n) + } + case TokenConst: + if n := p.parseConst(); n != nil { + doc.Nodes = append(doc.Nodes, n) + } + case TokenTypedef: + if n := p.parseTypedef(); n != nil { + doc.Nodes = append(doc.Nodes, n) + } + case TokenEnum: + if n := p.parseEnum(); n != nil { + doc.Nodes = append(doc.Nodes, n) + } + case TokenStruct, TokenUnion, TokenException: + if n := p.parseStruct(); n != nil { + doc.Nodes = append(doc.Nodes, n) + } + case TokenService: + if n := p.parseService(); n != nil { + doc.Nodes = append(doc.Nodes, n) + } + default: + p.errorfCur("unexpected token %q at top level", p.cur().Text) + p.synchronizeTo(TokenInclude, TokenCPPInclude, TokenNamespace, + TokenConst, TokenTypedef, TokenEnum, TokenStruct, + TokenUnion, TokenException, TokenService) + } + } +} + +// --- headers --------------------------------------------------------------- + +func (p *parser) parseInclude() *Include { + n := &Include{nodeBase: nodeBase{first: p.pos}} + p.advance() // include + if !p.at(TokenStringLiteral) { + p.errorfCur("expected include path string, got %q", p.cur().Text) + if !p.at(TokenEOF) { + p.advance() // consume the offending token so the top level can continue + } + return nil + } + n.Path = p.advance() + n.last = p.pos - 1 + return n +} + +func (p *parser) parseCPPInclude() *CPPInclude { + n := &CPPInclude{nodeBase: nodeBase{first: p.pos}} + p.advance() // cpp_include + if !p.at(TokenStringLiteral) { + p.errorfCur("expected cpp_include path string, got %q", p.cur().Text) + if !p.at(TokenEOF) { + p.advance() // consume the offending token so the top level can continue + } + return nil + } + n.Path = p.advance() + n.last = p.pos - 1 + return n +} + +func (p *parser) parseNamespace() *Namespace { + n := &Namespace{nodeBase: nodeBase{first: p.pos}} + p.advance() // namespace + switch { + case p.at(TokenIdentifier), p.at(TokenStar): + n.Scope = p.advance() + default: + p.errorfCur("expected namespace scope, got %q", p.cur().Text) + p.synchronizeTo(TokenEOF) + return nil + } + n.Name = p.expectIdentifier("namespace name") + if n.Name == nil { + p.synchronizeTo(TokenEOF) + return nil + } + n.Annotations = p.parseAnnotationsIfPresent() + n.last = p.pos - 1 + return n +} + +// --- definitions ----------------------------------------------------------- + +func (p *parser) parseConst() *Const { + n := &Const{nodeBase: nodeBase{first: p.pos}} + p.advance() // const + n.Type = p.parseFieldType() + if n.Type == nil { + p.synchronizeTo(TokenEOF) + return nil + } + n.Name = p.expectIdentifier("constant name") + if n.Name == nil { + p.synchronizeTo(TokenEOF) + return nil + } + if !p.expect(TokenEqual, "'='") { + p.synchronizeTo(TokenEOF) + return nil + } + n.Value = p.parseConstValue() + if n.Value == nil { + p.synchronizeTo(TokenEOF) + return nil + } + if sep := p.acceptSeparator(); sep != 0 { + n.Sep = sep + } + n.last = p.pos - 1 + return n +} + +func (p *parser) parseTypedef() *Typedef { + n := &Typedef{nodeBase: nodeBase{first: p.pos}} + p.advance() // typedef + n.Type = p.parseFieldType() + if n.Type == nil { + p.synchronizeTo(TokenEOF) + return nil + } + n.Name = p.expectIdentifier("typedef name") + if n.Name == nil { + p.synchronizeTo(TokenEOF) + return nil + } + n.Annotations = p.parseAnnotationsIfPresent() + if sep := p.acceptSeparator(); sep != 0 { + n.Sep = sep + } + n.last = p.pos - 1 + return n +} + +func (p *parser) parseEnum() *Enum { + n := &Enum{nodeBase: nodeBase{first: p.pos}} + p.advance() // enum + n.Name = p.expectIdentifier("enum name") + if n.Name == nil { + p.synchronizeTo(TokenEOF) + return nil + } + if p.accept(TokenLBrace) != nil { + for !p.at(TokenRBrace) && !p.at(TokenEOF) { + v := p.parseEnumValue() + if v == nil { + p.synchronizeTo(TokenRBrace) + continue + } + n.Values = append(n.Values, v) + } + if !p.at(TokenRBrace) { + p.errorfCur("expected '}' to close enum, got %q", p.cur().Text) + } else { + p.advance() + } + } else { + p.errorfCur("expected '{' after enum name, got %q", p.cur().Text) + } + n.Annotations = p.parseAnnotationsIfPresent() + n.last = p.pos - 1 + return n +} + +func (p *parser) parseEnumValue() *EnumValue { + if !p.at(TokenIdentifier) { + p.errorfCur("expected enum value name, got %q", p.cur().Text) + return nil + } + v := &EnumValue{nodeBase: nodeBase{first: p.pos}} + v.Name = p.identifier() + if p.at(TokenEqual) { + p.advance() + if !p.at(TokenIntConstant) { + p.errorfCur("expected integer enum value, got %q", p.cur().Text) + } else { + v.Value = p.advance() + } + } + v.Annotations = p.parseAnnotationsIfPresent() + if sep := p.acceptSeparator(); sep != 0 { + v.Sep = sep + } + v.last = p.pos - 1 + return v +} + +func (p *parser) parseStruct() *Struct { + n := &Struct{nodeBase: nodeBase{first: p.pos}, Kind: StructKind(p.cur().Kind)} + p.advance() // struct | union | exception + n.Name = p.expectIdentifier("struct name") + if n.Name == nil { + p.synchronizeTo(TokenEOF) + return nil + } + if p.accept(TokenLBrace) != nil { + n.Fields = p.parseFieldList(TokenRBrace) + if !p.at(TokenRBrace) { + p.errorfCur("expected '}' to close struct, got %q", p.cur().Text) + } else { + p.advance() + } + } else { + p.errorfCur("expected '{' after struct name, got %q", p.cur().Text) + } + n.Annotations = p.parseAnnotationsIfPresent() + n.last = p.pos - 1 + return n +} + +func (p *parser) parseService() *Service { + n := &Service{nodeBase: nodeBase{first: p.pos}} + p.advance() // service + n.Name = p.expectIdentifier("service name") + if n.Name == nil { + p.synchronizeTo(TokenEOF) + return nil + } + if p.at(TokenExtends) { + p.advance() + n.Extends = p.expectIdentifier("base service name") + if n.Extends == nil { + p.synchronizeTo(TokenLBrace, TokenEOF) + if !p.at(TokenLBrace) { + return nil + } + } + } + if p.accept(TokenLBrace) != nil { + for !p.at(TokenRBrace) && !p.at(TokenEOF) { + f := p.parseFunction() + if f == nil { + p.synchronizeTo(TokenRBrace) + continue + } + n.Functions = append(n.Functions, f) + } + if !p.at(TokenRBrace) { + p.errorfCur("expected '}' to close service, got %q", p.cur().Text) + } else { + p.advance() + } + } else { + p.errorfCur("expected '{' after service name, got %q", p.cur().Text) + } + n.Annotations = p.parseAnnotationsIfPresent() + n.last = p.pos - 1 + return n +} + +func (p *parser) parseFunction() *Function { + f := &Function{nodeBase: nodeBase{first: p.pos}} + + switch p.cur().Kind { + case TokenOneway: + f.Oneway = p.advance() + case TokenAsync: + f.Oneway = p.advance() + p.warnf(f.Oneway, "async is deprecated, use oneway") + } + + if p.at(TokenVoid) { + f.Void = p.advance() + } else { + f.Type = p.parseFieldType() + if f.Type == nil { + p.synchronizeTo(TokenLParen, TokenEOF) + return nil + } + } + + f.Name = p.expectIdentifier("function name") + if f.Name == nil { + p.synchronizeTo(TokenLParen, TokenEOF) + return nil + } + + if !p.expect(TokenLParen, "'('") { + p.synchronizeTo(TokenEOF) + return nil + } + f.Args = p.parseFieldList(TokenRParen) + if !p.at(TokenRParen) { + p.errorfCur("expected ')' to close arguments, got %q", p.cur().Text) + } else { + p.advance() + } + + if p.at(TokenThrows) { + p.advance() + if !p.expect(TokenLParen, "'(' after throws") { + p.synchronizeTo(TokenEOF) + return nil + } + f.Throws = &Throws{nodeBase: nodeBase{first: p.pos - 1}} + f.Throws.Fields = p.parseFieldList(TokenRParen) + if !p.at(TokenRParen) { + p.errorfCur("expected ')' to close throws, got %q", p.cur().Text) + } else { + p.advance() + } + f.Throws.last = p.pos - 1 + } + + f.Annotations = p.parseAnnotationsIfPresent() + if sep := p.acceptSeparator(); sep != 0 { + f.Sep = sep + } + f.last = p.pos - 1 + return f +} + +// --- fields ---------------------------------------------------------------- + +// parseFieldList parses fields until the terminator token. It is shared by +// struct bodies, function arguments, and throws clauses. +func (p *parser) parseFieldList(term TokenKind) []*Field { + var fields []*Field + for !p.at(term) && !p.at(TokenEOF) { + f, ok := p.parseField() + if !ok { + p.synchronizeTo(TokenComma, TokenSemicolon, term) + if !p.at(term) && !p.at(TokenEOF) { + p.advance() // consume the stray separator, if any + } + continue + } + fields = append(fields, f) + } + return fields +} + +func (p *parser) parseField() (*Field, bool) { + f := &Field{nodeBase: nodeBase{first: p.pos}} + + if p.at(TokenIntConstant) && p.toks[p.pos+1].Kind == TokenColon { + f.FieldID = p.advance() + if !p.expect(TokenColon, "':' after field id") { + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRBrace, TokenRParen) + return f, false + } + } + + switch p.cur().Kind { + case TokenRequired, TokenOptional: + f.Req = p.advance().Kind + } + + f.Type = p.parseFieldType() + if f.Type == nil { + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRBrace, TokenRParen) + return f, false + } + + if p.accept(TokenAmp) != nil { + f.Reference = true + } + + // Field names may be any keyword, not just identifiers: type names such + // as uuid are both valid types and common field names, and the thrift + // compiler accepts them there. Rejecting keywords as field names breaks + // valid IDLs. + if p.at(TokenIdentifier) || isKeyword(p.cur().Kind) { + f.Name = p.identifier() + } else { + p.errorfCur("expected field name, got %q", p.cur().Text) + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRBrace, TokenRParen) + return f, false + } + + if p.at(TokenEqual) { + p.advance() + f.Value = p.parseConstValue() + if f.Value == nil { + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRBrace, TokenRParen) + return f, false + } + } + + f.Annotations = p.parseAnnotationsIfPresent() + if sep := p.acceptSeparator(); sep != 0 { + f.Sep = sep + } + f.last = p.pos - 1 + return f, true +} + +// --- types ----------------------------------------------------------------- + +func (p *parser) parseFieldType() *FieldType { + t := &FieldType{nodeBase: nodeBase{first: p.pos}} + + switch p.cur().Kind { + case TokenMap, TokenList, TokenSet: + switch p.cur().Kind { + case TokenMap: + t.Kind = TypeMap + case TokenList: + t.Kind = TypeList + case TokenSet: + t.Kind = TypeSet + } + p.advance() + + if p.at(TokenCPPType) { + p.advance() + if !p.at(TokenStringLiteral) { + p.errorfCur("expected string literal after cpp_type, got %q", p.cur().Text) + } else { + t.CPPType = p.advance() + } + } + + if !p.expect(TokenLt, "'<' to open container type") { + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRBrace, TokenRParen) + return nil + } + + if t.Kind == TypeMap { + t.KeyType = p.parseFieldType() + if t.KeyType == nil { + p.synchronizeTo(TokenComma, TokenGt) + } + if p.accept(TokenComma) != nil { + t.ValueType = p.parseFieldType() + if t.ValueType == nil { + p.synchronizeTo(TokenGt) + } + } else { + p.errorfCur("expected ',' and a value type for map, got %q", p.cur().Text) + if !p.at(TokenGt) { + p.synchronizeTo(TokenGt) + } + } + } else { + t.ValueType = p.parseFieldType() + if t.ValueType == nil { + p.synchronizeTo(TokenGt) + } + } + + if !p.expect(TokenGt, "'>' to close container type") { + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRBrace, TokenRParen) + return nil + } + + case TokenIdentifier: + t.Kind = TypeIdent + t.Ident = p.identifier() + + default: + if !isBaseType(p.cur().Kind) { + p.errorfCur("expected type, got %q", p.cur().Text) + return nil + } + t.Kind = TypeBase + t.Base = p.cur().Kind + p.deprecationWarnings(p.cur()) + p.advance() + } + + // Base and container types take annotations; identifier types do not. + if t.Kind != TypeIdent { + t.Annotations = p.parseAnnotationsIfPresent() + } + t.last = p.pos - 1 + return t +} + +func (p *parser) deprecationWarnings(t Token) { + switch t.Kind { + case TokenByte: + p.warnf(&t, "byte is deprecated, use i8") + case TokenSlist: + p.warnf(&t, "slist is no longer supported, use string or binary") + } +} + +func isBaseType(k TokenKind) bool { + switch k { + case TokenBool, TokenByte, TokenI8, TokenI16, TokenI32, TokenI64, + TokenDouble, TokenString, TokenBinary, TokenSlist, TokenUUID: + return true + } + return false +} + +// --- constant values ------------------------------------------------------- + +func (p *parser) parseConstValue() *ConstValue { + v := &ConstValue{nodeBase: nodeBase{first: p.pos}} + + switch p.cur().Kind { + case TokenIntConstant, TokenTrue, TokenFalse: + v.Kind = ValueInt + v.Text = p.advance().Text + + case TokenDoubleConstant: + v.Kind = ValueDouble + v.Text = p.advance().Text + + case TokenStringLiteral: + v.Kind = ValueString + v.Text = p.advance().Text + + case TokenIdentifier: + v.Kind = ValueIdent + v.Text = p.advance().Text + + case TokenLBracket: + v.Kind = ValueList + p.advance() + for !p.at(TokenRBracket) && !p.at(TokenEOF) { + item := p.parseConstValue() + if item == nil { + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRBracket) + if !p.at(TokenRBracket) && !p.at(TokenEOF) { + p.advance() // consume the stray separator + } + continue + } + v.List = append(v.List, item) + p.acceptSeparator() + } + if !p.at(TokenRBracket) { + p.errorfCur("expected ']' to close list constant, got %q", p.cur().Text) + } else { + p.advance() + } + + case TokenLBrace: + v.Kind = ValueMap + p.advance() + for !p.at(TokenRBrace) && !p.at(TokenEOF) { + key := p.parseConstValue() + if key == nil { + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRBrace) + if !p.at(TokenRBrace) && !p.at(TokenEOF) { + p.advance() + } + continue + } + if !p.expect(TokenColon, "':' between map key and value") { + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRBrace) + if !p.at(TokenRBrace) && !p.at(TokenEOF) { + p.advance() + } + continue + } + value := p.parseConstValue() + if value == nil { + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRBrace) + if !p.at(TokenRBrace) && !p.at(TokenEOF) { + p.advance() + } + continue + } + v.Map = append(v.Map, ConstMapEntry{Key: key, Value: value}) + p.acceptSeparator() + } + if !p.at(TokenRBrace) { + p.errorfCur("expected '}' to close map constant, got %q", p.cur().Text) + } else { + p.advance() + } + + default: + p.errorfCur("expected constant value, got %q", p.cur().Text) + return nil + } + + v.last = p.pos - 1 + return v +} + +// --- annotations ----------------------------------------------------------- + +func (p *parser) parseAnnotationsIfPresent() *Annotations { + if !p.at(TokenLParen) { + return nil + } + return p.parseAnnotations() +} + +// parseAnnotations parses a parenthesized annotation group. Per the compiler +// grammar, each annotation is a name optionally followed by '=' and a string +// literal (a bare name means an implicit value of "1"), and each may end +// with an optional ',' or ';'. +func (p *parser) parseAnnotations() *Annotations { + a := &Annotations{nodeBase: nodeBase{first: p.pos}} + p.advance() // ( + + for !p.at(TokenRParen) && !p.at(TokenEOF) { + if !p.at(TokenIdentifier) { + p.errorfCur("expected annotation name, got %q", p.cur().Text) + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRParen) + if !p.at(TokenRParen) && !p.at(TokenEOF) { + p.advance() + } + continue + } + item := &Annotation{nodeBase: nodeBase{first: p.pos}} + item.Name = p.identifier() + if p.at(TokenEqual) { + p.advance() + if !p.at(TokenStringLiteral) { + p.errorfCur("expected string literal annotation value, got %q", p.cur().Text) + p.synchronizeTo(TokenComma, TokenSemicolon, TokenRParen) + } else { + item.Value = p.advance() + } + } + if sep := p.acceptSeparator(); sep != 0 { + item.Sep = sep + } + item.last = p.pos - 1 + a.Items = append(a.Items, item) + } + + if !p.at(TokenRParen) { + p.errorfCur("expected ')' to close annotations, got %q", p.cur().Text) + } else { + p.advance() + } + a.last = p.pos - 1 + return a +} + +// --- shared helpers -------------------------------------------------------- + +func (p *parser) acceptSeparator() TokenKind { + switch p.cur().Kind { + case TokenComma, TokenSemicolon: + k := p.cur().Kind + p.advance() + return k + } + return 0 +} + +func (p *parser) identifier() *Identifier { + i := p.pos + t := p.advance() + return &Identifier{nodeBase: nodeBase{first: i, last: i}, Text: t.Text} +} + +// expectIdentifier parses a plain identifier name and reports an error when +// the current token is not one. +func (p *parser) expectIdentifier(what string) *Identifier { + if !p.at(TokenIdentifier) { + p.errorfCur("expected %s, got %q", what, p.cur().Text) + return nil + } + return p.identifier() +} diff --git a/syntax/parser_fuzz_test.go b/syntax/parser_fuzz_test.go new file mode 100644 index 0000000..b93a8c9 --- /dev/null +++ b/syntax/parser_fuzz_test.go @@ -0,0 +1,56 @@ +package syntax + +import ( + "reflect" + "testing" +) + +// FuzzParse checks parser invariants that hold for every input: +// +// - parsing never panics or loops forever, even on malformed input +// - Parse always returns a document whose node ranges are well-formed and +// consistent with the token stream (via checkDoc) +// - errors are sorted by source offset +// - parsing is deterministic +func FuzzParse(f *testing.F) { + for _, seed := range [][]byte{ + []byte(""), + []byte("struct S {\n 1: i32 id\n}"), + []byte("service Foo extends Bar {\n oneway void ping() throws (E e)\n}"), + []byte("const map> M = {\"a\": [1, 2]}"), + []byte("enum E (anno = \"x\") {\n A = 1,\n B\n}"), + []byte("typedef i32 T (bare, a = \"1\"; b = '2')"), + []byte("namespace * all"), + []byte("include \"foo.thrift\""), + []byte("struct S {"), + []byte("const i32 x ="), + []byte("void"), + []byte("("), + []byte("}"), + []byte("struct S {\n 1: void bad\n}"), + []byte("list>>"), + } { + f.Add(seed) + } + + f.Fuzz(func(t *testing.T, src []byte) { + doc, errs := Parse(src) + + if doc == nil { + t.Fatal("Parse returned nil document") + } + checkDoc(t, doc) + + for i := 1; i < len(errs); i++ { + if errs[i-1].Offset > errs[i].Offset { + t.Fatalf("errors not sorted by offset: %v", errs) + } + } + + // Determinism. + doc2, errs2 := Parse(src) + if !reflect.DeepEqual(doc, doc2) || !reflect.DeepEqual(errs, errs2) { + t.Fatal("Parse is not deterministic") + } + }) +} diff --git a/syntax/parser_test.go b/syntax/parser_test.go new file mode 100644 index 0000000..22110ef --- /dev/null +++ b/syntax/parser_test.go @@ -0,0 +1,1036 @@ +package syntax + +import ( + "reflect" + "strconv" + "strings" + "testing" +) + +// isNilNode reports whether a Node is nil or a typed nil pointer. +func isNilNode(n Node) bool { + if n == nil { + return true + } + v := reflect.ValueOf(n) + return v.Kind() == reflect.Pointer && v.IsNil() +} + +func parseOK(t *testing.T, src string) *Document { + t.Helper() + doc, errs := Parse([]byte(src)) + for _, err := range errs { + if err.Severity == SeverityError { + t.Fatalf("unexpected parse errors: %v", errs) + } + } + checkDoc(t, doc) + return doc +} + +// checkDoc validates structural invariants of a parsed document: every node's +// token range is well-formed and its scalar texts match the token stream. +func checkDoc(t *testing.T, doc *Document) { + t.Helper() + var walk func(n Node) + walk = func(n Node) { + if isNilNode(n) { + return + } + start, end := n.TokStart(), n.TokEnd() + if start < 0 || end >= len(doc.Tokens) || start > end { + t.Errorf("node %T has invalid token range [%d, %d] of %d tokens", n, start, end, len(doc.Tokens)) + return + } + tokText := func(i int) string { return doc.Tokens[i].Text } + switch v := n.(type) { + case *Identifier: + if got := tokText(v.TokStart()); got != v.Text { + t.Errorf("identifier %q does not match token %q", v.Text, got) + } + case *ConstValue: + if v.Text != "" && v.Text != tokText(v.TokStart()) { + t.Errorf("const value %q does not match token %q", v.Text, tokText(v.TokStart())) + } + for _, item := range v.List { + walk(item) + } + for _, entry := range v.Map { + walk(entry.Key) + walk(entry.Value) + } + case *Field: + walk(v.Type) + walk(v.Name) + walk(v.Value) + walk(v.Annotations) + case *Const: + walk(v.Type) + walk(v.Name) + walk(v.Value) + case *Typedef: + walk(v.Type) + walk(v.Name) + walk(v.Annotations) + case *Enum: + walk(v.Name) + walk(v.Annotations) + for _, val := range v.Values { + walk(val) + } + case *EnumValue: + walk(v.Name) + walk(v.Annotations) + case *Struct: + walk(v.Name) + walk(v.Annotations) + for _, f := range v.Fields { + walk(f) + } + case *Service: + walk(v.Name) + walk(v.Extends) + walk(v.Annotations) + for _, f := range v.Functions { + walk(f) + } + case *Function: + walk(v.Type) + walk(v.Name) + walk(v.Annotations) + for _, a := range v.Args { + walk(a) + } + if v.Throws != nil { + for _, f := range v.Throws.Fields { + walk(f) + } + } + case *Namespace: + walk(v.Name) + walk(v.Annotations) + case *FieldType: + walk(v.Ident) + walk(v.KeyType) + walk(v.ValueType) + walk(v.Annotations) + case *Annotations: + for _, a := range v.Items { + walk(a) + } + case *Annotation: + walk(v.Name) + } + } + for _, n := range doc.Nodes { + walk(n) + } +} + +// top returns the first top-level node of the given type. +func top[T Node](t *testing.T, doc *Document) T { + t.Helper() + for _, n := range doc.Nodes { + if v, ok := n.(T); ok { + return v + } + } + var zero T + t.Fatalf("no node of type %T in document", zero) + return zero +} + +func TestParseStructs(t *testing.T) { + tests := []struct { + name string + src string + check func(t *testing.T, doc *Document) + }{ + { + name: "fields with ids and requiredness", + src: `struct User { + 1: required i64 id + 2: optional string name + 3: list scores +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + if s.Kind != StructDecl { + t.Errorf("kind = %v, want struct", s.Kind) + } + if got := s.Name.Text; got != "User" { + t.Errorf("name = %q", got) + } + if len(s.Fields) != 3 { + t.Fatalf("fields = %d, want 3", len(s.Fields)) + } + f := s.Fields[0] + if f.FieldID == nil || f.FieldID.Text != "1" { + t.Errorf("field 0 id = %v", f.FieldID) + } + if f.Req != TokenRequired { + t.Errorf("field 0 req = %v", f.Req) + } + if f.Type.Kind != TypeBase || f.Type.Base != TokenI64 { + t.Errorf("field 0 type = %+v", f.Type) + } + if f.Name.Text != "id" { + t.Errorf("field 0 name = %q", f.Name.Text) + } + if s.Fields[2].Type.Kind != TypeList || s.Fields[2].Type.ValueType.Base != TokenI32 { + t.Errorf("field 2 type = %+v", s.Fields[2].Type) + } + }, + }, + { + name: "implicit ids and defaults", + src: `struct S { + i32 a + string b = "x" + i64 c = 42 +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + if s.Fields[0].FieldID != nil { + t.Errorf("implicit id should be nil, got %v", s.Fields[0].FieldID) + } + if s.Fields[1].Value.Kind != ValueString || s.Fields[1].Value.Text != `"x"` { + t.Errorf("field 1 value = %+v", s.Fields[1].Value) + } + if s.Fields[2].Value.Kind != ValueInt || s.Fields[2].Value.Text != "42" { + t.Errorf("field 2 value = %+v", s.Fields[2].Value) + } + }, + }, + { + name: "field reference", + src: `struct S { + 1: i32 &parent +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + if !s.Fields[0].Reference { + t.Error("field should be a reference") + } + }, + }, + { + name: "keyword field names", + src: `struct S { + string namespace + i32 cpp_include +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + if s.Fields[0].Name.Text != "namespace" { + t.Errorf("field 0 name = %q", s.Fields[0].Name.Text) + } + if s.Fields[1].Name.Text != "cpp_include" { + t.Errorf("field 1 name = %q", s.Fields[1].Name.Text) + } + }, + }, + { + name: "type keywords as field names", + src: `struct S { + 1: string uuid + 2: uuid id + 3: i64 map + 4: i32 string = 1 +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + want := []string{"uuid", "id", "map", "string"} + for i, name := range want { + if s.Fields[i].Name.Text != name { + t.Errorf("field %d name = %q, want %q", i, s.Fields[i].Name.Text, name) + } + } + // uuid is both a base type (field 2's type) and a field name + // (field 1's name); both must survive. + if s.Fields[1].Type.Base != TokenUUID { + t.Errorf("field 2 type = %v, want TokenUUID", s.Fields[1].Type.Base) + } + }, + }, + { + name: "empty struct", + src: "struct Empty {}", + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + if len(s.Fields) != 0 { + t.Errorf("fields = %d, want 0", len(s.Fields)) + } + }, + }, + { + name: "semicolon separators", + src: `struct S { + 1: i32 a; + 2: string b; +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + if s.Fields[0].Sep != TokenSemicolon || s.Fields[1].Sep != TokenSemicolon { + t.Errorf("seps = %v, %v", s.Fields[0].Sep, s.Fields[1].Sep) + } + }, + }, + { + name: "annotations on struct and fields", + src: `struct S { + 1: i32 a (field_anno = "y") +} (struct_anno = "x")`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + if s.Annotations == nil || len(s.Annotations.Items) != 1 { + t.Fatalf("struct annotations = %+v", s.Annotations) + } + if s.Annotations.Items[0].Name.Text != "struct_anno" || s.Annotations.Items[0].Value.Text != `"x"` { + t.Errorf("struct annotation = %+v", s.Annotations.Items[0]) + } + if s.Fields[0].Annotations == nil { + t.Fatal("field annotations missing") + } + }, + }, + { + name: "type annotations come before the field name", + src: `struct S { + 1: map (tag = "t") m +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + ft := s.Fields[0].Type + if ft.Annotations == nil || ft.Annotations.Items[0].Name.Text != "tag" { + t.Errorf("type annotations = %+v", ft.Annotations) + } + if s.Fields[0].Annotations != nil { + t.Errorf("field annotations should be nil, got %+v", s.Fields[0].Annotations) + } + }, + }, + { + name: "field annotations come after the name", + src: `struct S { + 1: map m (tag = "t") +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + if s.Fields[0].Type.Annotations != nil { + t.Errorf("type annotations should be nil, got %+v", s.Fields[0].Type.Annotations) + } + if s.Fields[0].Annotations == nil || s.Fields[0].Annotations.Items[0].Name.Text != "tag" { + t.Errorf("field annotations = %+v", s.Fields[0].Annotations) + } + }, + }, + { + name: "cpp_type on containers", + src: `struct S { + 1: map cpp_type "std::map" < i32, string > m + 2: list cpp_type "std::vector" < i32 > l +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + if s.Fields[0].Type.CPPType == nil || s.Fields[0].Type.CPPType.Text != `"std::map"` { + t.Errorf("map cpp_type = %v", s.Fields[0].Type.CPPType) + } + if s.Fields[1].Type.CPPType == nil || s.Fields[1].Type.CPPType.Text != `"std::vector"` { + t.Errorf("list cpp_type = %v", s.Fields[1].Type.CPPType) + } + }, + }, + { + name: "union and exception", + src: `union U { + 1: i32 a +} + +exception E { + 1: string msg +}`, + check: func(t *testing.T, doc *Document) { + u := top[*Struct](t, doc) + if u.Kind != UnionDecl { + t.Errorf("kind = %v, want union", u.Kind) + } + var e *Struct + for _, n := range doc.Nodes { + if s, ok := n.(*Struct); ok && s.Kind == ExceptionDecl { + e = s + } + } + if e == nil || e.Name.Text != "E" { + t.Errorf("exception missing: %+v", e) + } + }, + }, + { + name: "hex field id", + src: `struct S { + 0x10: i32 a +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Struct](t, doc) + if s.Fields[0].FieldID == nil || s.Fields[0].FieldID.Text != "0x10" { + t.Errorf("field id = %v", s.Fields[0].FieldID) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := parseOK(t, tt.src) + tt.check(t, doc) + }) + } +} + +func TestParseConsts(t *testing.T) { + tests := []struct { + name string + src string + check func(t *testing.T, doc *Document) + }{ + { + name: "scalar values", + src: `const i16 a = 1 +const i32 b = 0xa1 +const i64 c = -42 +const double d = 1.33333333 +const double e = 1.3333e11 +const double f = 1E11 +const string g = "hello" +const string h = 'single' +const bool i = true +const bool j = false`, + check: func(t *testing.T, doc *Document) { + var kinds []ConstValueKind + for _, n := range doc.Nodes { + kinds = append(kinds, n.(*Const).Value.Kind) + } + want := []ConstValueKind{ValueInt, ValueInt, ValueInt, ValueDouble, ValueDouble, ValueDouble, ValueString, ValueString, ValueInt, ValueInt} + if len(kinds) != len(want) { + t.Fatalf("consts = %d, want %d", len(kinds), len(want)) + } + for i := range want { + if kinds[i] != want[i] { + t.Errorf("const %d kind = %v, want %v", i, kinds[i], want[i]) + } + } + // Raw text preserved. + if doc.Nodes[1].(*Const).Value.Text != "0xa1" { + t.Errorf("hex text = %q", doc.Nodes[1].(*Const).Value.Text) + } + if doc.Nodes[4].(*Const).Value.Text != "1.3333e11" { + t.Errorf("double text = %q", doc.Nodes[4].(*Const).Value.Text) + } + }, + }, + { + name: "identifier and negative values", + src: `const i32 a = OTHER_CONST +const i32 b = -1 +const double c = -1.5`, + check: func(t *testing.T, doc *Document) { + if doc.Nodes[0].(*Const).Value.Kind != ValueIdent || doc.Nodes[0].(*Const).Value.Text != "OTHER_CONST" { + t.Errorf("ident value = %+v", doc.Nodes[0].(*Const).Value) + } + if doc.Nodes[1].(*Const).Value.Text != "-1" { + t.Errorf("negative int = %q", doc.Nodes[1].(*Const).Value.Text) + } + if doc.Nodes[2].(*Const).Value.Text != "-1.5" { + t.Errorf("negative double = %q", doc.Nodes[2].(*Const).Value.Text) + } + }, + }, + { + name: "dotted identifier value", + src: "const string s = foo.bar.baz", + check: func(t *testing.T, doc *Document) { + if v := doc.Nodes[0].(*Const).Value; v.Text != "foo.bar.baz" { + t.Errorf("dotted ident = %q", v.Text) + } + }, + }, + { + name: "list values", + src: `const list a = [1, 2, 3] +const list b = [] +const list> c = [[1], [2, 3]]`, + check: func(t *testing.T, doc *Document) { + a := doc.Nodes[0].(*Const).Value + if a.Kind != ValueList || len(a.List) != 3 { + t.Fatalf("list a = %+v", a) + } + if a.List[1].Text != "2" { + t.Errorf("list item = %q", a.List[1].Text) + } + if b := doc.Nodes[1].(*Const).Value; len(b.List) != 0 { + t.Errorf("empty list = %+v", b) + } + c := doc.Nodes[2].(*Const).Value + if len(c.List) != 2 || len(c.List[1].List) != 2 { + t.Errorf("nested list = %+v", c) + } + }, + }, + { + name: "map values", + src: `const map a = {"x": 1, "y": 2} +const map b = {}`, + check: func(t *testing.T, doc *Document) { + a := doc.Nodes[0].(*Const).Value + if a.Kind != ValueMap || len(a.Map) != 2 { + t.Fatalf("map a = %+v", a) + } + if a.Map[0].Key.Text != `"x"` || a.Map[0].Value.Text != "1" { + t.Errorf("map entry = %+v", a.Map[0]) + } + if b := doc.Nodes[1].(*Const).Value; len(b.Map) != 0 { + t.Errorf("empty map = %+v", b) + } + }, + }, + { + name: "nested map in list", + src: `const list> a = [{"k": 1}]`, + check: func(t *testing.T, doc *Document) { + a := doc.Nodes[0].(*Const).Value + if len(a.List) != 1 || len(a.List[0].Map) != 1 { + t.Errorf("nested = %+v", a) + } + }, + }, + { + name: "trailing separators", + src: "const list a = [1, 2,];", + check: func(t *testing.T, doc *Document) { + c := doc.Nodes[0].(*Const) + if len(c.Value.List) != 2 { + t.Errorf("list = %+v", c.Value) + } + if c.Sep != TokenSemicolon { + t.Errorf("const sep = %v", c.Sep) + } + }, + }, + { + name: "const type is container", + src: "const map> m = {}", + check: func(t *testing.T, doc *Document) { + c := doc.Nodes[0].(*Const) + if c.Type.Kind != TypeMap || c.Type.ValueType.Kind != TypeList { + t.Errorf("type = %+v", c.Type) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := parseOK(t, tt.src) + tt.check(t, doc) + }) + } +} + +func TestParseEnums(t *testing.T) { + tests := []struct { + name string + src string + check func(t *testing.T, doc *Document) + }{ + { + name: "values with and without ints", + src: `enum E { + A = 1, + B, + C = 0x10, + D +}`, + check: func(t *testing.T, doc *Document) { + e := top[*Enum](t, doc) + if len(e.Values) != 4 { + t.Fatalf("values = %d", len(e.Values)) + } + if e.Values[0].Value == nil || e.Values[0].Value.Text != "1" { + t.Errorf("A value = %v", e.Values[0].Value) + } + if e.Values[1].Value != nil { + t.Errorf("B should auto-increment, got %v", e.Values[1].Value) + } + if e.Values[2].Value.Text != "0x10" { + t.Errorf("C value = %v", e.Values[2].Value) + } + if e.Values[3].Sep != 0 { + t.Errorf("D sep = %v", e.Values[3].Sep) + } + }, + }, + { + name: "negative enum value", + src: "enum E {\n A = -1\n}", + check: func(t *testing.T, doc *Document) { + e := top[*Enum](t, doc) + if e.Values[0].Value.Text != "-1" { + t.Errorf("value = %v", e.Values[0].Value) + } + }, + }, + { + name: "annotations on enum and values", + src: `enum E { + A (a_anno = "y") + B +} (e_anno = "x")`, + check: func(t *testing.T, doc *Document) { + e := top[*Enum](t, doc) + if e.Annotations == nil || e.Values[0].Annotations == nil { + t.Fatalf("annotations missing: %+v %+v", e.Annotations, e.Values[0].Annotations) + } + }, + }, + { + name: "semicolon separators", + src: "enum E {\n A = 1;\n B;\n}", + check: func(t *testing.T, doc *Document) { + e := top[*Enum](t, doc) + if e.Values[0].Sep != TokenSemicolon || e.Values[1].Sep != TokenSemicolon { + t.Errorf("seps = %v, %v", e.Values[0].Sep, e.Values[1].Sep) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := parseOK(t, tt.src) + tt.check(t, doc) + }) + } +} + +func TestParseServices(t *testing.T) { + tests := []struct { + name string + src string + check func(t *testing.T, doc *Document) + }{ + { + name: "functions with args and throws", + src: `service UserService { + User getUser(1: i64 id) throws (NotFound e) + oneway void ping() + void update(1: User user, 2: string note) +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Service](t, doc) + if len(s.Functions) != 3 { + t.Fatalf("functions = %d", len(s.Functions)) + } + f := s.Functions[0] + if f.Type.Kind != TypeIdent || f.Type.Ident.Text != "User" { + t.Errorf("getUser type = %+v", f.Type) + } + if f.Throws == nil || len(f.Throws.Fields) != 1 { + t.Fatalf("throws = %+v", f.Throws) + } + if f.Throws.Fields[0].Type.Ident.Text != "NotFound" { + t.Errorf("throws type = %+v", f.Throws.Fields[0].Type) + } + ping := s.Functions[1] + if ping.Oneway == nil || ping.Oneway.Kind != TokenOneway || ping.Void == nil { + t.Errorf("ping = %+v", ping) + } + if len(s.Functions[2].Args) != 2 { + t.Errorf("update args = %d", len(s.Functions[2].Args)) + } + }, + }, + { + name: "extends", + src: "service Child extends Parent {\n void f()\n}", + check: func(t *testing.T, doc *Document) { + s := top[*Service](t, doc) + if s.Extends == nil || s.Extends.Text != "Parent" { + t.Errorf("extends = %+v", s.Extends) + } + }, + }, + { + name: "async is deprecated oneway", + src: "service S {\n async void f()\n}", + check: func(t *testing.T, doc *Document) { + s := top[*Service](t, doc) + if s.Functions[0].Oneway == nil || s.Functions[0].Oneway.Kind != TokenAsync { + t.Errorf("oneway = %v", s.Functions[0].Oneway) + } + }, + }, + { + name: "function annotations", + src: `service S { + void f() (f_anno = "x") + void g() throws (E e) (g_anno = "y") +}`, + check: func(t *testing.T, doc *Document) { + s := top[*Service](t, doc) + if s.Functions[0].Annotations == nil { + t.Error("f annotations missing") + } + if s.Functions[1].Annotations == nil || s.Functions[1].Annotations.Items[0].Name.Text != "g_anno" { + t.Errorf("g annotations = %+v", s.Functions[1].Annotations) + } + }, + }, + { + name: "empty service", + src: "service S {}", + check: func(t *testing.T, doc *Document) { + if s := top[*Service](t, doc); len(s.Functions) != 0 { + t.Errorf("functions = %d", len(s.Functions)) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := parseOK(t, tt.src) + tt.check(t, doc) + }) + } +} + +func TestParseHeaders(t *testing.T) { + tests := []struct { + name string + src string + check func(t *testing.T, doc *Document) + }{ + { + name: "includes and namespaces", + src: `include "shared.thrift" +cpp_include "base.h" + +namespace java com.example +namespace * global`, + check: func(t *testing.T, doc *Document) { + if len(doc.Nodes) != 4 { + t.Fatalf("nodes = %d: %+v", len(doc.Nodes), doc.Nodes) + } + inc := doc.Nodes[0].(*Include) + if inc.Path.Text != `"shared.thrift"` { + t.Errorf("include path = %q", inc.Path.Text) + } + cpp := doc.Nodes[1].(*CPPInclude) + if cpp.Path.Text != `"base.h"` { + t.Errorf("cpp include = %q", cpp.Path.Text) + } + ns := doc.Nodes[2].(*Namespace) + if ns.Scope.Text != "java" || ns.Name.Text != "com.example" { + t.Errorf("namespace = %+v", ns) + } + if doc.Nodes[3].(*Namespace).Scope.Kind != TokenStar { + t.Errorf("star scope = %+v", doc.Nodes[3].(*Namespace).Scope) + } + }, + }, + { + name: "namespace with annotations", + src: `namespace go thrift (go_tag = "x")`, + check: func(t *testing.T, doc *Document) { + ns := doc.Nodes[0].(*Namespace) + if ns.Annotations == nil || ns.Annotations.Items[0].Name.Text != "go_tag" { + t.Errorf("annotations = %+v", ns.Annotations) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := parseOK(t, tt.src) + tt.check(t, doc) + }) + } +} + +func TestParseTypedefs(t *testing.T) { + tests := []struct { + name string + src string + check func(t *testing.T, doc *Document) + }{ + { + name: "simple and container typedefs", + src: `typedef i64 Timestamp +typedef map> Index`, + check: func(t *testing.T, doc *Document) { + td := doc.Nodes[0].(*Typedef) + if td.Type.Base != TokenI64 || td.Name.Text != "Timestamp" { + t.Errorf("typedef = %+v", td) + } + if doc.Nodes[1].(*Typedef).Type.Kind != TypeMap { + t.Errorf("container typedef = %+v", doc.Nodes[1].(*Typedef)) + } + }, + }, + { + name: "annotations and separators", + src: `typedef string Id (id_type = "uuid");`, + check: func(t *testing.T, doc *Document) { + td := doc.Nodes[0].(*Typedef) + if td.Annotations == nil || td.Sep != TokenSemicolon { + t.Errorf("typedef = %+v", td) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := parseOK(t, tt.src) + tt.check(t, doc) + }) + } +} + +func TestParseAnnotations(t *testing.T) { + tests := []struct { + name string + src string + check func(t *testing.T, doc *Document) + }{ + { + name: "bare annotation defaults to implicit value", + src: "typedef i32 T (foo)", + check: func(t *testing.T, doc *Document) { + td := doc.Nodes[0].(*Typedef) + a := td.Annotations.Items[0] + if a.Name.Text != "foo" || a.Value != nil { + t.Errorf("bare annotation = %+v", a) + } + }, + }, + { + name: "string values", + src: `typedef i32 T (a = "1", b = "x", c = 'y')`, + check: func(t *testing.T, doc *Document) { + td := doc.Nodes[0].(*Typedef) + items := td.Annotations.Items + if len(items) != 3 { + t.Fatalf("items = %d", len(items)) + } + if items[0].Value.Text != `"1"` || items[1].Value.Text != `"x"` || items[2].Value.Text != `'y'` { + t.Errorf("values = %v, %v, %v", items[0].Value, items[1].Value, items[2].Value) + } + if items[0].Sep != TokenComma || items[1].Sep != TokenComma { + t.Errorf("seps = %v, %v", items[0].Sep, items[1].Sep) + } + }, + }, + { + name: "semicolon and missing separators", + src: "typedef i32 T (a = \"1\"; b = \"2\" c = \"3\")", + check: func(t *testing.T, doc *Document) { + td := doc.Nodes[0].(*Typedef) + items := td.Annotations.Items + if len(items) != 3 { + t.Fatalf("items = %d", len(items)) + } + if items[0].Sep != TokenSemicolon || items[1].Sep != 0 || items[2].Sep != 0 { + t.Errorf("seps = %v, %v, %v", items[0].Sep, items[1].Sep, items[2].Sep) + } + }, + }, + { + name: "empty annotation group", + src: "typedef i32 T ()", + check: func(t *testing.T, doc *Document) { + td := doc.Nodes[0].(*Typedef) + if td.Annotations == nil || len(td.Annotations.Items) != 0 { + t.Errorf("annotations = %+v", td.Annotations) + } + }, + }, + { + name: "multiline annotations", + src: `typedef i32 T ( + a = "1", + b = "2" +)`, + check: func(t *testing.T, doc *Document) { + td := doc.Nodes[0].(*Typedef) + if len(td.Annotations.Items) != 2 { + t.Fatalf("items = %d", len(td.Annotations.Items)) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := parseOK(t, tt.src) + tt.check(t, doc) + }) + } +} + +func TestParseErrors(t *testing.T) { + tests := []struct { + name string + src string + wantErrs []string // substrings, one per expected error + }{ + { + name: "unterminated struct", + src: "struct S {", + wantErrs: []string{"expected '}' to close struct"}, + }, + { + name: "missing brace", + src: "struct S", + wantErrs: []string{"expected '{' after struct name"}, + }, + { + name: "missing field name", + src: "struct S {\n 1: i32\n}", + wantErrs: []string{"expected field name"}, + }, + { + name: "missing struct name", + src: "struct {}", + wantErrs: []string{"expected struct name"}, + }, + { + name: "annotation value must be string", + src: "typedef i32 T (x = 1, y = \"2\")", + wantErrs: []string{"expected string literal annotation value"}, + }, + { + name: "unquoted include", + src: "include foo.thrift\nstruct S {}", + wantErrs: []string{"expected include path string"}, + }, + { + name: "garbage at top level recovers", + src: ", , ,\nstruct S {}", + wantErrs: []string{"unexpected token"}, + }, + { + name: "bad field recovers to closing brace", + src: "struct S {\n 1: void bad\n}", + wantErrs: []string{"expected type"}, + }, + { + name: "missing const value", + src: "const i32 x =", + wantErrs: []string{"expected constant value"}, + }, + { + name: "unterminated list constant", + src: "const list x = [1, 2", + wantErrs: []string{"expected ']' to close list constant"}, + }, + { + name: "bad function recovers", + src: "service S {\n void ()\n void ok()\n}", + wantErrs: []string{"expected function name"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc, errs := Parse([]byte(tt.src)) + if doc == nil { + t.Fatal("parse returned nil document") + } + var got []string + for _, err := range errs { + if err.Severity == SeverityError { + got = append(got, err.Error()) + } + } + if len(got) != len(tt.wantErrs) { + t.Fatalf("got %d errors (%v), want %d", len(got), got, len(tt.wantErrs)) + } + for i, want := range tt.wantErrs { + if !strings.Contains(got[i], want) { + t.Errorf("error %d = %q, want substring %q", i, got[i], want) + } + } + }) + } +} + +func TestParseWarnings(t *testing.T) { + tests := []struct { + name string + src string + wantMsg []string + }{ + { + name: "async deprecated", + src: "service S {\n async void f()\n}", + wantMsg: []string{"async is deprecated"}, + }, + { + name: "byte deprecated", + src: "struct S {\n 1: byte b\n}", + wantMsg: []string{"byte is deprecated"}, + }, + { + name: "slist unsupported", + src: "struct S {\n 1: slist s\n}", + wantMsg: []string{"slist is no longer supported"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, errs := Parse([]byte(tt.src)) + var warns []string + for _, err := range errs { + if err.Severity == SeverityWarning { + warns = append(warns, err.Error()) + } + } + if len(warns) != len(tt.wantMsg) { + t.Fatalf("warnings = %v, want %d", warns, len(tt.wantMsg)) + } + for i, want := range tt.wantMsg { + if !strings.Contains(warns[i], want) { + t.Errorf("warning %d = %q, want substring %q", i, warns[i], want) + } + } + }) + } +} + +func TestParseRoundTripCorpus(t *testing.T) { + // A corpus of valid files that must parse with zero errors; the invariant + // checker in parseOK validates node ranges and texts for each. + corpus := []string{ + "", + "include \"a.thrift\"\ninclude \"b.thrift\"\n\nnamespace java com.example\n\nstruct A {\n 1: i32 id\n}", + "cpp_include \"\"\n\nstruct S {\n 1: list cpp_type \"std::vector\" < i32 > v\n}", + "enum Color {\n RED = 1,\n GREEN = 2\n}\n\nstruct S {\n 1: Color c\n}", + "service S extends Base {\n oneway void fire()\n i32 calc(1: i32 a, 2: i32 b) throws (E e, F f)\n}\n", + "const map> M = {\"a\": [1, 2], \"b\": []}", + "typedef map> Nested\n", + "struct S {\n // leading comment\n 1: i32 a // trailing comment\n}", + "/** doc comment */\nstruct S {\n 1: i32 a\n}", + "namespace * all\nstruct S {}", + "const double d = .5\nconst double e = 1.5e-3\nconst double f = -0.25E+2", + "struct S {\n 1: required i32 a = 0x10 (anno = \"x\", bare)\n}", + } + for i, src := range corpus { + t.Run("case-"+strconv.Itoa(i), func(t *testing.T) { + parseOK(t, src) + }) + } +} diff --git a/syntax/position.go b/syntax/position.go new file mode 100644 index 0000000..a7204bd --- /dev/null +++ b/syntax/position.go @@ -0,0 +1,69 @@ +package syntax + +import "unicode/utf8" + +// Position is a source position: 1-based line, 1-based rune column, and +// 0-based byte offset. +type Position struct { + Line int + Col int + Offset int +} + +// InvalidPosition is the zero Position. +var InvalidPosition = Position{} + +// IsValid reports whether the position has a line number. +func (p Position) IsValid() bool { + return p.Line > 0 +} + +// TokenPosition returns the start position of token i. +func (d *Document) TokenPosition(i int) Position { + t := d.Tokens[i] + return Position{Line: t.Line, Col: t.Col, Offset: t.Offset} +} + +// TokenEndPosition returns the position immediately after token i. Tokens +// never span lines, so the end position is on the same line. +func (d *Document) TokenEndPosition(i int) Position { + t := d.Tokens[i] + return Position{ + Line: t.Line, + Col: t.Col + utf8.RuneCountInString(t.Text), + Offset: t.Offset + len(t.Text), + } +} + +// TokenIndex returns the index of a token pointer in the document's token +// stream. Token pointers are stable: the parser stores pointers into the +// document's token slice. It returns 0 for nil or foreign tokens. +func (d *Document) TokenIndex(t *Token) int { + if t == nil { + return 0 + } + for i := range d.Tokens { + if &d.Tokens[i] == t { + return i + } + } + return 0 +} + +// TokenRange returns the span of a token. +func (d *Document) TokenRange(t *Token) (start, end Position) { + i := d.TokenIndex(t) + return d.TokenPosition(i), d.TokenEndPosition(i) +} + +// Range returns the span of a node: from the start of its first token to +// the end of its last token. +func (d *Document) Range(n Node) (start, end Position) { + return d.TokenPosition(n.TokStart()), d.TokenEndPosition(n.TokEnd()) +} + +// Contains reports whether pos lies within the node's span, inclusive. +func (d *Document) Contains(n Node, pos Position) bool { + start, end := d.Range(n) + return pos.Offset >= start.Offset && pos.Offset <= end.Offset +} diff --git a/syntax/position_test.go b/syntax/position_test.go new file mode 100644 index 0000000..b6226a9 --- /dev/null +++ b/syntax/position_test.go @@ -0,0 +1,225 @@ +package syntax + +import ( + "testing" +) + +func TestDocumentAccessors(t *testing.T) { + src := `include "a.thrift" +cpp_include "" +namespace go x + +const i32 C = 1 +typedef i64 T +enum E { A = 1 } +struct S { 1: i32 a } +union U { 1: i32 a } +exception X { 1: string m } +service Svc { void f() } +` + doc := parseOK(t, src) + + if len(doc.Includes()) != 1 || doc.Includes()[0].Path.Text != `"a.thrift"` { + t.Errorf("Includes = %+v", doc.Includes()) + } + if len(doc.CPPIncludes()) != 1 { + t.Errorf("CPPIncludes = %d", len(doc.CPPIncludes())) + } + if len(doc.Namespaces()) != 1 { + t.Errorf("Namespaces = %d", len(doc.Namespaces())) + } + if len(doc.Consts()) != 1 || doc.Consts()[0].Name.Text != "C" { + t.Errorf("Consts = %+v", doc.Consts()) + } + if len(doc.Typedefs()) != 1 { + t.Errorf("Typedefs = %d", len(doc.Typedefs())) + } + if len(doc.Enums()) != 1 { + t.Errorf("Enums = %d", len(doc.Enums())) + } + if len(doc.Structs()) != 1 || doc.Structs()[0].Name.Text != "S" { + t.Errorf("Structs = %+v", doc.Structs()) + } + if len(doc.Unions()) != 1 { + t.Errorf("Unions = %d", len(doc.Unions())) + } + if len(doc.Exceptions()) != 1 || doc.Exceptions()[0].Name.Text != "X" { + t.Errorf("Exceptions = %+v", doc.Exceptions()) + } + if len(doc.Services()) != 1 { + t.Errorf("Services = %d", len(doc.Services())) + } + // Structs must not include unions or exceptions. + if len(doc.Structs())+len(doc.Unions())+len(doc.Exceptions()) != 3 { + t.Errorf("struct-like split wrong") + } +} + +func TestDocumentRanges(t *testing.T) { + src := "struct S {\n 1: i32 a\n}" + doc := parseOK(t, src) + s := doc.Structs()[0] + + start, end := doc.Range(s.Name) + if start.Line != 1 || start.Col != 8 || end.Line != 1 || end.Col != 9 { + t.Errorf("name range = %+v..%+v", start, end) + } + + start, end = doc.Range(s) + if start.Line != 1 || start.Col != 1 || end.Line != 3 || end.Col != 2 { + t.Errorf("struct range = %+v..%+v", start, end) + } + + if !doc.Contains(s, Position{Line: 2, Col: 7, Offset: 18}) { + t.Error("position inside struct should be contained") + } + if doc.Contains(s, Position{Line: 5, Col: 1, Offset: 25}) { + t.Error("position outside struct should not be contained") + } +} + +func TestSearchNodePathByPosition(t *testing.T) { + src := `struct User { + 1: required i64 id + 2: optional string name = "bob" +} + +service Svc extends Base { + User getUser(1: i64 id) throws (NotFound e) +} + +const i32 X = SOME_VALUE +` + doc := parseOK(t, src) + + // Helper: position of the first occurrence of text in the source. + posOf := func(text string) Position { + idx := indexOf(src, text) + if idx < 0 { + t.Fatalf("text %q not found in source", text) + } + return doc.TokenPosition(tokIndexAt(doc, idx)) + } + + tests := []struct { + name string + text string + want string // expected deepest node type + }{ + {"struct name", "User", "*syntax.Identifier"}, + {"field type", "i64", "*syntax.FieldType"}, + {"field name", "id", "*syntax.Identifier"}, + {"type reference", "NotFound", "*syntax.Identifier"}, + {"extends", "Base", "*syntax.Identifier"}, + {"const value", "SOME_VALUE", "*syntax.ConstValue"}, + {"service name", "Svc", "*syntax.Identifier"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path := doc.SearchNodePathByPosition(posOf(tt.text)) + if len(path) == 0 { + t.Fatal("empty path") + } + deepest := path[len(path)-1] + if got := typeName(deepest); got != tt.want { + t.Errorf("deepest node = %s, want %s (path: %v)", got, tt.want, pathNames(path)) + } + }) + } + + // Field name identifier: the parent is the field. + path := doc.SearchNodePathByPosition(posOf("id")) + if len(path) < 3 { + t.Fatalf("path too short: %v", pathNames(path)) + } + if _, ok := path[len(path)-2].(*Field); !ok { + t.Errorf("parent of field name should be Field: %v", pathNames(path)) + } + + // Struct name identifier: the parent is the struct. + path = doc.SearchNodePathByPosition(posOf("User")) + if len(path) < 3 { + t.Fatalf("path too short: %v", pathNames(path)) + } + if _, ok := path[len(path)-2].(*Struct); !ok { + t.Errorf("parent of struct name should be Struct: %v", pathNames(path)) + } + + // Position on nothing (blank line between definitions). + idx := indexOf(src, "const i32") + if idx < 0 { + t.Fatal("const not found") + } + pos := doc.TokenPosition(tokIndexAt(doc, idx)) + pos.Offset -= 1 // the last newline of the blank line before const + path = doc.SearchNodePathByPosition(pos) + if len(path) != 1 { // only the document itself + t.Errorf("expected document-only path, got %v", pathNames(path)) + } +} + +func tokIndexAt(doc *Document, offset int) int { + // Find the last token starting at or before offset. + idx := 0 + for i, tok := range doc.Tokens { + if tok.Offset <= offset { + idx = i + } + } + return idx +} + +func typeName(n Node) string { + switch n.(type) { + case *Document: + return "*syntax.Document" + case *Const: + return "*syntax.Const" + case *Typedef: + return "*syntax.Typedef" + case *Enum: + return "*syntax.Enum" + case *EnumValue: + return "*syntax.EnumValue" + case *Struct: + return "*syntax.Struct" + case *Service: + return "*syntax.Service" + case *Function: + return "*syntax.Function" + case *Throws: + return "*syntax.Throws" + case *Field: + return "*syntax.Field" + case *FieldType: + return "*syntax.FieldType" + case *ConstValue: + return "*syntax.ConstValue" + case *Identifier: + return "*syntax.Identifier" + case *Namespace: + return "*syntax.Namespace" + case *Include: + return "*syntax.Include" + case *CPPInclude: + return "*syntax.CPPInclude" + } + return "unknown" +} + +func pathNames(path []Node) []string { + names := make([]string, 0, len(path)) + for _, n := range path { + names = append(names, typeName(n)) + } + return names +} + +func indexOf(s, sub string) int { + for i := 0; i+len(sub) <= len(s); i++ { + if s[i:i+len(sub)] == sub { + return i + } + } + return -1 +} diff --git a/syntax/testdata/fuzz/FuzzLex/f8dea28f0b31e428 b/syntax/testdata/fuzz/FuzzLex/f8dea28f0b31e428 new file mode 100644 index 0000000..2718459 --- /dev/null +++ b/syntax/testdata/fuzz/FuzzLex/f8dea28f0b31e428 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\"\\Հ") diff --git a/tests/e2e/enums/test.sh b/tests/e2e/enums/test.sh index b3a244d..943c657 100755 --- a/tests/e2e/enums/test.sh +++ b/tests/e2e/enums/test.sh @@ -13,7 +13,7 @@ do align=${options[1]} field_line_comma=${options[2]} echo "indent: ${indent}, align: ${align}, field_line_comma: ${field_line_comma}" - got=$(./bin/thriftls -format -indent "${indent}" -align "${align}" -fieldLineComma "${field_line_comma}" -f tests/e2e/enums/enums.thrift) + got=$(./bin/thriftls format -indent "${indent}" -align "${align}" -fieldLineComma "${field_line_comma}" tests/e2e/enums/enums.thrift) expected=$(cat "$f") if [ "$got" == "$expected" ];then echo "pass" diff --git a/tests/e2e/field_line_comma/test.sh b/tests/e2e/field_line_comma/test.sh index 367e6d8..2dad746 100755 --- a/tests/e2e/field_line_comma/test.sh +++ b/tests/e2e/field_line_comma/test.sh @@ -11,7 +11,7 @@ do read -ra options <<<"$substr" field_line_comma=${options[0]} echo "fieldLineComma: ${field_line_comma}" - got=$(./bin/thriftls -format -fieldLineComma "${field_line_comma}" -f tests/e2e/field_line_comma/fields.thrift) + got=$(./bin/thriftls format -fieldLineComma "${field_line_comma}" tests/e2e/field_line_comma/fields.thrift) expected=$(cat "$f") if [ "$got" == "$expected" ];then echo "pass" diff --git a/tests/e2e/fields/2spaces.assign.expect b/tests/e2e/fields/2spaces.assign.expect index 25d226e..4d72bf3 100644 --- a/tests/e2e/fields/2spaces.assign.expect +++ b/tests/e2e/fields/2spaces.assign.expect @@ -1,5 +1,5 @@ struct User { 1: required string Name, - 2: required i32 Age = 15, + 2: required i32 Age = 15, 3: optional string longlonglonglonglonglonglong_info = "", -} +} \ No newline at end of file diff --git a/tests/e2e/fields/tab.assign.expect b/tests/e2e/fields/tab.assign.expect index c6daa44..885e833 100644 --- a/tests/e2e/fields/tab.assign.expect +++ b/tests/e2e/fields/tab.assign.expect @@ -1,5 +1,5 @@ struct User { 1: required string Name, - 2: required i32 Age = 15, + 2: required i32 Age = 15, 3: optional string longlonglonglonglonglonglong_info = "", -} +} \ No newline at end of file diff --git a/tests/e2e/fields/test.sh b/tests/e2e/fields/test.sh index b4fd200..dbda413 100644 --- a/tests/e2e/fields/test.sh +++ b/tests/e2e/fields/test.sh @@ -12,7 +12,7 @@ do indent=${options[0]} align=${options[1]} echo "indent: ${indent}, align: ${align}" - got=$(./bin/thriftls -format -indent "${indent}" -align "${align}" -f tests/e2e/fields/fields.thrift) + got=$(./bin/thriftls format -indent "${indent}" -align "${align}" tests/e2e/fields/fields.thrift) expected=$(cat "$f") if [ "$got" == "$expected" ];then echo "pass" diff --git a/tests/testutil/resolver.go b/tests/testutil/resolver.go deleted file mode 100644 index fb91a9a..0000000 --- a/tests/testutil/resolver.go +++ /dev/null @@ -1,65 +0,0 @@ -package testutil - -import ( - "os" - "path/filepath" - "testing" -) - -// IncludeTestFixture represents a test fixture with include files -type IncludeTestFixture struct { - RootDir string // Root temp directory - MainDir string // Where main files go - IncludeDirs []string // Include paths -} - -// NewIncludeFixture creates a test fixture for include resolution testing -func NewIncludeFixture(tb testing.TB) *IncludeTestFixture { - tb.Helper() - root := tb.TempDir() - return &IncludeTestFixture{ - RootDir: root, - } -} - -// WithIncludePaths creates include directories -func (f *IncludeTestFixture) WithIncludePaths(dirs ...string) *IncludeTestFixture { - for _, dir := range dirs { - path := filepath.Join(f.RootDir, dir) - if err := os.MkdirAll(path, 0755); err != nil { - panic(err) - } - f.IncludeDirs = append(f.IncludeDirs, path) - } - return f -} - -// WriteFile writes a file relative to the root directory -func (f *IncludeTestFixture) WriteFile(filename, content string) string { - path := filepath.Join(f.RootDir, filename) - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0755); err != nil { - panic(err) - } - if err := os.WriteFile(path, []byte(content), 0644); err != nil { - panic(err) - } - return path -} - -// WriteInclude writes a file in an include directory -func (f *IncludeTestFixture) WriteInclude(dirIndex int, filename, content string) string { - if dirIndex >= len(f.IncludeDirs) { - panic("include directory index out of range") - } - path := filepath.Join(f.IncludeDirs[dirIndex], filename) - if err := os.WriteFile(path, []byte(content), 0644); err != nil { - panic(err) - } - return path -} - -// CreateMainFileWithInclude creates a main thrift file that includes another file -func (f *IncludeTestFixture) CreateMainFileWithInclude(mainContent, includePath string) string { - return f.WriteFile("main.thrift", mainContent) -} diff --git a/treefmt.toml b/treefmt.toml new file mode 100644 index 0000000..fd1d359 --- /dev/null +++ b/treefmt.toml @@ -0,0 +1,7 @@ +[formatter.go] +command = "gofumpt" +includes = ["*.go"] + +[formatter.nix] +command = "nixfmt" +includes = ["*.nix"] diff --git a/utils/errors/errors.go b/utils/errors/errors.go deleted file mode 100644 index 654614d..0000000 --- a/utils/errors/errors.go +++ /dev/null @@ -1,20 +0,0 @@ -package errors - -import "fmt" - -type AggregateError struct { - errs []error -} - -func NewAggregate(errs []error) *AggregateError { - return &AggregateError{ - errs: errs, - } -} - -func (e *AggregateError) Error() string { - if len(e.errs) == 0 { - return "" - } - return fmt.Sprintf("%v", e.errs) -} diff --git a/utils/json.go b/utils/json.go deleted file mode 100644 index e0286bf..0000000 --- a/utils/json.go +++ /dev/null @@ -1,25 +0,0 @@ -package utils - -import ( - "encoding/json" - "fmt" - "os" -) - -func MustDumpJsonToFile(obj any, file string) { - data, err := json.MarshalIndent(obj, "", " ") - if err != nil { - panic(err) - } - - os.WriteFile(file, data, os.ModePerm) -} - -func MustPrintJson(obj any) { - data, err := json.MarshalIndent(obj, "", " ") - if err != nil { - panic(err) - } - - fmt.Println(string(data)) -} diff --git a/utils/nil.go b/utils/nil.go deleted file mode 100644 index e12eb04..0000000 --- a/utils/nil.go +++ /dev/null @@ -1,10 +0,0 @@ -package utils - -import "reflect" - -func IsNil(v any) bool { - if v == nil { - return true - } - return reflect.ValueOf(v).IsNil() -} diff --git a/utils/string.go b/utils/string.go deleted file mode 100644 index 094e498..0000000 --- a/utils/string.go +++ /dev/null @@ -1,5 +0,0 @@ -package utils - -func Space(c byte) bool { - return c == ' ' || c == '\t' || c == '\n' || c == '\r' -}