/* * Copyright 2019 The Starlark in Rust Authors. * Copyright (c) Facebook, Inc. and its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use std::collections::HashMap; use std::collections::HashSet; use std::fs; use std::io; use std::iter; use std::path::Path; use std::path::PathBuf; use itertools::Either; use lsp_types::Url; use starlark::analysis::AstModuleLint; use starlark::docs::get_registered_starlark_docs; use starlark::docs::render_docs_as_code; use starlark::docs::Doc; use starlark::docs::DocItem; use starlark::docs::DocModule; use starlark::environment::FrozenModule; use starlark::environment::Globals; use starlark::environment::Module; use starlark::errors::EvalMessage; use starlark::eval::Evaluator; use starlark::syntax::AstModule; use starlark::syntax::Dialect; use starlark_lsp::error::eval_message_to_lsp_diagnostic; use starlark_lsp::server::LspContext; use starlark_lsp::server::LspEvalResult; use starlark_lsp::server::LspUrl; use starlark_lsp::server::StringLiteralResult; #[derive(Debug)] pub(crate) enum ContextMode { Check, Run, } #[derive(Debug, thiserror::Error)] enum ContextError { /// The provided Url was not absolute and it needs to be. #[error("Path for URL `{}` was not absolute", .0)] NotAbsolute(LspUrl), /// The scheme provided was not correct or supported. #[error("Url `{}` was expected to be of type `{}`", .1, .0)] WrongScheme(String, LspUrl), } #[derive(Debug)] pub(crate) struct Context { pub(crate) mode: ContextMode, pub(crate) print_non_none: bool, pub(crate) prelude: Vec, pub(crate) module: Option, pub(crate) builtin_docs: HashMap, pub(crate) builtin_symbols: HashMap, } /// The outcome of evaluating (checking, parsing or running) given starlark code. pub(crate) struct EvalResult> { /// The diagnostic and error messages from evaluating a given piece of starlark code. pub messages: T, /// If the code is only parsed, not run, and there were no errors, this will contain /// the parsed module. Otherwise, it will be `None` pub ast: Option, } /// Errors when [`LspContext::resolve_load()`] cannot resolve a given path. #[derive(thiserror::Error, Debug)] enum ResolveLoadError { /// Attempted to resolve a relative path, but no current_file_path was provided, /// so it is not known what to resolve the path against. #[error("Relative path `{}` provided, but current_file_path could not be determined", .0.display())] MissingCurrentFilePath(PathBuf), /// The scheme provided was not correct or supported. #[error("Url `{}` was expected to be of type `{}`", .1, .0)] WrongScheme(String, LspUrl), } impl Context { pub(crate) fn new( mode: ContextMode, print_non_none: bool, prelude: &[PathBuf], module: bool, ) -> anyhow::Result { let globals = globals(); let prelude: Vec<_> = prelude .iter() .map(|x| { let env = Module::new(); { let mut eval = Evaluator::new(&env); let module = AstModule::parse_file(x, &dialect()) .map_err(starlark::Error::into_anyhow)?; eval.eval_module(module, &globals) .map_err(starlark::Error::into_anyhow)?; } env.freeze() }) .collect::>()?; let module = if module { Some(Self::new_module(&prelude)) } else { None }; let mut builtins: HashMap> = HashMap::new(); let mut builtin_symbols: HashMap = HashMap::new(); for doc in get_registered_starlark_docs() { let uri = Self::url_for_doc(&doc); builtin_symbols.insert(doc.id.name.clone(), uri.clone()); builtins.entry(uri).or_default().push(doc); } let builtin_docs = builtins .into_iter() .map(|(u, ds)| (u, render_docs_as_code(&ds))) .collect(); Ok(Self { mode, print_non_none, prelude, module, builtin_docs, builtin_symbols, }) } fn url_for_doc(doc: &Doc) -> LspUrl { let url = match &doc.item { DocItem::Module(_) => Url::parse("starlark:/native/builtins.bzl").unwrap(), DocItem::Object(_) => { Url::parse(&format!("starlark:/native/builtins/{}.bzl", doc.id.name)).unwrap() } DocItem::Function(_) | DocItem::Property(_) => { Url::parse("starlark:/native/builtins.bzl").unwrap() } }; LspUrl::try_from(url).unwrap() } fn new_module(prelude: &[FrozenModule]) -> Module { let module = Module::new(); for p in prelude { module.import_public_symbols(p); } module } fn go(&self, file: &str, ast: AstModule) -> EvalResult> { let mut warnings = Either::Left(iter::empty()); let mut errors = Either::Left(iter::empty()); let final_ast = match self.mode { ContextMode::Check => { warnings = Either::Right(self.check(&ast)); Some(ast) } ContextMode::Run => { errors = Either::Right(self.run(file, ast).messages); None } }; EvalResult { messages: warnings.chain(errors), ast: final_ast, } } // Convert a result over iterator of EvalMessage, into an iterator of EvalMessage fn err( file: &str, result: starlark::Result>>, ) -> EvalResult> { match result { Err(e) => EvalResult { messages: Either::Left(iter::once(EvalMessage::from_error(Path::new(file), &e))), ast: None, }, Ok(res) => EvalResult { messages: Either::Right(res.messages), ast: res.ast, }, } } pub(crate) fn expression( &self, content: String, ) -> EvalResult> { let file = "expression"; Self::err( file, AstModule::parse(file, content, &dialect()) .map(|module| self.go(file, module)) .map_err(Into::into), ) } pub(crate) fn file(&self, file: &Path) -> EvalResult> { let filename = &file.to_string_lossy(); Self::err( filename, fs::read_to_string(file) .map(|content| self.file_with_contents(filename, content)) .map_err(|e| anyhow::Error::from(e).into()), ) } pub(crate) fn file_with_contents( &self, filename: &str, content: String, ) -> EvalResult> { Self::err( filename, AstModule::parse(filename, content, &dialect()) .map(|module| self.go(filename, module)) .map_err(Into::into), ) } fn run(&self, file: &str, ast: AstModule) -> EvalResult> { let new_module; let module = match self.module.as_ref() { Some(module) => module, None => { new_module = Self::new_module(&self.prelude); &new_module } }; let mut eval = Evaluator::new(module); eval.enable_terminal_breakpoint_console(); let globals = globals(); Self::err( file, eval.eval_module(ast, &globals) .map(|v| { if self.print_non_none && !v.is_none() { println!("{}", v); } EvalResult { messages: iter::empty(), ast: None, } }) .map_err(Into::into), ) } fn check(&self, module: &AstModule) -> impl Iterator { let globals = if self.prelude.is_empty() { None } else { let mut globals = HashSet::new(); for modu in &self.prelude { for name in modu.names() { globals.insert(name.as_str().to_owned()); } } for global_symbol in self.builtin_symbols.keys() { globals.insert(global_symbol.to_owned()); } Some(globals) }; module .lint(globals.as_ref()) .into_iter() .map(EvalMessage::from) } } impl LspContext for Context { fn parse_file_with_contents(&self, uri: &LspUrl, content: String) -> LspEvalResult { match uri { LspUrl::File(uri) => { let EvalResult { messages, ast } = self.file_with_contents(&uri.to_string_lossy(), content); LspEvalResult { diagnostics: messages.map(eval_message_to_lsp_diagnostic).collect(), ast, } } _ => LspEvalResult::default(), } } fn resolve_load( &self, path: &str, current_file: &LspUrl, _workspace_root: Option<&Path>, ) -> anyhow::Result { 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()?) } _ => Err( ResolveLoadError::WrongScheme("file://".to_owned(), current_file.clone()).into(), ), } } fn resolve_string_literal( &self, literal: &str, current_file: &LspUrl, workspace_root: Option<&Path>, ) -> anyhow::Result> { self.resolve_load(literal, current_file, workspace_root) .map(|url| { Some(StringLiteralResult { url, location_finder: None, }) }) } fn get_load_contents(&self, uri: &LspUrl) -> anyhow::Result> { match uri { LspUrl::File(path) => match path.is_absolute() { true => match fs::read_to_string(path) { Ok(contents) => Ok(Some(contents)), Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None), Err(e) => Err(e.into()), }, false => Err(ContextError::NotAbsolute(uri.clone()).into()), }, LspUrl::Starlark(_) => Ok(self.builtin_docs.get(uri).cloned()), _ => Err(ContextError::WrongScheme("file://".to_owned(), uri.clone()).into()), } } fn get_url_for_global_symbol( &self, _current_file: &LspUrl, symbol: &str, ) -> anyhow::Result> { Ok(self.builtin_symbols.get(symbol).cloned()) } fn render_as_load( &self, _target: &LspUrl, _current_file: &LspUrl, _workspace_root: Option<&Path>, ) -> anyhow::Result { Err(anyhow::anyhow!("Not yet implemented, render_as_load")) } fn get_environment(&self, _uri: &LspUrl) -> DocModule { DocModule::default() } } pub(crate) fn globals() -> Globals { Globals::extended_internal() } pub(crate) fn dialect() -> Dialect { Dialect::Extended }