Skip to content

Commit bc34622

Browse files
committed
added bazel workspace lsp support
1 parent e87a2d9 commit bc34622

File tree

3 files changed

+234
-21
lines changed

3 files changed

+234
-21
lines changed

starlark/bin/eval.rs

+184-11
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use std::path::PathBuf;
2525
use gazebo::prelude::*;
2626
use itertools::Either;
2727
use lsp_types::Diagnostic;
28+
use lsp_types::Range;
2829
use lsp_types::Url;
2930
use starlark::environment::FrozenModule;
3031
use starlark::environment::Globals;
@@ -50,6 +51,13 @@ pub(crate) enum ContextMode {
5051
Run,
5152
}
5253

54+
#[derive(Debug, Clone)]
55+
pub(crate) struct BazelInfo {
56+
pub(crate) workspace_root: PathBuf,
57+
pub(crate) output_base: PathBuf,
58+
pub(crate) execroot: PathBuf,
59+
}
60+
5361
#[derive(Debug)]
5462
pub(crate) struct Context {
5563
pub(crate) mode: ContextMode,
@@ -58,6 +66,7 @@ pub(crate) struct Context {
5866
pub(crate) module: Option<Module>,
5967
pub(crate) builtin_docs: HashMap<LspUrl, String>,
6068
pub(crate) builtin_symbols: HashMap<String, LspUrl>,
69+
pub(crate) bazel_info: Option<BazelInfo>,
6170
}
6271

6372
/// The outcome of evaluating (checking, parsing or running) given starlark code.
@@ -110,6 +119,7 @@ impl Context {
110119
module,
111120
builtin_docs,
112121
builtin_symbols,
122+
bazel_info: None,
113123
})
114124
}
115125

@@ -241,6 +251,147 @@ impl Context {
241251
}
242252
}
243253

