Skip to content

added bazel workspace lsp support #51

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bazel/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "bazel"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
84 changes: 84 additions & 0 deletions bazel/src/bazel_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::fmt::Debug;
use std::path::PathBuf;
use std::process::Command;

#[derive(Debug, Clone, PartialEq)]
pub struct BazelInfo {
pub workspace_root: PathBuf,
pub output_base: PathBuf,
pub execroot: PathBuf,
}

fn parse_bazel_info(info: &str) -> Option<BazelInfo> {
let mut workspace_root: Option<PathBuf> = None;
let mut output_base: Option<PathBuf> = None;
let mut execroot: Option<PathBuf> = None;
info.split('\n').for_each(|l| {
let split: Vec<&str> = l.splitn(2, ':').collect();
if split.len() != 2 {
return;
}
let first = split.get(0);
let next = split.get(1);
match (first, next) {
(Some(key), Some(value)) => match key {
&"execution_root" => execroot = Some(value.trim().into()),
&"output_base" => output_base = Some(value.trim().into()),
&"workspace" => workspace_root = Some(value.trim().into()),
_ => {}
},
_ => {}
}
});
match (execroot, output_base, workspace_root) {
(Some(execroot), Some(output_base), Some(workspace_root)) => Some(BazelInfo {
workspace_root,
execroot,
output_base,
}),
_ => {
eprintln!(
"Couldn't find workspace_root, execroot or output_base in output:\n`{}`",
info
);
None
}
}
}

pub fn get_bazel_info(workspace_dir: Option<&str>) -> Option<BazelInfo> {
let mut raw_command = Command::new("bazel");
let mut command = raw_command.arg("info");
command = match workspace_dir {
Some(d) => command.current_dir(d),
None => command,
};

let output = command.output().ok()?;

if !output.status.success() {
return None;
}

let s = std::str::from_utf8(output.stdout.as_slice()).ok()?;

parse_bazel_info(s)
}

#[cfg(test)]
mod tests {
use super::parse_bazel_info;
use super::BazelInfo;

#[test]
fn parses_info() {
assert_eq!(
parse_bazel_info(include_str!("info.txt")),
Some(BazelInfo {
workspace_root: "/home/user/dev/bazel/bazel".into(),
execroot: "/home/user/.cache/bazel/_bazel_user/726bdc44ca84ffc53f631c27e313c4cf/execroot/io_bazel".into(),
output_base: "/home/user/.cache/bazel/_bazel_user/726bdc44ca84ffc53f631c27e313c4cf".into()
})
)
}
}
23 changes: 23 additions & 0 deletions bazel/src/info.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
bazel-bin: /home/user/.cache/bazel/_bazel_user/726bdc44ca84ffc53f631c27e313c4cf/execroot/io_bazel/bazel-out/k8-fastbuild/bin
bazel-genfiles: /home/user/.cache/bazel/_bazel_user/726bdc44ca84ffc53f631c27e313c4cf/execroot/io_bazel/bazel-out/k8-fastbuild/bin
bazel-testlogs: /home/user/.cache/bazel/_bazel_user/726bdc44ca84ffc53f631c27e313c4cf/execroot/io_bazel/bazel-out/k8-fastbuild/testlogs
character-encoding: file.encoding = ISO-8859-1, defaultCharset = ISO-8859-1
command_log: /home/user/.cache/bazel/_bazel_user/726bdc44ca84ffc53f631c27e313c4cf/command.log
committed-heap-size: 418MB
execution_root: /home/user/.cache/bazel/_bazel_user/726bdc44ca84ffc53f631c27e313c4cf/execroot/io_bazel
gc-count: 11
gc-time: 42ms
install_base: /home/user/.cache/bazel/_bazel_user/install/41b71f1bb3ce13f20cfeeb31a9357113
java-home: /home/user/.cache/bazel/_bazel_user/install/41b71f1bb3ce13f20cfeeb31a9357113/embedded_tools/jdk
java-runtime: OpenJDK Runtime Environment (build 11.0.6+10-LTS) by Azul Systems, Inc.
java-vm: OpenJDK 64-Bit Server VM (build 11.0.6+10-LTS, mixed mode) by Azul Systems, Inc.
max-heap-size: 4175MB
output_base: /home/user/.cache/bazel/_bazel_user/726bdc44ca84ffc53f631c27e313c4cf
output_path: /home/user/.cache/bazel/_bazel_user/726bdc44ca84ffc53f631c27e313c4cf/execroot/io_bazel/bazel-out
package_path: %workspace%
release: release 5.2.0
repository_cache: /home/user/.cache/bazel/_bazel_user/cache/repos/v1
server_log: /home/user/.cache/bazel/_bazel_user/726bdc44ca84ffc53f631c27e313c4cf/java.log.home.user.log.java.20220821-211440.206313
server_pid: 206313
used-heap-size: 266MB
workspace: /home/user/dev/bazel/bazel
118 changes: 118 additions & 0 deletions bazel/src/label.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@

