├── .bazelignore ├── .bazeliskrc ├── .bazelrc ├── .bazelversion ├── .cargo └── config.toml ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── BUILD.bazel ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── MODULE.bazel ├── MODULE.bazel.lock ├── README.md ├── WORKSPACE.bazel ├── bazel ├── remote-cache.bazelrc └── setup-ci.sh ├── crates ├── starpls │ ├── BUILD.bazel │ ├── Cargo.toml │ └── src │ │ ├── bazel.rs │ │ ├── builtin │ │ └── builtin.pb │ │ ├── commands.rs │ │ ├── commands │ │ ├── check.rs │ │ └── server.rs │ │ ├── config.rs │ │ ├── convert.rs │ │ ├── debouncer.rs │ │ ├── diagnostics.rs │ │ ├── dispatcher.rs │ │ ├── document.rs │ │ ├── event_loop.rs │ │ ├── extensions.rs │ │ ├── handlers.rs │ │ ├── handlers │ │ ├── notifications.rs │ │ └── requests.rs │ │ ├── main.rs │ │ ├── server.rs │ │ ├── task_pool.rs │ │ └── utils.rs ├── starpls_bazel │ ├── BUILD.bazel │ ├── Cargo.toml │ ├── build.rs │ ├── data │ │ ├── build.builtins.json │ │ ├── build.proto │ │ ├── builtin.proto │ │ ├── bzl.builtins.json │ │ ├── commonAttributes.json │ │ ├── cquery.builtins.json │ │ ├── missingModuleFields.json │ │ ├── module-bazel.builtins.json │ │ ├── repo.builtins.json │ │ ├── vendor.builtins.json │ │ └── workspace.builtins.json │ └── src │ │ ├── attr.rs │ │ ├── bin │ │ └── main.rs │ │ ├── build_language.rs │ │ ├── client.rs │ │ ├── env.rs │ │ ├── label.rs │ │ └── lib.rs ├── starpls_common │ ├── BUILD.bazel │ ├── Cargo.toml │ └── src │ │ ├── diagnostics.rs │ │ ├── lib.rs │ │ └── util.rs ├── starpls_hir │ ├── BUILD.bazel │ ├── Cargo.toml │ └── src │ │ ├── def.rs │ │ ├── def │ │ ├── codeflow.rs │ │ ├── codeflow │ │ │ └── pretty.rs │ │ ├── lower.rs │ │ ├── resolver.rs │ │ ├── scope.rs │ │ └── tests.rs │ │ ├── display.rs │ │ ├── lib.rs │ │ ├── test_database.rs │ │ ├── typeck.rs │ │ └── typeck │ │ ├── builtins.rs │ │ ├── call.rs │ │ ├── infer.rs │ │ ├── intrinsics.rs │ │ └── tests.rs ├── starpls_ide │ ├── BUILD.bazel │ ├── Cargo.toml │ └── src │ │ ├── completions.rs │ │ ├── diagnostics.rs │ │ ├── document_symbols.rs │ │ ├── find_references.rs │ │ ├── goto_definition.rs │ │ ├── hover.rs │ │ ├── hover │ │ └── docs.rs │ │ ├── lib.rs │ │ ├── line_index.rs │ │ ├── show_hir.rs │ │ ├── show_syntax_tree.rs │ │ ├── signature_help.rs │ │ └── util.rs ├── starpls_intern │ ├── BUILD.bazel │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── starpls_lexer │ ├── BUILD.bazel │ ├── Cargo.toml │ └── src │ │ ├── cursor.rs │ │ ├── lib.rs │ │ ├── tests.rs │ │ ├── unescape.rs │ │ └── unescape │ │ └── tests.rs ├── starpls_parser │ ├── BUILD.bazel │ ├── Cargo.toml │ ├── src │ │ ├── grammar.rs │ │ ├── grammar │ │ │ ├── arguments.rs │ │ │ ├── expressions.rs │ │ │ ├── parameters.rs │ │ │ ├── statements.rs │ │ │ └── type_comments.rs │ │ ├── lib.rs │ │ ├── marker.rs │ │ ├── step.rs │ │ ├── syntax_kind.rs │ │ ├── tests.rs │ │ └── text.rs │ └── test_data │ │ ├── err │ │ ├── test_dot_expr_missing_member.rast │ │ ├── test_dot_expr_missing_member.star │ │ ├── test_error_block.rast │ │ ├── test_error_block.star │ │ ├── test_for_stmt_missing_colon.rast │ │ ├── test_for_stmt_missing_colon.star │ │ ├── test_for_stmt_missing_in.rast │ │ ├── test_for_stmt_missing_in.star │ │ ├── test_for_stmt_missing_iterable.rast │ │ ├── test_for_stmt_missing_iterable.star │ │ ├── test_for_stmt_missing_loop_variables.rast │ │ ├── test_for_stmt_missing_loop_variables.star │ │ ├── test_for_stmt_missing_suite.rast │ │ └── test_for_stmt_missing_suite.star │ │ └── ok │ │ ├── test_arguments_all.rast │ │ ├── test_arguments_all.star │ │ ├── test_break_stmt.rast │ │ ├── test_break_stmt.star │ │ ├── test_continue_stmt.rast │ │ ├── test_continue_stmt.star │ │ ├── test_dot_expr_full.rast │ │ ├── test_dot_expr_full.star │ │ ├── test_for_stmt_full.rast │ │ ├── test_for_stmt_full.star │ │ ├── test_pass_stmt.rast │ │ ├── test_pass_stmt.star │ │ ├── test_return_stmt.rast │ │ ├── test_return_stmt.star │ │ ├── test_suite_full.rast │ │ └── test_suite_full.star ├── starpls_syntax │ ├── BUILD.bazel │ ├── Cargo.toml │ └── src │ │ ├── ast.rs │ │ ├── lib.rs │ │ └── parser.rs └── starpls_test_util │ ├── BUILD.bazel │ ├── Cargo.toml │ └── src │ └── lib.rs ├── editors └── code │ ├── .eslintrc.js │ ├── .gitignore │ ├── .swcrc │ ├── BUILD.bazel │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ ├── commands.ts │ ├── context.ts │ ├── main.ts │ └── util.ts │ ├── syntaxes │ ├── starlark.configuration.json │ ├── starlark.tmLanguage.json │ └── starlark.tmLanguage.license │ └── tsconfig.json ├── rust-toolchain.toml ├── rustfmt.toml ├── vendor └── runfiles │ ├── BUILD.bazel │ ├── Cargo.toml │ ├── private │ ├── BUILD.bazel │ └── runfiles_utils.bzl │ └── src │ └── lib.rs └── xtask ├── BUILD.bazel ├── Cargo.toml └── src ├── main.rs ├── update_parser_test_data.rs └── util.rs /.bazelignore: -------------------------------------------------------------------------------- 1 | editors/code/node_modules 2 | -------------------------------------------------------------------------------- /.bazeliskrc: -------------------------------------------------------------------------------- 1 | USE_BAZEL_VERSION=8.0.0 2 | -------------------------------------------------------------------------------- /.bazelrc: -------------------------------------------------------------------------------- 1 | common --enable_platform_specific_config 2 | 3 | build --enable_runfiles 4 | 5 | # honor the setting of `skipLibCheck` in the tsconfig.json file 6 | build --@aspect_rules_ts//ts:skipLibCheck=honor_tsconfig 7 | fetch --@aspect_rules_ts//ts:skipLibCheck=honor_tsconfig 8 | query --@aspect_rules_ts//ts:skipLibCheck=honor_tsconfig 9 | 10 | build --experimental_convenience_symlinks=ignore 11 | 12 | build --@rules_rust//rust/toolchain/channel=nightly 13 | 14 | build --@rules_rust//:rustfmt.toml=//:rustfmt.toml 15 | build --aspects=@rules_rust//rust:defs.bzl%rustfmt_aspect 16 | build --output_groups=+rustfmt_checks 17 | build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect 18 | build --output_groups=+clippy_checks 19 | 20 | build --incompatible_strict_action_env 21 | 22 | test --test_output=errors 23 | 24 | build:linux --sandbox_add_mount_pair=/tmp 25 | build:linux --incompatible_enable_cc_toolchain_resolution 26 | build:linux --action_env BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 27 | build:linux --extra_toolchains @zig_sdk//toolchain:linux_arm64_gnu.2.17,@zig_sdk//toolchain:linux_amd64_gnu.2.17 28 | 29 | build:windows --nolegacy_external_runfiles 30 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | 8.0.0 2 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pb binary linguist-vendored 2 | 3 | # Make sure .star files always have LF line endings, since the parser tests 4 | # are snapshot tests and therefore depend on the content being the same. 5 | *.star text eol=lf 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Prepare for build 18 | run: | 19 | bazel/setup-ci.sh 20 | mkdir -p editors/code/dist 21 | env: 22 | BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} 23 | - name: Build 24 | run: bazel build //... 25 | - name: Run tests 26 | run: bazel test --build_tests_only //... 27 | 28 | build-windows: 29 | runs-on: windows-latest 30 | steps: 31 | - uses: actions/checkout@v3 32 | - name: Prepare for build 33 | run: | 34 | echo 'import %workspace%/bazel/remote-cache.bazelrc' >>.bazelrc 35 | echo "build --remote_header=x-buildbuddy-api-key=$BUILDBUDDY_API_KEY" >>.bazelrc 36 | echo "startup --output_user_root=C:/b" >>.bazelrc 37 | echo "startup --windows_enable_symlinks" >>.bazelrc 38 | mkdir -p editors/code/dist 39 | shell: bash 40 | env: 41 | BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} 42 | - name: Build 43 | run: bazel build //crates/... 44 | - name: Run tests 45 | run: bazel test --build_tests_only //crates/... 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ${{ matrix.target.runs-on }} 12 | strategy: 13 | matrix: 14 | target: 15 | - name: linux-amd64 16 | runs-on: ubuntu-20.04 17 | - name: linux-aarch64 18 | runs-on: ubuntu-20.04 19 | - name: darwin-amd64 20 | runs-on: macos-13 21 | - name: darwin-arm64 22 | runs-on: macos-14 23 | fail-fast: true 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Prepare for build 28 | run: | 29 | bazel/setup-ci.sh 30 | mkdir -p editors/code/dist 31 | env: 32 | BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} 33 | - name: Build 34 | run: bazel build -c opt ${{ matrix.target.name == 'linux-aarch64' && '--platforms @zig_sdk//platform:linux_arm64' || '' }} //crates/starpls 35 | - uses: actions/upload-artifact@v4 36 | with: 37 | name: starpls-${{ matrix.target.name }} 38 | path: bazel-bin/crates/starpls/starpls 39 | 40 | build-windows: 41 | name: Build (Windows) 42 | runs-on: windows-latest 43 | steps: 44 | - uses: actions/checkout@v3 45 | - name: Prepare for build 46 | run: | 47 | echo 'import %workspace%/bazel/remote-cache.bazelrc' >>.bazelrc 48 | echo "build --remote_header=x-buildbuddy-api-key=$BUILDBUDDY_API_KEY" >>.bazelrc 49 | echo "startup --output_user_root=C:/b" >>.bazelrc 50 | echo "startup --windows_enable_symlinks" >>.bazelrc 51 | mkdir -p editors/code/dist 52 | shell: bash 53 | env: 54 | BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} 55 | - name: Build 56 | run: bazel build -c opt //crates/starpls 57 | - uses: actions/upload-artifact@v4 58 | with: 59 | name: starpls-windows-amd64 60 | path: bazel-bin/crates/starpls/starpls.exe 61 | 62 | release: 63 | name: Release 64 | needs: [build, build-windows] 65 | runs-on: ubuntu-latest 66 | permissions: 67 | contents: write 68 | steps: 69 | - uses: actions/checkout@v4 70 | - uses: actions/download-artifact@v4 71 | with: 72 | path: artifacts 73 | pattern: starpls-* 74 | - name: Prepare release artifacts 75 | run: | 76 | mkdir -p release 77 | mv artifacts/starpls-linux-aarch64/starpls release/starpls-linux-aarch64 78 | mv artifacts/starpls-linux-amd64/starpls release/starpls-linux-amd64 79 | mv artifacts/starpls-darwin-amd64/starpls release/starpls-darwin-amd64 80 | mv artifacts/starpls-darwin-arm64/starpls release/starpls-darwin-arm64 81 | mv artifacts/starpls-windows-amd64/starpls.exe release/starpls-windows-amd64.exe 82 | chmod +x release/starpls-* 83 | 84 | # Also create archives for usage by installation methods like Homebrew. 85 | # Eventually, we'll remove the non-zipped binaries above. 86 | cd release 87 | cp starpls-linux-aarch64 starpls && tar -czvf starpls-linux-aarch64.tar.gz starpls && rm starpls 88 | cp starpls-linux-amd64 starpls && tar -czvf starpls-linux-amd64.tar.gz starpls && rm starpls 89 | cp starpls-darwin-amd64 starpls && tar -czvf starpls-darwin-amd64.tar.gz starpls && rm starpls 90 | cp starpls-darwin-arm64 starpls && tar -czvf starpls-darwin-arm64.tar.gz starpls && rm starpls 91 | cp starpls-windows-amd64.exe starpls.exe && zip starpls-windows-amd64.zip starpls.exe && rm starpls.exe 92 | - name: Show release artifacts 93 | run: find release 94 | - name: Create release 95 | run: | 96 | gh release create ${{ github.ref_name }} release/* 97 | env: 98 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bazel-* 2 | /target 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension (Debug Build)", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceFolder}/editors/code" 10 | ], 11 | "outFiles": [ 12 | "${workspaceFolder}/editors/code/dist/**/*.js" 13 | ], 14 | "preLaunchTask": "${defaultBuildTask}", 15 | "env": { 16 | "__STARPLS_SERVER_DEBUG": "${workspaceFolder}/editors/code/bin/starpls" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "pnpm: build", 6 | "type": "shell", 7 | "command": "pnpm", 8 | "args": [ 9 | "run", 10 | "build" 11 | ], 12 | "options": { 13 | "cwd": "${workspaceFolder}/editors/code" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | }, 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_rust//proto/prost:defs.bzl", "rust_prost_toolchain") 2 | load("@rules_rust//rust:defs.bzl", "rust_library_group") 3 | 4 | exports_files(["rustfmt.toml"]) 5 | 6 | rust_library_group( 7 | name = "prost_runtime", 8 | deps = [ 9 | "@crates//:prost", 10 | ], 11 | ) 12 | 13 | rust_library_group( 14 | name = "tonic_runtime", 15 | deps = [ 16 | ":prost_runtime", 17 | "@crates//:tonic", 18 | ], 19 | ) 20 | 21 | rust_prost_toolchain( 22 | name = "prost_toolchain_impl", 23 | prost_plugin = "@crates//:protoc-gen-prost__protoc-gen-prost", 24 | prost_runtime = ":prost_runtime", 25 | prost_types = "@crates//:prost-types", 26 | tonic_plugin = "@crates//:protoc-gen-tonic__protoc-gen-tonic", 27 | tonic_runtime = ":tonic_runtime", 28 | ) 29 | 30 | toolchain( 31 | name = "prost_toolchain", 32 | toolchain = "prost_toolchain_impl", 33 | toolchain_type = "@rules_rust//proto/prost:toolchain_type", 34 | ) 35 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/starpls", 5 | "crates/starpls_bazel", 6 | "crates/starpls_common", 7 | "crates/starpls_hir", 8 | "crates/starpls_ide", 9 | "crates/starpls_intern", 10 | "crates/starpls_lexer", 11 | "crates/starpls_parser", 12 | "crates/starpls_syntax", 13 | "crates/starpls_test_util", 14 | "vendor/runfiles", 15 | "xtask", 16 | ] 17 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /MODULE.bazel: -------------------------------------------------------------------------------- 1 | module(name = "starpls") 2 | 3 | bazel_dep(name = "aspect_bazel_lib", version = "2.9.4") 4 | bazel_dep(name = "aspect_rules_js", version = "2.1.1") 5 | bazel_dep(name = "aspect_rules_swc", version = "2.0.1") 6 | bazel_dep(name = "aspect_rules_ts", version = "3.3.1") 7 | bazel_dep(name = "bazel_skylib", version = "1.7.1") 8 | bazel_dep(name = "hermetic_cc_toolchain", version = "3.1.1") 9 | bazel_dep(name = "protobuf", version = "29.0") 10 | bazel_dep(name = "rules_proto", version = "7.0.2") 11 | bazel_dep(name = "rules_rust", version = "0.53.0") 12 | 13 | zig_toolchains = use_extension("@hermetic_cc_toolchain//toolchain:ext.bzl", "toolchains") 14 | use_repo(zig_toolchains, "zig_sdk") 15 | 16 | rust = use_extension("@rules_rust//rust:extensions.bzl", "rust") 17 | rust.toolchain( 18 | edition = "2021", 19 | versions = ["nightly/2023-12-06"], 20 | extra_target_triples = ["aarch64-unknown-linux-gnu"], 21 | ) 22 | 23 | use_repo(rust, "rust_toolchains") 24 | 25 | register_toolchains("@rust_toolchains//:all") 26 | 27 | crate = use_extension( 28 | "@rules_rust//crate_universe:extension.bzl", 29 | "crate", 30 | ) 31 | 32 | crate.from_cargo( 33 | name = "crates", 34 | cargo_lockfile = "Cargo.lock", 35 | manifests = [ 36 | "//:Cargo.toml", 37 | "//crates/starpls:Cargo.toml", 38 | "//crates/starpls_bazel:Cargo.toml", 39 | "//crates/starpls_common:Cargo.toml", 40 | "//crates/starpls_hir:Cargo.toml", 41 | "//crates/starpls_ide:Cargo.toml", 42 | "//crates/starpls_intern:Cargo.toml", 43 | "//crates/starpls_lexer:Cargo.toml", 44 | "//crates/starpls_parser:Cargo.toml", 45 | "//crates/starpls_syntax:Cargo.toml", 46 | "//crates/starpls_test_util:Cargo.toml", 47 | "//vendor/runfiles:Cargo.toml", 48 | "//xtask:Cargo.toml", 49 | ], 50 | ) 51 | use_repo(crate, "crates") 52 | 53 | crate.annotation( 54 | crate = "protoc-gen-prost", 55 | gen_binaries = ["protoc-gen-prost"], 56 | ) 57 | 58 | crate.annotation( 59 | crate = "protoc-gen-tonic", 60 | gen_binaries = ["protoc-gen-tonic"], 61 | ) 62 | 63 | register_toolchains("//:prost_toolchain") 64 | 65 | rules_ts_ext = use_extension("@aspect_rules_ts//ts:extensions.bzl", "ext", dev_dependency = True) 66 | 67 | rules_ts_ext.deps( 68 | ts_version_from = "//editors/code:package.json", 69 | ts_integrity = "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 70 | ) 71 | 72 | use_repo(rules_ts_ext, "npm_typescript") 73 | 74 | npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm", dev_dependency = True) 75 | 76 | npm.npm_translate_lock( 77 | name = "npm", 78 | pnpm_lock = "//editors/code:pnpm-lock.yaml", 79 | verify_node_modules_ignored = "//:.bazelignore", 80 | ) 81 | 82 | use_repo(npm, "npm") 83 | -------------------------------------------------------------------------------- /WORKSPACE.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withered-magic/starpls/a1e1ed945961a7158ab8e87cdc7e344fe8bf56ce/WORKSPACE.bazel -------------------------------------------------------------------------------- /bazel/remote-cache.bazelrc: -------------------------------------------------------------------------------- 1 | build --experimental_convenience_symlinks=normal 2 | 3 | build --bes_results_url=https://starpls.buildbuddy.io/invocation/ 4 | build --bes_backend=grpcs://starpls.buildbuddy.io 5 | build --remote_cache=grpcs://starpls.buildbuddy.io 6 | build --remote_timeout=3600 7 | -------------------------------------------------------------------------------- /bazel/setup-ci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo 'import %workspace%/bazel/remote-cache.bazelrc' >>.bazelrc 3 | echo "build --remote_header=x-buildbuddy-api-key=$BUILDBUDDY_API_KEY" >>.bazelrc 4 | -------------------------------------------------------------------------------- /crates/starpls/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@crates//:defs.bzl", "all_crate_deps") 2 | load("@rules_rust//rust:defs.bzl", "rust_binary") 3 | 4 | package(default_visibility = ["//visibility:public"]) 5 | 6 | rust_binary( 7 | name = "starpls", 8 | srcs = glob(["src/**/*.rs"]), 9 | compile_data = [":src/builtin/builtin.pb"], 10 | rustc_env_files = [":generate_rustc_env_file"], 11 | deps = all_crate_deps() + [ 12 | "//crates/starpls_bazel", 13 | "//crates/starpls_common", 14 | "//crates/starpls_ide", 15 | "//crates/starpls_syntax", 16 | ], 17 | ) 18 | 19 | genrule( 20 | name = "generate_rustc_env_file", 21 | srcs = [ 22 | "Cargo.toml", 23 | ], 24 | outs = ["rustc_env_file"], 25 | cmd = "echo \"CARGO_PKG_VERSION=$$(grep 'version =' $(location :Cargo.toml) | awk '$$1 == \"version\" {print $$3}' | tr -d '\"')\" > $@", 26 | ) 27 | -------------------------------------------------------------------------------- /crates/starpls/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starpls" 3 | version = "0.1.22" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.75" 10 | crossbeam-channel = "0.5.8" 11 | dashmap = "5.5.3" 12 | indexmap = "2.1.0" 13 | line-index = "0.1.0" 14 | lsp-server = "0.7.5" 15 | lsp-types = "0.94.1" 16 | rayon = "1.8.0" 17 | rustc-hash = "1.1.0" 18 | serde = "1.0.193" 19 | serde_json = "1.0.108" 20 | starpls_bazel = { path = "../starpls_bazel" } 21 | starpls_common = { path = "../starpls_common" } 22 | starpls_syntax = { path = "../starpls_syntax" } 23 | starpls_ide = { path = "../starpls_ide" } 24 | parking_lot = "0.12.1" 25 | clap = { version = "4.5.4", features = ["derive"] } 26 | env_logger = "0.11.5" 27 | log = "0.4.22" 28 | annotate-snippets = "0.11.5" 29 | anstream = "0.6.18" 30 | walkdir = "2.5.0" 31 | -------------------------------------------------------------------------------- /crates/starpls/src/bazel.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use log::debug; 3 | use log::info; 4 | use starpls_bazel::client::BazelClient; 5 | use starpls_bazel::client::BazelInfo; 6 | use starpls_bazel::Builtins; 7 | 8 | use crate::server::load_bazel_build_language; 9 | 10 | /// Contains information about the current Bazel configuration as fetched from 11 | /// various `bazel info` commands. 12 | #[derive(Default)] 13 | pub(crate) struct BazelContext { 14 | pub(crate) info: BazelInfo, 15 | pub(crate) rules: Builtins, 16 | pub(crate) bzlmod_enabled: bool, 17 | } 18 | 19 | impl BazelContext { 20 | pub(crate) fn new(client: &dyn BazelClient) -> anyhow::Result { 21 | let info = client 22 | .info() 23 | .map_err(|err| anyhow!("failed to run `bazel info`: {}", err))?; 24 | 25 | info!("workspace root: {:?}", info.workspace); 26 | info!("workspace name: {:?}", info.workspace_name); 27 | info!("starlark-semantics: {:?}", info.starlark_semantics); 28 | 29 | // Check if bzlmod is enabled for the current workspace. 30 | let bzlmod_enabled = { 31 | // bzlmod is enabled by default for Bazel versions 7 and later. 32 | // TODO(withered-magic): Just hardcoding this for now since I'm lazy to parse the actual versions. 33 | // This should last us pretty long since Bazel 9 isn't anywhere on the horizon. 34 | let bzlmod_enabled_by_default = ["release 7", "release 8", "release 9"] 35 | .iter() 36 | .any(|release| info.release.starts_with(release)); 37 | 38 | if bzlmod_enabled_by_default { 39 | info!("Bazel 7 or later detected"); 40 | } 41 | 42 | // Check starlark-semantics to determine whether bzlmod has been explicitly 43 | // enabled/disabled, e.g. in a .bazelrc file. 44 | if info.starlark_semantics.contains("enable_bzlmod=true") { 45 | info!("found enable_bzlmod=true in starlark semantics"); 46 | true 47 | } else if info.starlark_semantics.contains("enable_bzlmod=false") { 48 | info!("found enable_bzlmod=false in starlark semantics"); 49 | false 50 | } else { 51 | bzlmod_enabled_by_default 52 | } 53 | }; 54 | 55 | info!("bzlmod_enabled = {}", bzlmod_enabled); 56 | 57 | // If bzlmod is enabled, we also need to check if the `bazel mod dump_repo_mapping` command is supported. 58 | if bzlmod_enabled { 59 | debug!("checking for `bazel mod dump_repo_mapping` capability"); 60 | client 61 | .dump_repo_mapping("") 62 | .map_err(|err| anyhow!("failed to run `bazel mod dump_repo_mapping`: {}", err))?; 63 | } 64 | 65 | debug!("fetching builtin rules via `bazel info build-language`"); 66 | let rules = load_bazel_build_language(client) 67 | .map_err(|err| anyhow!("failed to run `bazel info build-language`: {}", err))?; 68 | 69 | Ok(BazelContext { 70 | info, 71 | rules, 72 | bzlmod_enabled, 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /crates/starpls/src/builtin/builtin.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withered-magic/starpls/a1e1ed945961a7158ab8e87cdc7e344fe8bf56ce/crates/starpls/src/builtin/builtin.pb -------------------------------------------------------------------------------- /crates/starpls/src/commands.rs: -------------------------------------------------------------------------------- 1 | use clap::Args; 2 | 3 | pub(crate) mod check; 4 | pub(crate) mod server; 5 | 6 | #[derive(Args, Default)] 7 | pub(crate) struct InferenceOptions { 8 | /// Infer attributes on a rule implementation function's context parameter. 9 | #[clap(long = "experimental_infer_ctx_attributes", default_value_t = false)] 10 | pub(crate) infer_ctx_attributes: bool, 11 | 12 | /// Use code-flow analysis during typechecking. 13 | #[clap(long = "experimental_use_code_flow_analysis", default_value_t = false)] 14 | pub(crate) use_code_flow_analysis: bool, 15 | } 16 | -------------------------------------------------------------------------------- /crates/starpls/src/commands/server.rs: -------------------------------------------------------------------------------- 1 | use clap::Args; 2 | use log::info; 3 | use lsp_server::Connection; 4 | use lsp_types::CompletionOptions; 5 | use lsp_types::HoverProviderCapability; 6 | use lsp_types::OneOf; 7 | use lsp_types::ServerCapabilities; 8 | use lsp_types::SignatureHelpOptions; 9 | use lsp_types::TextDocumentSyncCapability; 10 | use lsp_types::TextDocumentSyncKind; 11 | 12 | use crate::commands::InferenceOptions; 13 | use crate::event_loop; 14 | use crate::get_version; 15 | use crate::make_trigger_characters; 16 | 17 | const COMPLETION_TRIGGER_CHARACTERS: &[char] = &['.', '"', '\'', '/', ':', '@']; 18 | const SIGNATURE_HELP_TRIGGER_CHARACTERS: &[char] = &['(', ',', ')']; 19 | 20 | #[derive(Args, Default)] 21 | pub(crate) struct ServerCommand { 22 | /// Path to the Bazel binary. 23 | #[clap(long = "bazel_path")] 24 | pub(crate) bazel_path: Option, 25 | 26 | /// Enable completions for labels for targets in the current workspace. 27 | #[clap( 28 | long = "experimental_enable_label_completions", 29 | default_value_t = false 30 | )] 31 | pub(crate) enable_label_completions: bool, 32 | 33 | /// After receiving an edit event, the amount of time in milliseconds 34 | /// the server will wait for additional events before running analysis 35 | #[clap(long = "analysis_debounce_interval", default_value_t = 250)] 36 | pub(crate) analysis_debounce_interval: u64, 37 | 38 | #[command(flatten)] 39 | pub(crate) inference_options: InferenceOptions, 40 | } 41 | 42 | impl ServerCommand { 43 | pub(crate) fn run(self) -> anyhow::Result<()> { 44 | info!("starpls, v{}", get_version()); 45 | 46 | // Create the transport over stdio. 47 | let (connection, io_threads) = Connection::stdio(); 48 | 49 | // Initialize the connection with server capabilities. For now, this consists 50 | // only of `TextDocumentSyncKind.Full`. 51 | let server_capabilities = serde_json::to_value(ServerCapabilities { 52 | completion_provider: Some(CompletionOptions { 53 | trigger_characters: Some(make_trigger_characters(COMPLETION_TRIGGER_CHARACTERS)), 54 | ..Default::default() 55 | }), 56 | definition_provider: Some(OneOf::Left(true)), 57 | document_symbol_provider: Some(OneOf::Left(true)), 58 | hover_provider: Some(HoverProviderCapability::Simple(true)), 59 | references_provider: Some(OneOf::Left(true)), 60 | signature_help_provider: Some(SignatureHelpOptions { 61 | trigger_characters: Some(make_trigger_characters( 62 | SIGNATURE_HELP_TRIGGER_CHARACTERS, 63 | )), 64 | ..Default::default() 65 | }), 66 | text_document_sync: Some(TextDocumentSyncCapability::Kind( 67 | TextDocumentSyncKind::INCREMENTAL, 68 | )), 69 | ..Default::default() 70 | })?; 71 | let initialize_params = 72 | serde_json::from_value(connection.initialize(server_capabilities)?)?; 73 | event_loop::process_connection(connection, self, initialize_params)?; 74 | 75 | // Graceful shutdown. 76 | info!("connection closed, exiting"); 77 | io_threads.join()?; 78 | 79 | Ok(()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/starpls/src/config.rs: -------------------------------------------------------------------------------- 1 | use lsp_types::ClientCapabilities; 2 | 3 | use crate::commands::server::ServerCommand; 4 | 5 | #[derive(Default)] 6 | pub(crate) struct ServerConfig { 7 | pub(crate) args: ServerCommand, 8 | pub(crate) caps: ClientCapabilities, 9 | } 10 | 11 | macro_rules! try_or_default { 12 | ($expr:expr) => { 13 | (|| $expr)().unwrap_or_default() 14 | }; 15 | } 16 | 17 | impl ServerConfig { 18 | pub(crate) fn has_text_document_definition_link_support(&self) -> bool { 19 | try_or_default!(self.caps.text_document.as_ref()?.definition?.link_support) 20 | } 21 | 22 | pub(crate) fn has_insert_replace_support(&self) -> bool { 23 | try_or_default!( 24 | self.caps 25 | .text_document 26 | .as_ref()? 27 | .completion 28 | .as_ref()? 29 | .completion_item 30 | .as_ref()? 31 | .insert_replace_support 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/starpls/src/convert.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::anyhow; 4 | use line_index::LineIndex; 5 | use line_index::WideEncoding; 6 | use line_index::WideLineCol; 7 | use starpls_common::Diagnostic; 8 | use starpls_common::DiagnosticTag; 9 | use starpls_common::FileId; 10 | use starpls_common::Severity; 11 | use starpls_ide::DocumentSymbol; 12 | use starpls_ide::SymbolKind; 13 | use starpls_ide::SymbolTag; 14 | use starpls_syntax::TextRange; 15 | use starpls_syntax::TextSize; 16 | 17 | use crate::server::ServerSnapshot; 18 | 19 | pub(crate) fn path_buf_from_url(url: &lsp_types::Url) -> anyhow::Result { 20 | url.to_file_path() 21 | .map_err(|_| anyhow!("url is not a file: {}", url)) 22 | } 23 | 24 | pub(crate) fn lsp_diagnostic_from_native( 25 | diagnostic: Diagnostic, 26 | line_index: &LineIndex, 27 | ) -> Option { 28 | Some(lsp_types::Diagnostic { 29 | range: lsp_range_from_text_range(diagnostic.range.range, line_index)?, 30 | severity: Some(lsp_severity_from_native(diagnostic.severity)), 31 | code: None, 32 | code_description: None, 33 | source: Some("starpls".to_string()), 34 | message: diagnostic.message, 35 | related_information: None, 36 | tags: diagnostic.tags.map(|tags| { 37 | tags.into_iter() 38 | .map(|tag| match tag { 39 | DiagnosticTag::Unnecessary => lsp_types::DiagnosticTag::UNNECESSARY, 40 | DiagnosticTag::Deprecated => lsp_types::DiagnosticTag::DEPRECATED, 41 | }) 42 | .collect() 43 | }), 44 | data: None, 45 | }) 46 | } 47 | 48 | pub(crate) fn lsp_range_from_text_range( 49 | text_range: TextRange, 50 | line_index: &LineIndex, 51 | ) -> Option { 52 | let start = line_index.to_wide(WideEncoding::Utf16, line_index.line_col(text_range.start()))?; 53 | let end = line_index.to_wide(WideEncoding::Utf16, line_index.line_col(text_range.end()))?; 54 | Some(lsp_types::Range { 55 | start: lsp_types::Position { 56 | line: start.line, 57 | character: start.col, 58 | }, 59 | end: lsp_types::Position { 60 | line: end.line, 61 | character: end.col, 62 | }, 63 | }) 64 | } 65 | 66 | fn wide_line_col_from_lsp_position(pos: lsp_types::Position) -> WideLineCol { 67 | WideLineCol { 68 | line: pos.line, 69 | col: pos.character, 70 | } 71 | } 72 | 73 | pub(crate) fn text_size_from_lsp_position( 74 | snapshot: &ServerSnapshot, 75 | file_id: FileId, 76 | pos: lsp_types::Position, 77 | ) -> anyhow::Result> { 78 | let line_index = match snapshot.analysis_snapshot.line_index(file_id)? { 79 | Some(line_index) => line_index, 80 | None => return Ok(None), 81 | }; 82 | let line_col = 83 | match line_index.to_utf8(WideEncoding::Utf16, wide_line_col_from_lsp_position(pos)) { 84 | Some(line_col) => line_col, 85 | None => return Ok(None), 86 | }; 87 | Ok(line_index.offset(line_col)) 88 | } 89 | 90 | fn lsp_severity_from_native(severity: Severity) -> lsp_types::DiagnosticSeverity { 91 | match severity { 92 | Severity::Error => lsp_types::DiagnosticSeverity::ERROR, 93 | Severity::Warning => lsp_types::DiagnosticSeverity::WARNING, 94 | } 95 | } 96 | 97 | #[allow(deprecated)] 98 | pub(crate) fn lsp_document_symbol_from_native( 99 | DocumentSymbol { 100 | name, 101 | detail, 102 | kind, 103 | tags, 104 | range, 105 | selection_range, 106 | children, 107 | }: DocumentSymbol, 108 | line_index: &LineIndex, 109 | ) -> Option { 110 | Some(lsp_types::DocumentSymbol { 111 | name, 112 | detail, 113 | kind: match kind { 114 | SymbolKind::File => lsp_types::SymbolKind::FILE, 115 | SymbolKind::Module => lsp_types::SymbolKind::MODULE, 116 | SymbolKind::Namespace => lsp_types::SymbolKind::NAMESPACE, 117 | SymbolKind::Package => lsp_types::SymbolKind::PACKAGE, 118 | SymbolKind::Class => lsp_types::SymbolKind::CLASS, 119 | SymbolKind::Method => lsp_types::SymbolKind::METHOD, 120 | SymbolKind::Property => lsp_types::SymbolKind::PROPERTY, 121 | SymbolKind::Field => lsp_types::SymbolKind::FIELD, 122 | SymbolKind::Constructor => lsp_types::SymbolKind::CONSTRUCTOR, 123 | SymbolKind::Enum => lsp_types::SymbolKind::ENUM, 124 | SymbolKind::Interface => lsp_types::SymbolKind::INTERFACE, 125 | SymbolKind::Function => lsp_types::SymbolKind::FUNCTION, 126 | SymbolKind::Variable => lsp_types::SymbolKind::VARIABLE, 127 | SymbolKind::Constant => lsp_types::SymbolKind::CONSTANT, 128 | SymbolKind::String => lsp_types::SymbolKind::STRING, 129 | SymbolKind::Number => lsp_types::SymbolKind::NUMBER, 130 | SymbolKind::Boolean => lsp_types::SymbolKind::BOOLEAN, 131 | SymbolKind::Array => lsp_types::SymbolKind::ARRAY, 132 | SymbolKind::Object => lsp_types::SymbolKind::OBJECT, 133 | SymbolKind::Key => lsp_types::SymbolKind::KEY, 134 | SymbolKind::Null => lsp_types::SymbolKind::NULL, 135 | SymbolKind::EnumMember => lsp_types::SymbolKind::ENUM_MEMBER, 136 | SymbolKind::Struct => lsp_types::SymbolKind::STRUCT, 137 | SymbolKind::Event => lsp_types::SymbolKind::EVENT, 138 | SymbolKind::Operator => lsp_types::SymbolKind::OPERATOR, 139 | SymbolKind::TypeParameter => lsp_types::SymbolKind::TYPE_PARAMETER, 140 | }, 141 | tags: tags.map(|tags| { 142 | tags.into_iter() 143 | .map(|tag| match tag { 144 | SymbolTag::Deprecated => lsp_types::SymbolTag::DEPRECATED, 145 | }) 146 | .collect() 147 | }), 148 | range: lsp_range_from_text_range(range, line_index)?, 149 | selection_range: lsp_range_from_text_range(selection_range, line_index)?, 150 | children: children.map(|children| { 151 | children 152 | .into_iter() 153 | .filter_map(|child| lsp_document_symbol_from_native(child, line_index)) 154 | .collect() 155 | }), 156 | deprecated: None, 157 | }) 158 | } 159 | -------------------------------------------------------------------------------- /crates/starpls/src/debouncer.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crossbeam_channel::RecvError; 4 | use crossbeam_channel::RecvTimeoutError; 5 | use crossbeam_channel::Sender; 6 | use rustc_hash::FxHashSet; 7 | use starpls_common::FileId; 8 | 9 | use crate::event_loop::Task; 10 | 11 | pub(crate) struct AnalysisDebouncer { 12 | pub(crate) sender: Sender>, 13 | } 14 | 15 | impl AnalysisDebouncer { 16 | pub(crate) fn new(duration: Duration, sink: Sender) -> Self { 17 | let (source_tx, source_rx) = crossbeam_channel::unbounded::>(); 18 | 19 | std::thread::spawn(move || { 20 | let mut active = false; 21 | let mut pending_file_ids: FxHashSet = FxHashSet::default(); 22 | loop { 23 | if active { 24 | match source_rx.recv_timeout(duration) { 25 | Ok(file_ids) => pending_file_ids.extend(file_ids), 26 | Err(RecvTimeoutError::Disconnected) => break, 27 | Err(RecvTimeoutError::Timeout) => { 28 | sink.send(Task::AnalysisRequested(pending_file_ids.drain().collect())) 29 | .unwrap(); 30 | active = false; 31 | } 32 | } 33 | } else { 34 | match source_rx.recv() { 35 | Ok(file_ids) => { 36 | active = true; 37 | pending_file_ids.extend(file_ids); 38 | } 39 | Err(RecvError) => break, 40 | } 41 | } 42 | } 43 | }); 44 | 45 | Self { sender: source_tx } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/starpls/src/diagnostics.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::mem; 3 | 4 | use lsp_types::Diagnostic; 5 | use starpls_common::FileId; 6 | 7 | #[derive(Default)] 8 | pub(crate) struct DiagnosticsManager { 9 | diagnostics: HashMap>, 10 | files_with_changed_diagnostics: Vec, 11 | } 12 | 13 | impl DiagnosticsManager { 14 | pub(crate) fn set_diagnostics(&mut self, file_id: FileId, mut diagnostics: Vec) { 15 | self.diagnostics 16 | .entry(file_id) 17 | .and_modify(|current_diagnostics| { 18 | if current_diagnostics.len() != diagnostics.len() 19 | || current_diagnostics.iter().zip(diagnostics.iter()).any( 20 | |(current_diagnostic, diagnostic)| { 21 | !is_diagnostic_equal(current_diagnostic, diagnostic) 22 | }, 23 | ) 24 | { 25 | *current_diagnostics = mem::take(&mut diagnostics); 26 | } 27 | }) 28 | .or_insert(diagnostics); 29 | self.files_with_changed_diagnostics.push(file_id); 30 | } 31 | 32 | pub(crate) fn take_changes(&mut self) -> Vec { 33 | mem::take(&mut self.files_with_changed_diagnostics) 34 | } 35 | 36 | pub(crate) fn get_diagnostics( 37 | &self, 38 | file_id: FileId, 39 | ) -> impl Iterator { 40 | self.diagnostics.get(&file_id).into_iter().flatten() 41 | } 42 | } 43 | 44 | fn is_diagnostic_equal(left: &Diagnostic, right: &Diagnostic) -> bool { 45 | left.source == right.source 46 | && left.severity == right.severity 47 | && left.range == right.range 48 | && left.message == right.message 49 | } 50 | -------------------------------------------------------------------------------- /crates/starpls/src/dispatcher.rs: -------------------------------------------------------------------------------- 1 | use std::panic; 2 | 3 | use starpls_ide::Cancelled; 4 | 5 | use crate::event_loop::Task; 6 | use crate::server::Server; 7 | use crate::server::ServerSnapshot; 8 | 9 | pub(crate) struct RequestDispatcher<'a> { 10 | req: Option, 11 | server: &'a Server, 12 | } 13 | 14 | impl<'a> RequestDispatcher<'a> { 15 | pub(crate) fn new(req: lsp_server::Request, server: &'a Server) -> Self { 16 | Self { 17 | req: Some(req), 18 | server, 19 | } 20 | } 21 | 22 | pub(crate) fn on( 23 | &mut self, 24 | f: fn(&ServerSnapshot, R::Params) -> anyhow::Result, 25 | ) -> &mut Self 26 | where 27 | R: lsp_types::request::Request + 'static, 28 | R::Params: serde::de::DeserializeOwned + Send + panic::UnwindSafe, 29 | { 30 | let (req, params) = match self.parse::() { 31 | Some(res) => res, 32 | None => return self, 33 | }; 34 | 35 | let snapshot = self.server.snapshot(); 36 | self.server.task_pool_handle.spawn(move || { 37 | let res = panic::catch_unwind(|| f(&snapshot, params)); 38 | let response = match res { 39 | Ok(res) => match res { 40 | Ok(res) => lsp_server::Response::new_ok(req.id, res), 41 | Err(err) => match err.downcast::() { 42 | Ok(_) => return Task::Retry(req), 43 | Err(err) => lsp_server::Response::new_err( 44 | req.id, 45 | lsp_server::ErrorCode::RequestFailed as i32, 46 | err.to_string(), 47 | ), 48 | }, 49 | }, 50 | Err(err) => { 51 | let panic_message = err 52 | .downcast_ref::() 53 | .map(String::as_str) 54 | .or_else(|| err.downcast_ref::<&str>().copied()); 55 | lsp_server::Response::new_err( 56 | req.id, 57 | lsp_server::ErrorCode::RequestFailed as i32, 58 | format!( 59 | "request handler panicked: {}", 60 | panic_message.unwrap_or("unknown reason") 61 | ), 62 | ) 63 | } 64 | }; 65 | 66 | Task::ResponseReady(response) 67 | }); 68 | 69 | self 70 | } 71 | 72 | pub(crate) fn finish(&mut self) { 73 | let req = match self.req.take() { 74 | Some(req) => req, 75 | None => return, 76 | }; 77 | 78 | self.server.task_pool_handle.spawn(move || { 79 | Task::ResponseReady(lsp_server::Response::new_err( 80 | req.id, 81 | lsp_server::ErrorCode::MethodNotFound as i32, 82 | format!("method not found: {}", req.method), 83 | )) 84 | }); 85 | } 86 | 87 | pub(crate) fn parse(&mut self) -> Option<(lsp_server::Request, R::Params)> 88 | where 89 | R: lsp_types::request::Request, 90 | R::Params: serde::de::DeserializeOwned, 91 | { 92 | self.req.take().and_then(|req| { 93 | if req.method == R::METHOD { 94 | // Unwrapping here is fine, since if we see invalid JSON, we can't really recover parsing afterwards. 95 | let params = serde_json::from_value(req.params.clone()).expect("invalid JSON"); 96 | Some((req, params)) 97 | } else { 98 | self.req = Some(req); 99 | None 100 | } 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /crates/starpls/src/extensions.rs: -------------------------------------------------------------------------------- 1 | use lsp_types::request::Request; 2 | use lsp_types::TextDocumentIdentifier; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | 6 | #[derive(Serialize, Deserialize, Debug)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct ShowSyntaxTreeParams { 9 | pub text_document: TextDocumentIdentifier, 10 | } 11 | 12 | #[derive(Debug)] 13 | pub enum ShowSyntaxTree {} 14 | 15 | impl Request for ShowSyntaxTree { 16 | type Params = ShowSyntaxTreeParams; 17 | type Result = String; 18 | const METHOD: &'static str = "starpls/showSyntaxTree"; 19 | } 20 | 21 | #[derive(Serialize, Deserialize, Debug)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct ShowHirParams { 24 | pub text_document: TextDocumentIdentifier, 25 | } 26 | 27 | #[derive(Debug)] 28 | pub enum ShowHir {} 29 | 30 | impl Request for ShowHir { 31 | type Params = ShowHirParams; 32 | type Result = String; 33 | const METHOD: &'static str = "starpls/showHir"; 34 | } 35 | -------------------------------------------------------------------------------- /crates/starpls/src/handlers.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod notifications; 2 | pub(crate) mod requests; 3 | -------------------------------------------------------------------------------- /crates/starpls/src/handlers/notifications.rs: -------------------------------------------------------------------------------- 1 | use crate::convert; 2 | use crate::server::Server; 3 | use crate::utils::apply_document_content_changes; 4 | 5 | pub(crate) fn did_open_text_document( 6 | server: &mut Server, 7 | params: lsp_types::DidOpenTextDocumentParams, 8 | ) -> anyhow::Result<()> { 9 | let path = convert::path_buf_from_url(¶ms.text_document.uri)?; 10 | server.document_manager.write().open( 11 | path, 12 | params.text_document.version, 13 | params.text_document.text, 14 | ); 15 | Ok(()) 16 | } 17 | 18 | pub(crate) fn did_close_text_document( 19 | server: &mut Server, 20 | params: lsp_types::DidCloseTextDocumentParams, 21 | ) -> anyhow::Result<()> { 22 | let path = convert::path_buf_from_url(¶ms.text_document.uri)?; 23 | server.document_manager.write().close(&path); 24 | Ok(()) 25 | } 26 | 27 | pub(crate) fn did_change_text_document( 28 | server: &mut Server, 29 | params: lsp_types::DidChangeTextDocumentParams, 30 | ) -> anyhow::Result<()> { 31 | let mut document_manager = server.document_manager.write(); 32 | let path = convert::path_buf_from_url(¶ms.text_document.uri)?; 33 | if let Some(file_id) = document_manager.lookup_by_path_buf(&path) { 34 | let contents = document_manager 35 | .get(file_id) 36 | .map(|document| document.contents.clone()) 37 | .expect("lookup contents of non-existent file"); 38 | let contents = apply_document_content_changes(contents, params.content_changes); 39 | document_manager.modify(file_id, contents, Some(params.text_document.version)) 40 | } 41 | Ok(()) 42 | } 43 | 44 | pub(crate) fn did_save_text_document( 45 | server: &mut Server, 46 | params: lsp_types::DidSaveTextDocumentParams, 47 | ) -> anyhow::Result<()> { 48 | let path = convert::path_buf_from_url(¶ms.text_document.uri)?; 49 | if server 50 | .document_manager 51 | .read() 52 | .lookup_by_path_buf(&path) 53 | .is_some() 54 | { 55 | match path.file_name().and_then(|file_name| file_name.to_str()) { 56 | Some("MODULE.bazel" | "WORKSPACE" | "WORKSPACE.bazel" | "WORKSPACE.bzlmod") => {} 57 | Some(file_name) if file_name.ends_with(".MODULE.bazel") => {} 58 | Some("BUILD" | "BUILD.bazel") => { 59 | server.refresh_all_workspace_targets(); 60 | return Ok(()); 61 | } 62 | _ => return Ok(()), 63 | } 64 | server.bazel_client.clear_repo_mappings(); 65 | server.fetched_repos.clear(); 66 | } 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /crates/starpls/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use clap::Subcommand; 3 | use commands::check::CheckCommand; 4 | use commands::server::ServerCommand; 5 | 6 | mod bazel; 7 | mod commands; 8 | mod config; 9 | mod convert; 10 | mod debouncer; 11 | mod diagnostics; 12 | mod dispatcher; 13 | mod document; 14 | mod event_loop; 15 | mod extensions; 16 | mod handlers; 17 | mod server; 18 | mod task_pool; 19 | mod utils; 20 | 21 | #[derive(Parser)] 22 | struct Cli { 23 | #[command(subcommand)] 24 | command: Option, 25 | } 26 | 27 | /// Starpls is an LSP implementation for Starlark, the configuration language used by Bazel and Buck2. 28 | #[derive(Subcommand)] 29 | enum Commands { 30 | /// Analyze the specified Starlark files and report errors. 31 | Check(CheckCommand), 32 | 33 | /// Start the language server. 34 | Server(ServerCommand), 35 | 36 | /// Print version information and exit. 37 | Version, 38 | } 39 | 40 | fn main() -> anyhow::Result<()> { 41 | let cli = Cli::parse(); 42 | 43 | // Don't do any log filtering when running the language server. 44 | if matches!(cli.command, Some(Commands::Server(_)) | None) { 45 | env_logger::Builder::from_default_env() 46 | .filter(Some("starpls"), log::LevelFilter::max()) 47 | .init(); 48 | } else { 49 | env_logger::init(); 50 | } 51 | 52 | match cli.command { 53 | Some(Commands::Check(cmd)) => cmd.run(), 54 | Some(Commands::Server(cmd)) => cmd.run(), 55 | Some(Commands::Version) => run_version(), 56 | None => ServerCommand::default().run(), 57 | } 58 | } 59 | 60 | fn run_version() -> anyhow::Result<()> { 61 | println!("starpls version: v{}", get_version()); 62 | Ok(()) 63 | } 64 | 65 | fn make_trigger_characters(chars: &[char]) -> Vec { 66 | chars.iter().map(|c| c.to_string()).collect() 67 | } 68 | 69 | fn get_version() -> &'static str { 70 | env!("CARGO_PKG_VERSION") 71 | } 72 | -------------------------------------------------------------------------------- /crates/starpls/src/task_pool.rs: -------------------------------------------------------------------------------- 1 | use crossbeam_channel::Receiver; 2 | use crossbeam_channel::Sender; 3 | use rayon::ThreadPool; 4 | use rayon::ThreadPoolBuilder; 5 | 6 | pub(crate) struct TaskPool { 7 | sender: Sender, 8 | inner: ThreadPool, 9 | } 10 | 11 | impl TaskPool { 12 | pub(crate) fn with_num_threads( 13 | sender: Sender, 14 | num_threads: usize, 15 | ) -> anyhow::Result> { 16 | let thread_pool = ThreadPoolBuilder::new().num_threads(num_threads).build()?; 17 | Ok(TaskPool { 18 | sender, 19 | inner: thread_pool, 20 | }) 21 | } 22 | 23 | fn spawn(&self, f: F) 24 | where 25 | T: Send + 'static, 26 | F: FnOnce() -> T + Send + 'static, 27 | { 28 | self.inner.spawn({ 29 | let sender = self.sender.clone(); 30 | move || sender.send(f()).unwrap() 31 | }) 32 | } 33 | 34 | #[allow(unused)] 35 | fn spawn_with_sender(&self, f: F) 36 | where 37 | T: Send + 'static, 38 | F: FnOnce(Sender) + Send + 'static, 39 | { 40 | self.inner.spawn({ 41 | let sender = self.sender.clone(); 42 | move || f(sender) 43 | }) 44 | } 45 | } 46 | 47 | pub(crate) struct TaskPoolHandle { 48 | pub(crate) receiver: Receiver, 49 | pool: TaskPool, 50 | } 51 | 52 | impl TaskPoolHandle { 53 | pub(crate) fn new(receiver: Receiver, pool: TaskPool) -> Self { 54 | Self { receiver, pool } 55 | } 56 | 57 | pub(crate) fn spawn(&self, f: F) 58 | where 59 | T: Send + 'static, 60 | F: FnOnce() -> T + Send + 'static, 61 | { 62 | self.pool.spawn(f) 63 | } 64 | 65 | #[allow(unused)] 66 | pub(crate) fn spawn_with_sender(&self, f: F) 67 | where 68 | T: Send + 'static, 69 | F: FnOnce(Sender) + Send + 'static, 70 | { 71 | self.pool.spawn_with_sender(f) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/starpls/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use anyhow::format_err; 4 | use line_index::LineIndex; 5 | use line_index::WideEncoding; 6 | use line_index::WideLineCol; 7 | use starpls_common::FileId; 8 | use starpls_ide::LocationLink; 9 | 10 | use crate::convert; 11 | use crate::server::ServerSnapshot; 12 | 13 | pub(crate) fn text_offset( 14 | line_index: &LineIndex, 15 | pos: lsp_types::Position, 16 | ) -> anyhow::Result { 17 | let line_col = line_index 18 | .to_utf8( 19 | WideEncoding::Utf16, 20 | WideLineCol { 21 | line: pos.line, 22 | col: pos.character, 23 | }, 24 | ) 25 | .ok_or_else(|| format_err!("error converting wide line col to utf-8"))?; 26 | line_index 27 | .offset(line_col) 28 | .map(|offset| offset.into()) 29 | .ok_or_else(|| format_err!("invalid offset")) 30 | } 31 | 32 | pub(crate) fn text_range( 33 | line_index: &LineIndex, 34 | range: lsp_types::Range, 35 | ) -> anyhow::Result> { 36 | let start = text_offset(line_index, range.start)?; 37 | let end = text_offset(line_index, range.end)?; 38 | Ok(start..end) 39 | } 40 | 41 | pub(crate) fn apply_document_content_changes( 42 | mut current_document_contents: String, 43 | content_changes: Vec, 44 | ) -> String { 45 | let mut line_index = LineIndex::new(¤t_document_contents); 46 | for change in content_changes { 47 | let Some(pos_range) = change.range else { 48 | continue; 49 | }; 50 | if let Ok(range) = text_range(&line_index, pos_range) { 51 | current_document_contents.replace_range(range.clone(), &change.text); 52 | line_index = LineIndex::new(¤t_document_contents); 53 | } 54 | } 55 | 56 | current_document_contents 57 | } 58 | 59 | pub(crate) fn response_from_locations( 60 | snapshot: &ServerSnapshot, 61 | source_file_id: FileId, 62 | locations: T, 63 | ) -> U 64 | where 65 | T: Iterator, 66 | U: From> + From>, 67 | { 68 | let source_line_index = match snapshot.analysis_snapshot.line_index(source_file_id) { 69 | Ok(Some(source_line_index)) => source_line_index, 70 | _ => return Vec::::new().into(), 71 | }; 72 | 73 | // let get_line_index = |file_id| snapshot.analysis_snapshot.line_index(file_id); 74 | let to_lsp_location = |location: LocationLink| -> Option { 75 | let location = match location { 76 | LocationLink::Local { 77 | target_range, 78 | target_file_id, 79 | .. 80 | } => { 81 | let target_line_index = snapshot 82 | .analysis_snapshot 83 | .line_index(target_file_id) 84 | .ok()??; 85 | let range = convert::lsp_range_from_text_range(target_range, target_line_index); 86 | lsp_types::Location { 87 | uri: lsp_types::Url::from_file_path( 88 | snapshot 89 | .document_manager 90 | .read() 91 | .lookup_by_file_id(target_file_id), 92 | ) 93 | .ok()?, 94 | range: range?, 95 | } 96 | } 97 | LocationLink::External { target_path, .. } => lsp_types::Location { 98 | uri: lsp_types::Url::from_file_path(target_path).ok()?, 99 | range: Default::default(), 100 | }, 101 | }; 102 | 103 | Some(location) 104 | }; 105 | 106 | let to_lsp_location_link = |location: LocationLink| -> Option { 107 | let location_link = match location { 108 | LocationLink::Local { 109 | origin_selection_range, 110 | target_range, 111 | target_file_id, 112 | .. 113 | } => { 114 | let target_line_index = snapshot 115 | .analysis_snapshot 116 | .line_index(target_file_id) 117 | .ok()??; 118 | let range = convert::lsp_range_from_text_range(target_range, target_line_index); 119 | lsp_types::LocationLink { 120 | origin_selection_range: origin_selection_range.and_then(|range| { 121 | convert::lsp_range_from_text_range(range, source_line_index) 122 | }), 123 | target_range: range?, 124 | target_selection_range: range?, 125 | target_uri: lsp_types::Url::from_file_path( 126 | snapshot 127 | .document_manager 128 | .read() 129 | .lookup_by_file_id(target_file_id), 130 | ) 131 | .ok()?, 132 | } 133 | } 134 | LocationLink::External { 135 | origin_selection_range, 136 | target_path, 137 | } => lsp_types::LocationLink { 138 | origin_selection_range: origin_selection_range 139 | .and_then(|range| convert::lsp_range_from_text_range(range, source_line_index)), 140 | target_range: Default::default(), 141 | target_selection_range: Default::default(), 142 | target_uri: lsp_types::Url::from_file_path(target_path).ok()?, 143 | }, 144 | }; 145 | 146 | Some(location_link) 147 | }; 148 | 149 | if snapshot.config.has_text_document_definition_link_support() { 150 | locations 151 | .flat_map(to_lsp_location_link) 152 | .collect::>() 153 | .into() 154 | } else { 155 | locations 156 | .flat_map(to_lsp_location) 157 | .collect::>() 158 | .into() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /crates/starpls_bazel/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_proto//proto:defs.bzl", "proto_library") 2 | load("@rules_rust//proto/prost:defs.bzl", "rust_prost_library") 3 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") 4 | 5 | package(default_visibility = ["//visibility:public"]) 6 | 7 | proto_library( 8 | name = "builtin_proto", 9 | srcs = ["data/builtin.proto"], 10 | ) 11 | 12 | rust_prost_library( 13 | name = "builtin_proto_rust", 14 | proto = ":builtin_proto", 15 | visibility = ["//visibility:public"], 16 | ) 17 | 18 | proto_library( 19 | name = "build_proto", 20 | srcs = ["data/build.proto"], 21 | ) 22 | 23 | rust_prost_library( 24 | name = "build_proto_rust", 25 | proto = ":build_proto", 26 | visibility = ["//visibility:public"], 27 | ) 28 | 29 | rust_library( 30 | name = "starpls_bazel", 31 | srcs = glob(["src/**/*.rs"]), 32 | compile_data = [ 33 | "data/build.builtins.json", 34 | "data/bzl.builtins.json", 35 | "data/commonAttributes.json", 36 | "data/cquery.builtins.json", 37 | "data/missingModuleFields.json", 38 | "data/module-bazel.builtins.json", 39 | "data/repo.builtins.json", 40 | "data/vendor.builtins.json", 41 | "data/workspace.builtins.json", 42 | ], 43 | rustc_flags = ["--cfg=bazel"], 44 | deps = [ 45 | ":build_proto_rust", 46 | ":builtin_proto_rust", 47 | "@crates//:anyhow", 48 | "@crates//:bytes", 49 | "@crates//:parking_lot", 50 | "@crates//:prost", 51 | "@crates//:serde", 52 | "@crates//:serde_json", 53 | ], 54 | ) 55 | 56 | rust_test( 57 | name = "starpls_bazel_test", 58 | crate = ":starpls_bazel", 59 | rustc_flags = ["--cfg=bazel"], 60 | ) 61 | -------------------------------------------------------------------------------- /crates/starpls_bazel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starpls_bazel" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.79" 10 | bytes = "1.5.0" 11 | parking_lot = "0.12.1" 12 | prost = "0.12.3" 13 | serde = { version = "1.0.197", features = ["derive"] } 14 | serde_json = "1.0.114" 15 | 16 | [dev-dependencies] 17 | prost-types = "0.12.3" 18 | protoc-gen-prost = "0.2.3" 19 | protoc-gen-tonic = "0.3.0" 20 | tonic = "0.11.0" 21 | 22 | [build-dependencies] 23 | prost-build = "0.12.3" 24 | 25 | [[bin]] 26 | name = "inspect-builtins" 27 | path = "src/bin/main.rs" 28 | -------------------------------------------------------------------------------- /crates/starpls_bazel/build.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | 3 | fn main() -> Result<()> { 4 | prost_build::compile_protos(&["data/builtin.proto", "data/build.proto"], &["data/"])?; 5 | Ok(()) 6 | } 7 | -------------------------------------------------------------------------------- /crates/starpls_bazel/data/build.builtins.json: -------------------------------------------------------------------------------- 1 | { 2 | "builtins": [ 3 | { 4 | "name": "package", 5 | "doc": "This function declares metadata that applies to every rule in the package. It is used at most once within a package (BUILD file).\n\nFor the counterpart that declares metadata applying to every rule in the whole repository, use the `repo()` function in the `REPO.bazel` file at the root of your repo. The `repo()` function takes exactly the same arguments as `package()`.\n\nThe package() function should be called right after all the load() statements at the top of the file, before any rule.", 6 | "callable": { 7 | "params": [ 8 | { 9 | "name": "default_applicable_licenses", 10 | "type": "List of Labels", 11 | "doc": "Alias for `default_package_metadata`.", 12 | "default_value": "[]", 13 | "is_mandatory": false, 14 | "is_star_arg": false, 15 | "is_star_star_arg": false 16 | }, 17 | { 18 | "name": "default_visibility", 19 | "type": "List of Labels", 20 | "doc": "The default visibility of the rules in this package.\n\nEvery rule in this package has the visibility specified in this attribute, unless otherwise specified in the `visibility` attribute of the rule. For detailed information about the syntax of this attribute, see the documentation of visibility. The package default visibility does not apply to exports_files, which is public by default.", 21 | "default_value": "[]", 22 | "is_mandatory": false, 23 | "is_star_arg": false, 24 | "is_star_star_arg": false 25 | }, 26 | { 27 | "name": "default_deprecation", 28 | "type": "string", 29 | "doc": "Sets the default `deprecation` message for all rules in this package.", 30 | "default_value": "\"\"", 31 | "is_mandatory": false, 32 | "is_star_arg": false, 33 | "is_star_star_arg": false 34 | }, 35 | { 36 | "name": "default_package_metadata", 37 | "type": "List of Labels", 38 | "doc": "Sets a default list of metadata targets which apply to all other targets in the package. These are typically targets related to OSS package and license declarations. See rules_license for examples.", 39 | "default_value": "", 40 | "is_mandatory": false, 41 | "is_star_arg": false, 42 | "is_star_star_arg": false 43 | }, 44 | { 45 | "name": "default_testonly", 46 | "type": "boolean", 47 | "doc": "Sets the default `testonly` property for all rules in this package.\n\nIn packages under `javatests` the default value is `True`.", 48 | "default_value": "False", 49 | "is_mandatory": false, 50 | "is_star_arg": false, 51 | "is_star_star_arg": false 52 | }, 53 | { 54 | "name": "features", 55 | "type": "List of strings", 56 | "doc": "Sets various flags that affect the semantics of this BUILD file.\n\nThis feature is mainly used by the people working on the build system to tag packages that need some kind of special handling. Do not use this unless explicitly requested by someone working on the build system.", 57 | "default_value": "[]", 58 | "is_mandatory": false, 59 | "is_star_arg": false, 60 | "is_star_star_arg": false 61 | } 62 | ], 63 | "return_type": "None" 64 | } 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /crates/starpls_bazel/data/builtin.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Bazel Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // Proto that exposes all BUILD and Starlark builtin symbols. 16 | // 17 | // The API exporter is used for code completion in Cider. 18 | 19 | syntax = "proto3"; 20 | package builtin; 21 | 22 | // option java_api_version = 2; 23 | option java_package = "com.google.devtools.build.docgen.builtin"; 24 | option java_outer_classname = "BuiltinProtos"; 25 | 26 | // Top-level object for all BUILD and Starlark builtin modules. 27 | // Globals contains a list of all builtin variables, functions and packages 28 | // (e.g. "java_common" and "native" will be included, same as "None" and 29 | // "dict"). 30 | // Types contains a list of all builtin packages (e.g. "java_common" 31 | // and "native"). All types should be uniquely named. 32 | message Builtins { 33 | repeated Type type = 1; 34 | 35 | repeated Value global = 2; 36 | } 37 | 38 | // Representation for Starlark builtin packages. It contains all the symbols 39 | // (variables and functions) exposed by the package. 40 | // E.g. "list" is a Type that exposes a list of fields containing: "insert", 41 | // "index", "remove" etc. 42 | message Type { 43 | string name = 1; 44 | 45 | // List of fields and methods of this type. All such entities are listed as 46 | // fields, and methods are fields which are callable. 47 | repeated Value field = 2; 48 | 49 | // Module documentation. 50 | string doc = 3; 51 | } 52 | 53 | // ApiContext specifies the context(s) in which a symbol is available. For 54 | // example, a symbol may be available as a builtin only in .bzl files, but 55 | // not in BUILD files. 56 | enum ApiContext { 57 | ALL = 0; 58 | BZL = 1; 59 | BUILD = 2; 60 | } 61 | 62 | // Generic representation for a Starlark object. If the object is callable 63 | // (can act as a function), then callable will be set. 64 | message Value { 65 | string name = 1; 66 | 67 | // Name of the type. 68 | string type = 2; 69 | 70 | // Set when the object is a function. 71 | Callable callable = 3; 72 | 73 | // Value documentation. 74 | string doc = 4; 75 | 76 | // The context(s) in which the symbol is recognized. 77 | ApiContext api_context = 5; 78 | } 79 | 80 | message Callable { 81 | repeated Param param = 1; 82 | 83 | // Name of the return type. 84 | string return_type = 2; 85 | } 86 | 87 | message Param { 88 | string name = 1; 89 | 90 | // Parameter type represented as a name. 91 | string type = 2; 92 | 93 | // Parameter documentation. 94 | string doc = 3; 95 | 96 | // Default value for the parameter, written as Starlark expression (e.g. 97 | // "False", "True", "[]", "None") 98 | string default_value = 4; 99 | 100 | // Whether the param is mandatory or optional. 101 | bool is_mandatory = 5; 102 | 103 | // Whether the param is a star argument. 104 | bool is_star_arg = 6; 105 | 106 | // Whether the param is a star-star argument. 107 | bool is_star_star_arg = 7; 108 | } 109 | -------------------------------------------------------------------------------- /crates/starpls_bazel/data/cquery.builtins.json: -------------------------------------------------------------------------------- 1 | { 2 | "builtins": [ 3 | { 4 | "name": "build_options", 5 | "doc": "`build_options(target)` returns a map whose keys are build option identifiers (see Configurations) and whose values are their Starlark values. Build options whose values are not legal Starlark values are omitted from this map.\n\nIf the target is an input file, `build_options(target)` returns None, as input file targets have a null configuration.", 6 | "callable": { 7 | "params": [ 8 | { 9 | "name": "target", 10 | "type": "Target", 11 | "doc": "", 12 | "default_value": "", 13 | "is_mandatory": true, 14 | "is_star_arg": false, 15 | "is_star_star_arg": false 16 | } 17 | ], 18 | "return_type": "dict" 19 | } 20 | }, 21 | { 22 | "name": "providers", 23 | "doc": "`providers(target)` returns a map whose keys are names of providers (for example, `\"DefaultInfo\"`) and whose values are their Starlark values. Providers whose values are not legal Starlark values are omitted from this map.", 24 | "callable": { 25 | "params": [ 26 | { 27 | "name": "target", 28 | "type": "Target", 29 | "doc": "", 30 | "default_value": "", 31 | "is_mandatory": true, 32 | "is_star_arg": false, 33 | "is_star_star_arg": false 34 | } 35 | ], 36 | "return_type": "dict" 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /crates/starpls_bazel/data/repo.builtins.json: -------------------------------------------------------------------------------- 1 | { 2 | "builtins": [ 3 | { 4 | "name": "ignore_directories", 5 | "doc": "The list of directories to ignore in this repository.\n\nThis function takes a list of strings and a directory is ignored if any of the given strings matches its repository-relative path according to the semantics of the `glob()` function. This function can be used to ignore directories that are implementation details of source control systems, output files of other build systems, etc.", 6 | "callable": { 7 | "params": [ 8 | { 9 | "name": "dirs", 10 | "type": "sequence of strings", 11 | "doc": "", 12 | "default_value": "", 13 | "is_mandatory": true, 14 | "is_star_arg": false, 15 | "is_star_star_arg": false 16 | } 17 | ], 18 | "return_type": "None" 19 | } 20 | }, 21 | { 22 | "name": "repo", 23 | "doc": "Declares metadata that applies to every rule in the repository. It must be called at most once per REPO.bazel file. If called, it must be the first call in the REPO.bazel file.", 24 | "callable": { 25 | "params": [ 26 | { 27 | "name": "default_applicable_licenses", 28 | "type": "List of Labels", 29 | "doc": "", 30 | "default_value": "[]", 31 | "is_mandatory": false, 32 | "is_star_arg": false, 33 | "is_star_star_arg": false 34 | }, 35 | { 36 | "name": "default_visibility", 37 | "type": "List of Labels", 38 | "doc": "", 39 | "default_value": "[]", 40 | "is_mandatory": false, 41 | "is_star_arg": false, 42 | "is_star_star_arg": false 43 | }, 44 | { 45 | "name": "default_deprecation", 46 | "type": "string", 47 | "doc": "", 48 | "default_value": "\"\"", 49 | "is_mandatory": false, 50 | "is_star_arg": false, 51 | "is_star_star_arg": false 52 | }, 53 | { 54 | "name": "default_package_metadata", 55 | "type": "List of Labels", 56 | "doc": "", 57 | "default_value": "", 58 | "is_mandatory": false, 59 | "is_star_arg": false, 60 | "is_star_star_arg": false 61 | }, 62 | { 63 | "name": "default_testonly", 64 | "type": "boolean", 65 | "doc": "", 66 | "default_value": "False", 67 | "is_mandatory": false, 68 | "is_star_arg": false, 69 | "is_star_star_arg": false 70 | }, 71 | { 72 | "name": "features", 73 | "type": "List of strings", 74 | "doc": "", 75 | "default_value": "[]", 76 | "is_mandatory": false, 77 | "is_star_arg": false, 78 | "is_star_star_arg": false 79 | } 80 | ], 81 | "return_type": "None" 82 | } 83 | } 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /crates/starpls_bazel/data/vendor.builtins.json: -------------------------------------------------------------------------------- 1 | { 2 | "builtins": [ 3 | { 4 | "name": "ignore", 5 | "doc": "Ignore this repo from vendoring. Bazel will never vendor it or use the corresponding directory (if exists) while building in vendor mode.", 6 | "callable": { 7 | "params": [ 8 | { 9 | "name": "args", 10 | "type": "Unknown", 11 | "doc": "The canonical repo names of the repos to ignore.", 12 | "default_value": "", 13 | "is_mandatory": false, 14 | "is_star_arg": true, 15 | "is_star_star_arg": false 16 | } 17 | ], 18 | "return_type": "None" 19 | } 20 | }, 21 | { 22 | "name": "pin", 23 | "doc": "Pin the contents of this repo under the vendor directory. Bazel will not update this repo while vendoring, and will use the vendored source as if there is a --override_repository flag when building in vendor mode", 24 | "callable": { 25 | "params": [ 26 | { 27 | "name": "args", 28 | "type": "Unknown", 29 | "doc": "The canonical repo names of the repos to pin.", 30 | "default_value": "", 31 | "is_mandatory": false, 32 | "is_star_arg": true, 33 | "is_star_star_arg": false 34 | } 35 | ], 36 | "return_type": "None" 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /crates/starpls_bazel/data/workspace.builtins.json: -------------------------------------------------------------------------------- 1 | { 2 | "builtins": [ 3 | { 4 | "name": "bind", 5 | "doc": "DEPRECATED: see Consider removing bind for a long discussion of its issues and alternatives. `bind()` is not be available in Bzlmod.\nGives a target an alias in the `//external` package.", 6 | "callable": { 7 | "params": [ 8 | { 9 | "name": "name", 10 | "type": "string", 11 | "doc": "The label under `//external` to serve as the alias name", 12 | "default_value": "", 13 | "is_mandatory": true, 14 | "is_star_arg": false, 15 | "is_star_star_arg": false 16 | }, 17 | { 18 | "name": "actual", 19 | "type": "string; or None", 20 | "doc": "The real label to be aliased", 21 | "default_value": "None", 22 | "is_mandatory": false, 23 | "is_star_arg": false, 24 | "is_star_star_arg": false 25 | } 26 | ], 27 | "return_type": "None" 28 | } 29 | }, 30 | { 31 | "name": "register_execution_platforms", 32 | "doc": "Register an already-defined platform so that Bazel can use it as an execution platform during toolchain resolution.", 33 | "callable": { 34 | "params": [ 35 | { 36 | "name": "platform_labels", 37 | "type": "sequence of Labels", 38 | "doc": "The labels of the platforms to register.", 39 | "default_value": "", 40 | "is_mandatory": false, 41 | "is_star_arg": true, 42 | "is_star_star_arg": false 43 | } 44 | ], 45 | "return_type": "None" 46 | } 47 | }, 48 | { 49 | "name": "register_toolchains", 50 | "doc": "Register an already-defined toolchain so that Bazel can use it during toolchain resolution. See examples of defining and registering toolchains.", 51 | "callable": { 52 | "params": [ 53 | { 54 | "name": "toolchain_labels", 55 | "type": "sequence of Labels", 56 | "doc": "The labels of the toolchains to register. Labels can include `:all`, in which case, all toolchain-providing targets in the package will be registered in lexicographical order by name.", 57 | "default_value": "", 58 | "is_mandatory": false, 59 | "is_star_arg": true, 60 | "is_star_star_arg": false 61 | } 62 | ], 63 | "return_type": "None" 64 | } 65 | }, 66 | { 67 | "name": "workspace", 68 | "doc": "This function can only be used in a `WORKSPACE` file and must be declared before all other functions in the `WORKSPACE` file. Each `WORKSPACE` file should have a `workspace` function.\n\nSets the name for this workspace. Workspace names should be a Java-package-style description of the project, using underscores as separators, e.g., github.com/bazelbuild/bazel should use com_github_bazelbuild_bazel.\n\nThis name is used for the directory that the repository's runfiles are stored in. For example, if there is a runfile `foo/bar` in the local repository and the WORKSPACE file contains `workspace(name = 'baz')`, then the runfile will be available under `mytarget.runfiles/baz/foo/bar`. If no workspace name is specified, then the runfile will be symlinked to `bar.runfiles/foo/bar.`\n\nRemote repository rule names must be valid workspace names. For example, you could have `maven_jar(name = 'foo')`, but not `maven_jar(name = 'foo%bar')`, as Bazel would attempt to write a WORKSPACE file for the `maven_jar` containing `workspace(name = 'foo%bar')`.", 69 | "callable": { 70 | "params": [ 71 | { 72 | "name": "name", 73 | "type": "string", 74 | "doc": "the name of the workspace. Names must start with a letter and can only contain letters, numbers, underscores, dashes, and dots.", 75 | "default_value": "", 76 | "is_mandatory": true, 77 | "is_star_arg": false, 78 | "is_star_star_arg": false 79 | } 80 | ], 81 | "return_type": "None" 82 | } 83 | } 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /crates/starpls_bazel/src/attr.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | #[derive(Clone, Debug, Serialize, Deserialize)] 5 | pub struct CommonAttributes { 6 | pub build: Vec, 7 | pub repository: Vec, 8 | } 9 | 10 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 11 | pub enum AttributeKind { 12 | #[serde(rename = "boolean")] 13 | Bool, 14 | #[serde(rename = "int")] 15 | Int, 16 | #[serde(rename = "List of ints")] 17 | IntList, 18 | #[serde(rename = "Label")] 19 | Label, 20 | // TODO(withered-magic): Add a `rename`. 21 | LabelKeyedStringDict, 22 | #[serde(rename = "List of Labels")] 23 | LabelList, 24 | // TODO(withered-magic): Add a `rename`. 25 | Output, 26 | #[serde(rename = "List of Outputs")] 27 | OutputList, 28 | #[serde(rename = "string")] 29 | String, 30 | #[serde(rename = "List of strings")] 31 | StringList, 32 | #[serde(rename = "Dictionary of strings")] 33 | StringDict, 34 | // TODO(withered-magic): Add a `rename`. 35 | StringKeyedLabelDict, 36 | // TODO(withered-magic): Add a `rename`. 37 | StringListDict, 38 | } 39 | 40 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 41 | pub struct Attribute { 42 | pub name: String, 43 | pub r#type: AttributeKind, 44 | pub doc: String, 45 | pub default_value: String, 46 | pub is_mandatory: bool, 47 | } 48 | 49 | pub fn make_common_attributes() -> CommonAttributes { 50 | serde_json::from_str(include_str!("../data/commonAttributes.json")) 51 | .expect("bug: invalid commonAttributes.json") 52 | } 53 | -------------------------------------------------------------------------------- /crates/starpls_bazel/src/bin/main.rs: -------------------------------------------------------------------------------- 1 | fn main() -> anyhow::Result<()> { 2 | Ok(()) 3 | } 4 | -------------------------------------------------------------------------------- /crates/starpls_bazel/src/build_language.rs: -------------------------------------------------------------------------------- 1 | use prost::Message; 2 | 3 | use crate::build::attribute::Discriminator; 4 | use crate::build::BuildLanguage; 5 | use crate::build::RuleDefinition; 6 | use crate::builtin::Callable; 7 | use crate::builtin::Param; 8 | use crate::builtin::Value; 9 | use crate::Builtins; 10 | 11 | pub fn decode_rules(build_language_output: &[u8]) -> anyhow::Result { 12 | let build_language = BuildLanguage::decode(build_language_output)?; 13 | Ok(Builtins { 14 | global: build_language 15 | .rule 16 | .into_iter() 17 | .map( 18 | |RuleDefinition { 19 | name, 20 | documentation, 21 | attribute, 22 | .. 23 | }| { 24 | Value { 25 | name, 26 | doc: documentation.unwrap_or_else(String::new), 27 | callable: Some(Callable { 28 | param: attribute 29 | .into_iter() 30 | .filter(|attr| !attr.name.starts_with(['$', ':'])) 31 | .map(|attr| { 32 | let doc = attr.documentation().to_string(); 33 | let r#type = 34 | attribute_type_string_from_discriminator(attr.r#type()); 35 | Param { 36 | name: attr.name, 37 | r#type, 38 | doc, 39 | is_mandatory: false, 40 | ..Default::default() 41 | } 42 | }) 43 | .collect(), 44 | return_type: "None".to_string(), 45 | }), 46 | ..Default::default() 47 | } 48 | }, 49 | ) 50 | .collect(), 51 | ..Default::default() 52 | }) 53 | } 54 | 55 | pub fn attribute_type_string_from_discriminator(value: Discriminator) -> String { 56 | use Discriminator::*; 57 | 58 | match value { 59 | Integer | Tristate => "int", 60 | String | License => "string", 61 | Label => "Label", 62 | StringList | DistributionSet => "List of strings", 63 | LabelList => "List of Labels", 64 | Boolean => "boolean", 65 | IntegerList => "List of ints", 66 | LabelListDict => "Dict of Labels", 67 | StringDict => "Dict of strings", 68 | // TODO(withered-magic): Handle StringListDict. 69 | StringListDict => "Unknown", 70 | // TODO(withered-magic): Handle LabelKeyedStringDict. 71 | LabelKeyedStringDict => "Unknown", 72 | _ => "Unknown", 73 | } 74 | .to_string() 75 | } 76 | -------------------------------------------------------------------------------- /crates/starpls_bazel/src/env.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | 6 | use crate::builtin::Callable; 7 | use crate::builtin::Param; 8 | use crate::builtin::Value; 9 | use crate::Builtins; 10 | 11 | #[derive(Debug, Serialize, Deserialize)] 12 | struct BuiltinsJson { 13 | builtins: Vec, 14 | } 15 | 16 | impl From for Builtins { 17 | fn from(value: BuiltinsJson) -> Self { 18 | Self { 19 | global: value 20 | .builtins 21 | .into_iter() 22 | .map(|value| value.into()) 23 | .collect(), 24 | ..Default::default() 25 | } 26 | } 27 | } 28 | 29 | #[derive(Debug, Serialize, Deserialize)] 30 | struct ValueJson { 31 | name: String, 32 | doc: String, 33 | callable: Option, 34 | } 35 | 36 | impl From for Value { 37 | fn from(val: ValueJson) -> Self { 38 | Value { 39 | name: val.name, 40 | doc: val.doc, 41 | callable: val.callable.map(|callable| Callable { 42 | param: callable 43 | .params 44 | .into_iter() 45 | .map(|param| Param { 46 | name: param.name, 47 | r#type: param.r#type, 48 | doc: param.doc, 49 | default_value: param.default_value, 50 | is_mandatory: param.is_mandatory, 51 | is_star_arg: param.is_star_arg, 52 | is_star_star_arg: param.is_star_star_arg, 53 | }) 54 | .collect(), 55 | return_type: callable.return_type, 56 | }), 57 | ..Default::default() 58 | } 59 | } 60 | } 61 | 62 | #[derive(Debug, Serialize, Deserialize)] 63 | struct CallableJson { 64 | params: Vec, 65 | return_type: String, 66 | } 67 | 68 | #[derive(Debug, Serialize, Deserialize)] 69 | struct ParamJson { 70 | name: String, 71 | r#type: String, 72 | doc: String, 73 | default_value: String, 74 | is_mandatory: bool, 75 | is_star_arg: bool, 76 | is_star_star_arg: bool, 77 | } 78 | 79 | /// The builtin.pb file is missing `module_extension`, `repository_rule` and `tag_class`. 80 | pub fn make_bzl_builtins() -> Builtins { 81 | serde_json::from_str::(include_str!("../data/bzl.builtins.json")) 82 | .expect("bug: invalid bzl.builtins.json") 83 | .into() 84 | } 85 | 86 | pub fn make_build_builtins() -> Builtins { 87 | serde_json::from_str::(include_str!("../data/build.builtins.json")) 88 | .expect("bug: invalid build.builtins.json") 89 | .into() 90 | } 91 | 92 | pub fn make_module_bazel_builtins() -> Builtins { 93 | serde_json::from_str::(include_str!("../data/module-bazel.builtins.json")) 94 | .expect("bug: invalid module-bazel.builtins.json") 95 | .into() 96 | } 97 | 98 | pub fn make_workspace_builtins() -> Builtins { 99 | serde_json::from_str::(include_str!("../data/workspace.builtins.json")) 100 | .expect("bug: invalid workspace.builtins.json") 101 | .into() 102 | } 103 | 104 | pub fn make_repo_builtins() -> Builtins { 105 | serde_json::from_str::(include_str!("../data/repo.builtins.json")) 106 | .expect("bug: invalid repo.builtins.json") 107 | .into() 108 | } 109 | 110 | pub fn make_cquery_builtins() -> Builtins { 111 | serde_json::from_str::(include_str!("../data/cquery.builtins.json")) 112 | .expect("bug: invalid cquery.builtins.json") 113 | .into() 114 | } 115 | 116 | pub fn make_vendor_builtins() -> Builtins { 117 | serde_json::from_str::(include_str!("../data/vendor.builtins.json")) 118 | .expect("bug: invalid vendor.builtins.json") 119 | .into() 120 | } 121 | 122 | pub fn make_missing_module_members() -> HashMap> { 123 | serde_json::from_str::>>(include_str!( 124 | "../data/missingModuleFields.json" 125 | )) 126 | .expect("bug: invalid missingModuleFields.json") 127 | .into_iter() 128 | .map(|(name, fields)| (name, fields.into_iter().map(|field| field.into()).collect())) 129 | .collect() 130 | } 131 | -------------------------------------------------------------------------------- /crates/starpls_bazel/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::Path; 4 | use std::path::PathBuf; 5 | 6 | use prost::Message; 7 | 8 | pub use crate::builtin::Builtins; 9 | pub use crate::label::Label; 10 | pub use crate::label::ParseError; 11 | 12 | pub mod attr; 13 | pub mod build_language; 14 | pub mod client; 15 | pub mod env; 16 | pub mod label; 17 | 18 | #[cfg(bazel)] 19 | pub mod builtin { 20 | pub use builtin_proto::builtin::*; 21 | } 22 | 23 | #[cfg(not(bazel))] 24 | pub mod builtin { 25 | include!(concat!(env!("OUT_DIR"), "/builtin.rs")); 26 | } 27 | 28 | #[cfg(bazel)] 29 | pub mod build { 30 | pub use build_proto::blaze_query::*; 31 | } 32 | 33 | #[cfg(not(bazel))] 34 | pub mod build { 35 | include!(concat!(env!("OUT_DIR"), "/blaze_query.rs")); 36 | } 37 | 38 | pub const BUILTINS_TYPES_DENY_LIST: &[&str] = &[ 39 | "Attribute", 40 | "bool", 41 | "bytes", 42 | "builtin_function_or_method", 43 | "dict", 44 | "float", 45 | "function", 46 | "int", 47 | "list", 48 | "range", 49 | "string", 50 | "struct", 51 | "Target", 52 | "tuple", 53 | "None", 54 | "NoneType", 55 | ]; 56 | 57 | pub const BUILTINS_VALUES_DENY_LIST: &[&str] = &[ 58 | "False", 59 | "True", 60 | "None", 61 | "abs", 62 | "all", 63 | "any", 64 | "bool", 65 | "dict", 66 | "dir", 67 | "enumerate", 68 | "fail", 69 | "float", 70 | "getattr", 71 | "hasattr", 72 | "hash", 73 | "int", 74 | "len", 75 | "list", 76 | "max", 77 | "min", 78 | "print", 79 | "range", 80 | "repr", 81 | "reversed", 82 | "sorted", 83 | "str", 84 | "tuple", 85 | "type", 86 | "zip", 87 | ]; 88 | 89 | pub const KNOWN_PROVIDER_TYPES: &[&str] = &[ 90 | "AnalysisTestResultInfo", 91 | "AndroidNeverLinkLibrariesProvider", 92 | "ApkInfo", 93 | "BaselineProfileProvider", 94 | "CcInfo", 95 | "CcToolchainConfigInfo", 96 | "CcToolchainInfo", 97 | "ConstraintSettingInfo", 98 | "ConstraintValueInfo", 99 | "DebugPackageInfo", 100 | "DefaultInfo", 101 | "ExecutionInfo", 102 | "FeatureFlagInfo", 103 | "FilesToRunProvider", 104 | "GeneratedExtensionRegistryProvider", 105 | "IncompatiblePlatformProvider", 106 | "InstrumentedFilesInfo", 107 | "java_compilation_info", 108 | "java_output_jars", 109 | "JavaRuntimeInfo", 110 | "JavaToolchainInfo", 111 | "ObjcProvider", 112 | "OutputGroupInfo", 113 | "PackageSpecificationInfo", 114 | "PlatformInfo", 115 | "ProguardSpecProvider", 116 | "ProtoRegistryProvider", 117 | "PyInfo", 118 | "PyRuntimeInfo", 119 | "RunEnvironmentInfo", 120 | "TemplateVariableInfo", 121 | "ToolchainInfo", 122 | "ToolchainTypeInfo", 123 | ]; 124 | 125 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 126 | pub enum APIContext { 127 | Bzl, 128 | Build, 129 | Module, 130 | Repo, 131 | Workspace, 132 | Prelude, 133 | Cquery, 134 | Vendor, 135 | } 136 | 137 | pub fn load_builtins(path: impl AsRef) -> anyhow::Result { 138 | let data = fs::read(path)?; 139 | decode_builtins(&data) 140 | } 141 | 142 | pub fn decode_builtins(data: &[u8]) -> anyhow::Result { 143 | let builtins = Builtins::decode(data)?; 144 | Ok(builtins) 145 | } 146 | 147 | pub fn resolve_workspace(from: impl AsRef) -> io::Result> { 148 | let mut package: Option = None; 149 | for ancestor in from 150 | .as_ref() 151 | .ancestors() 152 | .filter(|ancestor| ancestor.is_dir()) 153 | { 154 | for entry in fs::read_dir(ancestor)? { 155 | match entry 156 | .ok() 157 | .map(|entry| entry.file_name()) 158 | .as_ref() 159 | .and_then(|file_name| file_name.to_str()) 160 | { 161 | Some("WORKSPACE" | "WORKSPACE.bazel" | "MODULE.bazel" | "REPO.bazel") => { 162 | return Ok(Some(( 163 | ancestor.to_path_buf(), 164 | package.unwrap_or_else(|| ancestor.to_path_buf()), 165 | ))); 166 | } 167 | Some("BUILD" | "BUILD.bazel") => { 168 | package.get_or_insert(ancestor.to_path_buf()); 169 | } 170 | _ => {} 171 | } 172 | } 173 | } 174 | 175 | Ok(None) 176 | } 177 | -------------------------------------------------------------------------------- /crates/starpls_common/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@crates//:defs.bzl", "all_crate_deps") 2 | load("@rules_rust//rust:defs.bzl", "rust_library") 3 | 4 | package(default_visibility = ["//visibility:public"]) 5 | 6 | rust_library( 7 | name = "starpls_common", 8 | srcs = glob(["src/**/*.rs"]), 9 | aliases = { 10 | "@crates//:salsa": "salsa", 11 | }, 12 | deps = all_crate_deps() + [ 13 | "//crates/starpls_bazel", 14 | "//crates/starpls_syntax", 15 | ], 16 | ) 17 | -------------------------------------------------------------------------------- /crates/starpls_common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starpls_common" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.81" 10 | salsa = { git = "https://github.com/withered-magic/salsa", package = "salsa-2022", rev = "91fdda90b344ef74e9bf35c3a5bb0fbae22ed6fb" } 11 | starpls_bazel = { path = "../starpls_bazel" } 12 | starpls_syntax = { path = "../starpls_syntax" } 13 | -------------------------------------------------------------------------------- /crates/starpls_common/src/diagnostics.rs: -------------------------------------------------------------------------------- 1 | use starpls_syntax::TextRange; 2 | 3 | use crate::FileId; 4 | 5 | /// An IDE diagnostic. This is the common data structure used to report errors to the user. 6 | #[derive(Clone, Debug, PartialEq, Eq)] 7 | pub struct Diagnostic { 8 | pub message: String, 9 | pub severity: Severity, 10 | pub range: FileRange, 11 | pub tags: Option>, 12 | } 13 | 14 | #[derive(Clone, Debug, PartialEq, Eq)] 15 | pub struct FileRange { 16 | pub file_id: FileId, 17 | pub range: TextRange, 18 | } 19 | 20 | /// A severity level for diagnostic messages. 21 | #[derive(Clone, Debug, PartialEq, Eq)] 22 | pub enum Severity { 23 | Warning, 24 | Error, 25 | } 26 | 27 | #[derive(Clone, Debug, PartialEq, Eq)] 28 | pub enum DiagnosticTag { 29 | Unnecessary, 30 | Deprecated, 31 | } 32 | 33 | #[salsa::accumulator] 34 | pub struct Diagnostics(Diagnostic); 35 | -------------------------------------------------------------------------------- /crates/starpls_common/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::path::PathBuf; 3 | 4 | use starpls_bazel::APIContext; 5 | use starpls_syntax::line_index as syntax_line_index; 6 | use starpls_syntax::parse_module; 7 | use starpls_syntax::LineIndex; 8 | use starpls_syntax::Module; 9 | use starpls_syntax::ParseTree; 10 | use starpls_syntax::SyntaxNode; 11 | 12 | pub use crate::diagnostics::Diagnostic; 13 | pub use crate::diagnostics::DiagnosticTag; 14 | pub use crate::diagnostics::Diagnostics; 15 | pub use crate::diagnostics::FileRange; 16 | pub use crate::diagnostics::Severity; 17 | 18 | mod diagnostics; 19 | mod util; 20 | 21 | #[salsa::jar(db = Db)] 22 | pub struct Jar( 23 | Diagnostics, 24 | File, 25 | LineIndexResult, 26 | Parse, 27 | parse, 28 | line_index_query, 29 | ); 30 | 31 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 32 | pub enum Dialect { 33 | Standard, 34 | Bazel, 35 | } 36 | 37 | #[derive(Clone, Debug, PartialEq, Eq)] 38 | pub enum LoadItemCandidateKind { 39 | Directory, 40 | File, 41 | } 42 | 43 | #[derive(Clone, Debug, PartialEq, Eq)] 44 | pub struct LoadItemCandidate { 45 | pub kind: LoadItemCandidateKind, 46 | pub path: String, 47 | pub replace_trailing_slash: bool, 48 | } 49 | 50 | /// A Key corresponding to an interned file path. Use these instead of `Path`s to refer to files. 51 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 52 | pub struct FileId(pub u32); 53 | 54 | pub enum ResolvedPath { 55 | Source { 56 | path: PathBuf, 57 | }, 58 | BuildTarget { 59 | build_file: FileId, 60 | target: String, 61 | contents: Option, 62 | }, 63 | } 64 | 65 | /// The base Salsa database. Supports file-related operations, like getting/setting file contents. 66 | pub trait Db: salsa::DbWithJar { 67 | /// Creates a `File` in the database. This will overwrite the currently active 68 | /// `File` for the given `FileId`, if it exists. 69 | fn create_file( 70 | &mut self, 71 | file_id: FileId, 72 | dialect: Dialect, 73 | info: Option, 74 | contents: String, 75 | ) -> File; 76 | 77 | /// Sets the contents the `File` identified by the given `FileId`. Has no affect 78 | /// if the file doesn't exist. 79 | fn update_file(&mut self, file_id: FileId, contents: String); 80 | 81 | /// Loads a file from the filesystem. 82 | fn load_file(&self, path: &str, dialect: Dialect, from: FileId) 83 | -> anyhow::Result>; 84 | 85 | /// Returns the `File` identified by the given `FileId`. 86 | fn get_file(&self, file_id: FileId) -> Option; 87 | 88 | fn list_load_candidates( 89 | &self, 90 | path: &str, 91 | from: FileId, 92 | ) -> anyhow::Result>>; 93 | 94 | fn resolve_path( 95 | &self, 96 | path: &str, 97 | dialect: Dialect, 98 | from: FileId, 99 | ) -> anyhow::Result>; 100 | 101 | fn resolve_build_file(&self, file_id: FileId) -> Option; 102 | } 103 | 104 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 105 | pub enum FileInfo { 106 | Bazel { 107 | api_context: APIContext, 108 | is_external: bool, 109 | }, 110 | } 111 | 112 | #[salsa::input] 113 | pub struct File { 114 | pub id: FileId, 115 | pub dialect: Dialect, 116 | pub info: Option, 117 | #[return_ref] 118 | pub contents: String, 119 | } 120 | 121 | impl File { 122 | pub fn api_context(&self, db: &dyn Db) -> Option { 123 | self.info(db).map(|data| match data { 124 | FileInfo::Bazel { api_context, .. } => api_context, 125 | }) 126 | } 127 | 128 | pub fn is_external(&self, db: &dyn Db) -> Option { 129 | self.info(db).map(|data| match data { 130 | FileInfo::Bazel { is_external, .. } => is_external, 131 | }) 132 | } 133 | } 134 | 135 | #[salsa::tracked] 136 | pub struct Parse { 137 | pub file: File, 138 | module: ParseTree, 139 | } 140 | 141 | impl Parse { 142 | pub fn tree(&self, db: &dyn Db) -> Module { 143 | self.module(db).tree() 144 | } 145 | 146 | pub fn syntax(&self, db: &dyn Db) -> SyntaxNode { 147 | self.module(db).syntax() 148 | } 149 | } 150 | 151 | #[salsa::tracked] 152 | pub fn parse(db: &dyn Db, file: File) -> Parse { 153 | let parse = parse_module(file.contents(db), &mut |err| { 154 | Diagnostics::push( 155 | db, 156 | Diagnostic { 157 | message: err.message, 158 | range: FileRange { 159 | file_id: file.id(db), 160 | range: err.range, 161 | }, 162 | severity: Severity::Error, 163 | tags: None, 164 | }, 165 | ) 166 | }); 167 | Parse::new(db, file, parse) 168 | } 169 | 170 | #[salsa::tracked] 171 | struct LineIndexResult { 172 | #[return_ref] 173 | pub inner: LineIndex, 174 | } 175 | 176 | #[salsa::tracked] 177 | fn line_index_query(db: &dyn Db, file: File) -> LineIndexResult { 178 | let line_index = syntax_line_index(file.contents(db)); 179 | LineIndexResult::new(db, line_index) 180 | } 181 | 182 | pub fn line_index(db: &dyn Db, file: File) -> &LineIndex { 183 | line_index_query(db, file).inner(db) 184 | } 185 | 186 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 187 | pub struct InFile { 188 | pub file: File, 189 | pub value: T, 190 | } 191 | 192 | impl Copy for InFile {} 193 | -------------------------------------------------------------------------------- /crates/starpls_common/src/util.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/starpls_hir/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@crates//:defs.bzl", "all_crate_deps") 2 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") 3 | 4 | package(default_visibility = ["//visibility:public"]) 5 | 6 | rust_library( 7 | name = "starpls_hir", 8 | srcs = glob(["src/**/*.rs"]), 9 | aliases = { 10 | "@crates//:salsa": "salsa", 11 | }, 12 | deps = all_crate_deps() + [ 13 | "//crates/starpls_bazel", 14 | "//crates/starpls_common", 15 | "//crates/starpls_intern", 16 | "//crates/starpls_syntax", 17 | "//crates/starpls_test_util", 18 | ], 19 | ) 20 | 21 | rust_test( 22 | name = "starpls_hir_test", 23 | aliases = { 24 | "@crates//:salsa": "salsa", 25 | }, 26 | crate = ":starpls_hir", 27 | deps = all_crate_deps(normal_dev = True), 28 | ) 29 | -------------------------------------------------------------------------------- /crates/starpls_hir/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starpls_hir" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | crossbeam = "0.8.3" 10 | dashmap = "5.5.3" 11 | id-arena = "2.2.1" 12 | salsa = { git = "https://github.com/withered-magic/salsa", package = "salsa-2022", rev = "91fdda90b344ef74e9bf35c3a5bb0fbae22ed6fb" } 13 | starpls_bazel = { path = "../starpls_bazel" } 14 | starpls_common = { path = "../starpls_common" } 15 | starpls_intern = { path = "../starpls_intern" } 16 | starpls_syntax = { path = "../starpls_syntax" } 17 | starpls_test_util = { path = "../starpls_test_util" } 18 | rustc-hash = "1.1.0" 19 | parking_lot = "0.12.1" 20 | smol_str = "0.2.0" 21 | smallvec = "1.11.2" 22 | either = "1.10.0" 23 | anyhow = "1.0.81" 24 | 25 | [dev-dependencies] 26 | expect-test = "1.4.1" 27 | itertools = "0.12.1" 28 | -------------------------------------------------------------------------------- /crates/starpls_hir/src/def/codeflow/pretty.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use crate::def::codeflow::CodeFlowGraph; 4 | use crate::def::codeflow::FlowNode; 5 | use crate::def::codeflow::FlowNodeId; 6 | 7 | macro_rules! w { 8 | ($dst:expr, $($arg:tt)*) => { 9 | { let _ = write!($dst, $($arg)*); } 10 | }; 11 | } 12 | 13 | macro_rules! wln { 14 | ($dst:expr) => { 15 | { let _ = writeln!($dst); } 16 | }; 17 | ($dst:expr, $($arg:tt)*) => { 18 | { let _ = writeln!($dst, $($arg)*); } 19 | }; 20 | } 21 | 22 | impl CodeFlowGraph { 23 | pub(crate) fn pretty_print(&self) -> String { 24 | CodeFlowGraphPrettyCtx { 25 | cfg: self, 26 | result: String::new(), 27 | indent: String::new(), 28 | } 29 | .pretty_print() 30 | } 31 | } 32 | 33 | struct CodeFlowGraphPrettyCtx<'a> { 34 | cfg: &'a CodeFlowGraph, 35 | result: String, 36 | indent: String, 37 | } 38 | 39 | impl<'a> CodeFlowGraphPrettyCtx<'a> { 40 | fn pretty_print(mut self) -> String { 41 | wln!(&mut self.result, "def main():"); 42 | self.push_indent_level(); 43 | self.format_flow_nodes(); 44 | self.result 45 | } 46 | 47 | fn format_flow_nodes(&mut self) { 48 | for (id, flow_node) in &self.cfg.flow_nodes { 49 | let formatted_id = self.format_flow_node_id(id); 50 | wln!(&mut self.result, "{}{}: {{", self.indent, formatted_id); 51 | self.push_indent_level(); 52 | wln!(&mut self.result, "{}data: {:?}", self.indent, flow_node); 53 | w!(&mut self.result, "{}antecedents: [", self.indent); 54 | match flow_node { 55 | FlowNode::Assign { antecedent, .. } => { 56 | self.result.push_str(&self.format_flow_node_id(*antecedent)); 57 | } 58 | FlowNode::Branch { antecedents } | FlowNode::Loop { antecedents } => { 59 | for (i, antecedent) in antecedents.iter().enumerate() { 60 | if i > 0 { 61 | self.result.push_str(", "); 62 | } 63 | self.result.push_str(&self.format_flow_node_id(*antecedent)); 64 | } 65 | } 66 | _ => {} 67 | } 68 | 69 | self.result.push_str("]\n"); 70 | self.pop_indent_level(); 71 | wln!(&mut self.result, "{}}}", self.indent); 72 | self.result.push('\n'); 73 | } 74 | } 75 | 76 | fn format_flow_node_id(&self, id: FlowNodeId) -> String { 77 | format!("'bb{}", id.index()) 78 | } 79 | 80 | fn push_indent_level(&mut self) { 81 | self.indent.push_str(" "); 82 | } 83 | 84 | fn pop_indent_level(&mut self) { 85 | for _ in 0..4 { 86 | self.indent.pop(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/starpls_hir/src/def/tests.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use starpls_bazel::env::make_build_builtins; 4 | use starpls_bazel::env::make_bzl_builtins; 5 | use starpls_bazel::APIContext; 6 | use starpls_common::Db as _; 7 | use starpls_common::Dialect; 8 | use starpls_common::FileId; 9 | use starpls_common::FileInfo; 10 | use starpls_test_util::FixtureFile; 11 | 12 | use crate::def::resolver::Resolver; 13 | use crate::test_database::TestDatabase; 14 | use crate::typeck::intrinsics::intrinsic_functions; 15 | use crate::Db as _; 16 | 17 | fn check_scope(fixture: &str, expected: &[&str]) { 18 | check_scope_full(fixture, expected, None) 19 | } 20 | 21 | fn check_scope_full(fixture: &str, expected: &[&str], prelude: Option<&str>) { 22 | let mut test_db: TestDatabase = Default::default(); 23 | let file_id = FileId(0); 24 | let fixture = FixtureFile::parse(fixture); 25 | let file = test_db.create_file( 26 | file_id, 27 | Dialect::Bazel, 28 | Some(FileInfo::Bazel { 29 | api_context: APIContext::Build, 30 | is_external: false, 31 | }), 32 | fixture.contents, 33 | ); 34 | 35 | if let Some(prelude) = prelude { 36 | let prelude_file_id = FileId(1); 37 | test_db.create_file( 38 | prelude_file_id, 39 | Dialect::Bazel, 40 | Some(FileInfo::Bazel { 41 | api_context: APIContext::Prelude, 42 | is_external: false, 43 | }), 44 | prelude.to_string(), 45 | ); 46 | test_db.set_bazel_prelude_file(prelude_file_id); 47 | } 48 | 49 | // Filter out intrinsic function names as well as the hardcoded `BUILD.bazel` and `.bzl` 50 | // builtins, which are always added when `APIContext::Build` is the current API context. 51 | let names_to_filter = intrinsic_functions(&test_db) 52 | .functions(&test_db) 53 | .keys() 54 | .map(|name| name.to_string()) 55 | .chain( 56 | make_bzl_builtins() 57 | .global 58 | .into_iter() 59 | .map(|global| global.name), 60 | ) 61 | .chain( 62 | make_build_builtins() 63 | .global 64 | .into_iter() 65 | .map(|global| global.name), 66 | ) 67 | .collect::>(); 68 | 69 | let resolver = Resolver::new_for_offset(&test_db, file, fixture.cursor_pos.unwrap()); 70 | let names = resolver.names(); 71 | let mut actual = names 72 | .keys() 73 | .filter(|name| !names_to_filter.contains(name.as_str())) 74 | .map(|name| name.as_str()) 75 | .collect::>(); 76 | actual.sort(); 77 | assert_eq!(expected, &actual[..]); 78 | } 79 | 80 | #[test] 81 | fn smoke_test() { 82 | check_scope( 83 | r" 84 | g = 0 85 | def foo(): 86 | x = 1 87 | y = 2 88 | $0 89 | 90 | def bar(): 91 | pass 92 | ", 93 | &["bar", "foo", "g", "x", "y"], 94 | ) 95 | } 96 | 97 | #[test] 98 | fn test_empty_scope() { 99 | check_scope( 100 | r" 101 | $0 102 | ", 103 | &[], 104 | ) 105 | } 106 | 107 | #[test] 108 | fn test_assign() { 109 | check_scope( 110 | r" 111 | a = 0 112 | b, c = 1, 2 113 | d, e = 3, 4 114 | [f, g] = 5, 6 115 | $0 116 | ", 117 | &["a", "b", "c", "d", "e", "f", "g"], 118 | ) 119 | } 120 | 121 | #[test] 122 | fn test_params() { 123 | check_scope( 124 | r" 125 | def foo(x, *args, **kwargs): 126 | print(x) 127 | $0 128 | ", 129 | &["args", "foo", "kwargs", "x"], 130 | ) 131 | } 132 | 133 | #[test] 134 | fn test_loop_variables() { 135 | check_scope( 136 | r" 137 | for x, y in 1, 2, 3: 138 | print(x, y) 139 | $0 140 | ", 141 | &["x", "y"], 142 | ) 143 | } 144 | 145 | #[test] 146 | fn test_lambda() { 147 | check_scope( 148 | r" 149 | a = 1 150 | f = lambda x: x + 1$0 151 | ", 152 | &["a", "x"], 153 | ) 154 | } 155 | 156 | #[test] 157 | fn test_def() { 158 | check_scope( 159 | r"def foo(): 160 | x = 1 161 | $0", 162 | &["foo", "x"], 163 | ) 164 | } 165 | 166 | #[test] 167 | fn test_list_comprehension() { 168 | check_scope( 169 | r" 170 | [x*y$0 for x in range(5) for y in range(5)] 171 | ", 172 | &["x", "y"], 173 | ) 174 | } 175 | 176 | #[test] 177 | fn test_list_comprehension_clause1() { 178 | check_scope( 179 | r" 180 | [x*y for x in range(5) for y in range(5) if x*y$0 > 10] 181 | ", 182 | &["x", "y"], 183 | ) 184 | } 185 | 186 | #[test] 187 | fn test_list_comprehension_clause2() { 188 | check_scope( 189 | r" 190 | [x*y for x in range(5) if x$0yz > 2 for y in range(5) if x*y > 10] 191 | ", 192 | &["x"], 193 | ) 194 | } 195 | 196 | #[test] 197 | fn test_load() { 198 | check_scope( 199 | r#" 200 | load("foo.star", "go_binary") 201 | $0 202 | "#, 203 | &["go_binary"], 204 | ) 205 | } 206 | 207 | #[test] 208 | fn test_param_defaults() { 209 | check_scope( 210 | r#" 211 | _tsc = "" 212 | 213 | def ts_project(tsc = _t$0sc): 214 | pass 215 | "#, 216 | &["_tsc"], 217 | ) 218 | } 219 | 220 | #[test] 221 | fn test_prelude() { 222 | check_scope_full( 223 | r#" 224 | foo = 123 225 | $0 226 | "#, 227 | &["bar", "f", "foo"], 228 | Some( 229 | r#" 230 | bar = "abc" 231 | 232 | def f(): 233 | pass 234 | "#, 235 | ), 236 | ) 237 | } 238 | -------------------------------------------------------------------------------- /crates/starpls_ide/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@crates//:defs.bzl", "all_crate_deps") 2 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") 3 | 4 | package(default_visibility = ["//visibility:public"]) 5 | 6 | rust_library( 7 | name = "starpls_ide", 8 | srcs = glob(["src/**/*.rs"]), 9 | aliases = { 10 | "@crates//:salsa": "salsa", 11 | }, 12 | deps = all_crate_deps() + [ 13 | "//crates/starpls_bazel", 14 | "//crates/starpls_common", 15 | "//crates/starpls_hir", 16 | "//crates/starpls_syntax", 17 | "//crates/starpls_test_util", 18 | ], 19 | ) 20 | 21 | rust_test( 22 | name = "starpls_ide_test", 23 | aliases = { 24 | "@crates//:salsa": "salsa", 25 | }, 26 | crate = ":starpls_ide", 27 | deps = all_crate_deps(normal_dev = True), 28 | ) 29 | -------------------------------------------------------------------------------- /crates/starpls_ide/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starpls_ide" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.81" 10 | dashmap = "5.5.3" 11 | memchr = "2.7.4" 12 | rustc-hash = "1.1.0" 13 | salsa = { git = "https://github.com/withered-magic/salsa", package = "salsa-2022", rev = "91fdda90b344ef74e9bf35c3a5bb0fbae22ed6fb" } 14 | starpls_bazel = { path = "../starpls_bazel" } 15 | starpls_common = { path = "../starpls_common" } 16 | starpls_hir = { path = "../starpls_hir" } 17 | starpls_syntax = { path = "../starpls_syntax" } 18 | starpls_test_util = { path = "../starpls_test_util" } 19 | unindent = "0.2.3" 20 | 21 | [dev-dependencies] 22 | expect-test = "1.5.0" 23 | -------------------------------------------------------------------------------- /crates/starpls_ide/src/diagnostics.rs: -------------------------------------------------------------------------------- 1 | use starpls_common::Db; 2 | use starpls_common::Diagnostic; 3 | use starpls_common::FileId; 4 | use starpls_hir::diagnostics_for_file; 5 | 6 | use crate::Database; 7 | 8 | pub(crate) fn diagnostics(db: &Database, file_id: FileId) -> Vec { 9 | let file = match db.get_file(file_id) { 10 | Some(file) => file, 11 | None => return Vec::new(), 12 | }; 13 | 14 | let diagnostics = db.gcx.with_tcx(db, |tcx| tcx.diagnostics_for_file(file)); 15 | 16 | // Limit the amount of syntax errors we send, as this many syntax errors probably means something 17 | // is really wrong with the file being analyzed. 18 | diagnostics_for_file(db, file) 19 | .take(128) 20 | .chain(diagnostics) 21 | .collect() 22 | } 23 | -------------------------------------------------------------------------------- /crates/starpls_ide/src/find_references.rs: -------------------------------------------------------------------------------- 1 | use memchr::memmem::Finder; 2 | use starpls_common::Db; 3 | use starpls_common::File; 4 | use starpls_hir::Name; 5 | use starpls_hir::ScopeDef; 6 | use starpls_hir::Semantics; 7 | use starpls_syntax::ast::AstNode; 8 | use starpls_syntax::ast::{self}; 9 | use starpls_syntax::match_ast; 10 | use starpls_syntax::TextSize; 11 | use starpls_syntax::T; 12 | 13 | use crate::util::pick_best_token; 14 | use crate::Database; 15 | use crate::FilePosition; 16 | use crate::Location; 17 | 18 | struct FindReferencesHandler<'a> { 19 | sema: &'a Semantics<'a>, 20 | file: File, 21 | name: Name, 22 | defs: Vec, 23 | locations: Vec, 24 | } 25 | 26 | impl<'a> FindReferencesHandler<'a> { 27 | fn handle(mut self) -> Vec { 28 | let name = self.name.clone(); 29 | let finder = Finder::new(name.as_str()); 30 | let offsets = finder 31 | .find_iter(self.file.contents(self.sema.db).as_bytes()) 32 | .map(|index| { 33 | let offset: TextSize = index.try_into().unwrap(); 34 | offset 35 | }); 36 | 37 | for offset in offsets { 38 | let Some(parent) = self 39 | .sema 40 | .parse(self.file) 41 | .syntax(self.sema.db) 42 | .token_at_offset(offset) 43 | .find(|token| token.text() == self.name.as_str()) 44 | .and_then(|token| token.parent()) 45 | else { 46 | continue; 47 | }; 48 | 49 | match_ast! { 50 | match parent { 51 | ast::Name(name) => self.check_matches_name(name), 52 | ast::NameRef(name_ref) => self.check_matches_name_ref(name_ref), 53 | _ => () 54 | } 55 | }; 56 | } 57 | 58 | self.locations 59 | } 60 | 61 | fn check_matches_name(&mut self, node: ast::Name) { 62 | let Some(callable) = node 63 | .syntax() 64 | .parent() 65 | .and_then(ast::DefStmt::cast) 66 | .and_then(|def_stmt| self.sema.resolve_def_stmt(self.file, &def_stmt)) 67 | else { 68 | return; 69 | }; 70 | if self.defs.contains(&ScopeDef::Callable(callable)) { 71 | self.locations.push(Location { 72 | file_id: self.file.id(self.sema.db), 73 | range: node.syntax().text_range(), 74 | }); 75 | } 76 | } 77 | 78 | fn check_matches_name_ref(&mut self, node: ast::NameRef) { 79 | let Some(scope) = ast::Expression::cast(node.syntax().clone()) 80 | .and_then(|expr| self.sema.scope_for_expr(self.file, &expr)) 81 | else { 82 | return; 83 | }; 84 | for def in scope.resolve_name(&self.name).into_iter() { 85 | if self.defs.contains(&def) { 86 | self.locations.push(Location { 87 | file_id: self.file.id(self.sema.db), 88 | range: node.syntax().text_range(), 89 | }); 90 | 91 | // Add the current location at most once. 92 | break; 93 | } 94 | } 95 | } 96 | } 97 | 98 | pub(crate) fn find_references( 99 | db: &Database, 100 | FilePosition { file_id, pos }: FilePosition, 101 | ) -> Option> { 102 | let sema = Semantics::new(db); 103 | let file = db.get_file(file_id)?; 104 | let parse = sema.parse(file); 105 | let token = pick_best_token(parse.syntax(db).token_at_offset(pos), |kind| match kind { 106 | T![ident] => 2, 107 | T!['('] | T![')'] | T!['['] | T![']'] | T!['{'] | T!['}'] => 0, 108 | kind if kind.is_trivia_token() => 0, 109 | _ => 1, 110 | })?; 111 | let node = token.parent()?; 112 | 113 | let (name, defs) = if let Some(node) = ast::NameRef::cast(node.clone()) { 114 | let name = Name::from_ast_name_ref(node.clone()); 115 | let scope = sema.scope_for_expr(file, &ast::Expression::cast(node.syntax().clone())?)?; 116 | let defs = scope 117 | .resolve_name(&name) 118 | .into_iter() 119 | .flat_map(|def| match &def { 120 | ScopeDef::Variable(_) => Some(def), 121 | ScopeDef::Callable(ref callable) if callable.is_user_defined() => Some(def), 122 | _ => None, 123 | }) 124 | .collect::>(); 125 | 126 | if defs.is_empty() { 127 | return None; 128 | } 129 | 130 | (name, defs) 131 | } else if let Some(node) = ast::Name::cast(node) { 132 | let def_stmt = ast::DefStmt::cast(node.syntax().parent()?)?; 133 | let callable = sema.resolve_def_stmt(file, &def_stmt)?; 134 | ( 135 | Name::from_ast_name(node), 136 | vec![ScopeDef::Callable(callable)], 137 | ) 138 | } else { 139 | return None; 140 | }; 141 | 142 | Some( 143 | FindReferencesHandler { 144 | sema: &sema, 145 | file, 146 | name, 147 | defs, 148 | locations: vec![], 149 | } 150 | .handle(), 151 | ) 152 | } 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | use crate::Analysis; 157 | use crate::FilePosition; 158 | 159 | fn check_find_references(fixture: &str) { 160 | let (analysis, fixture) = Analysis::from_single_file_fixture(fixture); 161 | let references = analysis 162 | .snapshot() 163 | .find_references( 164 | fixture 165 | .cursor_pos 166 | .map(|(file_id, pos)| FilePosition { file_id, pos }) 167 | .unwrap(), 168 | ) 169 | .unwrap() 170 | .unwrap(); 171 | 172 | let mut actual_locations = references 173 | .into_iter() 174 | .map(|location| (location.file_id, location.range)) 175 | .collect::>(); 176 | actual_locations.sort_by_key(|(_, range)| (range.start())); 177 | actual_locations.sort_by_key(|(file_id, _)| *file_id); 178 | 179 | assert_eq!(fixture.selected_ranges, actual_locations); 180 | } 181 | 182 | #[test] 183 | fn test_variable() { 184 | check_find_references( 185 | r#" 186 | abc = 123 187 | #^^ 188 | 189 | a$0bc 190 | #^^ 191 | "#, 192 | ); 193 | } 194 | 195 | #[test] 196 | fn test_variable_with_function_definition() { 197 | check_find_references( 198 | r#" 199 | def foo(): 200 | #^^ 201 | pass 202 | 203 | f$0oo() 204 | #^^ 205 | "#, 206 | ); 207 | } 208 | 209 | #[test] 210 | fn test_function_definition() { 211 | check_find_references( 212 | r#" 213 | def f$0oo(): 214 | #^^ 215 | pass 216 | 217 | foo() 218 | #^^ 219 | "#, 220 | ); 221 | } 222 | 223 | #[test] 224 | fn test_redeclared_variable() { 225 | check_find_references( 226 | r#" 227 | foo = 123 228 | #^^ 229 | foo 230 | #^^ 231 | foo = "abc" 232 | #^^ 233 | f$0oo 234 | #^^ 235 | "#, 236 | ); 237 | } 238 | 239 | #[test] 240 | fn test_redeclared_function() { 241 | check_find_references( 242 | r#" 243 | def foo(): 244 | #^^ 245 | pass 246 | foo 247 | #^^ 248 | foo = "abc" 249 | #^^ 250 | f$0oo 251 | #^^ 252 | "#, 253 | ); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /crates/starpls_ide/src/line_index.rs: -------------------------------------------------------------------------------- 1 | use starpls_common::Db as _; 2 | use starpls_common::FileId; 3 | use starpls_syntax::LineIndex; 4 | 5 | use crate::Database; 6 | 7 | pub(crate) fn line_index(db: &Database, file_id: FileId) -> Option<&LineIndex> { 8 | let file = db.get_file(file_id)?; 9 | Some(starpls_common::line_index(db, file)) 10 | } 11 | -------------------------------------------------------------------------------- /crates/starpls_ide/src/show_hir.rs: -------------------------------------------------------------------------------- 1 | use starpls_common::FileId; 2 | 3 | use crate::Database; 4 | 5 | pub(crate) fn show_hir(_db: &Database, _file_id: FileId) -> Option { 6 | Some("Note: This functionality is now deprecated.".to_string()) 7 | } 8 | -------------------------------------------------------------------------------- /crates/starpls_ide/src/show_syntax_tree.rs: -------------------------------------------------------------------------------- 1 | use starpls_common::parse; 2 | use starpls_common::Db as _; 3 | use starpls_common::FileId; 4 | 5 | use crate::Database; 6 | 7 | pub(crate) fn show_syntax_tree(db: &Database, file_id: FileId) -> Option { 8 | let file = db.get_file(file_id)?; 9 | let parse = parse(db, file); 10 | Some(format!("{:#?}", parse.syntax(db))) 11 | } 12 | -------------------------------------------------------------------------------- /crates/starpls_ide/src/signature_help.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use starpls_common::Db as _; 4 | use starpls_hir::DisplayWithDb; 5 | use starpls_hir::Semantics; 6 | use starpls_syntax::ast::AstNode; 7 | use starpls_syntax::ast::Direction; 8 | use starpls_syntax::ast::{self}; 9 | use starpls_syntax::T; 10 | 11 | use crate::util::pick_best_token; 12 | use crate::util::unindent_doc; 13 | use crate::Database; 14 | use crate::FilePosition; 15 | 16 | const DEFAULT_ACTIVE_PARAMETER_INDEX: usize = 100; 17 | 18 | #[derive(Clone, Debug, PartialEq, Eq)] 19 | pub struct SignatureHelp { 20 | pub signatures: Vec, 21 | } 22 | 23 | #[derive(Clone, Debug, PartialEq, Eq)] 24 | pub struct SignatureInfo { 25 | pub label: String, 26 | pub documentation: Option, 27 | pub parameters: Option>, 28 | pub active_parameter: Option, 29 | } 30 | 31 | #[derive(Clone, Debug, PartialEq, Eq)] 32 | pub struct ParameterInfo { 33 | pub label: String, 34 | pub documentation: Option, 35 | } 36 | 37 | pub(crate) fn signature_help( 38 | db: &Database, 39 | FilePosition { file_id, pos }: FilePosition, 40 | ) -> Option { 41 | let sema = Semantics::new(db); 42 | let file = db.get_file(file_id)?; 43 | let parse = sema.parse(file); 44 | let token = pick_best_token(parse.syntax(db).token_at_offset(pos), |kind| match kind { 45 | // '(', ')', and ',' are typically the main tokens in a call expression that are not part of 46 | // one of the arguments. 47 | T!['('] | T![')'] | T![,] => 0, 48 | kind if kind.is_trivia_token() => 0, 49 | _ => 1, 50 | })?; 51 | 52 | // Find the argument node containing the current token. 53 | let expr = token.parent_ancestors().find_map(ast::CallExpr::cast)?; 54 | let func = sema.resolve_call_expr(file, &expr)?; 55 | let params = func.params(db); 56 | let param_labels: Vec = params 57 | .iter() 58 | .map(|(param, ty)| { 59 | let mut s = String::new(); 60 | if param.is_args_list(db) { 61 | s.push('*'); 62 | } else if param.is_kwargs_dict(db) { 63 | s.push_str("**"); 64 | } 65 | 66 | match param.name(db) { 67 | Some(name) if !name.is_missing() && !name.as_str().is_empty() => { 68 | s.push_str(name.as_str()); 69 | 70 | let ty = if param.is_args_list(db) { 71 | ty.variable_tuple_element_ty() 72 | } else if param.is_kwargs_dict(db) { 73 | ty.dict_value_ty() 74 | } else { 75 | ty.clone().into() 76 | }; 77 | 78 | match ty { 79 | Some(ty) if !ty.is_unknown() => { 80 | let _ = write!(&mut s, ": {}", ty.display(db)); 81 | } 82 | _ => {} 83 | } 84 | 85 | match param.default_value(db) { 86 | Some(default_value) if !default_value.is_empty() => { 87 | s.push_str(" = "); 88 | s.push_str(&default_value); 89 | } 90 | _ => {} 91 | } 92 | } 93 | _ => {} 94 | } 95 | 96 | s 97 | }) 98 | .collect(); 99 | 100 | // Construct the labels for the function signature. 101 | // TODO(withered-magic): Some of this logic is duplicated from the `DisplayWithDb` implementation on `TyKind`. 102 | let mut label = String::new(); 103 | label.push_str("def "); 104 | label.push_str(func.name(db).as_str()); 105 | label.push('('); 106 | 107 | let is_rule_or_tag = func.is_rule() || func.is_tag() || func.is_macro(); 108 | if is_rule_or_tag { 109 | label.push('*'); 110 | } 111 | 112 | for (index, param_label) in param_labels.iter().enumerate() { 113 | if index > 0 || is_rule_or_tag { 114 | label.push_str(", "); 115 | } 116 | label.push_str(param_label); 117 | } 118 | 119 | label.push_str(") -> "); 120 | let _ = write!(&mut label, "{}", func.ret_ty(db).display(db)); 121 | 122 | // Check if token's direct parent is an `Arguments` node. If so, that means we are at a ',', '(', or ')'. 123 | // The active parameter index is equal to the number of commas that we see to the left (including ourselves). 124 | // If the number of commas is greater than the number of arguments in the CallExpr, then 125 | // the active parameter is considered fake. 126 | let active_arg = if ast::Arguments::can_cast(token.parent()?.kind()) { 127 | token 128 | .siblings_with_tokens(Direction::Prev) 129 | .filter_map(|el| el.into_token()) 130 | .filter(|token| token.kind() == T![,]) 131 | .count() 132 | } else { 133 | // Otherwise, check if there is a parent `Argument` node. If so, the active parameter index 134 | // is equal to the number of `Argument`s to the left of us. The active parameter is never fake 135 | // in this scenario. 136 | let arg = token.parent_ancestors().find_map(ast::Argument::cast)?; 137 | arg.syntax() 138 | .siblings(Direction::Prev) 139 | .skip(1) 140 | .filter_map(ast::Argument::cast) 141 | .count() 142 | }; 143 | 144 | let active_parameter = sema 145 | .resolve_call_expr_active_param(file, &expr, active_arg) 146 | .unwrap_or(DEFAULT_ACTIVE_PARAMETER_INDEX); // active_parameter defaults to 0, so we just add a crazy high value here to avoid a false positive 147 | 148 | Some(SignatureHelp { 149 | signatures: vec![SignatureInfo { 150 | label, 151 | documentation: func.doc(db).map(|doc| unindent_doc(&doc)), 152 | parameters: Some( 153 | params 154 | .into_iter() 155 | .zip(param_labels.into_iter()) 156 | .map(|((param, _), label)| ParameterInfo { 157 | label, 158 | documentation: param.doc(db).map(|doc| unindent_doc(&doc)), 159 | }) 160 | .collect(), 161 | ), 162 | active_parameter: Some(active_parameter), 163 | }], 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /crates/starpls_ide/src/util.rs: -------------------------------------------------------------------------------- 1 | use starpls_syntax::SyntaxKind; 2 | use starpls_syntax::SyntaxToken; 3 | use starpls_syntax::TokenAtOffset; 4 | 5 | pub(crate) fn pick_best_token( 6 | tokens: TokenAtOffset, 7 | mut f: impl FnMut(SyntaxKind) -> usize, 8 | ) -> Option { 9 | tokens.max_by_key(|token| f(token.kind())) 10 | } 11 | 12 | // TODO(withered-magic): This logic should probably be more sophisticated, but it works well 13 | // enough for now. 14 | pub(crate) fn unindent_doc(doc: &str) -> String { 15 | let mut is_in_code_block = false; 16 | unindent::unindent(doc) 17 | .lines() 18 | .map(|line| { 19 | let trimmed = line.trim_start(); 20 | let num_trimmed = line.len() - trimmed.len(); 21 | let mut s = String::new(); 22 | 23 | if trimmed.starts_with("```") { 24 | is_in_code_block = !is_in_code_block; 25 | } 26 | 27 | (0..num_trimmed) 28 | .for_each(|_| s.push_str(if is_in_code_block { " " } else { " " })); 29 | s.push_str(trimmed); 30 | s.push_str(" "); 31 | s 32 | }) 33 | .collect::>() 34 | .join("\n") 35 | } 36 | -------------------------------------------------------------------------------- /crates/starpls_intern/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@crates//:defs.bzl", "all_crate_deps") 2 | load("@rules_rust//rust:defs.bzl", "rust_library") 3 | 4 | package(default_visibility = ["//visibility:public"]) 5 | 6 | rust_library( 7 | name = "starpls_intern", 8 | srcs = glob(["src/**/*.rs"]), 9 | deps = all_crate_deps(), 10 | ) 11 | -------------------------------------------------------------------------------- /crates/starpls_intern/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starpls_intern" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | dashmap = { version = "=5.5.3", features = ["raw-api"] } 10 | hashbrown = "0.14.3" 11 | rustc-hash = "1.1.0" 12 | triomphe = "0.1.11" 13 | -------------------------------------------------------------------------------- /crates/starpls_intern/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Taken from https://github.com/rust-lang/rust-analyzer/blob/efc87092b3d9e350f12b87f5e3823bc44561ba9b/crates/intern/src/lib.rs 2 | //! Global `Arc`-based object interning infrastructure. 3 | //! 4 | //! Eventually this should probably be replaced with salsa-based interning. 5 | 6 | use std::fmt::Debug; 7 | use std::fmt::Display; 8 | use std::fmt::{self}; 9 | use std::hash::BuildHasherDefault; 10 | use std::hash::Hash; 11 | use std::hash::Hasher; 12 | use std::ops::Deref; 13 | use std::sync::OnceLock; 14 | 15 | use dashmap::DashMap; 16 | use dashmap::SharedValue; 17 | use hashbrown::hash_map::RawEntryMut; 18 | use hashbrown::HashMap; 19 | use rustc_hash::FxHasher; 20 | use triomphe::Arc; 21 | 22 | type InternMap = DashMap, (), BuildHasherDefault>; 23 | type Guard = dashmap::RwLockWriteGuard< 24 | 'static, 25 | HashMap, SharedValue<()>, BuildHasherDefault>, 26 | >; 27 | 28 | pub struct Interned { 29 | arc: Arc, 30 | } 31 | 32 | impl Interned { 33 | pub fn new(obj: T) -> Self { 34 | let (mut shard, hash) = Self::select(&obj); 35 | // Atomically, 36 | // - check if `obj` is already in the map 37 | // - if so, clone its `Arc` and return it 38 | // - if not, box it up, insert it, and return a clone 39 | // This needs to be atomic (locking the shard) to avoid races with other thread, which could 40 | // insert the same object between us looking it up and inserting it. 41 | match shard.raw_entry_mut().from_key_hashed_nocheck(hash, &obj) { 42 | RawEntryMut::Occupied(occ) => Self { 43 | arc: occ.key().clone(), 44 | }, 45 | RawEntryMut::Vacant(vac) => Self { 46 | arc: vac 47 | .insert_hashed_nocheck(hash, Arc::new(obj), SharedValue::new(())) 48 | .0 49 | .clone(), 50 | }, 51 | } 52 | } 53 | } 54 | 55 | impl Interned { 56 | pub fn new_str(s: &str) -> Self { 57 | let (mut shard, hash) = Self::select(s); 58 | // Atomically, 59 | // - check if `obj` is already in the map 60 | // - if so, clone its `Arc` and return it 61 | // - if not, box it up, insert it, and return a clone 62 | // This needs to be atomic (locking the shard) to avoid races with other thread, which could 63 | // insert the same object between us looking it up and inserting it. 64 | match shard.raw_entry_mut().from_key_hashed_nocheck(hash, s) { 65 | RawEntryMut::Occupied(occ) => Self { 66 | arc: occ.key().clone(), 67 | }, 68 | RawEntryMut::Vacant(vac) => Self { 69 | arc: vac 70 | .insert_hashed_nocheck(hash, Arc::from(s), SharedValue::new(())) 71 | .0 72 | .clone(), 73 | }, 74 | } 75 | } 76 | } 77 | 78 | impl Interned { 79 | #[inline] 80 | fn select(obj: &T) -> (Guard, u64) { 81 | let storage = T::storage().get(); 82 | let hash = { 83 | let mut hasher = std::hash::BuildHasher::build_hasher(storage.hasher()); 84 | obj.hash(&mut hasher); 85 | hasher.finish() 86 | }; 87 | let shard_idx = storage.determine_shard(hash as usize); 88 | let shard = &storage.shards()[shard_idx]; 89 | (shard.write(), hash) 90 | } 91 | } 92 | 93 | impl Drop for Interned { 94 | #[inline] 95 | fn drop(&mut self) { 96 | // When the last `Ref` is dropped, remove the object from the global map. 97 | if Arc::count(&self.arc) == 2 { 98 | // Only `self` and the global map point to the object. 99 | 100 | self.drop_slow(); 101 | } 102 | } 103 | } 104 | 105 | impl Interned { 106 | #[cold] 107 | fn drop_slow(&mut self) { 108 | let (mut shard, hash) = Self::select(&self.arc); 109 | 110 | if Arc::count(&self.arc) != 2 { 111 | // Another thread has interned another copy 112 | return; 113 | } 114 | 115 | match shard 116 | .raw_entry_mut() 117 | .from_key_hashed_nocheck(hash, &self.arc) 118 | { 119 | RawEntryMut::Occupied(occ) => occ.remove(), 120 | RawEntryMut::Vacant(_) => unreachable!(), 121 | }; 122 | 123 | // Shrink the backing storage if the shard is less than 50% occupied. 124 | if shard.len() * 2 < shard.capacity() { 125 | shard.shrink_to_fit(); 126 | } 127 | } 128 | } 129 | 130 | /// Compares interned `Ref`s using pointer equality. 131 | impl PartialEq for Interned { 132 | // NOTE: No `?Sized` because `ptr_eq` doesn't work right with trait objects. 133 | 134 | #[inline] 135 | fn eq(&self, other: &Self) -> bool { 136 | Arc::ptr_eq(&self.arc, &other.arc) 137 | } 138 | } 139 | 140 | impl Eq for Interned {} 141 | 142 | impl PartialEq for Interned { 143 | fn eq(&self, other: &Self) -> bool { 144 | Arc::ptr_eq(&self.arc, &other.arc) 145 | } 146 | } 147 | 148 | impl Eq for Interned {} 149 | 150 | impl Hash for Interned { 151 | fn hash(&self, state: &mut H) { 152 | // NOTE: Cast disposes vtable pointer / slice/str length. 153 | state.write_usize(Arc::as_ptr(&self.arc) as *const () as usize) 154 | } 155 | } 156 | 157 | impl AsRef for Interned { 158 | #[inline] 159 | fn as_ref(&self) -> &T { 160 | &self.arc 161 | } 162 | } 163 | 164 | impl Deref for Interned { 165 | type Target = T; 166 | 167 | #[inline] 168 | fn deref(&self) -> &Self::Target { 169 | &self.arc 170 | } 171 | } 172 | 173 | impl Clone for Interned { 174 | fn clone(&self) -> Self { 175 | Self { 176 | arc: self.arc.clone(), 177 | } 178 | } 179 | } 180 | 181 | impl Debug for Interned { 182 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 183 | (*self.arc).fmt(f) 184 | } 185 | } 186 | 187 | impl Display for Interned { 188 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 189 | (*self.arc).fmt(f) 190 | } 191 | } 192 | 193 | pub struct InternStorage { 194 | map: OnceLock>, 195 | } 196 | 197 | impl InternStorage { 198 | pub const fn new() -> Self { 199 | Self { 200 | map: OnceLock::new(), 201 | } 202 | } 203 | } 204 | 205 | impl InternStorage { 206 | fn get(&self) -> &InternMap { 207 | self.map.get_or_init(DashMap::default) 208 | } 209 | } 210 | 211 | pub trait Internable: Hash + Eq + 'static { 212 | fn storage() -> &'static InternStorage; 213 | } 214 | 215 | /// Implements `Internable` for a given list of types, making them usable with `Interned`. 216 | #[macro_export] 217 | #[doc(hidden)] 218 | macro_rules! _impl_internable { 219 | ( $($t:path),+ $(,)? ) => { $( 220 | impl $crate::Internable for $t { 221 | fn storage() -> &'static $crate::InternStorage { 222 | static STORAGE: $crate::InternStorage<$t> = $crate::InternStorage::new(); 223 | &STORAGE 224 | } 225 | } 226 | )+ }; 227 | } 228 | 229 | pub use crate::_impl_internable as impl_internable; 230 | 231 | impl_internable!(str,); 232 | -------------------------------------------------------------------------------- /crates/starpls_lexer/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@crates//:defs.bzl", "all_crate_deps") 2 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") 3 | 4 | package(default_visibility = ["//visibility:public"]) 5 | 6 | rust_library( 7 | name = "starpls_lexer", 8 | srcs = glob(["src/**/*.rs"]), 9 | ) 10 | 11 | rust_test( 12 | name = "starpls_lexer_test", 13 | crate = ":starpls_lexer", 14 | deps = all_crate_deps(normal_dev = True), 15 | ) 16 | -------------------------------------------------------------------------------- /crates/starpls_lexer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starpls_lexer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | [dev-dependencies] 11 | expect-test = "1.4.1" 12 | -------------------------------------------------------------------------------- /crates/starpls_lexer/src/cursor.rs: -------------------------------------------------------------------------------- 1 | use std::str::Chars; 2 | 3 | /// Sentinel value used to mark end-of-file. 4 | pub(crate) const EOF_CHAR: char = '\0'; 5 | 6 | pub(crate) enum CursorState { 7 | BeforeLeadingSpaces, 8 | Dedenting { 9 | num_remaining: u32, 10 | consistent: bool, 11 | }, 12 | AfterLeadingSpaces, 13 | } 14 | 15 | pub struct Cursor<'a> { 16 | pub(crate) state: CursorState, 17 | pub(crate) indents: Vec, 18 | pub(crate) type_comment_tokens: bool, 19 | chars: Chars<'a>, 20 | len_remaining: usize, 21 | input: &'a str, 22 | closers: Vec, 23 | } 24 | 25 | impl<'a> Cursor<'a> { 26 | pub fn new(input: &'a str) -> Self { 27 | Self { 28 | state: CursorState::BeforeLeadingSpaces, 29 | chars: input.chars(), 30 | type_comment_tokens: false, 31 | len_remaining: input.len(), 32 | input, 33 | indents: Vec::new(), 34 | closers: Vec::new(), 35 | } 36 | } 37 | 38 | pub fn new_for_type_comment(input: &'a str) -> Self { 39 | Self { 40 | type_comment_tokens: true, 41 | ..Self::new(input) 42 | } 43 | } 44 | 45 | pub(crate) fn first(&self) -> char { 46 | self.chars.clone().next().unwrap_or(EOF_CHAR) 47 | } 48 | 49 | pub(crate) fn second(&self) -> char { 50 | let mut chars = self.chars.clone(); 51 | chars.next(); 52 | chars.next().unwrap_or(EOF_CHAR) 53 | } 54 | 55 | // TODO(withered-magic): This is only used in eat_whitespace(). 56 | pub(crate) fn third(&self) -> char { 57 | let mut chars = self.chars.clone(); 58 | chars.next(); 59 | chars.next(); 60 | chars.next().unwrap_or(EOF_CHAR) 61 | } 62 | 63 | pub(crate) fn is_eof(&self) -> bool { 64 | self.chars.as_str().is_empty() 65 | } 66 | 67 | pub(crate) fn bump(&mut self) -> Option { 68 | self.chars.next() 69 | } 70 | 71 | pub(crate) fn pos_within_token(&self) -> u32 { 72 | (self.len_remaining - self.chars.as_str().len()) as u32 73 | } 74 | 75 | pub(crate) fn reset_pos_within_token(&mut self) -> u32 { 76 | let pos_within_token = self.pos_within_token(); 77 | self.len_remaining = self.chars.as_str().len(); 78 | pos_within_token 79 | } 80 | 81 | pub(crate) fn eat_while(&mut self, mut predicate: impl FnMut(char) -> bool) { 82 | while predicate(self.first()) && !self.is_eof() { 83 | self.bump(); 84 | } 85 | } 86 | 87 | pub(crate) fn str_until_pos_within_token(&self) -> &str { 88 | let start = self.input.len() - self.len_remaining; 89 | &self.input[start..start + self.pos_within_token() as usize] 90 | } 91 | 92 | pub(crate) fn has_open_block(&self) -> bool { 93 | !self.closers.is_empty() 94 | } 95 | 96 | pub(crate) fn open_block(&mut self, closer: char) { 97 | self.closers.push(closer); 98 | } 99 | 100 | pub(crate) fn close_block(&mut self, closer: char) { 101 | if closer == self.closers.last().cloned().unwrap_or(EOF_CHAR) { 102 | self.closers.pop(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /crates/starpls_lexer/src/unescape/tests.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | use std::str; 3 | 4 | use expect_test::expect; 5 | use expect_test::Expect; 6 | 7 | use super::*; 8 | 9 | #[test] 10 | fn test_unescape_string() { 11 | fn check(input: &str, raw: bool, expect: Expect) { 12 | let mut actual = String::new(); 13 | unescape_string(input, raw, false, &mut |range, res| { 14 | write!(&mut actual, "{:?} ", range).unwrap(); 15 | match res { 16 | Ok(c) => { 17 | writeln!(&mut actual, "{:?}", c).unwrap(); 18 | } 19 | Err(err) => { 20 | writeln!(&mut actual, "{:?}", err).unwrap(); 21 | } 22 | } 23 | }); 24 | expect.assert_eq(&actual); 25 | } 26 | check( 27 | "\\0", 28 | false, 29 | expect![[r#" 30 | 0..2 '\0' 31 | "#]], 32 | ); 33 | check( 34 | "\\世a", 35 | true, 36 | expect![[r#" 37 | 0..1 '\\' 38 | 1..4 '世' 39 | 4..5 'a' 40 | "#]], 41 | ); 42 | check( 43 | "\\\"", 44 | true, 45 | expect![[r#" 46 | 0..2 '"' 47 | "#]], 48 | ); 49 | check( 50 | "\\'", 51 | true, 52 | expect![[r#" 53 | 0..2 '\'' 54 | "#]], 55 | ); 56 | check( 57 | "\\\n", 58 | true, 59 | expect![[r#" 60 | 0..1 '\\' 61 | 1..2 '\n' 62 | "#]], 63 | ); 64 | } 65 | 66 | #[test] 67 | fn test_unescape_byte_string() { 68 | fn check(input: &str, expect: Expect) { 69 | let mut actual = String::new(); 70 | unescape_byte_string(input, &mut |range, res| { 71 | write!(&mut actual, "{:?} ", range).unwrap(); 72 | match res { 73 | Ok(b) => { 74 | if let Ok(s) = str::from_utf8(b) { 75 | writeln!(&mut actual, "{:?}", s).unwrap(); 76 | } else { 77 | writeln!(&mut actual, "{:x?}", b).unwrap(); 78 | } 79 | } 80 | Err(err) => { 81 | writeln!(&mut actual, "{:?}", err).unwrap(); 82 | } 83 | } 84 | }); 85 | expect.assert_eq(&actual); 86 | } 87 | 88 | check( 89 | "\\0", 90 | expect![[r#" 91 | 0..2 "\0" 92 | "#]], 93 | ); 94 | check( 95 | r#"AЀ世😿"#, 96 | expect![[r#" 97 | 0..1 "A" 98 | 1..3 "Ѐ" 99 | 3..6 "世" 100 | 6..10 "😿" 101 | "#]], 102 | ); 103 | check( 104 | r#"\x41\u0400\u4e16\U0001F63F"#, 105 | expect![[r#" 106 | 0..4 "A" 107 | 4..10 "Ѐ" 108 | 10..16 "世" 109 | 16..26 "😿" 110 | "#]], 111 | ); 112 | check( 113 | r#"\377\378\x80\xff\xff"#, 114 | expect![[r#" 115 | 0..4 [ff] 116 | 4..7 "\u{1f}" 117 | 7..8 "8" 118 | 8..12 [80] 119 | 12..16 [ff] 120 | 16..20 [ff] 121 | "#]], 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /crates/starpls_parser/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@crates//:defs.bzl", "all_crate_deps") 2 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") 3 | 4 | package(default_visibility = ["//visibility:public"]) 5 | 6 | rust_library( 7 | name = "starpls_parser", 8 | srcs = glob(["src/**/*.rs"]), 9 | deps = all_crate_deps() + [ 10 | "//crates/starpls_lexer", 11 | ], 12 | ) 13 | 14 | filegroup( 15 | name = "test_data", 16 | srcs = glob(["test_data/**"]), 17 | ) 18 | 19 | rust_test( 20 | name = "starpls_parser_test", 21 | crate = ":starpls_parser", 22 | data = [":test_data"], 23 | deps = all_crate_deps(normal_dev = True) + [ 24 | "//vendor/runfiles", 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /crates/starpls_parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starpls_parser" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | drop_bomb = "0.1.5" 10 | runfiles = { path = "../../vendor/runfiles" } 11 | starpls_lexer = { path = "../starpls_lexer" } 12 | 13 | [dev-dependencies] 14 | expect-test = "1.4.1" 15 | -------------------------------------------------------------------------------- /crates/starpls_parser/src/grammar.rs: -------------------------------------------------------------------------------- 1 | use crate::grammar::arguments::*; 2 | use crate::grammar::expressions::*; 3 | use crate::grammar::parameters::*; 4 | use crate::grammar::statements::*; 5 | use crate::grammar::type_comments::*; 6 | use crate::syntax_kind::SyntaxKindSet; 7 | use crate::Parser; 8 | use crate::SyntaxKind::*; 9 | use crate::T; 10 | 11 | mod arguments; 12 | mod expressions; 13 | mod parameters; 14 | mod statements; 15 | mod type_comments; 16 | 17 | pub(crate) fn module(p: &mut Parser) { 18 | let m = p.start(); 19 | while !p.at(EOF) { 20 | statement(p); 21 | } 22 | m.complete(p, MODULE); 23 | } 24 | 25 | pub(crate) fn type_comment_body(p: &mut Parser) { 26 | let m = p.start(); 27 | match p.current() { 28 | T![ignore] => { 29 | let m = p.start(); 30 | p.bump(T![ignore]); 31 | m.complete(p, IGNORE_TYPE); 32 | } 33 | T!['('] => { 34 | function_type(p); 35 | } 36 | _ => union_type(p), 37 | } 38 | 39 | // We only parse one type, so if there's any remaining tokens, add them 40 | // to an error node. 41 | if !p.at(EOF) { 42 | p.error_recover_until("Unexpected token", SyntaxKindSet::new(&[])); 43 | } 44 | m.complete(p, TYPE_COMMENT_BODY); 45 | } 46 | -------------------------------------------------------------------------------- /crates/starpls_parser/src/grammar/arguments.rs: -------------------------------------------------------------------------------- 1 | use crate::grammar::*; 2 | use crate::syntax_kind::SyntaxKindSet; 3 | 4 | pub(crate) const ARGUMENT_START: SyntaxKindSet = 5 | EXPR_START.union(SyntaxKindSet::new(&[T![**], T![*]])); 6 | 7 | pub(crate) fn arguments(p: &mut Parser) { 8 | argument(p); 9 | while p.at(T![,]) && ARGUMENT_START.contains(p.nth(1)) { 10 | p.bump(T![,]); 11 | argument(p); 12 | } 13 | p.eat(T![,]); 14 | } 15 | 16 | pub(crate) fn argument(p: &mut Parser) { 17 | // test test_arguments_all 18 | // f(x, *args, y=1, **kwargs) 19 | let m = p.start(); 20 | match p.current() { 21 | T![*] => { 22 | p.bump(T![*]); 23 | test(p); 24 | m.complete(p, UNPACKED_LIST_ARGUMENT); 25 | } 26 | T![**] => { 27 | p.bump(T![**]); 28 | test(p); 29 | m.complete(p, UNPACKED_DICT_ARGUMENT); 30 | } 31 | T![ident] if p.nth(1) == T![=] => { 32 | name(p); 33 | p.bump(T![=]); 34 | test(p); 35 | m.complete(p, KEYWORD_ARGUMENT); 36 | } 37 | _ => { 38 | test(p); 39 | m.complete(p, SIMPLE_ARGUMENT); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/starpls_parser/src/grammar/parameters.rs: -------------------------------------------------------------------------------- 1 | use crate::grammar::*; 2 | use crate::syntax_kind::SyntaxKindSet; 3 | 4 | pub(crate) const PARAMETER_START: SyntaxKindSet = SyntaxKindSet::new(&[T![ident], T![*], T![**]]); 5 | 6 | /// Grammar: `Parameters = Parameter {',' Parameter}.` 7 | pub(crate) fn parameters(p: &mut Parser) { 8 | // let m = p.start(); 9 | parameter(p); 10 | while p.at(T![,]) && PARAMETER_START.contains(p.nth(1)) { 11 | p.bump(T![,]); 12 | parameter(p); 13 | } 14 | // m.complete(p, PARAMETERS); 15 | p.eat(T![,]); 16 | } 17 | 18 | /// Grammar: `Parameter = identifier | identifier '=' Test | '*' | '*' identifier | '**' identifier` 19 | pub(crate) fn parameter(p: &mut Parser) { 20 | let m = p.start(); 21 | match p.current() { 22 | T![*] => { 23 | p.bump(T![*]); 24 | name(p); 25 | m.complete(p, ARGS_LIST_PARAMETER); 26 | } 27 | T![**] => { 28 | p.bump(T![**]); 29 | if name(p).is_none() { 30 | p.error("Expected identifier") 31 | } 32 | m.complete(p, KWARGS_DICT_PARAMETER); 33 | } 34 | T![ident] => { 35 | assert!(name(p).is_some()); 36 | if p.eat(T![=]) { 37 | if p.at_kinds(EXPR_START) { 38 | test(p); 39 | } else { 40 | p.error("Expected expression") 41 | } 42 | } 43 | m.complete(p, SIMPLE_PARAMETER); 44 | } 45 | _ => unreachable!(), 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/starpls_parser/src/grammar/type_comments.rs: -------------------------------------------------------------------------------- 1 | use crate::grammar::*; 2 | use crate::marker::CompletedMarker; 3 | use crate::syntax_kind::SyntaxKindSet; 4 | use crate::SyntaxKind; 5 | 6 | const TYPE_START: SyntaxKindSet = SyntaxKindSet::new(&[T![ident], ELLIPSIS]); 7 | 8 | const PARAMETER_TYPE_START: SyntaxKindSet = TYPE_START.union(SyntaxKindSet::new(&[T![*], T![**]])); 9 | 10 | const EMPTY: SyntaxKindSet = SyntaxKindSet::new(&[]); 11 | 12 | pub(crate) fn types(p: &mut Parser, stop: Option) { 13 | let cond = |p: &mut Parser| match stop { 14 | Some(stop) => !p.at(EOF) && !p.at(stop), 15 | None => !p.at(EOF), 16 | }; 17 | if !cond(p) { 18 | return; 19 | } 20 | union_type(p); 21 | while cond(p) { 22 | if !p.eat(T![,]) { 23 | p.error_recover_until("Expected \",\"", EMPTY); 24 | break; 25 | } 26 | if !p.at_kinds(TYPE_START) { 27 | p.error_recover_until("Expected type", EMPTY); 28 | break; 29 | } 30 | union_type(p); 31 | } 32 | } 33 | 34 | pub(crate) fn parameter_types(p: &mut Parser) { 35 | parameter_type(p); 36 | while p.at(T![,]) && PARAMETER_TYPE_START.contains(p.nth(1)) { 37 | p.bump(T![,]); 38 | parameter_type(p); 39 | } 40 | } 41 | 42 | pub(crate) fn parameter_type(p: &mut Parser) { 43 | let m = p.start(); 44 | match p.current() { 45 | T![ident] => { 46 | union_type(p); 47 | m.complete(p, SIMPLE_PARAMETER_TYPE); 48 | } 49 | T![*] => { 50 | p.bump(T![*]); 51 | union_type(p); 52 | m.complete(p, ARGS_LIST_PARAMETER_TYPE); 53 | } 54 | T![**] => { 55 | p.bump(T![**]); 56 | union_type(p); 57 | m.complete(p, KWARGS_DICT_PARAMETER_TYPE); 58 | } 59 | ELLIPSIS => { 60 | p.bump(ELLIPSIS); 61 | m.complete(p, SIMPLE_PARAMETER_TYPE); 62 | } 63 | _ => unreachable!(), 64 | } 65 | } 66 | 67 | pub(crate) fn union_type(p: &mut Parser) { 68 | let mut m = match type_(p) { 69 | Some(m) => m, 70 | None => return, 71 | }; 72 | let union_marker = if p.at(T![|]) { 73 | m.precede(p) 74 | } else { 75 | return; 76 | }; 77 | while p.at(T![|]) { 78 | p.bump(T![|]); 79 | type_(p); 80 | } 81 | union_marker.complete(p, UNION_TYPE); 82 | } 83 | 84 | pub(crate) fn type_(p: &mut Parser) -> Option { 85 | Some(match p.current() { 86 | T![None] => { 87 | let m = p.start(); 88 | p.bump(T![None]); 89 | m.complete(p, NONE_TYPE) 90 | } 91 | T![ident] => { 92 | let m = p.start(); 93 | path_segment(p); 94 | while p.eat(T![.]) { 95 | if !p.at(T![ident]) { 96 | p.error_recover_until("Expected type segment", EMPTY); 97 | return Some(m.complete(p, PATH_TYPE)); 98 | } 99 | path_segment(p); 100 | } 101 | if p.at(T!['[']) { 102 | let m = p.start(); 103 | p.bump(T!['[']); 104 | types(p, Some(T![']'])); 105 | p.eat(T![']']); 106 | m.complete(p, GENERIC_ARGUMENTS); 107 | } 108 | m.complete(p, PATH_TYPE) 109 | } 110 | T!['('] => function_type(p), 111 | ELLIPSIS => { 112 | let m = p.start(); 113 | p.bump(ELLIPSIS); 114 | m.complete(p, ELLIPSIS_TYPE) 115 | } 116 | _ => { 117 | p.error_recover_until("Expected type", EMPTY); 118 | return None; 119 | } 120 | }) 121 | } 122 | 123 | pub(crate) fn function_type(p: &mut Parser) -> CompletedMarker { 124 | let m = p.start(); 125 | p.bump(T!['(']); 126 | if p.at_kinds(TYPE_START) { 127 | let m = p.start(); 128 | parameter_types(p); 129 | m.complete(p, PARAMETER_TYPES); 130 | } 131 | if !p.eat(T![')']) { 132 | p.error_recover_until("\"(\" was not closed", EMPTY); 133 | return m.complete(p, FUNCTION_TYPE); 134 | } 135 | if !p.eat(ARROW) { 136 | p.error_recover_until("Expected \"->\"", EMPTY); 137 | return m.complete(p, FUNCTION_TYPE); 138 | } 139 | type_(p); 140 | m.complete(p, FUNCTION_TYPE) 141 | } 142 | 143 | pub(crate) fn path_segment(p: &mut Parser) { 144 | let m = p.start(); 145 | p.bump(T![ident]); 146 | m.complete(p, PATH_SEGMENT); 147 | } 148 | -------------------------------------------------------------------------------- /crates/starpls_parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | use marker::Marker; 2 | 3 | pub use crate::step::Step; 4 | use crate::step::StepEvent; 5 | pub use crate::syntax_kind::SyntaxKind; 6 | use crate::syntax_kind::SyntaxKind::*; 7 | use crate::syntax_kind::SyntaxKindSet; 8 | pub use crate::text::StrStep; 9 | pub use crate::text::StrWithTokens; 10 | 11 | mod grammar; 12 | mod marker; 13 | mod step; 14 | mod syntax_kind; 15 | mod text; 16 | 17 | #[cfg(test)] 18 | mod tests; 19 | 20 | /// Parses a Starlark module from the given sequence of tokens. This function operates on a sequence of 21 | /// non-trivia tokens; use the `.to_input()` method on an instance of `StrWithTokens` to obtain an `Input` 22 | /// to pass to this function. 23 | pub fn parse(input: &Input) -> Output { 24 | let mut p = Parser::new(input); 25 | grammar::module(&mut p); 26 | step::postprocess_step_events(p.events) 27 | } 28 | 29 | pub fn parse_type_list(input: &Input) -> Output { 30 | let mut p = Parser::new(input); 31 | grammar::type_comment_body(&mut p); 32 | step::postprocess_step_events(p.events) 33 | } 34 | 35 | /// The input to the parser, consisting of a list of tokens. 36 | pub struct Input { 37 | tokens: Vec, 38 | } 39 | 40 | /// The output of the lexer, consisting of a series of steps that can be used to construct the parse tree. 41 | pub struct Output { 42 | steps: Vec, 43 | } 44 | 45 | /// A parser for Starlark code. It takes a stream of non-trivia tokens as input and processes that stream 46 | /// to construct a parse tree. Because the parser operates only on token types and has no knowledge of 47 | /// text offsets, etc., it instead outputs a series of steps that can be consumed by a separate parse tree 48 | /// builder to construct the final tree. 49 | pub(crate) struct Parser<'a> { 50 | input: &'a Input, 51 | events: Vec, 52 | pos: usize, 53 | } 54 | 55 | impl<'a> Parser<'a> { 56 | fn new(input: &'a Input) -> Self { 57 | Self { 58 | input, 59 | events: Vec::new(), 60 | pos: 0, 61 | } 62 | } 63 | 64 | pub(crate) fn current(&self) -> SyntaxKind { 65 | self.nth(0) 66 | } 67 | 68 | pub(crate) fn nth(&self, n: usize) -> SyntaxKind { 69 | let pos = self.pos + n; 70 | if pos >= self.input.tokens.len() { 71 | EOF 72 | } else { 73 | self.input.tokens[pos] 74 | } 75 | } 76 | 77 | pub(crate) fn at(&self, kind: SyntaxKind) -> bool { 78 | self.nth_at(0, kind) 79 | } 80 | 81 | pub(crate) fn nth_at(&self, n: usize, kind: SyntaxKind) -> bool { 82 | self.nth(n) == kind 83 | } 84 | 85 | pub(crate) fn eat(&mut self, kind: SyntaxKind) -> bool { 86 | if !self.at(kind) { 87 | false 88 | } else { 89 | self.push_event(StepEvent::Token { kind }); 90 | self.pos += 1; 91 | true 92 | } 93 | } 94 | 95 | pub(crate) fn bump(&mut self, kind: SyntaxKind) { 96 | assert!(self.eat(kind)); 97 | } 98 | 99 | pub(crate) fn bump_any(&mut self) { 100 | if !self.at(EOF) { 101 | self.push_event(StepEvent::Token { 102 | kind: self.input.tokens[self.pos], 103 | }); 104 | self.pos += 1; 105 | } 106 | } 107 | 108 | /// Starts a new node in the syntax tree. All nodes and tokens consumed between the call to `start` and the 109 | /// corresponding invocation of `Marker::complete` belong to the same node. 110 | pub(crate) fn start(&mut self) -> Marker { 111 | let pos = self.events.len() as u32; 112 | self.push_event(StepEvent::Tombstone); 113 | Marker::new(pos) 114 | } 115 | 116 | pub(crate) fn push_event(&mut self, event: StepEvent) { 117 | self.events.push(event); 118 | } 119 | 120 | pub(crate) fn error(&mut self, message: T) 121 | where 122 | T: Into, 123 | { 124 | self.events.push(StepEvent::Error { 125 | message: message.into(), 126 | }); 127 | } 128 | 129 | pub(crate) fn error_recover_until( 130 | &mut self, 131 | message: impl Into, 132 | recover: SyntaxKindSet, 133 | ) { 134 | self.error(message); 135 | 136 | // Start a new ERROR node and consume tokens until we are at either a token specified in the recovery set, or EOF. 137 | if !self.at(EOF) && !recover.contains(self.current()) { 138 | let m = self.start(); 139 | while !self.at(EOF) && !recover.contains(self.current()) { 140 | self.bump_any(); 141 | } 142 | m.complete(self, ERROR); 143 | } 144 | } 145 | 146 | pub(crate) fn at_kinds(&self, set: SyntaxKindSet) -> bool { 147 | set.contains(self.current()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /crates/starpls_parser/src/marker.rs: -------------------------------------------------------------------------------- 1 | use drop_bomb::DropBomb; 2 | 3 | use crate::step::StepEvent; 4 | use crate::Parser; 5 | use crate::SyntaxKind; 6 | 7 | pub(crate) struct Marker { 8 | pos: u32, 9 | bomb: DropBomb, 10 | } 11 | 12 | impl Marker { 13 | pub(crate) fn new(pos: u32) -> Marker { 14 | Marker { 15 | pos, 16 | bomb: DropBomb::new("marker must either be completed or abandoned"), 17 | } 18 | } 19 | 20 | pub(crate) fn complete(mut self, p: &mut Parser, kind: SyntaxKind) -> CompletedMarker { 21 | self.bomb.defuse(); 22 | p.events[self.pos as usize] = StepEvent::Start { 23 | kind, 24 | forward_parent: None, 25 | }; 26 | p.push_event(StepEvent::Finish); 27 | CompletedMarker::new(self.pos) 28 | } 29 | 30 | pub(crate) fn abandon(mut self, p: &mut Parser) { 31 | self.bomb.defuse(); 32 | 33 | // Optimization: If this marker corresponds to the most recent event, we can actually 34 | // get rid of it altogether, saving us some space. 35 | if self.pos as usize == p.events.len() - 1 { 36 | match p.events.pop() { 37 | Some(StepEvent::Tombstone) => (), 38 | _ => unreachable!(), 39 | } 40 | } 41 | } 42 | } 43 | 44 | pub(crate) struct CompletedMarker { 45 | pos: u32, 46 | } 47 | 48 | impl CompletedMarker { 49 | pub(crate) fn new(pos: u32) -> Self { 50 | Self { pos } 51 | } 52 | 53 | pub(crate) fn precede(&mut self, p: &mut Parser) -> Marker { 54 | let m = p.start(); 55 | match &mut p.events[self.pos as usize] { 56 | StepEvent::Start { forward_parent, .. } => *forward_parent = Some(m.pos), 57 | _ => unreachable!(), 58 | } 59 | m 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/starpls_parser/src/step.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use crate::Output; 4 | use crate::SyntaxKind; 5 | 6 | /// A step in the process of building a syntax tree. 7 | #[derive(Debug)] 8 | pub enum Step { 9 | Start { kind: SyntaxKind }, 10 | Finish, 11 | Token { kind: SyntaxKind }, 12 | Error { message: String }, 13 | } 14 | 15 | /// Raw events produced by the processor. They contain additional fields, such 16 | /// as `forward_parent`, and thus are unsuitable for direct usage; instead, we need 17 | /// to run postprocessing steps to clean up the events. 18 | #[derive(Debug)] 19 | pub(crate) enum StepEvent { 20 | Start { 21 | kind: SyntaxKind, 22 | forward_parent: Option, 23 | }, 24 | Finish, 25 | Token { 26 | kind: SyntaxKind, 27 | }, 28 | Error { 29 | message: String, 30 | }, 31 | Tombstone, 32 | } 33 | 34 | pub(super) fn postprocess_step_events(mut events: Vec) -> Output { 35 | let mut steps = Vec::new(); 36 | let mut forward_parent_kinds = Vec::new(); 37 | 38 | for i in 0..events.len() { 39 | match mem::replace(&mut events[i], StepEvent::Tombstone) { 40 | StepEvent::Start { 41 | kind, 42 | forward_parent, 43 | } => { 44 | let mut fp = forward_parent; 45 | forward_parent_kinds.push(kind); 46 | while let Some(forward_parent_pos) = fp { 47 | match mem::replace( 48 | &mut events[forward_parent_pos as usize], 49 | StepEvent::Tombstone, 50 | ) { 51 | StepEvent::Start { 52 | kind, 53 | forward_parent, 54 | } => { 55 | fp = forward_parent; 56 | forward_parent_kinds.push(kind); 57 | } 58 | StepEvent::Tombstone => (), 59 | _ => unreachable!(), 60 | } 61 | } 62 | for kind in forward_parent_kinds.drain(..).rev() { 63 | steps.push(Step::Start { kind }); 64 | } 65 | } 66 | StepEvent::Finish => steps.push(Step::Finish), 67 | StepEvent::Token { kind } => steps.push(Step::Token { kind }), 68 | StepEvent::Error { message } => steps.push(Step::Error { message }), 69 | StepEvent::Tombstone => (), 70 | } 71 | } 72 | 73 | Output { steps } 74 | } 75 | -------------------------------------------------------------------------------- /crates/starpls_parser/src/tests.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error; 3 | use std::fmt::Write; 4 | use std::fs; 5 | use std::path::PathBuf; 6 | 7 | use expect_test::expect_file; 8 | use expect_test::ExpectFile; 9 | use runfiles::find_runfiles_dir; 10 | 11 | use crate::parse; 12 | use crate::StrStep; 13 | use crate::StrWithTokens; 14 | 15 | fn check(input: &str, expected: ExpectFile) { 16 | let str_with_tokens = StrWithTokens::new(input); 17 | let output = parse(&str_with_tokens.to_input()); 18 | 19 | // Render the parse tree, including trivia tokens. 20 | let mut buf = String::new(); 21 | let mut indent = String::new(); 22 | let mut errors = Vec::new(); 23 | 24 | str_with_tokens.build_with_trivia(output, &mut |step| match step { 25 | StrStep::Start { kind } => { 26 | writeln!(buf, "{indent}{kind:?}").unwrap(); 27 | indent.push_str(" "); 28 | } 29 | StrStep::Finish => { 30 | indent.pop(); 31 | indent.pop(); 32 | } 33 | StrStep::Token { kind, text, .. } => { 34 | writeln!(buf, "{indent}{kind:?} {text:?}").unwrap(); 35 | } 36 | StrStep::Error { message, pos } => errors.push((message, pos)), 37 | }); 38 | 39 | for (message, pos) in errors { 40 | writeln!(buf, "error {pos}: {message}").unwrap(); 41 | } 42 | 43 | expected.assert_eq(&buf); 44 | } 45 | 46 | #[test] 47 | fn test_parse_ok() { 48 | for test_case in collect_test_cases("test_data/ok").unwrap() { 49 | check(&test_case.input, expect_file![test_case.expect_file]); 50 | } 51 | } 52 | 53 | #[test] 54 | fn test_parse_error() { 55 | for test_case in collect_test_cases("test_data/err").unwrap() { 56 | check(&test_case.input, expect_file![test_case.expect_file]); 57 | } 58 | } 59 | 60 | #[derive(Debug)] 61 | struct TestCase { 62 | input: String, 63 | expect_file: PathBuf, 64 | } 65 | 66 | fn collect_test_cases(dir: &'static str) -> Result, Box> { 67 | let mut test_cases = Vec::new(); 68 | 69 | // Check for a test filter. 70 | let filter = env::var("TEST_FILTER").ok(); 71 | 72 | // let crate_root = find_runfiles_dir()?.join("starpls/crates/starpls_parser"); 73 | let root = find_runfiles_dir() 74 | .map(|dir| dir.join("_main/crates/starpls_parser")) 75 | .unwrap_or_else(|_| { 76 | PathBuf::from( 77 | env::var("CARGO_MANIFEST_DIR") 78 | .unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_string()), 79 | ) 80 | }); 81 | 82 | // Determine the test data directory. 83 | let test_data_dir = root.join(dir); 84 | 85 | for entry in fs::read_dir(test_data_dir)? { 86 | let entry = entry?; 87 | let entry_path = entry.path(); 88 | 89 | // Skip non-Starlark files. 90 | if entry_path.extension().unwrap_or_default() != "star" || { 91 | let file_type = entry.file_type()?; 92 | !(file_type.is_file() || file_type.is_symlink()) 93 | } { 94 | continue; 95 | } 96 | 97 | // If a filter was specified, check if the test name (the base name of the file without the extension) matches it. 98 | let stripped = entry_path.with_extension(""); 99 | let test_name = match stripped.file_name().and_then(|name| name.to_str()) { 100 | Some(test_name) => test_name, 101 | None => { 102 | continue; 103 | } 104 | }; 105 | if let Some(ref filter) = filter { 106 | if !test_name.contains(filter) { 107 | continue; 108 | } 109 | } 110 | 111 | // For a Starlark source file `source.star`, the corresponding expect file is `source.rast`. 112 | let input = fs::read_to_string(&entry_path)?; 113 | let expect_file = stripped.with_extension("rast"); 114 | 115 | test_cases.push(TestCase { input, expect_file }) 116 | } 117 | 118 | Ok(test_cases) 119 | } 120 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_dot_expr_missing_member.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | DOT_EXPR 3 | NAME_REF 4 | IDENT "a" 5 | DOT "." 6 | error 2: Expected member name 7 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_dot_expr_missing_member.star: -------------------------------------------------------------------------------- 1 | a. -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_error_block.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | ASSIGN_STMT 3 | NAME_REF 4 | IDENT "x" 5 | WHITESPACE " " 6 | EQ "=" 7 | WHITESPACE " " 8 | LITERAL_EXPR 9 | INT "1" 10 | NEWLINE "\n" 11 | ERROR 12 | INDENT " " 13 | IDENT "y" 14 | WHITESPACE " " 15 | EQ "=" 16 | WHITESPACE " " 17 | INT "2" 18 | NEWLINE "\n" 19 | WHITESPACE " " 20 | IDENT "z" 21 | WHITESPACE " " 22 | EQ "=" 23 | WHITESPACE " " 24 | INT "3" 25 | NEWLINE "\n" 26 | DEDENT "" 27 | ASSIGN_STMT 28 | NAME_REF 29 | IDENT "a" 30 | WHITESPACE " " 31 | EQ "=" 32 | WHITESPACE " " 33 | LITERAL_EXPR 34 | INT "4" 35 | error 6: Unexpected indentation 36 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_error_block.star: -------------------------------------------------------------------------------- 1 | x = 1 2 | y = 2 3 | z = 3 4 | a = 4 -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_for_stmt_missing_colon.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | IF_STMT 3 | FOR "for" 4 | WHITESPACE " " 5 | LOOP_VARIABLES 6 | NAME_REF 7 | IDENT "x" 8 | WHITESPACE " " 9 | IN "in" 10 | WHITESPACE " " 11 | NAME_REF 12 | IDENT "y" 13 | error 7: Expected ":" 14 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_for_stmt_missing_colon.star: -------------------------------------------------------------------------------- 1 | for x in y -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_for_stmt_missing_in.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | FOR_STMT 3 | FOR "for" 4 | WHITESPACE " " 5 | LOOP_VARIABLES 6 | NAME_REF 7 | IDENT "x" 8 | WHITESPACE " " 9 | ERROR 10 | STAR "*" 11 | WHITESPACE " " 12 | IDENT "y" 13 | error 3: Expected "in" 14 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_for_stmt_missing_in.star: -------------------------------------------------------------------------------- 1 | for x * y -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_for_stmt_missing_iterable.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | FOR_STMT 3 | FOR "for" 4 | WHITESPACE " " 5 | LOOP_VARIABLES 6 | NAME_REF 7 | IDENT "x" 8 | WHITESPACE " " 9 | IN "in" 10 | ERROR 11 | COLON ":" 12 | error 5: Expected expression 13 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_for_stmt_missing_iterable.star: -------------------------------------------------------------------------------- 1 | for x in: -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_for_stmt_missing_loop_variables.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | FOR_STMT 3 | FOR "for" 4 | error 1: Expected loop variables 5 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_for_stmt_missing_loop_variables.star: -------------------------------------------------------------------------------- 1 | for -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_for_stmt_missing_suite.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | FOR_STMT 3 | FOR "for" 4 | WHITESPACE " " 5 | LOOP_VARIABLES 6 | NAME_REF 7 | IDENT "x" 8 | WHITESPACE " " 9 | IN "in" 10 | WHITESPACE " " 11 | NAME_REF 12 | IDENT "y" 13 | COLON ":" 14 | error 8: Expected statement suite 15 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/err/test_for_stmt_missing_suite.star: -------------------------------------------------------------------------------- 1 | for x in y: -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_arguments_all.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | CALL_EXPR 3 | NAME_REF 4 | IDENT "f" 5 | ARGUMENTS 6 | OPEN_PAREN "(" 7 | SIMPLE_ARGUMENT 8 | NAME_REF 9 | IDENT "x" 10 | COMMA "," 11 | WHITESPACE " " 12 | UNPACKED_LIST_ARGUMENT 13 | STAR "*" 14 | NAME_REF 15 | IDENT "args" 16 | COMMA "," 17 | WHITESPACE " " 18 | KEYWORD_ARGUMENT 19 | NAME 20 | IDENT "y" 21 | EQ "=" 22 | LITERAL_EXPR 23 | INT "1" 24 | COMMA "," 25 | WHITESPACE " " 26 | UNPACKED_DICT_ARGUMENT 27 | STAR_STAR "**" 28 | NAME_REF 29 | IDENT "kwargs" 30 | CLOSE_PAREN ")" 31 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_arguments_all.star: -------------------------------------------------------------------------------- 1 | f(x, *args, y=1, **kwargs) -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_break_stmt.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | BREAK_STMT 3 | BREAK "break" 4 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_break_stmt.star: -------------------------------------------------------------------------------- 1 | break -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_continue_stmt.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | CONTINUE_STMT 3 | CONTINUE "continue" 4 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_continue_stmt.star: -------------------------------------------------------------------------------- 1 | continue -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_dot_expr_full.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | DOT_EXPR 3 | NAME_REF 4 | IDENT "a" 5 | DOT "." 6 | NAME 7 | IDENT "b" 8 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_dot_expr_full.star: -------------------------------------------------------------------------------- 1 | a.b -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_for_stmt_full.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | FOR_STMT 3 | FOR "for" 4 | WHITESPACE " " 5 | LOOP_VARIABLES 6 | NAME_REF 7 | IDENT "i" 8 | COMMA "," 9 | WHITESPACE " " 10 | NAME_REF 11 | IDENT "value" 12 | WHITESPACE " " 13 | IN "in" 14 | WHITESPACE " " 15 | CALL_EXPR 16 | NAME_REF 17 | IDENT "enumerate" 18 | ARGUMENTS 19 | OPEN_PAREN "(" 20 | SIMPLE_ARGUMENT 21 | NAME_REF 22 | IDENT "values" 23 | CLOSE_PAREN ")" 24 | COLON ":" 25 | SUITE 26 | NEWLINE "\n" 27 | INDENT " " 28 | CALL_EXPR 29 | NAME_REF 30 | IDENT "print" 31 | ARGUMENTS 32 | OPEN_PAREN "(" 33 | SIMPLE_ARGUMENT 34 | NAME_REF 35 | IDENT "i" 36 | COMMA "," 37 | WHITESPACE " " 38 | SIMPLE_ARGUMENT 39 | NAME_REF 40 | IDENT "value" 41 | CLOSE_PAREN ")" 42 | DEDENT "" 43 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_for_stmt_full.star: -------------------------------------------------------------------------------- 1 | for i, value in enumerate(values): 2 | print(i, value) -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_pass_stmt.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | PASS_STMT 3 | PASS "pass" 4 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_pass_stmt.star: -------------------------------------------------------------------------------- 1 | pass -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_return_stmt.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | RETURN_STMT 3 | RETURN "return" 4 | NEWLINE "\n" 5 | RETURN_STMT 6 | RETURN "return" 7 | WHITESPACE " " 8 | BINARY_EXPR 9 | LITERAL_EXPR 10 | INT "1" 11 | WHITESPACE " " 12 | PLUS "+" 13 | WHITESPACE " " 14 | LITERAL_EXPR 15 | INT "2" 16 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_return_stmt.star: -------------------------------------------------------------------------------- 1 | return 2 | return 1 + 2 -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_suite_full.rast: -------------------------------------------------------------------------------- 1 | MODULE 2 | DEF_STMT 3 | DEF "def" 4 | WHITESPACE " " 5 | NAME 6 | IDENT "f" 7 | PARAMETERS 8 | OPEN_PAREN "(" 9 | SIMPLE_PARAMETER 10 | NAME 11 | IDENT "x" 12 | COMMA "," 13 | WHITESPACE " " 14 | SIMPLE_PARAMETER 15 | NAME 16 | IDENT "y" 17 | CLOSE_PAREN ")" 18 | COLON ":" 19 | WHITESPACE " " 20 | SUITE 21 | CALL_EXPR 22 | NAME_REF 23 | IDENT "print" 24 | ARGUMENTS 25 | OPEN_PAREN "(" 26 | SIMPLE_ARGUMENT 27 | NAME_REF 28 | IDENT "x" 29 | CLOSE_PAREN ")" 30 | SEMI ";" 31 | WHITESPACE " " 32 | CALL_EXPR 33 | NAME_REF 34 | IDENT "print" 35 | ARGUMENTS 36 | OPEN_PAREN "(" 37 | SIMPLE_ARGUMENT 38 | NAME_REF 39 | IDENT "y" 40 | CLOSE_PAREN ")" 41 | NEWLINE "\n" 42 | DEF_STMT 43 | DEF "def" 44 | WHITESPACE " " 45 | NAME 46 | IDENT "g" 47 | PARAMETERS 48 | OPEN_PAREN "(" 49 | SIMPLE_PARAMETER 50 | NAME 51 | IDENT "x" 52 | COMMA "," 53 | WHITESPACE " " 54 | SIMPLE_PARAMETER 55 | NAME 56 | IDENT "y" 57 | CLOSE_PAREN ")" 58 | COLON ":" 59 | SUITE 60 | NEWLINE "\n" 61 | INDENT " " 62 | CALL_EXPR 63 | NAME_REF 64 | IDENT "print" 65 | ARGUMENTS 66 | OPEN_PAREN "(" 67 | SIMPLE_ARGUMENT 68 | NAME_REF 69 | IDENT "x" 70 | CLOSE_PAREN ")" 71 | NEWLINE "\n" 72 | WHITESPACE " " 73 | CALL_EXPR 74 | NAME_REF 75 | IDENT "print" 76 | ARGUMENTS 77 | OPEN_PAREN "(" 78 | SIMPLE_ARGUMENT 79 | NAME_REF 80 | IDENT "y" 81 | CLOSE_PAREN ")" 82 | DEDENT "" 83 | -------------------------------------------------------------------------------- /crates/starpls_parser/test_data/ok/test_suite_full.star: -------------------------------------------------------------------------------- 1 | def f(x, y): print(x); print(y) 2 | def g(x, y): 3 | print(x) 4 | print(y) -------------------------------------------------------------------------------- /crates/starpls_syntax/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@crates//:defs.bzl", "all_crate_deps") 2 | load("@rules_rust//rust:defs.bzl", "rust_library") 3 | 4 | package(default_visibility = ["//visibility:public"]) 5 | 6 | rust_library( 7 | name = "starpls_syntax", 8 | srcs = glob(["src/**/*.rs"]), 9 | deps = all_crate_deps() + [ 10 | "//crates/starpls_lexer", 11 | "//crates/starpls_parser", 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /crates/starpls_syntax/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starpls_syntax" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | starpls_lexer = { path = "../starpls_lexer" } 10 | starpls_parser = { path = "../starpls_parser" } 11 | rowan = "0.15.11" 12 | line-index = "0.1.0" 13 | 14 | [build-dependencies] 15 | cc = "*" 16 | -------------------------------------------------------------------------------- /crates/starpls_syntax/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use line_index::LineIndex; 2 | pub use rowan::TextRange; 3 | pub use rowan::TextSize; 4 | pub use rowan::TokenAtOffset; 5 | pub use starpls_parser::SyntaxKind; 6 | pub use starpls_parser::T; 7 | 8 | pub use crate::ast::Module; 9 | pub use crate::parser::line_index; 10 | pub use crate::parser::parse_module; 11 | pub use crate::parser::ParseTree; 12 | pub use crate::parser::SyntaxError; 13 | 14 | pub mod ast; 15 | mod parser; 16 | 17 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 18 | pub enum StarlarkLanguage {} 19 | 20 | impl rowan::Language for StarlarkLanguage { 21 | type Kind = SyntaxKind; 22 | 23 | fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind { 24 | raw.0.into() 25 | } 26 | 27 | fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind { 28 | rowan::SyntaxKind(kind.into()) 29 | } 30 | } 31 | 32 | pub type SyntaxNode = rowan::SyntaxNode; 33 | pub type SyntaxToken = rowan::SyntaxToken; 34 | pub type SyntaxElement = rowan::SyntaxElement; 35 | pub type SyntaxNodeChildren = rowan::SyntaxNodeChildren; 36 | -------------------------------------------------------------------------------- /crates/starpls_syntax/src/parser.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use rowan::ast::AstNode; 4 | use rowan::GreenNode; 5 | use rowan::GreenNodeBuilder; 6 | use rowan::Language; 7 | use rowan::TextRange; 8 | use rowan::TextSize; 9 | use starpls_parser::parse; 10 | use starpls_parser::parse_type_list; 11 | use starpls_parser::StrStep; 12 | use starpls_parser::StrWithTokens; 13 | use starpls_parser::SyntaxKind::*; 14 | 15 | use crate::LineIndex; 16 | use crate::Module; 17 | use crate::StarlarkLanguage; 18 | use crate::SyntaxNode; 19 | 20 | const TYPE_COMMENT_PREFIX_STR: &str = "# type: "; 21 | 22 | #[derive(Debug, Clone, PartialEq, Eq)] 23 | pub struct SyntaxError { 24 | pub message: String, 25 | pub range: TextRange, 26 | } 27 | 28 | /// The result of parsing a Starlark module and constructing a Rowan syntax tree. 29 | #[derive(Debug, Clone, PartialEq, Eq)] 30 | pub struct ParseTree { 31 | green: GreenNode, 32 | _ty: PhantomData T>, 33 | } 34 | 35 | impl ParseTree { 36 | fn new(green: GreenNode) -> Self { 37 | ParseTree { 38 | green, 39 | _ty: PhantomData, 40 | } 41 | } 42 | 43 | pub fn syntax(&self) -> SyntaxNode { 44 | SyntaxNode::new_root(self.green.clone()) 45 | } 46 | } 47 | 48 | impl> ParseTree { 49 | pub fn tree(&self) -> T { 50 | T::cast(self.syntax()).unwrap() 51 | } 52 | } 53 | 54 | pub fn parse_module(input: &str, errors_sink: &mut dyn FnMut(SyntaxError)) -> ParseTree { 55 | let str_with_tokens = StrWithTokens::new(input); 56 | let output = parse(&str_with_tokens.to_input()); 57 | let mut builder = GreenNodeBuilder::new(); 58 | 59 | add_lexer_errors(&str_with_tokens, errors_sink); 60 | 61 | str_with_tokens.build_with_trivia(output, &mut |str_step| match str_step { 62 | StrStep::Start { kind } => { 63 | builder.start_node(StarlarkLanguage::kind_to_raw(kind)); 64 | } 65 | StrStep::Finish => { 66 | builder.finish_node(); 67 | } 68 | StrStep::Token { kind, text, pos } => { 69 | if kind == COMMENT && text.starts_with(TYPE_COMMENT_PREFIX_STR) { 70 | build_type_comment( 71 | &mut builder, 72 | text, 73 | str_with_tokens.token_pos(pos) as usize, 74 | errors_sink, 75 | ); 76 | return; 77 | } 78 | builder.token(StarlarkLanguage::kind_to_raw(kind), text); 79 | } 80 | StrStep::Error { message, pos } => { 81 | let token_pos = str_with_tokens.token_pos(pos); 82 | errors_sink(SyntaxError { 83 | message, 84 | range: TextRange::new(TextSize::new(token_pos), TextSize::new(token_pos)), 85 | }); 86 | } 87 | }); 88 | 89 | let green_node = builder.finish(); 90 | 91 | // The root of the parse tree must always be a `Module`. 92 | assert_eq!(green_node.kind(), StarlarkLanguage::kind_to_raw(MODULE)); 93 | 94 | ParseTree::new(green_node) 95 | } 96 | 97 | fn build_type_comment( 98 | builder: &mut GreenNodeBuilder, 99 | text: &str, 100 | text_start: usize, 101 | errors_sink: &mut dyn FnMut(SyntaxError), 102 | ) { 103 | builder.start_node(StarlarkLanguage::kind_to_raw(TYPE_COMMENT)); 104 | builder.token( 105 | StarlarkLanguage::kind_to_raw(TYPE_COMMENT_PREFIX), 106 | TYPE_COMMENT_PREFIX_STR, 107 | ); 108 | 109 | let str_with_tokens = 110 | StrWithTokens::new_for_type_comment(&text[TYPE_COMMENT_PREFIX_STR.len()..]); 111 | let output = parse_type_list(&str_with_tokens.to_input()); 112 | add_lexer_errors(&str_with_tokens, errors_sink); 113 | 114 | str_with_tokens.build_with_trivia(output, &mut |str_step| match str_step { 115 | StrStep::Start { kind } => builder.start_node(StarlarkLanguage::kind_to_raw(kind)), 116 | StrStep::Finish => builder.finish_node(), 117 | StrStep::Token { kind, text, .. } => { 118 | builder.token(StarlarkLanguage::kind_to_raw(kind), text) 119 | } 120 | StrStep::Error { message, pos } => { 121 | let offset = ((text_start + TYPE_COMMENT_PREFIX_STR.len()) as u32 122 | + str_with_tokens.token_pos(pos)) 123 | .into(); 124 | errors_sink(SyntaxError { 125 | message, 126 | range: TextRange::new(offset, offset), 127 | }) 128 | } 129 | }); 130 | 131 | builder.finish_node(); 132 | } 133 | 134 | fn add_lexer_errors(str_with_tokens: &StrWithTokens, errors_sink: &mut dyn FnMut(SyntaxError)) { 135 | for lexer_error in str_with_tokens.lexer_errors() { 136 | errors_sink(SyntaxError { 137 | message: lexer_error.message.to_string(), 138 | range: TextRange::new( 139 | TextSize::new(lexer_error.start as u32), 140 | TextSize::new(lexer_error.end as u32), 141 | ), 142 | }); 143 | } 144 | } 145 | 146 | pub fn line_index(input: &str) -> LineIndex { 147 | LineIndex::new(input) 148 | } 149 | -------------------------------------------------------------------------------- /crates/starpls_test_util/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_rust//rust:defs.bzl", "rust_library") 2 | 3 | package(default_visibility = ["//visibility:public"]) 4 | 5 | rust_library( 6 | name = "starpls_test_util", 7 | srcs = glob(["src/**/*.rs"]), 8 | deps = [ 9 | "//crates/starpls_bazel", 10 | "//crates/starpls_syntax", 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /crates/starpls_test_util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starpls_test_util" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | starpls_bazel = { path = "../starpls_bazel" } 10 | starpls_syntax = { path = "../starpls_syntax" } 11 | -------------------------------------------------------------------------------- /crates/starpls_test_util/src/lib.rs: -------------------------------------------------------------------------------- 1 | use starpls_bazel::builtin::Callable; 2 | use starpls_bazel::builtin::Param; 3 | use starpls_bazel::builtin::Type; 4 | use starpls_bazel::builtin::Value; 5 | use starpls_bazel::Builtins; 6 | use starpls_syntax::TextRange; 7 | use starpls_syntax::TextSize; 8 | 9 | pub const CURSOR_MARKER: &str = "$0"; 10 | 11 | pub struct FixtureFile { 12 | pub contents: String, 13 | pub cursor_pos: Option, 14 | pub selected_ranges: Vec, 15 | } 16 | 17 | impl FixtureFile { 18 | pub fn parse(input: &str) -> Self { 19 | let mut contents = String::with_capacity(input.len()); 20 | let mut cursor_pos = None; 21 | if let Some(offset) = input.find(CURSOR_MARKER) { 22 | contents.push_str(&input[..offset]); 23 | contents.push_str(&input[offset + CURSOR_MARKER.len()..]); 24 | cursor_pos = Some(TextSize::new(offset as u32)); 25 | } else { 26 | contents.push_str(input); 27 | } 28 | 29 | let selected_ranges = find_selected_ranges(&contents); 30 | 31 | Self { 32 | contents, 33 | cursor_pos, 34 | selected_ranges, 35 | } 36 | } 37 | } 38 | 39 | fn find_selected_ranges(contents: &str) -> Vec { 40 | let mut line_starts = vec![TextSize::new(0)]; 41 | let mut ranges = Vec::new(); 42 | for line in contents.split_inclusive('\n') { 43 | if let Some(start) = line.find("#^") { 44 | let remaining = &line[start + "#^".len()..]; 45 | let additional_carets = remaining.chars().take_while(|c| c == &'^').count(); 46 | if let Some(prev_line_start) = line_starts.get(line_starts.len() - 2) { 47 | let range_start = prev_line_start + TextSize::try_from(start).unwrap(); 48 | let range_end = 49 | range_start + TextSize::try_from("#^".len() + additional_carets).unwrap(); 50 | ranges.push(TextRange::new(range_start, range_end)) 51 | } 52 | } 53 | line_starts.push(line_starts.last().unwrap() + TextSize::of(line)); 54 | } 55 | ranges 56 | } 57 | 58 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 59 | pub struct FixtureType { 60 | pub name: String, 61 | pub fields: Vec<(String, String)>, 62 | pub methods: Vec, 63 | } 64 | 65 | impl FixtureType { 66 | pub fn new(name: &str, fields: Vec<(&str, &str)>, methods: Vec<&str>) -> Self { 67 | FixtureType { 68 | name: name.into(), 69 | fields: fields 70 | .into_iter() 71 | .map(|(name, ty)| (name.into(), ty.into())) 72 | .collect(), 73 | methods: methods.into_iter().map(|method| method.into()).collect(), 74 | } 75 | } 76 | } 77 | 78 | pub fn make_test_builtins( 79 | functions: Vec, 80 | globals: Vec<(impl ToString, impl ToString)>, 81 | types: Vec, 82 | ) -> Builtins { 83 | Builtins { 84 | global: functions 85 | .into_iter() 86 | .map(|name| Value { 87 | name: name.to_string(), 88 | callable: Some(Callable { 89 | param: vec![ 90 | Param { 91 | name: "*args".to_string(), 92 | is_star_arg: true, 93 | ..Default::default() 94 | }, 95 | Param { 96 | name: "**kwargs".to_string(), 97 | is_star_star_arg: true, 98 | ..Default::default() 99 | }, 100 | ], 101 | return_type: "Unknown".to_string(), 102 | }), 103 | ..Default::default() 104 | }) 105 | .chain(globals.into_iter().map(|(name, ty)| Value { 106 | name: name.to_string(), 107 | r#type: ty.to_string(), 108 | ..Default::default() 109 | })) 110 | .collect(), 111 | r#type: types 112 | .into_iter() 113 | .map(|ty| Type { 114 | name: ty.name, 115 | field: ty 116 | .fields 117 | .into_iter() 118 | .map(|field| Value { 119 | name: field.0, 120 | r#type: field.1, 121 | ..Default::default() 122 | }) 123 | .chain(ty.methods.into_iter().map(|name| Value { 124 | name, 125 | callable: Some(Callable { 126 | param: vec![ 127 | Param { 128 | name: "args".to_string(), 129 | is_star_arg: true, 130 | ..Default::default() 131 | }, 132 | Param { 133 | name: "kwargs".to_string(), 134 | is_star_star_arg: true, 135 | ..Default::default() 136 | }, 137 | ], 138 | return_type: "Unknown".to_string(), 139 | }), 140 | ..Default::default() 141 | })) 142 | .collect(), 143 | ..Default::default() 144 | }) 145 | .collect(), 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /editors/code/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /**@type {import('eslint').Linter.Config} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | plugins: [ 7 | '@typescript-eslint', 8 | ], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | rules: { 14 | 'semi': [2, 'always'], 15 | '@typescript-eslint/no-unused-vars': 0, 16 | '@typescript-eslint/no-explicit-any': 0, 17 | '@typescript-eslint/explicit-module-boundary-types': 0, 18 | '@typescript-eslint/no-non-null-assertion': 0, 19 | 'quotes': ['error', 'single'], 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /editors/code/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /editors/code/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript" 5 | } 6 | }, 7 | "module": { 8 | "type": "commonjs", 9 | "strict": false 10 | } 11 | } -------------------------------------------------------------------------------- /editors/code/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory") 2 | load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_file", "write_source_files") 3 | load("@aspect_rules_swc//swc:defs.bzl", "swc") 4 | load("@aspect_rules_ts//ts:defs.bzl", "ts_project") 5 | load("@bazel_skylib//lib:partial.bzl", "partial") 6 | load("@npm//:defs.bzl", "npm_link_all_packages") 7 | 8 | npm_link_all_packages(name = "node_modules") 9 | 10 | ts_project( 11 | name = "swc", 12 | srcs = glob(["src/**/*.ts"]), 13 | declaration = True, 14 | root_dir = "src", 15 | source_map = True, 16 | transpiler = partial.make( 17 | swc, 18 | out_dir = "dist", 19 | root_dir = "src", 20 | source_maps = "true", 21 | swcrc = ":.swcrc", 22 | ), 23 | deps = [ 24 | ":node_modules/@types/node", 25 | ":node_modules/@types/vscode", 26 | ":node_modules/vscode-languageclient", 27 | ], 28 | ) 29 | 30 | copy_to_directory( 31 | name = "swc_copy", 32 | srcs = [":swc"], 33 | root_paths = ["editors/code/dist"], 34 | ) 35 | 36 | write_source_files( 37 | name = "code", 38 | diff_test = False, 39 | files = { 40 | "dist": ":swc_copy", 41 | }, 42 | ) 43 | 44 | write_source_file( 45 | name = "copy_starpls", 46 | diff_test = False, 47 | executable = True, 48 | in_file = "//crates/starpls", 49 | out_file = "bin/starpls", 50 | ) 51 | -------------------------------------------------------------------------------- /editors/code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starpls", 3 | "version": "0.1.0", 4 | "description": "An LSP implementation for Starlark", 5 | "publisher": "withered-magic", 6 | "main": "./dist/main", 7 | "scripts": { 8 | "build": "pnpm run build-lsp && pnpm run build-ext", 9 | "build-ext": "bazelisk run //editors/code", 10 | "build-lsp": "bazelisk run //editors/code:copy_starpls", 11 | "test": "bazelisk test //editors/code/...", 12 | "watch": "ibazel run //editors/code" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "engines": { 18 | "vscode": "^1.75.0" 19 | }, 20 | "contributes": { 21 | "commands": [ 22 | { 23 | "command": "starpls.showHir", 24 | "title": "Show HIR", 25 | "category": "starpls" 26 | }, 27 | { 28 | "command": "starpls.showSyntaxTree", 29 | "title": "Show Syntax Tree", 30 | "category": "starpls" 31 | }, 32 | { 33 | "command": "starpls.showVersion", 34 | "title": "Show Language Server Version", 35 | "category": "starpls" 36 | } 37 | ], 38 | "grammars": [ 39 | { 40 | "language": "starlark", 41 | "scopeName": "source.starlark", 42 | "path": "./syntaxes/starlark.tmLanguage.json" 43 | } 44 | ], 45 | "languages": [ 46 | { 47 | "id": "starlark", 48 | "aliases": [ 49 | "Starlark", 50 | "starlark" 51 | ], 52 | "extensions": [ 53 | ".sky", 54 | ".star" 55 | ], 56 | "configuration": "./syntaxes/starlark.configuration.json" 57 | } 58 | ] 59 | }, 60 | "devDependencies": { 61 | "@bazel/bazelisk": "^1.19.0", 62 | "@types/node": "^20.10.4", 63 | "@types/vscode": "^1.75.0", 64 | "@typescript-eslint/eslint-plugin": "^6.13.2", 65 | "@typescript-eslint/parser": "^6.13.2", 66 | "eslint": "^8.55.0", 67 | "typescript": "5.3.3" 68 | }, 69 | "dependencies": { 70 | "@bazel/ibazel": "^0.24.0", 71 | "vscode-languageclient": "^8.1.0" 72 | } 73 | } -------------------------------------------------------------------------------- /editors/code/src/commands.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { type Context } from './context'; 3 | import { isStarlarkDocument, isStarlarkTextEditor } from './util'; 4 | 5 | /** 6 | * A factory for creating commands with access to the `Context` object. 7 | */ 8 | export type CommandFactory = (ctx: Context) => (...args: any[]) => unknown; 9 | 10 | function showVersion() { 11 | return () => { 12 | vscode.window.showInformationMessage('starpls, v0.1.0'); 13 | }; 14 | } 15 | 16 | function showHir(ctx: Context) { 17 | const hirScheme = 'starpls-hir'; 18 | const hirUri = vscode.Uri.parse(`${hirScheme}://hir/hir`); 19 | 20 | const hirProvider = new class implements vscode.TextDocumentContentProvider { 21 | private readonly emitter = new vscode.EventEmitter(); 22 | onDidChange = this.emitter.event; 23 | 24 | constructor() { 25 | vscode.window.onDidChangeActiveTextEditor((textEditor) => this.onDidChangeActiveTextEditor(textEditor), this, ctx.disposables); 26 | vscode.workspace.onDidChangeTextDocument((textDocumentChangeEvent) => this.onDidChangeTextDocument(textDocumentChangeEvent), this, ctx.disposables); 27 | } 28 | 29 | provideTextDocumentContent(_uri: vscode.Uri, _token: vscode.CancellationToken): vscode.ProviderResult { 30 | if (!ctx.activeStarlarkTextEditor) { 31 | return; 32 | } 33 | return ctx.client.sendRequest('starpls/showHir', { 34 | textDocument: { 35 | uri: ctx.activeStarlarkTextEditor.document.uri.toString(), 36 | }, 37 | }); 38 | } 39 | 40 | onDidChangeActiveTextEditor(textEditor: vscode.TextEditor | undefined) { 41 | if (textEditor && isStarlarkTextEditor(textEditor)) { 42 | this.emitter.fire(hirUri); 43 | } 44 | } 45 | 46 | onDidChangeTextDocument(textDocumentChangeEvent: vscode.TextDocumentChangeEvent) { 47 | if (isStarlarkDocument(textDocumentChangeEvent.document)) { 48 | setTimeout(() => this.emitter.fire(hirUri), 10); 49 | } 50 | } 51 | }; 52 | 53 | ctx.disposables.push(vscode.workspace.registerTextDocumentContentProvider(hirScheme, hirProvider)); 54 | 55 | return async () => { 56 | const document = await vscode.workspace.openTextDocument(hirUri); 57 | await vscode.window.showTextDocument(document, { 58 | preserveFocus: true, 59 | viewColumn: vscode.ViewColumn.Two, 60 | }); 61 | }; 62 | } 63 | 64 | function showSyntaxTree(ctx: Context) { 65 | // Define and register a content provider for the syntax tree viewer. 66 | const syntaxTreeScheme = 'starpls-syntax-tree'; 67 | const syntaxTreeUri = vscode.Uri.parse(`${syntaxTreeScheme}://syntaxtree/tree.rast`); 68 | 69 | const syntaxTreeProvider = new class implements vscode.TextDocumentContentProvider { 70 | private readonly emitter = new vscode.EventEmitter(); 71 | onDidChange = this.emitter.event; 72 | 73 | constructor() { 74 | vscode.window.onDidChangeActiveTextEditor((textEditor) => this.onDidChangeActiveTextEditor(textEditor), this, ctx.disposables); 75 | vscode.workspace.onDidChangeTextDocument((textDocumentChangeEvent) => this.onDidChangeTextDocument(textDocumentChangeEvent), this, ctx.disposables); 76 | } 77 | 78 | provideTextDocumentContent(_uri: vscode.Uri, _token: vscode.CancellationToken): vscode.ProviderResult { 79 | if (!ctx.activeStarlarkTextEditor) { 80 | return; 81 | } 82 | return ctx.client.sendRequest('starpls/showSyntaxTree', { 83 | textDocument: { 84 | uri: ctx.activeStarlarkTextEditor.document.uri.toString(), 85 | }, 86 | }); 87 | } 88 | 89 | onDidChangeActiveTextEditor(textEditor: vscode.TextEditor | undefined) { 90 | // Check if the new editor is an editor for Starlark source files. If so, update our content. 91 | if (textEditor && isStarlarkTextEditor(textEditor)) { 92 | this.emitter.fire(syntaxTreeUri); 93 | } 94 | } 95 | 96 | onDidChangeTextDocument(textDocumentChangeEvent: vscode.TextDocumentChangeEvent) { 97 | // The sleep here is a best-effort attempt at making sure the `onDidChangeTextDocument` notification was processed by the language 98 | // server before we request a content update. 99 | if (isStarlarkDocument(textDocumentChangeEvent.document)) { 100 | setTimeout(() => this.emitter.fire(syntaxTreeUri), 10); 101 | } 102 | } 103 | }; 104 | 105 | ctx.disposables.push(vscode.workspace.registerTextDocumentContentProvider(syntaxTreeScheme, syntaxTreeProvider)); 106 | 107 | return async () => { 108 | const document = await vscode.workspace.openTextDocument(syntaxTreeUri); 109 | await vscode.window.showTextDocument(document, { 110 | preserveFocus: true, 111 | viewColumn: vscode.ViewColumn.Two, 112 | }); 113 | }; 114 | } 115 | 116 | export default function createCommandFactories(): Record { 117 | return { 118 | 'starpls.showHir': showHir, 119 | 'starpls.showSyntaxTree': showSyntaxTree, 120 | 'starpls.showVersion': showVersion, 121 | }; 122 | } 123 | -------------------------------------------------------------------------------- /editors/code/src/context.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as vscode from 'vscode'; 3 | import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'; 4 | import { CommandFactory } from './commands'; 5 | import { StarlarkTextEditor, isStarlarkTextEditor } from './util'; 6 | 7 | /** 8 | * The `Context` class wraps a `vscode.ExtensionContext and keeps track of the current state 9 | * of the editor. 10 | */ 11 | export class Context { 12 | /** 13 | * The active client connection to the language server. 14 | */ 15 | private _client!: LanguageClient; 16 | 17 | private _disposables: vscode.Disposable[]; 18 | 19 | constructor(readonly extensionContext: vscode.ExtensionContext, private commandFactories: Record) { 20 | this._disposables = []; 21 | } 22 | 23 | get client(): LanguageClient { 24 | return this._client; 25 | } 26 | 27 | get disposables(): vscode.Disposable[] { 28 | return this._disposables; 29 | } 30 | 31 | /** 32 | * Initializes the context and establishes 33 | */ 34 | async start() { 35 | // Establish connection to the language server. 36 | console.log('context: connecting to the language server'); 37 | const client = await this.getOrCreateClient(); 38 | await client.start(); 39 | 40 | // Register commands with the VSCode API. 41 | console.log('context: registering commands'); 42 | this.registerCommands(); 43 | } 44 | 45 | async stop() { 46 | this.disposables.forEach((disposable) => disposable.dispose()); 47 | return this._client.stop(); 48 | } 49 | 50 | private async getOrCreateClient(): Promise { 51 | if (this._client) { 52 | return this._client; 53 | } 54 | 55 | // Determine the path to the server executable, installing it if necessary. 56 | const serverPath = await this.ensureServerInstalled(); 57 | 58 | // Set up language client/server options. 59 | const executable: Executable = { command: serverPath }; 60 | const serverOptions: ServerOptions = { debug: executable, run: executable }; 61 | const clientOptions: LanguageClientOptions = { 62 | documentSelector: [{ scheme: 'file', language: 'starlark' }], 63 | }; 64 | 65 | return this._client = new LanguageClient( 66 | 'starpls', 67 | 'Starlark Language Server', 68 | serverOptions, 69 | clientOptions, 70 | ); 71 | } 72 | 73 | private async ensureServerInstalled(): Promise { 74 | const defaultServerPath = path.join(this.extensionContext.extensionPath, '/bin/starpls'); 75 | const serverPath = process.env.__STARPLS_SERVER_DEBUG ? process.env.__STARPLS_SERVER_DEBUG : defaultServerPath; 76 | console.log('context: using server executable at %s', serverPath); 77 | return serverPath; 78 | } 79 | 80 | private registerCommands() { 81 | // Dispose of any currently active commands. 82 | this.disposables.forEach((disposable) => disposable.dispose()); 83 | this._disposables = []; 84 | 85 | // Register the commands. 86 | for (const [name, factory] of Object.entries(this.commandFactories)) { 87 | const disposable = vscode.commands.registerCommand(name, factory(this)); 88 | this.disposables.push(disposable); 89 | } 90 | } 91 | 92 | get activeStarlarkTextEditor(): StarlarkTextEditor | undefined { 93 | const activeTextEditor = vscode.window.activeTextEditor; 94 | return activeTextEditor && isStarlarkTextEditor(activeTextEditor) ? activeTextEditor : undefined; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /editors/code/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Context } from './context'; 3 | import createCommandFactories from './commands'; 4 | 5 | let context: Context; 6 | 7 | export function activate(extensionContext: vscode.ExtensionContext) { 8 | console.log('activate: starting extension'); 9 | context = new Context(extensionContext, createCommandFactories()); 10 | void context.start(); 11 | } 12 | 13 | export function deactivate(): Thenable | undefined { 14 | if (!context) { 15 | return; 16 | } 17 | return context.stop(); 18 | } 19 | -------------------------------------------------------------------------------- /editors/code/src/util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export type StarlarkDocument = vscode.TextDocument & { languageId: 'starlark '}; 4 | export type StarlarkTextEditor = vscode.TextEditor & { document: StarlarkDocument }; 5 | 6 | export function isStarlarkDocument(document: vscode.TextDocument): document is StarlarkDocument { 7 | return document.languageId === 'starlark' && document.uri.scheme === 'file'; 8 | } 9 | 10 | export function isStarlarkTextEditor(textEditor: vscode.TextEditor): textEditor is StarlarkTextEditor { 11 | return isStarlarkDocument(textEditor.document); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /editors/code/syntaxes/starlark.configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#" 4 | }, 5 | "brackets": [ 6 | ["{", "}"], 7 | ["[", "]"], 8 | ["(", ")"] 9 | ], 10 | "autoClosingPairs": [ 11 | ["{", "}"], 12 | ["[", "]"], 13 | ["(", ")"], 14 | { 15 | "open": "\"", 16 | "close": "\"", 17 | "notIn": ["string", "comment"] 18 | }, 19 | { 20 | "open": "'", 21 | "close": "'", 22 | "notIn": ["string", "comment"] 23 | } 24 | ], 25 | "surroundingPairs": [ 26 | ["{", "}"], 27 | ["[", "]"], 28 | ["(", ")"], 29 | ["\"", "\""], 30 | ["'", "'"] 31 | ] 32 | } -------------------------------------------------------------------------------- /editors/code/syntaxes/starlark.tmLanguage.license: -------------------------------------------------------------------------------- 1 | starlark.tmLanguage.json is derived from MagicPython.tmLanguage.json, 2 | which can be found at https://github.com/MagicStack/MagicPython. 3 | 4 | --- 5 | 6 | The MIT License 7 | 8 | Copyright (c) 2015 MagicStack Inc. http://magic.io 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. -------------------------------------------------------------------------------- /editors/code/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2021", 5 | "outDir": "dist", 6 | "lib": [ 7 | "es2021" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true, 12 | "useUnknownInCatchVariables": false, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "newLine": "LF", 18 | "declaration": true 19 | }, 20 | "exclude": [ 21 | "node_modules" 22 | ], 23 | "include": [ 24 | "src" 25 | ] 26 | } -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2023-12-06" 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Item" 2 | group_imports = "StdExternalCrate" 3 | unstable_features = true 4 | -------------------------------------------------------------------------------- /vendor/runfiles/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load( 2 | "@rules_rust//rust:defs.bzl", 3 | "rust_library", 4 | ) 5 | load("//vendor/runfiles/private:runfiles_utils.bzl", "workspace_name") 6 | 7 | workspace_name( 8 | name = "workspace_name.env", 9 | ) 10 | 11 | rust_library( 12 | name = "runfiles", 13 | srcs = ["src/lib.rs"], 14 | edition = "2021", 15 | rustc_env_files = [ 16 | ":workspace_name.env", 17 | ], 18 | visibility = ["//visibility:public"], 19 | ) 20 | -------------------------------------------------------------------------------- /vendor/runfiles/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "runfiles" 3 | version = "0.38.0" 4 | edition = "2021" 5 | -------------------------------------------------------------------------------- /vendor/runfiles/private/BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//tools/runfiles:__pkg__"]) 2 | 3 | filegroup( 4 | name = "distro", 5 | srcs = glob([ 6 | "**/*.bzl", 7 | ]) + [ 8 | "BUILD.bazel", 9 | ], 10 | ) 11 | -------------------------------------------------------------------------------- /vendor/runfiles/private/runfiles_utils.bzl: -------------------------------------------------------------------------------- 1 | """Utilities for the `@rules_rust//tools/runfiles` library""" 2 | 3 | _RULES_RUST_RUNFILES_WORKSPACE_NAME = "RULES_RUST_RUNFILES_WORKSPACE_NAME" 4 | 5 | def _workspace_name_impl(ctx): 6 | output = ctx.actions.declare_file(ctx.label.name) 7 | 8 | ctx.actions.write( 9 | output = output, 10 | content = "{}={}\n".format( 11 | _RULES_RUST_RUNFILES_WORKSPACE_NAME, 12 | ctx.workspace_name, 13 | ), 14 | ) 15 | 16 | return [DefaultInfo( 17 | files = depset([output]), 18 | )] 19 | 20 | workspace_name = rule( 21 | implementation = _workspace_name_impl, 22 | doc = """\ 23 | A rule for detecting the current workspace name and writing it to a file for 24 | for use with `rustc_env_files` attributes on `rust_*` rules. The workspace 25 | name is exposed by the variable `{}`.""".format(_RULES_RUST_RUNFILES_WORKSPACE_NAME), 26 | ) 27 | -------------------------------------------------------------------------------- /xtask/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@crates//:defs.bzl", "all_crate_deps") 2 | load("@rules_rust//rust:defs.bzl", "rust_binary") 3 | 4 | package(default_visibility = ["//visibility:public"]) 5 | 6 | rust_binary( 7 | name = "xtask", 8 | srcs = glob(["src/**/*.rs"]), 9 | deps = all_crate_deps(), 10 | ) 11 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.76" 10 | clap = { version = "4.4.11", features = ["derive"] } 11 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use clap::Subcommand; 3 | 4 | mod update_parser_test_data; 5 | mod util; 6 | 7 | #[derive(Parser)] 8 | #[command(author, version, about, long_about = None)] 9 | struct Cli { 10 | #[command(subcommand)] 11 | command: Option, 12 | } 13 | 14 | #[derive(Subcommand)] 15 | enum Commands { 16 | UpdateParserTestData { filters: Vec }, 17 | } 18 | 19 | fn main() -> anyhow::Result<()> { 20 | let cli = Cli::parse(); 21 | 22 | match &cli.command { 23 | Some(Commands::UpdateParserTestData { filters }) => update_parser_test_data::run(filters), 24 | None => Ok(()), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /xtask/src/update_parser_test_data.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::collections::HashSet; 3 | use std::fs; 4 | use std::mem; 5 | 6 | use anyhow::anyhow; 7 | 8 | use crate::util::project_root; 9 | 10 | /// A contiguous block of comments in a Rust source file. 11 | #[derive(Default, Debug)] 12 | struct CommentBlock { 13 | lines: Vec, 14 | } 15 | 16 | /// The kind of parser test. There are two kinds: Tests that expect no parser errors are located under the `ok` 17 | /// directory, and tests expecting parser errors are located under the `err` directory. 18 | #[derive(Debug, PartialEq, Eq)] 19 | enum TestKind { 20 | Ok, 21 | Err, 22 | } 23 | 24 | /// A parser test, as derived from a `CommentBlock`. 25 | #[derive(Debug)] 26 | struct Test { 27 | kind: TestKind, 28 | name: String, 29 | text: String, 30 | } 31 | 32 | /// Extracts comment blocks from a Rust source file. 33 | fn extract_comment_blocks(text: &str) -> Vec { 34 | let comment_prefix = "// "; 35 | let lines = text.lines().map(str::trim_start); 36 | let mut blocks = Vec::new(); 37 | let mut current_block = CommentBlock::default(); 38 | 39 | // Process the source file line-by-line. If we see a comment, add it to the intermediate block. 40 | // Subsequent comment lines are also added to the intermediate block until a non-comment line is reached, 41 | // at which point the intermediate block's contents are pushed to our accumulator, and the intermediate 42 | // block is reset. 43 | for line in lines { 44 | if let Some(stripped) = line.strip_prefix(comment_prefix) { 45 | current_block.lines.push(stripped.to_string()); 46 | } else if !current_block.lines.is_empty() { 47 | blocks.push(mem::take(&mut current_block)); 48 | } 49 | } 50 | 51 | // If the last processed line was a comment, we might have a non-empty intermediate block. If so, simply add it 52 | // to our accumulator as well. 53 | if !current_block.lines.is_empty() { 54 | blocks.push(current_block); 55 | } 56 | 57 | blocks 58 | } 59 | 60 | fn add_tests_from_comment_blocks( 61 | tests: &mut HashMap, 62 | blocks: &[CommentBlock], 63 | ) -> anyhow::Result<()> { 64 | for block in blocks { 65 | if block.lines.is_empty() { 66 | continue; 67 | } 68 | 69 | // Try to find a test header, e.g. "test first_example". 70 | let mut lines = block.lines.iter().map(|line| line.as_str()); 71 | let header = loop { 72 | match lines.next() { 73 | Some(line) => { 74 | let mut parts = line.trim_start().split_ascii_whitespace(); 75 | match (parts.next(), parts.next()) { 76 | (Some("test"), Some(name)) => break Some((TestKind::Ok, name)), 77 | (Some("test_err"), Some(name)) => break Some((TestKind::Err, name)), 78 | _ => (), 79 | } 80 | } 81 | None => break None, 82 | } 83 | }; 84 | 85 | // If this comment block doesn't have a test header, continue. 86 | let (kind, name) = match header { 87 | Some(header) => header, 88 | None => continue, 89 | }; 90 | 91 | // Check for an existing test with the given name. 92 | if tests.contains_key(name) { 93 | return Err(anyhow!("duplicate test name: {}", name)); 94 | } 95 | 96 | let text = lines.collect::>().join("\n"); 97 | if !text.is_empty() { 98 | tests.insert( 99 | name.to_string(), 100 | Test { 101 | kind, 102 | name: name.to_string(), 103 | text, 104 | }, 105 | ); 106 | } 107 | } 108 | 109 | Ok(()) 110 | } 111 | 112 | pub(crate) fn run(filters: &[String]) -> anyhow::Result<()> { 113 | let update_patterns: HashSet = filters.iter().cloned().collect::>(); 114 | let mut tests: HashMap = HashMap::new(); 115 | let source_dir = project_root().join("crates/starpls_parser/src/grammar"); 116 | 117 | // Collect tests from all `*.rs` files in the `src` directory. 118 | for entry in fs::read_dir(source_dir)? { 119 | let entry = entry?; 120 | let path = entry.path(); 121 | 122 | // Skip non-`*.rs` files. 123 | if path.extension().unwrap_or_default() != "rs" || !entry.file_type()?.is_file() { 124 | continue; 125 | } 126 | 127 | // Extract tests from the source file's comment blocks. 128 | let input = fs::read_to_string(&path)?; 129 | let blocks = extract_comment_blocks(&input); 130 | add_tests_from_comment_blocks(&mut tests, &blocks)?; 131 | } 132 | 133 | // Create the `test_data/ok` and `test_data/err` directories. 134 | let test_data_dir = project_root().join("crates/starpls_parser/test_data"); 135 | let ok_dir = &test_data_dir.join("ok"); 136 | let err_dir = &test_data_dir.join("err"); 137 | fs::create_dir_all(ok_dir)?; 138 | fs::create_dir_all(err_dir)?; 139 | 140 | // Write tests to their corresponding files. 141 | for test in tests 142 | .values() 143 | .filter(|&test| update_patterns.is_empty() || update_patterns.contains(&test.name)) 144 | { 145 | let dir = match test.kind { 146 | TestKind::Ok => ok_dir, 147 | TestKind::Err => err_dir, 148 | }; 149 | let path = dir.join(format!("{}.star", test.name)); 150 | fs::write(path, &test.text)?; 151 | } 152 | 153 | Ok(()) 154 | } 155 | -------------------------------------------------------------------------------- /xtask/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::Path; 3 | use std::path::PathBuf; 4 | 5 | pub(crate) fn project_root() -> PathBuf { 6 | Path::new( 7 | &env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_string()), 8 | ) 9 | .ancestors() 10 | .nth(1) 11 | .unwrap() 12 | .to_path_buf() 13 | } 14 | --------------------------------------------------------------------------------