254+
fn handle_local_bazel_repository(
255+
info: &Option<BazelInfo>,
256+
path: &str,
257+
path_buf: PathBuf,
258+
current_file_dir: &PathBuf,
259+
) -> Result<PathBuf, ResolveLoadError> {
260+
match info {
261+
None => Err(ResolveLoadError::MissingBazelInfo(path_buf)),
262+
Some(info) => {
263+
let malformed_err = ResolveLoadError::PathMalformed(path_buf.clone());
264+
let mut split_parts = path.trim_start_match("//").split(':');
265+
let package = split_parts.next().ok_or(malformed_err.clone())?;
266+
let target = split_parts.next().ok_or(malformed_err.clone())?;
267+
match split_parts.next().is_some() {
268+
true => Err(malformed_err.clone()),
269+
false => {
270+
let file_path = PathBuf::from(package).join(target);
271+
let root_path = current_file_dir
272+
.ancestors()
273+
.find(|a| match a.read_dir() {
274+
Ok(mut entries) => entries
275+
.find(|f| match f {
276+
Ok(f) => ["MODULE.bazel", "WORKSPACE", "WORKSPACE.bazel"]
277+
.contains(&f.file_name().to_str().unwrap_or("")),
278+
_ => false,
279+
})
280+
.is_some(),
281+
_ => false,
282+
})
283+
.unwrap_or(&info.workspace_root);
284+
Ok(root_path.join(file_path))
285+
}
286+
}
287+
}
288+
}
289+
}
290+
fn handle_remote_bazel_repository(
291+
info: &Option<BazelInfo>,
292+
path: &str,
293+
path_buf: PathBuf,
294+
) -> Result<PathBuf, ResolveLoadError> {
295+
match info {
296+
None => Err(ResolveLoadError::MissingBazelInfo(path_buf)),
297+
Some(info) => {
298+
let malformed_err = ResolveLoadError::PathMalformed(path_buf.clone());
299+
let mut split_parts = path.trim_start_match("@").split("//");
300+
let repository = split_parts.next().ok_or(malformed_err.clone())?;
301+
split_parts = split_parts.next().ok_or(malformed_err.clone())?.split(":");
302+
let package = split_parts.next().ok_or(malformed_err.clone())?;
303+
let target = split_parts.next().ok_or(malformed_err.clone())?;
304+
match split_parts.next().is_some() {
305+
true => Err(malformed_err.clone()),
306+
false => {
307+
let execroot_dirname =
308+
info.execroot.file_name().ok_or(malformed_err.clone())?;
309+
310+
if repository == execroot_dirname {
311+
Ok(info.workspace_root.join(package).join(target))
312+
} else {
313+
Ok(info
314+
.output_base
315+
.join("external")
316+
.join(repository)
317+
.join(package)
318+
.join(target))
319+
}
320+
}
321+
}
322+
}
323+
}
324+
}
325+
326+
fn get_relative_file(
327+
current_file_dir: &PathBuf,
328+
path: &str,
329+
pathbuf: PathBuf,
330+
) -> Result<PathBuf, ResolveLoadError> {
331+
let malformed_err = ResolveLoadError::MissingCurrentFilePath(pathbuf.clone());
332+
let mut split_parts = path.split(":");
333+
let package = split_parts.next().ok_or(malformed_err.clone())?;
334+
let target = split_parts.next().ok_or(malformed_err.clone())?;
335+
match split_parts.next().is_some() {
336+
true => Err(malformed_err.clone()),
337+
false => Ok(current_file_dir.join(package).join(target)),
338+
}
339+
}
340+
fn label_into_file(
341+
bazel_info: &Option<BazelInfo>,
342+
path: &str,
343+
current_file_path: &PathBuf,
344+
) -> Result<PathBuf, ResolveLoadError> {
345+
let current_file_dir = current_file_path.parent();
346+
let path_buf = PathBuf::from(path);
347+
348+
if path.starts_with("@") {
349+
handle_remote_bazel_repository(bazel_info, path, path_buf.clone())
350+
} else if path.starts_with("//") {
351+
handle_local_bazel_repository(bazel_info, path, path_buf.clone(), current_file_path)
352+
} else if path.contains(":") {
353+
match current_file_dir {
354+
Some(dir) => get_relative_file(&dir.to_path_buf(), path, path_buf.clone()),
355+
None => Err(ResolveLoadError::MissingCurrentFilePath(path_buf)),
356+
}
357+
} else {
358+
match (current_file_dir, path_buf.is_absolute()) {
359+
(_, true) => Ok(path_buf),
360+
(Some(current_file_dir), false) => Ok(current_file_dir.join(&path_buf)),
361+
(None, false) => Err(ResolveLoadError::MissingCurrentFilePath(path_buf)),
362+
}
363+
}
364+
}
365+
366+
fn replace_fake_file_with_build_target(fake_file: PathBuf) -> Option<PathBuf> {
367+
fake_file.parent().and_then(|p| {
368+
let build = p.join("BUILD");
369+
let build_bazel = p.join("BUILD.bazel");
370+
if build.exists() {
371+
Some(build)
372+
} else if build_bazel.exists() {
373+
Some(build_bazel)
374+
} else {
375+
None
376+
}
377+
})
378+
}
379+
380+
fn find_location_in_build_file(
381+
info: Option<BazelInfo>,
382+
literal: String,
383+
current_file_pathbuf: PathBuf,
384+
ast: &AstModule,
385+
) -> anyhow::Result<Option<Range>> {
386+
let resolved_file = label_into_file(&info, literal.as_str(), &current_file_pathbuf)?;
387+
let basename = resolved_file.file_name().and_then(|f| f.to_str()).ok_or(
388+
ResolveLoadError::ResolvedDoesNotExist(resolved_file.clone()),
389+
)?;
390+
let resolved_span = ast
391+
.find_function_call_with_name(basename)
392+
.and_then(|r| Some(Range::from(r)));
393+
Ok(resolved_span)
394+
}
244395
impl LspContext for Context {
245396
fn parse_file_with_contents(&self, uri: &LspUrl, content: String) -> LspEvalResult {
246397
match uri {
@@ -257,16 +408,23 @@ impl LspContext for Context {
257408
}
258409

259410
fn resolve_load(&self, path: &str, current_file: &LspUrl) -> anyhow::Result<LspUrl> {
260-
let path = PathBuf::from(path);
261411
match current_file {
262412
LspUrl::File(current_file_path) => {
263-
let current_file_dir = current_file_path.parent();
264-
let absolute_path = match (current_file_dir, path.is_absolute()) {
265-
(_, true) => Ok(path),
266-
(Some(current_file_dir), false) => Ok(current_file_dir.join(&path)),
267-
(None, false) => Err(ResolveLoadError::MissingCurrentFilePath(path)),
413+
let mut resolved_file = label_into_file(&self.bazel_info, path, current_file_path)?;
414+
resolved_file = match resolved_file.canonicalize() {
415+
Ok(f) => {
416+
if f.exists() {
417+
Ok(f)
418+
} else {
419+
replace_fake_file_with_build_target(resolved_file.clone())
420+
.ok_or(ResolveLoadError::ResolvedDoesNotExist(resolved_file))
421+
}
422+
}
423+
_ => replace_fake_file_with_build_target(resolved_file.clone())
424+
.ok_or(ResolveLoadError::ResolvedDoesNotExist(resolved_file)),
268425
}?;
269-
Ok(Url::from_file_path(absolute_path).unwrap().try_into()?)
426+
427+
Ok(Url::from_file_path(resolved_file).unwrap().try_into()?)
270428
}
271429
_ => Err(
272430
ResolveLoadError::WrongScheme("file://".to_owned(), current_file.clone()).into(),
@@ -279,11 +437,26 @@ impl LspContext for Context {
279437
literal: &str,
280438
current_file: &LspUrl,
281439
) -> anyhow::Result<Option<StringLiteralResult>> {
440+
let current_file_pathbuf = current_file.path().to_path_buf();
282441
self.resolve_load(literal, current_file).map(|url| {
283-
Some(StringLiteralResult {
284-
url,
285-
location_finder: None,
286-
})
442+
let p = url.path();
443+
// TODO: we can always give literal location finder
444+
// 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
445+
if p.ends_with("BUILD") || p.ends_with("BUILD.bazel") {
446+
let info = self.bazel_info.clone();
447+
let literal_copy = literal.to_owned();
448+
Some(StringLiteralResult {
449+
url,
450+
location_finder: Some(box |ast: &AstModule, _url| {
451+
find_location_in_build_file(info, literal_copy, current_file_pathbuf, ast)
452+
}),
453+
})
454+
} else {
455+
Some(StringLiteralResult {
456+
url,
457+
location_finder: None,
458+
})
459+
}
287460
})
288461
}
289462

starlark/bin/main.rs

+25
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ use std::ffi::OsStr;
3030
use std::fmt;
3131
use std::fmt::Display;
3232
use std::path::PathBuf;
33+
use std::process::Command;
3334
use std::sync::Arc;
3435

3536
use anyhow::anyhow;
37+
use eval::BazelInfo;
3638
use eval::Context;
3739
use gazebo::prelude::*;
3840
use itertools::Either;
@@ -214,6 +216,28 @@ fn interactive(ctx: &Context) -> anyhow::Result<()> {
214216
}
215217
}
216218

219+
fn get_bazel_info_path(path_type: &str) -> anyhow::Result<PathBuf> {
220+
let output = Command::new("bazel").arg("info").arg(path_type).output()?;
221+
222+
if !output.status.success() {
223+
return Err(anyhow!("Failed running command bazel info"));
224+
}
225+
let s = std::str::from_utf8(output.stdout.as_slice())?;
226+
Ok(PathBuf::from(s.trim()))
227+
}
228+
229+
fn get_bazel_info() -> Option<BazelInfo> {
230+
let workspace_root = get_bazel_info_path("workspace").ok()?;
231+
let output_base = get_bazel_info_path("output_base").ok()?;
232+
let execroot = get_bazel_info_path("execution_root").ok()?;
233+
234+
Some(BazelInfo {
235+
workspace_root,
236+
output_base,
237+
execroot,
238+
})
239+
}
240+
217241
fn main() -> anyhow::Result<()> {
218242
gazebo::terminate_on_panic();
219243

@@ -242,6 +266,7 @@ fn main() -> anyhow::Result<()> {
242266

243267
if args.lsp {
244268
ctx.mode = ContextMode::Check;
269+
ctx.bazel_info = get_bazel_info();
245270
lsp::server::stdio_server(ctx)?;
246271
} else if is_interactive {
247272
interactive(&ctx)?;

starlark/src/lsp/server.rs

+25-10
Original file line numberDiff line numberDiff line change
@@ -292,15 +292,24 @@ pub trait LspContext {
292292
}
293293

294294
/// Errors when [`LspContext::resolve_load()`] cannot resolve a given path.
295-
#[derive(thiserror::Error, Debug)]
295+
#[derive(thiserror::Error, Debug, Clone)]
296296
pub enum ResolveLoadError {
297+
/// Attempted to resolve a load but the path was malformed
298+
#[error("path `{}` provided, but was malformed", .0.display())]
299+
PathMalformed(PathBuf),
300+
/// Attempted to resolve a bazel dependent load but no bazel info could be found
301+
#[error("path `{}` provided, but bazel info could not be determined", .0.display())]
302+
MissingBazelInfo(PathBuf),
297303
/// Attempted to resolve a relative path, but no current_file_path was provided,
298304
/// so it is not known what to resolve the path against.
299305
#[error("Relative path `{}` provided, but current_file_path could not be determined", .0.display())]
300306
MissingCurrentFilePath(PathBuf),
301307
/// The scheme provided was not correct or supported.
302308
#[error("Url `{}` was expected to be of type `{}`", .1, .0)]
303309
WrongScheme(String, LspUrl),
310+
/// Resolved Loaded file does not exist.
311+
#[error("Resolved file `{}` did not exist", .0.display())]
312+
ResolvedDoesNotExist(PathBuf),
304313
}
305314

306315
/// Errors when loading contents of a starlark program.
@@ -494,8 +503,7 @@ impl<T: LspContext> Backend<T> {
494503
}) => {
495504
// If there's an error loading the file to parse it, at least
496505
// try to get to the file.
497-
let target_range = self
498-
.get_ast_or_load_from_disk(&url)
506+
self.get_ast_or_load_from_disk(&url)
499507
.and_then(|ast| match ast {
500508
Some(module) => location_finder(&module.ast, &url),
501509
None => Ok(None),
@@ -504,13 +512,20 @@ impl<T: LspContext> Backend<T> {
504512
eprintln!("Error jumping to definition: {:#}", e);
505513
})
506514
.unwrap_or_default()
507-
.unwrap_or_default();
508-
Some(LocationLink {
509-
origin_selection_range: Some(source.into()),
510-
target_uri: url.try_into()?,
511-
target_range,
512-
target_selection_range: target_range,
513-
})
515+
.and_then(|target_range| {
516+
Some(LocationLink {
517+
origin_selection_range: Some(source.into()),
518+
target_uri: url.clone().try_into().ok()?,
519+
target_range,
520+
target_selection_range: target_range,
521+
})
522+
})
523+
.or(Some(LocationLink {
524+
origin_selection_range: Some(source.into()),
525+
target_uri: url.try_into()?,
526+
target_range: Range::default(),
527+
target_selection_range: Range::default(),
528+
}))
514529
}
515530
Some(StringLiteralResult {
516531
url,

0 commit comments

Comments
 (0)