use std::path::PathBuf;

use crate::bazel_info::BazelInfo;

pub struct ExternalLabel {
repository: String,
package: String,
target: String,
}
pub struct LocalLabel {
package: String,
target: String,
}

pub struct RelativeLabel {
sub_package: String,
target: String,
}

pub enum Label {
External(ExternalLabel),
Local(LocalLabel),
Relative(RelativeLabel),
}

impl Label {
fn split_package_target(label: &str) -> Option<(&str, &str)> {
let mut split_parts = label.split(":");
let package = split_parts.next()?;
let target = split_parts.next()?;
Some((package, target))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, //foo can be used as a short-hand for //foo:foo,
and relative labels can skip the :, e.g. foo instead of :foo.

}

fn split_repository_package_target(label: &str) -> Option<(&str, &str, &str)> {
let mut split_parts = label.split("//");
let repository = split_parts.next()?;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And @foo as a short-hand for @foo//:foo.

let package_target = split_parts.next()?;
let (package, target) = Self::split_package_target(package_target)?;
Some((repository, package, target))
}

pub fn replace_fake_file_with_build_target(fake_file: PathBuf) -> Option<PathBuf> {
if fake_file.exists() {
return Some(fake_file);
}
fake_file.parent().and_then(|p| {
let build = p.join("BUILD");
let build_bazel = p.join("BUILD.bazel");
if build.exists() {
Some(build)
} else if build_bazel.exists() {
Some(build_bazel)
} else {
None
}
})
}

pub fn resolve(
self,
bazel_info: &BazelInfo,
current_file_dir: Option<PathBuf>,
) -> Option<PathBuf> {
// TODO: support nested workspaces either by getting info again or at the start getting the info for all the workspaces in the directory
match self {
Label::External(l) => {
let execroot_dirname = bazel_info.execroot.file_name()?;

if l.repository == execroot_dirname.to_str()? {
Some(bazel_info.workspace_root.join(l.package).join(l.target))
} else {
Some(
bazel_info
.output_base
.join("external")
.join(l.repository)
.join(l.package)
.join(l.target),
)
}
}
Label::Local(l) => Some(bazel_info.workspace_root.join(l.package).join(l.target)),
Label::Relative(l) => {
current_file_dir.and_then(|d| Some(d.join(l.sub_package).join(l.target)))
}
}
}

pub fn new(label: &str) -> Option<Self> {
if !label.contains(":") {
return None;
}

if label.starts_with("@") {
let (repository, package, target) =
Self::split_repository_package_target(label.trim_start_matches('@'))?;
return Some(Label::External(ExternalLabel {
repository: repository.to_owned(),
package: package.to_owned(),
target: target.to_owned(),
}));
}
if label.starts_with("//") {
let (package, target) = Self::split_package_target(label.trim_start_matches("//"))?;
return Some(Label::Local(LocalLabel {
package: package.to_owned(),
target: target.to_owned(),
}));
}

let (package, target) = Self::split_package_target(label)?;
Some(Label::Relative(RelativeLabel {
sub_package: package.to_owned(),
target: target.to_owned(),
}))
}
}
2 changes: 2 additions & 0 deletions bazel/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod bazel_info;
pub mod label;
1 change: 1 addition & 0 deletions starlark/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ either = "1.6.1"
static_assertions = "1.1.0"
memoffset = "0.6.4"
thiserror = "1.0.30"
bazel = { version = "0.1.0", path = "../bazel" }
starlark_derive = { version = "0.9.0-pre", path = "../starlark_derive" }
starlark_map = { version = "0.9.0-pre", path = "../starlark_map" }
gazebo.version = "0.8.0"
Expand Down
89 changes: 77 additions & 12 deletions starlark/bin/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ use std::iter;
use std::path::Path;
use std::path::PathBuf;

use bazel::bazel_info::BazelInfo;
use bazel::label::Label;
use gazebo::prelude::*;
use itertools::Either;
use lsp_types::Diagnostic;
use lsp_types::Range;
use lsp_types::Url;
use starlark::environment::FrozenModule;
use starlark::environment::Globals;
Expand Down Expand Up @@ -58,6 +61,7 @@ pub(crate) struct Context {
pub(crate) module: Option<Module>,
pub(crate) builtin_docs: HashMap<LspUrl, String>,
pub(crate) builtin_symbols: HashMap<String, LspUrl>,
pub(crate) bazel_info: Option<BazelInfo>,
}

/// The outcome of evaluating (checking, parsing or running) given starlark code.
Expand Down Expand Up @@ -110,6 +114,7 @@ impl Context {
module,
builtin_docs,
builtin_symbols,
bazel_info: None,
})
}

Expand Down Expand Up @@ -241,6 +246,56 @@ impl Context {
}
}

fn find_location_in_build_file(
info: &Option<BazelInfo>,
literal: String,
current_file_pathbuf: PathBuf,
ast: &AstModule,
) -> anyhow::Result<Option<Range>> {
let resolved_file = label_into_file(info, literal.as_str(), &current_file_pathbuf, false)?;
let basename = resolved_file.file_name().and_then(|f| f.to_str()).ok_or(
ResolveLoadError::ResolvedDoesNotExist(resolved_file.clone()),
)?;
let resolved_span = ast
.find_function_call_with_name(basename)
.and_then(|r| Some(Range::from(r)));
Ok(resolved_span)
}

fn label_into_file(
bazel_info: &Option<BazelInfo>,
path: &str,
current_file_path: &PathBuf,
replace_build_file: bool,
) -> Result<PathBuf, ResolveLoadError> {
let current_file_dir = current_file_path
.parent()
.and_then(|x| Some(PathBuf::from(x)));
let path_buf = PathBuf::from(path);
let label = Label::new(path);

// TODO: not really malformed should we propogate error from label.resolve or just create a new error: CouldntFind Bazel Label
match (bazel_info, label) {
(Some(info), Some(label)) => label
.resolve(info, current_file_dir)
.and_then(|x| {
if replace_build_file {
Label::replace_fake_file_with_build_target(x)
} else {
Some(x)
}
})
.and_then(|x| Some(x.canonicalize().unwrap_or(x)))
.ok_or(ResolveLoadError::PathMalformed(path_buf.clone())),

_ => match (current_file_dir, path_buf.is_absolute()) {
(_, true) => Ok(path_buf),
(Some(current_file_dir), false) => Ok(current_file_dir.join(&path_buf)),
(None, false) => Err(ResolveLoadError::MissingCurrentFilePath(path_buf)),
},
}
}

impl LspContext for Context {
fn parse_file_with_contents(&self, uri: &LspUrl, content: String) -> LspEvalResult {
match uri {
Expand All @@ -257,16 +312,11 @@ impl LspContext for Context {
}

fn resolve_load(&self, path: &str, current_file: &LspUrl) -> anyhow::Result<LspUrl> {
let path = PathBuf::from(path);
match current_file {
LspUrl::File(current_file_path) => {
let current_file_dir = current_file_path.parent();
let absolute_path = match (current_file_dir, path.is_absolute()) {
(_, true) => Ok(path),
(Some(current_file_dir), false) => Ok(current_file_dir.join(&path)),
(None, false) => Err(ResolveLoadError::MissingCurrentFilePath(path)),
}?;
Ok(Url::from_file_path(absolute_path).unwrap().try_into()?)
let resolved_file =
label_into_file(&self.bazel_info, path, current_file_path, true)?;
Ok(Url::from_file_path(resolved_file).unwrap().try_into()?)
}
_ => Err(
ResolveLoadError::WrongScheme("file://".to_owned(), current_file.clone()).into(),
Expand All @@ -279,11 +329,26 @@ impl LspContext for Context {
literal: &str,
current_file: &LspUrl,
) -> anyhow::Result<Option<StringLiteralResult>> {
let current_file_pathbuf = current_file.path().to_path_buf();
self.resolve_load(literal, current_file).map(|url| {
Some(StringLiteralResult {
url,
location_finder: None,
})
let p = url.path();
// TODO: we can always give literal location finder
// TODO: but if its a file it will always try to resolve the location but won't be able to and an error will be printed
if self.bazel_info.is_some() && p.ends_with("BUILD") || p.ends_with("BUILD.bazel") {
let info = self.bazel_info.clone();
let literal_copy = literal.to_owned();
Some(StringLiteralResult {
url,
location_finder: Some(box move |ast: &AstModule, _url| {
find_location_in_build_file(&info, literal_copy, current_file_pathbuf, ast)
}),
})
} else {
Some(StringLiteralResult {
url,
location_finder: None,
})
}
})
}

Expand Down
Loading