/*
 * Copyright 2018 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.
 */

//! A [Starlark interpreter in Rust](https://github.com/facebookexperimental/starlark-rust).
//! Starlark is a deterministic version of Python, with [a specification](https://github.com/bazelbuild/starlark/blob/master/spec.md),
//! used by (amongst others) the [Buck](https://buck.build) and [Bazel](https://bazel.build) build systems.
//!
//! To evaluate a simple file:
//!
//! ```
//! # fn run() -> anyhow::Result<()> {
//! use starlark::eval::Evaluator;
//! use starlark::environment::{Module, Globals};
//! use starlark::values::Value;
//! use starlark::syntax::{AstModule, Dialect};
//!
//! let content = r#"
//! def hello():
//!    return "hello"
//! hello() + " world!"
//! "#;
//!
//! // We first parse the content, giving a filename and the Starlark
//! // `Dialect` we'd like to use (we pick standard).
//! let ast: AstModule = AstModule::parse("hello_world.star", content.to_owned(), &Dialect::Standard)?;
//!
//! // We create a `Globals`, defining the standard library functions available.
//! // The `standard` function uses those defined in the Starlark specification.
//! let globals: Globals = Globals::standard();
//!
//! // We create a `Module`, which stores the global variables for our calculation.
//! let module: Module = Module::new();
//!
//! // We create an evaluator, which controls how evaluation occurs.
//! let mut eval: Evaluator = Evaluator::new(&module);
//!
//! // And finally we evaluate the code using the evaluator.
//! let res: Value = eval.eval_module(ast, &globals)?;
//! assert_eq!(res.unpack_str(), Some("hello world!"));
//! # Ok(())
//! # }
//! # fn main(){ run().unwrap(); }
//! ```
//!
//! From this example there are lots of ways to extend it, which we do so below.
//!
//! ## Call Rust functions from Starlark
//!
//! We want to define a function in Rust (that computes quadratics), and then call it from Starlark.
//! We define the function using the [`#[starlark_module]`](macro@starlark_module) attribute, and add it to
//! a [`Globals`](environment::Globals) object.
//!
//! ```
//! #[macro_use]
//! extern crate starlark;
//! # fn run() -> anyhow::Result<()> {
//! use starlark::environment::{GlobalsBuilder, Module};
//! use starlark::eval::Evaluator;
//! use starlark::syntax::{AstModule, Dialect};
//! use starlark::values::Value;
//!
//! // This defines the function that is visible to Starlark
//! #[starlark_module]
//! fn starlark_quadratic(builder: &mut GlobalsBuilder) {
//!     fn quadratic(a: i32, b: i32, c: i32, x: i32) -> anyhow::Result<i32> {
//!         Ok(a * x * x + b * x + c)
//!     }
//! }
//!
//! // We build our globals to make the function available in Starlark
//! let globals = GlobalsBuilder::new().with(starlark_quadratic).build();
//! let module = Module::new();
//! let mut eval = Evaluator::new(&module);
//!
//! // Let's test calling the function from Starlark code
//! let starlark_code = r#"
//! quadratic(4, 2, 1, x = 8)
//! "#;
//!
//! let ast = AstModule::parse("quadratic.star", starlark_code.to_owned(), &Dialect::Standard)?;
//! let res = eval.eval_module(ast, &globals)?;
//! assert_eq!(res.unpack_i32(), Some(273)); // Verify that we got an `int` return value of 4 * 8^2 + 2 * 8 + 1 = 273
//! # Ok(())
//! # }
//! # fn main(){ run().unwrap(); }
//! ```
//!
//! ## Collect Starlark values
//!
//! If we want to use Starlark as an enhanced JSON, we can define an `emit` function
//! to "write out" a JSON value, and use the [`Evaluator.extra`](eval::Evaluator::extra) field to store it.
//!
//! ```
//! #[macro_use]
//! extern crate starlark;
//! # fn run() -> anyhow::Result<()> {
//! use starlark::environment::{GlobalsBuilder, Module};
//! use starlark::eval::Evaluator;
//! use starlark::syntax::{AstModule, Dialect};
//! use starlark::values::{none::NoneType, Value, ValueLike};
//! use starlark::any::ProvidesStaticType;
//! use std::cell::RefCell;
//!
//! let content = r#"
//! emit(1)
//! emit(["test"])
//! emit({"x": "y"})
//! "#;
//!
//! // Define a store in which to accumulate JSON strings
//! #[derive(Debug, ProvidesStaticType, Default)]
//! struct Store(RefCell<Vec<String>>);
//!
//! impl Store {
//!     fn add(&self, x: String) {
//!          self.0.borrow_mut().push(x)
//!     }
//! }
//!
//! #[starlark_module]
//! fn starlark_emit(builder: &mut GlobalsBuilder) {
//!     fn emit(x: Value, eval: &mut Evaluator) -> anyhow::Result<NoneType> {
//!         // We modify extra (which we know is a Store) and add the JSON of the
//!         // value the user gave.
//!         eval.extra
//!             .unwrap()
//!             .downcast_ref::<Store>()
//!             .unwrap()
//!             .add(x.to_json()?);
//!         Ok(NoneType)
//!     }
//! }
//!
//! let ast = AstModule::parse("json.star", content.to_owned(), &Dialect::Standard)?;
//! // We build our globals adding some functions we wrote
//! let globals = GlobalsBuilder::new().with(starlark_emit).build();
//! let module = Module::new();
//! let store = Store::default();
//! {
//!     let mut eval = Evaluator::new(&module);
//!     // We add a reference to our store
//!     eval.extra = Some(&store);
//!     eval.eval_module(ast, &globals)?;
//! }
//! assert_eq!(&*store.0.borrow(), &["1", "[\"test\"]", "{\"x\":\"y\"}"]);
//! # Ok(())
//! # }
//! # fn main(){ run().unwrap(); }
//! ```
//!
//! ## Enable Starlark extensions (e.g. types)
//!
//! Our Starlark supports a number of extensions, including type annotations, which are
//! controlled by the [`Dialect`](syntax::Dialect) type.
//!
//! ```
//! # fn run() -> anyhow::Result<()> {
//! use starlark::environment::{Globals, Module};
//! use starlark::eval::Evaluator;
//! use starlark::syntax::{AstModule, Dialect, DialectTypes};
//!
//! let content = r#"
//! def takes_int(x: int.type):
//!     pass
//! takes_int("test")
//! "#;
//!
//! // Make the dialect enable types
//! let dialect = Dialect {enable_types: DialectTypes::Enable, ..Dialect::Standard};
//! // We could equally have done `dialect = Dialect::Extended`.
//! let ast = AstModule::parse("json.star", content.to_owned(), &dialect)?;
//! let globals = Globals::standard();
//! let module = Module::new();
//! let mut eval = Evaluator::new(&module);
//! let res = eval.eval_module(ast, &globals);
//! // We expect this to fail, since it is a type violation
//! assert!(res.unwrap_err().to_string().contains("Value `test` of type `string` does not match the type annotation `int.type`"));
//! # Ok(())
//! # }
//! # fn main(){ run().unwrap(); }
//! ```
//!
//! ## Enable the `load` statement
//!
//! You can have Starlark load files imported by the user.
//! That requires that the loaded modules are first frozen with [`Module.freeze`](environment::Module::freeze).
//! There is no requirement that the files are on disk, but that would be a common pattern.
//!
//! ```
//! # fn run() -> anyhow::Result<()> {
//! use starlark::environment::{FrozenModule, Globals, Module};
//! use starlark::eval::{Evaluator, ReturnFileLoader};
//! use starlark::syntax::{AstModule, Dialect};
//!
//! // Get the file contents (for the demo), in reality use `AstModule::parse_file`.
//! fn get_source(file: &str) -> &str {
//!     match file {
//!         "a.star" => "a = 7",
//!         "b.star" => "b = 6",
//!         _ => { r#"
//! load('a.star', 'a')
//! load('b.star', 'b')
//! ab = a * b
//! "#
//!         }
//!     }
//! }
//!
//! fn get_module(file: &str) -> anyhow::Result<FrozenModule> {
//!    let ast = AstModule::parse(file, get_source(file).to_owned(), &Dialect::Standard)?;
//!
//!    // We can get the loaded modules from `ast.loads`.
//!    // And ultimately produce a `loader` capable of giving those modules to Starlark.
//!    let mut loads = Vec::new();
//!    for load in ast.loads() {
//!        loads.push((load.module_id.to_owned(), get_module(load.module_id)?));
//!    }
//!    let modules = loads.iter().map(|(a, b)| (a.as_str(), b)).collect();
//!    let mut loader = ReturnFileLoader { modules: &modules };
//!
//!    let globals = Globals::standard();
//!    let module = Module::new();
//!    {
//!        let mut eval = Evaluator::new(&module);
//!        eval.set_loader(&mut loader);
//!        eval.eval_module(ast, &globals)?;
//!    }
//!    // After creating a module we freeze it, preventing further mutation.
//!    // It can now be used as the input for other Starlark modules.
//!    Ok(module.freeze()?)
//! }
//!
//! let ab = get_module("ab.star")?;
//! assert_eq!(ab.get("ab").unwrap().unpack_int(), Some(42));
//! # Ok(())
//! # }
//! # fn main(){ run().unwrap(); }
//! ```
//!
//! ## Call a Starlark function from Rust
//!
//! You can extract functions from Starlark, and call them from Rust, using [`eval_function`](eval::Evaluator::eval_function).
//!
//! ```
//! # fn run() -> anyhow::Result<()> {
//! use starlark::environment::{Globals, Module};
//! use starlark::eval::Evaluator;
//! use starlark::syntax::{AstModule, Dialect};
//! use starlark::values::Value;
//!
//! let content = r#"
//! def quadratic(a, b, c, x):
//!     return a*x*x + b*x + c
//! quadratic
//! "#;
//!
//! let ast = AstModule::parse("quadratic.star", content.to_owned(), &Dialect::Extended)?;
//! let globals = Globals::standard();
//! let module = Module::new();
//! let mut eval = Evaluator::new(&module);
//! let quad = eval.eval_module(ast, &globals)?;
//! let heap = module.heap();
//! let res = eval.eval_function(
//!     quad,
//!     &[heap.alloc(4), heap.alloc(2), heap.alloc(1)],
//!     &[("x", heap.alloc(8))],
//! )?;
//! assert_eq!(res.unpack_i32(), Some(273));
//! # Ok(())
//! # }
//! # fn main(){ run().unwrap(); }
//! ```
//!
//! ## Defining Rust objects that are used from Starlark
//!
//! Finally, we can define our own types in Rust which live in the Starlark heap.
//! Such types are relatively complex, see the details at [`StarlarkValue`](values::StarlarkValue).
//!
//! ```
//! # fn run() -> anyhow::Result<()> {
//! use starlark::environment::{Globals, Module};
//! use starlark::eval::Evaluator;
//! use starlark::syntax::{AstModule, Dialect};
//! use starlark::values::{Heap, StarlarkValue, Value, ValueError, ValueLike, ProvidesStaticType, NoSerialize};
//! use starlark::starlark_simple_value;
//! use std::fmt::{self, Display, Write};
//! use allocative::Allocative;
//! use starlark_derive::starlark_value;
//!
//! // Define complex numbers
//! #[derive(Debug, PartialEq, Eq, ProvidesStaticType, NoSerialize, Allocative)]
//! struct Complex {
//!     real: i32,
//!     imaginary: i32,
//! }
//! starlark_simple_value!(Complex);
//!
//! impl Display for Complex {
//!     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
//!         write!(f, "{} + {}i", self.real, self.imaginary)
//!     }
//! }
//!
//! #[starlark_value(type = "complex")]
//! impl<'v> StarlarkValue<'v> for Complex {
//!     // How we add them
//!     fn add(&self, rhs: Value<'v>, heap: &'v Heap)
//!             -> Option<anyhow::Result<Value<'v>>> {
//!         if let Some(rhs) = rhs.downcast_ref::<Self>() {
//!             Some(Ok(heap.alloc(Complex {
//!                 real: self.real + rhs.real,
//!                 imaginary: self.imaginary + rhs.imaginary,
//!             })))
//!         } else {
//!             None
//!         }
//!     }
//! }
//!
//! let content = "str(a + b)";
//!
//! let ast = AstModule::parse("complex.star", content.to_owned(), &Dialect::Standard)?;
//! let globals = Globals::standard();
//! let module = Module::new();
//! // We inject some complex numbers into the module before we start.
//! let a = module.heap().alloc(Complex {real: 1, imaginary: 8});
//! module.set("a", a);
//! let b = module.heap().alloc(Complex {real: 4, imaginary: 2});
//! module.set("b", b);
//! let mut eval = Evaluator::new(&module);
//! let res = eval.eval_module(ast, &globals)?;
//! assert_eq!(res.unpack_str(), Some("5 + 10i"));
//! # Ok(())
//! # }
//! # fn main(){ run().unwrap(); }
//! ```

// Features we use
#![allow(stable_features)]
#![cfg_attr(rust_nightly, feature(const_type_id))]
#![cfg_attr(rust_nightly, feature(core_intrinsics))]
// Plugins
#![cfg_attr(feature = "gazebo_lint", feature(plugin))]
#![cfg_attr(feature = "gazebo_lint", allow(deprecated))] // :(
#![cfg_attr(feature = "gazebo_lint", plugin(gazebo_lint))]
//
// Good reasons
#![allow(clippy::needless_return)] // Mixing explicit returns with implicit ones sometimes looks odd
#![allow(clippy::new_ret_no_self)] // We often return Value, even though its morally a Self
// Disagree these are good hints
#![allow(clippy::comparison_chain)]
#![allow(clippy::comparison_to_empty)]
#![allow(clippy::enum_variant_names)]
#![allow(clippy::float_cmp)]
#![allow(clippy::len_without_is_empty)]
#![allow(clippy::manual_range_contains)]
#![allow(clippy::match_like_matches_macro)]
#![allow(clippy::missing_safety_doc)]
#![allow(clippy::needless_lifetimes)]
#![allow(clippy::new_without_default)]
#![allow(clippy::should_implement_trait)]
#![allow(clippy::single_match)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::type_complexity)]
#![allow(clippy::wrong_self_convention)]
// FIXME: Temporary
#![allow(clippy::useless_transmute)] // Seems to be a clippy bug, but we should be using less transmute anyway
#![deny(missing_docs)]

mod macros;

pub use starlark_derive::starlark_module;
pub use starlark_derive::StarlarkDocs;
pub use stdlib::PrintHandler;

pub(crate) mod analysis;
pub mod any;
pub mod assert;
pub mod codemap;
pub mod collections;
pub mod debug;
pub mod docs;
pub mod environment;
pub mod errors;
pub mod eval;
pub mod lsp;
mod private;
pub mod read_line;
mod sealed;
pub(crate) mod slice_vec_ext;
pub mod typing;

pub(crate) mod cast;
mod hint;
mod stdlib;
pub mod syntax;
pub mod values;
pub(crate) mod wasm;

pub mod coerce;
#[cfg(test)]
mod tests;

/// __macro_refs allows us to reference other crates in macro rules without users needing to be
///  aware of those dependencies. We make them public here and then can reference them like
///  `$crate::__macro_refs::foo`.
#[doc(hidden)]
pub mod __macro_refs {
    pub use either::Either;
    pub use paste::item;

    pub use crate::coerce::coerce;
}

/// __derive_refs allows us to reference other crates in starlark_derive without users needing to be
///  aware of those dependencies. We make them public here and then can reference them like
///  `starlark::__derive_refs::foo`.
#[doc(hidden)]
pub mod __derive_refs {
    pub mod serde {
        pub use serde::ser::Error;
        pub use serde::Serialize;
        pub use serde::Serializer;
    }
    pub use inventory;
}


/*
 * Copyright 2018 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.
 */

// Marker for code which is used in public API, but should not be used outside of the crate.
// No `///` documentation here to trigger `#![deny(missing_docs)]`
// if this accidentally becomes visible.
pub struct Private;


/*
 * 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.
 */

// This is not public API, but it is used by Starlark command line utility.
#![doc(hidden)]

#[cfg(not(target_arch = "wasm32"))]
mod with_or_without_rustyline {
    use std::env;
    use std::io;

    use rustyline::error::ReadlineError;
    use rustyline::history::DefaultHistory;
    use rustyline::Editor;

    /// Wrapper for the readline library, whichever we are using at the moment.
    pub struct ReadLine {
        editor: Editor<(), DefaultHistory>,
        histfile: Option<String>,
    }

    impl ReadLine {
        pub fn new(histfile_env: &str) -> anyhow::Result<ReadLine> {
            let mut editor = Editor::new()?;
            let histfile = if let Ok(histfile) = env::var(histfile_env) {
                if let Err(e) = editor.load_history(&histfile) {
                    match e {
                        ReadlineError::Io(e) if e.kind() == io::ErrorKind::NotFound => {}
                        e => eprintln!("Failed to load history from `{}`: {}", histfile, e),
                    }
                }
                Some(histfile)
            } else {
                None
            };
            Ok(ReadLine { editor, histfile })
        }

        /// Read line. Return `None` on EOF or interrupt.
        pub fn read_line(&mut self, prompt: &str) -> anyhow::Result<Option<String>> {
            match self.editor.readline(prompt) {
                Ok(line) => {
                    self.editor.add_history_entry(line.as_str())?;
                    if let Some(histfile) = &self.histfile {
                        if let Err(e) = self.editor.save_history(&histfile) {
                            eprintln!("Failed to save history to `{}`: {}", histfile, e);
                        }
                    }
                    Ok(Some(line))
                }
                // User pressed EOF - disconnected terminal, or similar
                Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => Ok(None),
                Err(e) => Err(e.into()),
            }
        }
    }
}

#[cfg(target_arch = "wasm32")]
mod with_or_without_rustyline {
    #[derive(thiserror::Error, Debug)]
    #[error("Rustyline is not supported on wasm32")]
    struct NoRustyline;

    pub struct ReadLine(());

    impl ReadLine {
        pub fn new(_histfile_env: &str) -> anyhow::Result<ReadLine> {
            Err(NoRustyline.into())
        }

        pub fn read_line(&mut self, _prompt: &str) -> anyhow::Result<Option<String>> {
            Err(NoRustyline.into())
        }
    }
}

pub use with_or_without_rustyline::ReadLine;


/*
 * Copyright 2018 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.
 */

// Marker for traits which should not be implemented outside of the crate.
// No `///` documentation here to trigger `#![deny(missing_docs)]`
// if this accidentally becomes visible.
pub trait Sealed {}


/*
 * Copyright 2018 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.
 */

/// Optimised collect iterator into Vec, which might be a Result.
///
/// If we do a standard .collect() on the iterator it will never have a good size hint,
/// as the lower bound will always be zero, so might reallocate several times.
/// We know the Vec will either be thrown away, or exactly `len`, so aim if we do allocate,
/// make sure it is at `len`. However, if the first element throws an error, we don't need
/// to allocate at all, so special case that.
fn collect_result<T, E>(mut it: impl ExactSizeIterator<Item = Result<T, E>>) -> Result<Vec<T>, E> {
    match it.next() {
        None => Ok(Vec::new()),
        Some(Err(e)) => Err(e),
        Some(Ok(x)) => {
            // +1 for the element we have already consumed
            let mut res = Vec::with_capacity(it.len() + 1);
            res.push(x);
            for x in it {
                res.push(x?);
            }
            Ok(res)
        }
    }
}

/// Extension traits on slices/[`Vec`](Vec).
pub(crate) trait SliceExt {
    type Item;

    /// A shorthand for `iter().map(f).collect::<Vec<_>>()`. For example:
    fn map<'a, B, F>(&'a self, f: F) -> Vec<B>
    where
        F: FnMut(&'a Self::Item) -> B;

    /// A shorthand for `iter().map(f).collect::<Result<Vec<_>, _>>()`. For example:
    fn try_map<'a, B, E, F>(&'a self, f: F) -> Result<Vec<B>, E>
    where
        F: FnMut(&'a Self::Item) -> Result<B, E>;
}

impl<T> SliceExt for [T] {
    type Item = T;

    fn map<'a, B, F>(&'a self, f: F) -> Vec<B>
    where
        F: FnMut(&'a Self::Item) -> B,
    {
        self.iter().map(f).collect()
    }

    fn try_map<'a, B, E, F>(&'a self, f: F) -> Result<Vec<B>, E>
    where
        F: FnMut(&'a Self::Item) -> Result<B, E>,
    {
        collect_result(self.iter().map(f))
    }
}

/// Extension traits on [`Vec`](Vec).
pub(crate) trait VecExt {
    type Item;

    /// A shorthand for `into_iter().map(f).collect::<Vec<_>>()`. For example:
    fn into_map<B, F>(self, f: F) -> Vec<B>
    where
        F: FnMut(Self::Item) -> B;

    /// A shorthand for `into_iter().map(f).collect::<Result<Vec<_>, _>>()`. For example:
    fn into_try_map<B, E, F>(self, f: F) -> Result<Vec<B>, E>
    where
        F: FnMut(Self::Item) -> Result<B, E>;
}

impl<T> VecExt for Vec<T> {
    type Item = T;

    fn into_map<B, F>(self, f: F) -> Vec<B>
    where
        F: FnMut(Self::Item) -> B,
    {
        self.into_iter().map(f).collect()
    }

    fn into_try_map<B, E, F>(self, f: F) -> Result<Vec<B>, E>
    where
        F: FnMut(Self::Item) -> Result<B, E>,
    {
        collect_result(self.into_iter().map(f))
    }
}


/*
 * Copyright 2018 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.
 */

#[cfg(test)]
pub(crate) fn is_wasm() -> bool {
    cfg!(target_arch = "wasm32")
}


/*
 * 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 crate::codemap::Pos;
use crate::codemap::Span;
use crate::syntax::ast::AssignIdentP;
use crate::syntax::ast::AstAssign;
use crate::syntax::ast::AstAssignIdent;
use crate::syntax::ast::AstExpr;
use crate::syntax::ast::AstIdent;
use crate::syntax::ast::AstParameter;
use crate::syntax::ast::AstStmt;
use crate::syntax::ast::AstString;
use crate::syntax::ast::AstTypeExpr;
use crate::syntax::ast::Clause;
use crate::syntax::ast::DefP;
use crate::syntax::ast::Expr;
use crate::syntax::ast::ForClause;
use crate::syntax::ast::IdentP;
use crate::syntax::ast::LambdaP;
use crate::syntax::ast::Stmt;
use crate::syntax::AstModule;

#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) enum Assigner {
    /// Obtained from `load`. `name` is the symbol in that file, not necessarily the local name
    Load {
        path: AstString,
        name: AstString,
    },
    Argument, // From a function argument
    Assign,   // From an assignment
}

#[derive(Debug)]
pub(crate) enum Bind {
    Set(Assigner, AstAssignIdent), // Variable assigned to directly
    Get(AstIdent),                 // Variable that is referenced
    GetDotted(GetDotted),          // Variable is referenced, but is part of a dotted access
    Flow,         // Flow control occurs here (if, for etc) - can arrive or leave at this point
    Scope(Scope), // Entering a new scope (lambda/def/comprehension)
}

/// A 'get' bind that was part of a dotted member access pattern.
///
/// e.g. `x.y.z` would be considered a variable of `x` with attributes `y` and `z`
#[derive(Debug, Clone)]
pub(crate) struct GetDotted {
    pub(crate) variable: AstIdent,
    pub(crate) attributes: Vec<AstString>,
}

impl GetDotted {
    fn new(variable: AstIdent, attributes: Vec<AstString>) -> Self {
        Self {
            variable,
            attributes,
        }
    }

    /// Determines if a position is contained in this dot access.
    ///
    /// The returned value is the index of the segment where `pos` was found, and
    /// the span that contained it. If the Pos is inside the variable the index is None,
    /// otherwise the index inside the attributes.
    pub(crate) fn contains(&self, pos: Pos) -> Option<(Option<usize>, Span)> {
        if self.variable.span.contains(pos) {
            return Some((None, self.variable.span));
        }
        for (i, s) in self.attributes.iter().enumerate() {
            if s.span.contains(pos) {
                return Some((Some(i), s.span));
            }
        }
        None
    }
}

#[derive(Debug)]
pub(crate) struct Scope {
    pub inner: Vec<Bind>,
    pub(crate) free: HashMap<String, Span>, // Things referred to in this scope, or inner scopes, that we don't define
    pub(crate) bound: HashMap<String, (Assigner, Span)>, // Things bound in this scope, doesn't include inner scope bindings
}

impl Scope {
    fn new(inner: Vec<Bind>) -> Self {
        let mut bound: HashMap<String, _> = HashMap::new();
        let mut free: HashMap<String, _> = HashMap::new();
        for x in &inner {
            match x {
                Bind::Set(assigner, x) => {
                    bound
                        .entry(x.0.clone())
                        .or_insert((assigner.clone(), x.span));
                }
                Bind::Get(x) => {
                    free.entry(x.node.0.clone()).or_insert(x.span);
                }
                Bind::GetDotted(x) => {
                    free.entry(x.variable.node.0.clone())
                        .or_insert(x.variable.span);
                }
                Bind::Scope(scope) => scope.free.iter().for_each(|(k, v)| {
                    free.entry(k.clone()).or_insert(*v);
                }),
                Bind::Flow => {}
            }
        }
        for x in bound.keys() {
            free.remove(x);
        }

        Self { inner, free, bound }
    }
}

fn opt_expr(x: Option<&AstExpr>, res: &mut Vec<Bind>) {
    if let Some(x) = x {
        expr(x, res)
    }
}

fn opt_type_expr(x: Option<&AstTypeExpr>, res: &mut Vec<Bind>) {
    if let Some(x) = x {
        expr(&x.expr, res)
    }
}

fn comprehension(
    for_: &ForClause,
    clauses: &[Clause],
    res: &mut Vec<Bind>,
    end: impl Fn(&mut Vec<Bind>),
) {
    expr(&for_.over, res);
    let mut inner = Vec::new();
    expr_lvalue(&for_.var, &mut inner);
    for clause in clauses {
        match clause {
            Clause::For(ForClause { var, over }) => {
                expr(over, &mut inner);
                expr_lvalue(var, &mut inner);
            }
            Clause::If(x) => expr(x, &mut inner),
        }
    }
    end(&mut inner);
    res.push(Bind::Scope(Scope::new(inner)))
}

/// Can we interpret this as a Dotted expression? If not, just treat it normally.
/// Note that we treat `foo.bar().baz` like the dotted expression `foo.bar.baz`.
fn dot_access<'a>(lhs: &'a AstExpr, attribute: &'a AstString, res: &mut Vec<Bind>) {
    // The attributes are stored in reverse.
    // Keep recursing while we have a chain of dots, if we find something else bail out
    fn f<'a>(lhs: &'a AstExpr, mut attributes: Vec<&'a AstString>, res: &mut Vec<Bind>) {
        match &**lhs {
            Expr::Identifier(id) => {
                let attrs = attributes.into_iter().rev().cloned().collect();
                res.push(Bind::GetDotted(GetDotted::new(id.clone(), attrs)));
            }
            Expr::Dot(lhs, attribute) => {
                attributes.push(attribute);
                f(lhs, attributes, res);
            }
            Expr::Call(name, parameters) => {
                f(name, attributes, res);
                // make sure that if someone does a(b).c, 'b' is bound and considered used.
                for parameter in parameters {
                    expr(parameter.expr(), res);
                }
            }
            _ => {
                // We saw through a series of dots, but it isn't a dotted-identifier
                // Bail out and just deal with the LHS normally
                expr(lhs, res);
            }
        }
    }
    f(lhs, vec![attribute], res);
}

fn expr(x: &AstExpr, res: &mut Vec<Bind>) {
    match &**x {
        Expr::Identifier(x) => res.push(Bind::Get(x.clone())),
        Expr::Lambda(LambdaP {
            params,
            body,
            payload: _,
        }) => {
            let mut inner = Vec::new();
            parameters(params, res, &mut inner);
            expr(body, &mut inner);
            res.push(Bind::Scope(Scope::new(inner)));
        }
        Expr::Dot(lhs, attribute) => dot_access(lhs, attribute, res),
        Expr::ListComprehension(x, for_, clauses) => {
            comprehension(for_, clauses, res, |res| expr(x, res))
        }
        Expr::DictComprehension(x, for_, clauses) => comprehension(for_, clauses, res, |res| {
            expr(&x.0, res);
            expr(&x.1, res)
        }),

        // Uninteresting - just recurse
        _ => x.visit_expr(|x| expr(x, res)),
    }
}

fn expr_lvalue(x: &AstAssign, res: &mut Vec<Bind>) {
    x.visit_expr(|x| expr(x, res));
    x.visit_lvalue(|x| res.push(Bind::Set(Assigner::Assign, x.clone())))
}

fn parameters(args: &[AstParameter], res: &mut Vec<Bind>, inner: &mut Vec<Bind>) {
    for a in args {
        let (name, typ, default) = a.split();
        opt_type_expr(typ, res);
        opt_expr(default, res);
        if let Some(name) = name {
            inner.push(Bind::Set(Assigner::Argument, name.clone()))
        }
    }
}

fn flow(res: &mut Vec<Bind>) {
    res.push(Bind::Flow)
}

fn stmt(x: &AstStmt, res: &mut Vec<Bind>) {
    match &**x {
        Stmt::Statements(xs) => {
            for x in xs {
                stmt(x, res)
            }
        }
        Stmt::Break | Stmt::Continue | Stmt::Return(None) => flow(res),
        Stmt::Pass => {}
        Stmt::Return(Some(x)) => {
            expr(x, res);
            flow(res)
        }
        Stmt::Expression(x) => expr(x, res),
        Stmt::If(a, b) => {
            expr(a, res);
            flow(res);
            stmt(b, res);
            flow(res);
        }
        Stmt::IfElse(a, b_c) => {
            let (b, c) = &**b_c;
            expr(a, res);
            flow(res);
            stmt(b, res);
            flow(res);
            stmt(c, res);
            flow(res);
        }
        Stmt::Def(DefP {
            name,
            params,
            return_type,
            body,
            payload: _,
        }) => {
            opt_type_expr(return_type.as_ref().map(|x| &**x), res);
            let mut inner = Vec::new();
            parameters(params, res, &mut inner);
            res.push(Bind::Set(Assigner::Assign, name.clone()));
            stmt(body, &mut inner);
            res.push(Bind::Scope(Scope::new(inner)));
        }
        Stmt::Assign(lhs, ty_rhs) => {
            let (ty, rhs) = &**ty_rhs;
            opt_type_expr(ty.as_ref(), res);
            expr(rhs, res);
            expr_lvalue(lhs, res);
        }
        Stmt::AssignModify(lhs, _, rhs) => {
            // For a += b, we:
            // 1. Evaluate all variables and expressions in a.
            // 2. Evaluate b.
            // 3. Assign to all variables in a.
            lhs.visit_expr(|x| expr(x, res));
            lhs.visit_lvalue(|x| {
                res.push(Bind::Get(
                    x.clone().into_map(|AssignIdentP(s, ())| IdentP(s, ())),
                ))
            });
            expr(rhs, res);
            expr_lvalue(lhs, res);
        }
        Stmt::For(dest, inner_body) => {
            let (inner, body) = &**inner_body;
            expr(inner, res);
            expr_lvalue(dest, res);
            flow(res);
            stmt(body, res);
            flow(res)
        }
        Stmt::Load(load) => {
            for x in &load.args {
                res.push(Bind::Set(
                    Assigner::Load {
                        path: load.module.clone(),
                        name: x.1.clone(),
                    },
                    x.0.clone(),
                ))
            }
        }
    }
}

pub(crate) fn scope(module: &AstModule) -> Scope {
    let mut res = Vec::new();
    stmt(&module.statement, &mut res);
    Scope::new(res)
}

#[cfg(test)]
mod test {
    use std::iter;

    use super::*;
    use crate::codemap::Pos;
    use crate::codemap::Span;
    use crate::slice_vec_ext::SliceExt;
    use crate::slice_vec_ext::VecExt;
    use crate::syntax::AstModule;
    use crate::syntax::Dialect;

    #[test]
    fn dotted_access_is_correct() -> anyhow::Result<()> {
        let contents = "x1.y\nx1.y().z\nx2().y\nx2().y.z";

        let expected = vec![
            vec!["x1", "y"],
            vec!["x1", "y", "z"],
            vec!["x2", "y"],
            vec!["x2", "y", "z"],
        ]
        .into_map(|names| names.into_map(String::from));

        let module = AstModule::parse("foo.star", contents.to_owned(), &Dialect::Extended)?;
        let scope = scope(&module);

        let found_bindings = scope
            .inner
            .iter()
            .map(|b| match b {
                Bind::GetDotted(get) => Ok(iter::once(get.variable.0.as_str())
                    .chain(get.attributes.map(|a| a.node.as_str()))
                    .map(|s| s.to_owned())
                    .collect()),
                _ => Err(anyhow::anyhow!("Unexpected bind {:?}", b)),
            })
            .collect::<anyhow::Result<Vec<Vec<_>>>>()?;

        assert_eq!(expected, found_bindings);

        Ok(())
    }

    #[test]
    fn dotted_contains_is_correct() -> anyhow::Result<()> {
        let contents = "x1.y1.z1\nx2.y2.z2";
        let module = AstModule::parse("foo.star", contents.to_owned(), &Dialect::Extended)?;
        let scope = scope(&module);

        let get = scope
            .inner
            .iter()
            .find_map(|b| match b {
                Bind::GetDotted(get) => Some((*get).clone()),
                _ => None,
            })
            .unwrap();

        let x1_pos = Pos::new(1);
        let y1_pos = Pos::new(4);
        let z1_pos = Pos::new(7);
        let x2_pos = Pos::new(10);

        let x1_expected = Some((None, Span::new(Pos::new(0), Pos::new(2))));
        let y1_expected = Some((Some(0), Span::new(Pos::new(3), Pos::new(5))));
        let z1_expected = Some((Some(1), Span::new(Pos::new(6), Pos::new(8))));

        assert_eq!(x1_expected, get.contains(x1_pos));
        assert_eq!(y1_expected, get.contains(y1_pos));
        assert_eq!(z1_expected, get.contains(z1_pos));
        assert!(get.contains(x2_pos).is_none());

        Ok(())
    }
}


/*
 * 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::iter;

use crate::analysis::bind::scope;
use crate::analysis::bind::Assigner;
use crate::analysis::bind::Bind;
use crate::analysis::bind::Scope;
use crate::codemap::CodeMap;
use crate::codemap::Pos;
use crate::codemap::ResolvedSpan;
use crate::codemap::Span;
use crate::codemap::Spanned;
use crate::slice_vec_ext::SliceExt;
use crate::syntax::ast::ArgumentP;
use crate::syntax::ast::AssignP;
use crate::syntax::ast::AstIdent;
use crate::syntax::ast::AstLiteral;
use crate::syntax::ast::AstNoPayload;
use crate::syntax::ast::AstString;
use crate::syntax::ast::Expr;
use crate::syntax::ast::ExprP;
use crate::syntax::ast::Stmt;
use crate::syntax::ast::StmtP;
use crate::syntax::uniplate::Visit;
use crate::syntax::AstModule;

/// The location of a definition for a given identifier. See [`AstModule::find_definition`].
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) enum IdentifierDefinition {
    /// The definition was found at this location in the current file.
    Location {
        source: ResolvedSpan,
        destination: ResolvedSpan,
    },
    /// The symbol was loaded from another file. "destination" is the position within the
    /// "load()" statement, but additionally, the path in that load statement, and the
    /// name of the symbol within that file are provided so that additional lookups can
    /// happen to find the original definition.
    LoadedLocation {
        source: ResolvedSpan,
        destination: ResolvedSpan,
        path: String,
        name: String,
    },
    /// The symbol is the path component of a `load` statement. This is the raw string
    /// that is in the AST, and needs to be properly resolved to a path to be useful.
    LoadPath { source: ResolvedSpan, path: String },
    /// A literal string in the source code. This can be a standalone string, or part of e.g. a
    /// list, a dictionary, a return value, etc.
    StringLiteral {
        source: ResolvedSpan,
        literal: String,
    },
    /// A named symbol was found, but it was not found in any of the current scopes. It
    /// should be considered a global symbol, and attempted to be resolved externally.
    Unresolved { source: ResolvedSpan, name: String },
    /// Either the provided location was not an access of a variable, or no definition
    /// could be found.
    NotFound,
}

impl IdentifierDefinition {
    fn source(&self) -> Option<ResolvedSpan> {
        match self {
            IdentifierDefinition::Location { source, .. }
            | IdentifierDefinition::LoadedLocation { source, .. }
            | IdentifierDefinition::LoadPath { source, .. }
            | IdentifierDefinition::StringLiteral { source, .. }
            | IdentifierDefinition::Unresolved { source, .. } => Some(*source),
            IdentifierDefinition::NotFound => None,
        }
    }
}

/// A definition as in [`IdentifierDefinition`], but the source is within a dot expression.
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct DottedDefinition {
    /// The location of the member access that was requested.
    pub(crate) source: ResolvedSpan,
    /// The [`IdentifierDefinition`] for the left most component of the expression.
    ///
    /// For example, this would be the location of `x` if `source` enclosed `y` in `x.y.z`.
    pub(crate) root_definition_location: IdentifierDefinition,
    /// All of the identifiers up to the one that `source` includes.
    ///
    /// For example. if the `y` in `x.y.z` is the source, this would contain `x` and `y`.
    pub(crate) segments: Vec<String>,
}

/// An inner structure that only holds references and unresolved spans. Used while
/// finding locations for identifiers. Resolved to a fuller, owned structure
/// ([`IdentifierDefinition`]) after the entire AST is visited.
///
/// This references temporary variables, so cannot be returned to external users directly.
#[derive(Debug, Clone, Eq, PartialEq)]
enum TempIdentifierDefinition<'a> {
    /// The location of the definition of the symbol at the current line/column
    Location { source: Span, destination: Span },
    LoadedLocation {
        source: Span,
        destination: Span,
        path: &'a str,
        name: &'a str,
    },
    /// The definition was not found in the current scope but the name of an identifier
    /// was found at the given position. This should be checked in outer scopes
    /// to attempt to find the definition.
    Name { source: Span, name: &'a str },
    /// None of the accesses matched the position that was provided.
    NotFound,
}

/// Reference version of [`DottedDefinitionLocation`]. Analogous to [`IdentifierDefinition`].
#[derive(Debug, Clone, Eq, PartialEq)]
struct TempDottedDefinition<'a> {
    /// The location of the member access that was requested.
    source: Span,
    /// The [`IdentifierDefinition`] for the left most component of the expression.
    ///
    /// For example, this would be the location of `x` if `source` enclosed `y` in `x.y.z`.
    root_definition_location: TempIdentifierDefinition<'a>,
    /// The variable you are pointing at, e.g. `x.y.z` would be `x`.
    variable: &'a AstIdent,
    /// All of the attributes up to the one that `source` includes.
    ///
    /// For example. if the `y` in `x.y.z` is the source, this would contain `y`.
    attributes: &'a [AstString],
}

#[derive(Debug, Clone, Eq, PartialEq)]
enum TempDefinition<'a> {
    Identifier(TempIdentifierDefinition<'a>),
    Dotted(TempDottedDefinition<'a>),
}

impl<'a> From<TempIdentifierDefinition<'a>> for TempDefinition<'a> {
    fn from(def: TempIdentifierDefinition<'a>) -> Self {
        Self::Identifier(def)
    }
}

impl<'a> From<TempDottedDefinition<'a>> for TempDefinition<'a> {
    fn from(def: TempDottedDefinition<'a>) -> Self {
        Self::Dotted(def)
    }
}

/// The container for both definition locations of a standalone identifier, and
/// for ones that access members (via '.' syntax)
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) enum Definition {
    Identifier(IdentifierDefinition),
    Dotted(DottedDefinition),
}

impl From<IdentifierDefinition> for Definition {
    fn from(def: IdentifierDefinition) -> Self {
        Self::Identifier(def)
    }
}

impl From<DottedDefinition> for Definition {
    fn from(def: DottedDefinition) -> Self {
        Self::Dotted(def)
    }
}

impl Definition {
    /// Get the "destination" of this location, but only within the current module.
    ///
    /// Some definition location types do not have a local definition.
    fn local_destination(&self) -> Option<ResolvedSpan> {
        match self {
            Definition::Identifier(i)
            | Definition::Dotted(DottedDefinition {
                root_definition_location: i,
                ..
            }) => match i {
                IdentifierDefinition::Location { destination, .. }
                | IdentifierDefinition::LoadedLocation { destination, .. } => Some(*destination),
                _ => None,
            },
        }
    }

    pub(crate) fn source(&self) -> Option<ResolvedSpan> {
        match self {
            Definition::Identifier(i) => i.source(),
            Definition::Dotted(DottedDefinition { source, .. }) => Some(*source),
        }
    }
}

/// Container that holds an AST module and returns things like definition locations,
/// lists of symbols, etc.
pub(crate) struct LspModule {
    pub(crate) ast: AstModule,
}

impl LspModule {
    pub(crate) fn new(ast: AstModule) -> Self {
        Self { ast }
    }

    /// Attempts to find the location where a symbol is defined in the module.
    ///
    /// `line` and `col` are zero based indexes of a location of the symbol to attempt to lookup.
    ///
    /// This method also handles scoping properly (i.e. an access of "foo" in a function
    /// will return location of the parameter "foo", even if there is a global called "foo").
    pub(crate) fn find_definition(&self, line: u32, col: u32) -> Definition {
        // TODO(nmj): This should probably just store references to all of the AST nodes
        //            when the LSPModule object is created, and then we can do a much faster
        //            lookup, especially in cases where a file has not been changed, so the
        //            LSPModule doesn't need to reparse anything.

        let scope = scope(&self.ast);
        let line_span = match self.ast.codemap.line_span_opt(line as usize) {
            None => {
                // The document got edited to add new lines, just bail out
                return Definition::Identifier(IdentifierDefinition::NotFound);
            }
            Some(line_span) => line_span,
        };
        let current_pos = std::cmp::min(line_span.begin() + col, line_span.end());

        // Finalize the results after recursing down from and back up to the the top level scope.
        match Self::find_definition_in_scope(&scope, current_pos) {
            TempDefinition::Identifier(def) => self
                .get_definition_location(def, &scope, current_pos)
                .into(),
            TempDefinition::Dotted(def) => DottedDefinition {
                source: self.ast.codemap.resolve_span(def.source),
                root_definition_location: self.get_definition_location(
                    def.root_definition_location,
                    &scope,
                    current_pos,
                ),
                segments: iter::once(def.variable.0.clone())
                    .chain(def.attributes.map(|s| s.node.to_owned()))
                    .collect(),
            }
            .into(),
        }
    }

    /// Look at the given scope and child scopes to try to find where the identifier
    /// accessed at Pos is defined.
    fn find_definition_in_scope<'a>(scope: &'a Scope, pos: Pos) -> TempDefinition<'a> {
        /// Look for a name in the given scope, with a given source, and return the right
        /// type of `TempIdentifierDefinition` based on whether / how the variable is bound.
        fn resolve_get_in_scope<'a>(
            scope: &'a Scope,
            name: &'a str,
            source: Span,
        ) -> TempIdentifierDefinition<'a> {
            match scope.bound.get(name) {
                Some((Assigner::Load { path, name }, span)) => {
                    TempIdentifierDefinition::LoadedLocation {
                        source,
                        destination: *span,
                        path,
                        name: name.as_str(),
                    }
                }
                Some((_, span)) => TempIdentifierDefinition::Location {
                    source,
                    destination: *span,
                },
                // We know the symbol name, but it might only be available in
                // an outer scope.
                None => TempIdentifierDefinition::Name { source, name },
            }
        }

        for bind in &scope.inner {
            match bind {
                Bind::Get(g) if g.span.contains(pos) => {
                    return resolve_get_in_scope(scope, g.node.0.as_str(), g.span).into();
                }
                Bind::Scope(inner_scope) => {
                    match Self::find_definition_in_scope(inner_scope, pos) {
                        TempDefinition::Identifier(TempIdentifierDefinition::Name {
                            source,
                            name,
                        }) => {
                            return resolve_get_in_scope(scope, name, source).into();
                        }
                        TempDefinition::Identifier(TempIdentifierDefinition::NotFound) => {}
                        TempDefinition::Dotted(TempDottedDefinition {
                            source,
                            root_definition_location:
                                TempIdentifierDefinition::Name {
                                    source: root_source,
                                    name: root_name,
                                },
                            variable,
                            attributes,
                        }) => {
                            return TempDefinition::Dotted(TempDottedDefinition {
                                source,
                                root_definition_location: resolve_get_in_scope(
                                    scope,
                                    root_name,
                                    root_source,
                                ),
                                variable,
                                attributes,
                            });
                        }
                        x @ TempDefinition::Identifier(TempIdentifierDefinition::Location {
                            ..
                        })
                        | x @ TempDefinition::Identifier(
                            TempIdentifierDefinition::LoadedLocation { .. },
                        )
                        | x @ TempDefinition::Dotted(_) => {
                            return x;
                        }
                    }
                }
                Bind::GetDotted(dotted) => {
                    if let Some((idx, source)) = dotted.contains(pos) {
                        let root_identifier = &dotted.variable;
                        let root_definition_location = resolve_get_in_scope(
                            scope,
                            root_identifier.node.0.as_str(),
                            root_identifier.span,
                        );
                        // If someone clicks on the "root" identifier, just treat it as a "get"
                        match idx {
                            None => return root_definition_location.into(),
                            Some(idx) => {
                                return TempDottedDefinition {
                                    source,
                                    root_definition_location,
                                    variable: &dotted.variable,
                                    attributes: &dotted.attributes[0..idx + 1],
                                }
                                .into();
                            }
                        }
                    }
                }
                // For everything else, just ignore it. Note that the `Get` is ignored
                // because we already checked the pos above.
                Bind::Set(_, _) | Bind::Flow | Bind::Get(_) => {}
            }
        }
        TempIdentifierDefinition::NotFound.into()
    }

    /// Converts a `TempIdentifierDefinition` to an `IdentifierDefinition`, resolving spans
    /// against the current AST, owning strings, etc.
    fn get_definition_location(
        &self,
        definition: TempIdentifierDefinition,
        scope: &Scope,
        current_pos: Pos,
    ) -> IdentifierDefinition {
        match definition {
            TempIdentifierDefinition::Location {
                source,
                destination,
            } => IdentifierDefinition::Location {
                source: self.ast.codemap.resolve_span(source),
                destination: self.ast.codemap.resolve_span(destination),
            },
            TempIdentifierDefinition::Name { source, name } => match scope.bound.get(name) {
                None => IdentifierDefinition::Unresolved {
                    source: self.ast.codemap.resolve_span(source),
                    name: name.to_owned(),
                },
                Some((Assigner::Load { path, name }, span)) => {
                    IdentifierDefinition::LoadedLocation {
                        source: self.ast.codemap.resolve_span(source),
                        destination: self.ast.codemap.resolve_span(*span),
                        path: path.node.clone(),
                        name: name.node.clone(),
                    }
                }
                Some((_, span)) => IdentifierDefinition::Location {
                    source: self.ast.codemap.resolve_span(source),
                    destination: self.ast.codemap.resolve_span(*span),
                },
            },
            // If we could not find the symbol, see if the current position is within
            // a load statement (these are not exposed as Get/Set in bind).
            TempIdentifierDefinition::NotFound => self.find_definition_from_ast(current_pos),
            TempIdentifierDefinition::LoadedLocation {
                source,
                destination,
                path,
                name,
            } => IdentifierDefinition::LoadedLocation {
                source: self.ast.codemap.resolve_span(source),
                destination: self.ast.codemap.resolve_span(destination),
                path: path.to_owned(),
                name: name.to_owned(),
            },
        }
    }

    /// Attempt to find the location in this module where an exported symbol is defined.
    pub(crate) fn find_exported_symbol(&self, name: &str) -> Option<ResolvedSpan> {
        self.ast.exported_symbols().iter().find_map(|symbol| {
            if symbol.name == name {
                Some(symbol.span.resolve_span())
            } else {
                None
            }
        })
    }

    /// Attempt to find the location in this module where a member of a struct (named `name`)
    /// is defined.
    ///
    /// This helps with the common idiom of
    /// ```python
    /// ...
    /// Foo = struct(
    ///     member1 = _member1,
    /// )
    /// ```
    ///
    /// which is imported by other files and used as `Foo.member1`. Rather than jumping to `Foo`,
    /// this would jump to `_member1` if it exists.
    pub(crate) fn find_exported_symbol_and_member(
        &self,
        name: &str,
        member: &str,
    ) -> Option<ResolvedSpan> {
        // If we can't fully resolve the symbol, try to get as "close" as possible.
        // Whether this is the left hand side of an assign statement, or maybe the
        // left side of a named argument in the struct that matches the member name.
        let mut arg_span = None;
        let mut identifier_span = None;
        let mut symbol_to_lookup = None;

        'outer: for v in self.ast.top_level_statements() {
            if let StmtP::Assign(l, ty_r) = &v.node {
                let (_ty, r) = &**ty_r;
                let main_assign_span = match &l.node {
                    AssignP::Identifier(main_assign_id) if main_assign_id.0 == name => {
                        main_assign_id.span
                    }
                    _ => {
                        continue 'outer;
                    }
                };
                // If nothing else, go to the left hand of the assignment expression.
                if identifier_span.is_none() {
                    identifier_span = Some(main_assign_span);
                }

                // Look for a function call to `struct`.
                if let ExprP::Call(function_name, args) = &r.node {
                    match &function_name.node {
                        ExprP::Identifier(function_name) if function_name.node.0 == "struct" => {}
                        _ => {
                            continue 'outer;
                        }
                    }

                    for arg in args {
                        if let ArgumentP::Named(arg_name, arg_expr) = &arg.node {
                            if arg_name.node != member {
                                continue;
                            }
                            if arg_span.is_none() {
                                arg_span = Some(arg_name.span);
                            }
                            if let ExprP::Identifier(arg_value_id) = &arg_expr.node {
                                symbol_to_lookup = Some(arg_value_id.span);
                                break 'outer;
                            }
                            break;
                        }
                    }
                }
            }
        }

        // Try to find the symbol that is assigned, but if not, try to get to that "closest" span.
        symbol_to_lookup
            .and_then(|span| {
                let resolved = self.ast.codemap.resolve_span(span);
                self.find_definition(resolved.begin_line as u32, resolved.begin_column as u32)
                    .local_destination()
            })
            .or_else(|| match (arg_span, identifier_span) {
                (Some(span), _) => Some(self.ast.codemap.resolve_span(span)),
                (None, Some(span)) => Some(self.ast.codemap.resolve_span(span)),
                (None, None) => None,
            })
    }

    fn find_definition_from_ast(&self, pos: Pos) -> IdentifierDefinition {
        fn visit_node(
            codemap: &CodeMap,
            pos: Pos,
            ret: &mut Option<IdentifierDefinition>,
            node: Visit<AstNoPayload>,
        ) {
            if ret.is_some() {
                return;
            }
            match node {
                Visit::Expr(Spanned {
                    node: Expr::Literal(AstLiteral::String(s)),
                    ..
                }) if s.span.contains(pos) => {
                    *ret = Some(IdentifierDefinition::StringLiteral {
                        source: codemap.resolve_span(s.span),
                        literal: s.node.to_owned(),
                    });
                }
                Visit::Stmt(Spanned {
                    node: Stmt::Load(load),
                    ..
                }) => {
                    *ret = if load.module.span.contains(pos) {
                        Some(IdentifierDefinition::LoadPath {
                            source: codemap.resolve_span(load.module.span),
                            path: load.module.node.to_owned(),
                        })
                    } else {
                        load.args.iter().find_map(|(assign, name)| {
                            if assign.span.contains(pos) || name.span.contains(pos) {
                                Some(IdentifierDefinition::LoadedLocation {
                                    source: codemap.resolve_span(name.span),
                                    destination: codemap.resolve_span(name.span),
                                    path: load.module.node.to_owned(),
                                    name: name.node.to_owned(),
                                })
                            } else {
                                None
                            }
                        })
                    }
                }
                v => v.visit_children(|node| visit_node(codemap, pos, ret, node)),
            }
        }

        let mut ret = None;
        visit_node(
            &self.ast.codemap,
            pos,
            &mut ret,
            Visit::Stmt(&self.ast.statement),
        );
        ret.unwrap_or(IdentifierDefinition::NotFound)
    }
}

#[cfg(test)]
pub(crate) mod helpers {
    use std::collections::hash_map::Entry;
    use std::collections::HashMap;

    use textwrap::dedent;

    use super::*;
    use crate::codemap::CodeMap;
    use crate::codemap::Pos;
    use crate::codemap::ResolvedSpan;
    use crate::codemap::Span;
    use crate::syntax::AstModule;
    use crate::syntax::Dialect;

    /// Result of parsing a starlark fixture that has range markers in it. See `FixtureWithRanges::from_fixture`
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub(crate) struct FixtureWithRanges {
        filename: String,
        /// The starlark program with markers removed.
        program: String,
        /// The location of all of the symbols that were indicated by the test fixture.
        ranges: HashMap<String, ResolvedSpan>,
    }

    impl FixtureWithRanges {
        /// Parse a program that has markers in it indicating a "range" by name.
        ///
        /// Markers are simple `<\w+>` and `</\w+>` tags. The returned program is the
        /// program content with these markers removed.
        ///
        /// For example:
        /// ```#starlark
        /// load("foo.star", <a>"bar"</a>);
        ///
        /// x = 1
        /// <bar_highlight><bar_click>b</bar_click>ar</bar_highlight>(<x>x</x>)
        /// ```
        ///
        /// Would return a program body of:
        /// ```#starlark
        /// load("foo.star", "bar");
        ///
        /// x = 1
        /// bar(x)
        /// ```
        ///
        /// And ranges of:
        ///  `a`: 0:17-0:22
        ///  `bar_highlight`: 3:0-3:2
        ///  `bar_click`: 3:0-3:1
        ///  `x`: 3:4-3:5
        pub(crate) fn from_fixture(filename: &str, fixture: &str) -> anyhow::Result<Self> {
            let re = regex::Regex::new(r#"<(/)?(\w+)>"#).unwrap();
            let mut program = String::new();
            let mut ranges: HashMap<String, (Option<usize>, Option<usize>)> = HashMap::new();

            let mut fixture_idx = 0;
            for matches in re.captures_iter(fixture) {
                let full_tag = matches.get(0).unwrap();
                let is_end_tag = matches.get(1).is_some();
                let identifier = matches.get(2).unwrap().as_str().to_owned();

                program.push_str(&fixture[fixture_idx..full_tag.start()]);
                fixture_idx = full_tag.end();

                match (is_end_tag, ranges.entry(identifier.clone())) {
                    (false, Entry::Occupied(_)) => {
                        return Err(anyhow::anyhow!("duplicate open tag `{}` found", identifier));
                    }
                    (false, Entry::Vacant(e)) => {
                        e.insert((Some(program.len()), None));
                    }
                    (true, Entry::Occupied(mut e)) => {
                        e.insert((e.get().0, Some(program.len())));
                    }
                    (true, Entry::Vacant(_)) => {
                        return Err(anyhow::anyhow!(
                            "found closing tag for `{}`, but no open tag",
                            identifier
                        ));
                    }
                }
            }
            program.push_str(&fixture[fixture_idx..fixture.len()]);

            let code_map = CodeMap::new(filename.to_owned(), program.clone());
            let spans: HashMap<String, ResolvedSpan> = ranges
                .into_iter()
                .map(|(id, (start, end))| {
                    let span = Span::new(
                        Pos::new(start.unwrap() as u32),
                        Pos::new(end.unwrap() as u32),
                    );
                    (id, code_map.resolve_span(span))
                })
                .collect();

            Ok(Self {
                filename: filename.to_owned(),
                program,
                ranges: spans,
            })
        }

        pub(crate) fn begin_line(&self, identifier: &str) -> u32 {
            self.ranges
                .get(identifier)
                .expect("identifier to be present")
                .begin_line as u32
        }

        pub(crate) fn begin_column(&self, identifier: &str) -> u32 {
            self.ranges
                .get(identifier)
                .expect("identifier to be present")
                .begin_column as u32
        }

        pub(crate) fn span(&self, identifier: &str) -> ResolvedSpan {
            *self
                .ranges
                .get(identifier)
                .expect("identifier to be present")
        }

        pub(crate) fn module(&self) -> anyhow::Result<LspModule> {
            Ok(LspModule::new(AstModule::parse(
                &self.filename,
                self.program.clone(),
                &Dialect::Extended,
            )?))
        }

        #[cfg(not(windows))]
        pub(crate) fn program(&self) -> String {
            self.program.clone()
        }
    }

    #[test]
    fn parses_fixtures() -> anyhow::Result<()> {
        let fixture = dedent(
            r#"
            load("foo.star", <a>"bar"</a>);

            x = 1
            <bar_highlight><bar_click>b</bar_click>ar</bar_highlight>(<x>x</x>)
            "#,
        )
        .trim()
        .to_owned();

        let expected_program = dedent(
            r#"
            load("foo.star", "bar");

            x = 1
            bar(x)
            "#,
        )
        .trim()
        .to_owned();

        let expected_locations = [
            ("a", 0, 17, 0, 22),
            ("bar_highlight", 3, 0, 3, 3),
            ("bar_click", 3, 0, 3, 1),
            ("x", 3, 4, 3, 5),
        ]
        .into_iter()
        .map(|(id, begin_line, begin_column, end_line, end_column)| {
            let span = ResolvedSpan {
                begin_line,
                begin_column,
                end_line,
                end_column,
            };
            (id.to_owned(), span)
        })
        .collect();

        let expected = FixtureWithRanges {
            filename: "test.star".to_owned(),
            program: expected_program,
            ranges: expected_locations,
        };

        let parsed = FixtureWithRanges::from_fixture("test.star", &fixture)?;

        assert_eq!(expected, parsed);

        Ok(())
    }
}

#[cfg(test)]
mod test {
    use textwrap::dedent;

    use super::helpers::*;
    use super::*;

    #[test]
    fn find_definition_loaded_symbol() -> anyhow::Result<()> {
        let contents = dedent(
            r#"
        load("bar.star", <print>print</print> = "other_print");

        x = 1
        y = 2

        def add(y: "int"):
            return x + y

        def invalid_symbol():
            return y + z

        <print_click><p1>p</p1>r<p2>i</p2>n<p3>t</p3></print_click>(x)
        add(3)
        invalid_symbol()
        "#,
        )
        .trim()
        .to_owned();
        let parsed = FixtureWithRanges::from_fixture("foo.star", &contents)?;
        let module = parsed.module()?;

        let expected: Definition = IdentifierDefinition::LoadedLocation {
            source: parsed.span("print_click"),
            destination: parsed.span("print"),
            path: "bar.star".to_owned(),
            name: "other_print".to_owned(),
        }
        .into();
        assert_eq!(
            expected,
            module.find_definition(parsed.begin_line("p1"), parsed.begin_column("p1"))
        );
        assert_eq!(
            expected,
            module.find_definition(parsed.begin_line("p2"), parsed.begin_column("p2"))
        );
        assert_eq!(
            expected,
            module.find_definition(parsed.begin_line("p3"), parsed.begin_column("p3"))
        );
        Ok(())
    }

    #[test]
    fn find_definition_function_calls() -> anyhow::Result<()> {
        let contents = dedent(
            r#"
        load("bar.star", "print");

        x = 1
        y = 2

        def <add>add</add>(y: "int"):
            return x + y

        def <invalid_symbol>invalid_symbol</invalid_symbol>():
            return y + z

        print(x)
        <add_click><a1>a</a1><a2>d</a2><a3>d</a3></add_click>(3)
        <invalid_symbol_click><i1>i</i1>nv<i2>a</i2>lid_symbo<i3>l</i3></invalid_symbol_click>()
        "#,
        )
        .trim()
        .to_owned();
        let parsed = FixtureWithRanges::from_fixture("foo.star", &contents)?;
        let module = parsed.module()?;

        let expected_add = Definition::from(IdentifierDefinition::Location {
            source: parsed.span("add_click"),
            destination: parsed.span("add"),
        });
        let expected_invalid = Definition::from(IdentifierDefinition::Location {
            source: parsed.span("invalid_symbol_click"),
            destination: parsed.span("invalid_symbol"),
        });

        assert_eq!(
            expected_add,
            module.find_definition(parsed.begin_line("a1"), parsed.begin_column("a1"))
        );
        assert_eq!(
            expected_add,
            module.find_definition(parsed.begin_line("a2"), parsed.begin_column("a2"))
        );
        assert_eq!(
            expected_add,
            module.find_definition(parsed.begin_line("a3"), parsed.begin_column("a3"))
        );

        assert_eq!(
            expected_invalid,
            module.find_definition(parsed.begin_line("i1"), parsed.begin_column("i1"))
        );
        assert_eq!(
            expected_invalid,
            module.find_definition(parsed.begin_line("i2"), parsed.begin_column("i2"))
        );
        assert_eq!(
            expected_invalid,
            module.find_definition(parsed.begin_line("i3"), parsed.begin_column("i3"))
        );
        Ok(())
    }

    #[test]
    fn find_definition_function_params() -> anyhow::Result<()> {
        let contents = dedent(
            r#"
        load("bar.star", "print");

        <x>x</x> = 1
        y = 2

        def add(y: "int"):
            return x + y

        def invalid_symbol():
            return y + z

        print(<x_param>x</x_param>)
        add(3)
        invalid_symbol()
        "#,
        )
        .trim()
        .to_owned();
        let parsed = FixtureWithRanges::from_fixture("foo.star", &contents)?;
        let module = parsed.module()?;

        assert_eq!(
            Definition::from(IdentifierDefinition::Location {
                source: parsed.span("x_param"),
                destination: parsed.span("x")
            }),
            module.find_definition(parsed.begin_line("x_param"), parsed.begin_column("x_param"))
        );
        Ok(())
    }

    #[test]
    fn find_definition_scopes_locals() -> anyhow::Result<()> {
        let contents = dedent(
            r#"
        load("bar.star", "print");

        <x>x</x> = 1
        <y1>y</y1> = 2

        def add(<y2>y</y2>: "int"):
            return <x_var>x</x_var> + <y_var1>y</y_var1>

        def invalid_symbol():
            return <y_var2>y</y_var2> + <z_var>z</z_var>

        print(x)
        add(3)
        invalid_symbol()
        "#,
        )
        .trim()
        .to_owned();
        let parsed = FixtureWithRanges::from_fixture("foo.star", &contents)?;
        let module = parsed.module()?;

        assert_eq!(
            Definition::from(IdentifierDefinition::Location {
                source: parsed.span("x_var"),
                destination: parsed.span("x")
            }),
            module.find_definition(parsed.begin_line("x_var"), parsed.begin_column("x_var"))
        );
        assert_eq!(
            Definition::from(IdentifierDefinition::Location {
                source: parsed.span("y_var1"),
                destination: parsed.span("y2")
            }),
            module.find_definition(parsed.begin_line("y_var1"), parsed.begin_column("y_var1"))
        );

        assert_eq!(
            Definition::from(IdentifierDefinition::Location {
                source: parsed.span("y_var2"),
                destination: parsed.span("y1")
            }),
            module.find_definition(parsed.begin_line("y_var2"), parsed.begin_column("y_var2"))
        );
        assert_eq!(
            Definition::from(IdentifierDefinition::Unresolved {
                source: parsed.span("z_var"),
                name: "z".to_owned()
            }),
            module.find_definition(parsed.begin_line("z_var"), parsed.begin_column("z_var"))
        );
        Ok(())
    }

    #[test]
    fn find_definition_unknown_clicks() -> anyhow::Result<()> {
        let contents = dedent(
            r#"
        load("bar.star", "print");

        x = 1
        y = 2

        def <no_def>add</no_def>(y: "int"):
            return x + y

        def invalid_symbol():
            return y + z

        print(x)
        add(3)
        invalid_symbol()
        "#,
        )
        .trim()
        .to_owned();
        let parsed = FixtureWithRanges::from_fixture("foo.star", &contents)?;
        let module = parsed.module()?;

        assert_eq!(
            Definition::from(IdentifierDefinition::NotFound),
            module.find_definition(parsed.begin_line("no_def"), parsed.begin_column("no_def"))
        );

        Ok(())
    }

    #[test]
    fn finds_definition_in_strings() -> anyhow::Result<()> {
        let contents = dedent(
            r#"
        <foo1_click>"f<foo1>o</foo1>o1"</foo1_click>
        [
            <foo2_click>"f<foo2>o</foo2>o2"</foo2_click>,
            "ignored"
        ]
        {
            <foo3_click>"f<foo3>o</foo3>o3"</foo3_click>: "ignored",
            "ignored": <foo4_click>"f<foo4>o</foo4>o4"</foo4_click>,
            "ignored_other": "ignored",
        }

        def f(x = <foo5_click>"f<foo5>o</foo5>o5"</foo5_click>):
            <foo6_click>"f<foo6>o</foo6>o6"</foo6_click>
            [
                <foo7_click>"f<foo7>o</foo7>o7"</foo7_click>,
                "ignored"
            ]
            {
                <foo8_click>"f<foo8>o</foo8>o8"</foo8_click>: "ignored",
                "ignored": <foo9_click>"f<foo9>o</foo9>o9"</foo9_click>,
                "ignored_other": "ignored",
            }
            if x == <foo10_click>"f<foo10>o</foo10>o10"</foo10_click>:
                <foo11_click>"f<foo11>o</foo11>o11"</foo11_click>
                [
                    <foo12_click>"f<foo12>o</foo12>o12"</foo12_click>,
                    "ignored"
                ]
                {
                    <foo13_click>"f<foo13>o</foo13>o13"</foo13_click>: "ignored",
                    "ignored": <foo14_click>"f<foo14>o</foo14>o14"</foo14_click>,
                    "ignored_other": "ignored",
                }
            return <foo15_click>"f<foo15>o</foo15>o15"</foo15_click>

        foo16 = <foo16_click>"f<foo16>o</foo16>o16"</foo16_click>
        "#,
        )
        .trim()
        .to_owned();

        let parsed = FixtureWithRanges::from_fixture("foo.star", &contents)?;
        let module = parsed.module()?;

        fn test(parsed: &FixtureWithRanges, module: &LspModule, name: &str) {
            let expected = Definition::from(IdentifierDefinition::StringLiteral {
                source: parsed.span(&format!("{}_click", name)),
                literal: name.to_owned(),
            });
            let actual = module.find_definition(parsed.begin_line(name), parsed.begin_column(name));

            assert_eq!(
                expected, actual,
                "Expected `{:?}` == `{:?}` for test `{}`",
                expected, actual, name
            );
        }

        test(&parsed, &module, "foo1");
        test(&parsed, &module, "foo2");
        test(&parsed, &module, "foo3");
        test(&parsed, &module, "foo4");
        test(&parsed, &module, "foo5");
        test(&parsed, &module, "foo6");
        test(&parsed, &module, "foo7");
        test(&parsed, &module, "foo8");
        test(&parsed, &module, "foo9");
        test(&parsed, &module, "foo10");
        test(&parsed, &module, "foo11");
        test(&parsed, &module, "foo12");
        test(&parsed, &module, "foo13");
        test(&parsed, &module, "foo14");
        test(&parsed, &module, "foo15");
        test(&parsed, &module, "foo16");

        Ok(())
    }

    #[test]
    fn find_definition_dot_access_unresolved_root() -> anyhow::Result<()> {
        let contents = dedent(
            r#"
            <foo_root><foo>f<foo_click>o</foo_click>o</foo></foo_root>.bar.baz().quz
            <bar_root>foo</bar_root>.<bar>b<bar_click>a</bar_click>r</bar>.baz().quz
            <baz_root>foo</baz_root>.bar.<baz>b<baz_click>a</baz_click>z</baz>().quz
            <quz_root>foo</quz_root>.bar.baz().<quz>q<quz_click>u</quz_click>z</quz>
            "#,
        )
        .trim()
        .to_owned();

        let parsed = FixtureWithRanges::from_fixture("foo.star", &contents)?;
        let module = parsed.module()?;

        let expected = |span_id: &str, segments: &[&str]| -> Definition {
            let root_definition_location = IdentifierDefinition::Unresolved {
                source: parsed.span(&format!("{}_root", span_id)),
                name: "foo".to_owned(),
            };
            if segments.len() > 1 {
                DottedDefinition {
                    source: parsed.span(span_id),
                    root_definition_location,
                    segments: segments.iter().map(|s| (*s).to_owned()).collect(),
                }
                .into()
            } else {
                root_definition_location.into()
            }
        };

        let find_definition = |span_id: &str| {
            module.find_definition(parsed.begin_line(span_id), parsed.begin_column(span_id))
        };

        let expected_foo = expected("foo", &["foo"]);
        let expected_bar = expected("bar", &["foo", "bar"]);
        let expected_baz = expected("baz", &["foo", "bar", "baz"]);
        let expected_quz = expected("quz", &["foo", "bar", "baz", "quz"]);

        assert_eq!(expected_foo, find_definition("foo_click"));
        assert_eq!(expected_bar, find_definition("bar_click"));
        assert_eq!(expected_baz, find_definition("baz_click"));
        assert_eq!(expected_quz, find_definition("quz_click"));

        Ok(())
    }

    #[test]
    fn find_definition_dot_access_loaded_root() -> anyhow::Result<()> {
        let contents = dedent(
            r#"
            load("defs.bzl", <root>"foo"</root>);

            <foo_root><foo>f<foo_click>o</foo_click>o</foo></foo_root>.bar.baz().quz
            <bar_root>foo</bar_root>.<bar>b<bar_click>a</bar_click>r</bar>.baz().quz
            <baz_root>foo</baz_root>.bar.<baz>b<baz_click>a</baz_click>z</baz>().quz
            <quz_root>foo</quz_root>.bar.baz().<quz>q<quz_click>u</quz_click>z</quz>
            "#,
        )
        .trim()
        .to_owned();

        let parsed = FixtureWithRanges::from_fixture("foo.star", &contents)?;
        let module = parsed.module()?;

        let expected = |span_id: &str, segments: &[&str]| -> Definition {
            let root_definition_location = IdentifierDefinition::LoadedLocation {
                source: parsed.span(&format!("{}_root", span_id)),
                destination: parsed.span("root"),
                path: "defs.bzl".to_owned(),
                name: "foo".to_owned(),
            };
            if segments.len() > 1 {
                DottedDefinition {
                    source: parsed.span(span_id),
                    root_definition_location,
                    segments: segments.iter().map(|s| (*s).to_owned()).collect(),
                }
                .into()
            } else {
                root_definition_location.into()
            }
        };

        let find_definition = |span_id: &str| {
            module.find_definition(parsed.begin_line(span_id), parsed.begin_column(span_id))
        };

        let expected_foo = expected("foo", &["foo"]);
        let expected_bar = expected("bar", &["foo", "bar"]);
        let expected_baz = expected("baz", &["foo", "bar", "baz"]);
        let expected_quz = expected("quz", &["foo", "bar", "baz", "quz"]);

        assert_eq!(expected_foo, find_definition("foo_click"));
        assert_eq!(expected_bar, find_definition("bar_click"));
        assert_eq!(expected_baz, find_definition("baz_click"));
        assert_eq!(expected_quz, find_definition("quz_click"));

        Ok(())
    }

    #[test]
    fn find_definition_dot_access_local_root() -> anyhow::Result<()> {
        let contents = dedent(
            r#"
            def func_1(<root>foo</root>):
                <foo_root><foo>f<foo_click>o</foo_click>o</foo></foo_root>.bar.baz().quz
                <bar_root>foo</bar_root>.<bar>b<bar_click>a</bar_click>r</bar>.baz().quz
                <baz_root>foo</baz_root>.bar.<baz>b<baz_click>a</baz_click>z</baz>().quz
                <quz_root>foo</quz_root>.bar.baz().<quz>q<quz_click>u</quz_click>z</quz>
            "#,
        )
        .trim()
        .to_owned();

        let parsed = FixtureWithRanges::from_fixture("foo.star", &contents)?;
        let module = parsed.module()?;

        let expected = |span_id: &str, segments: &[&str]| -> Definition {
            let root_definition_location = IdentifierDefinition::Location {
                source: parsed.span(&format!("{}_root", span_id)),
                destination: parsed.span("root"),
            };
            if segments.len() > 1 {
                DottedDefinition {
                    source: parsed.span(span_id),
                    root_definition_location,
                    segments: segments.iter().map(|s| (*s).to_owned()).collect(),
                }
                .into()
            } else {
                root_definition_location.into()
            }
        };

        let find_definition = |span_id: &str| {
            module.find_definition(parsed.begin_line(span_id), parsed.begin_column(span_id))
        };

        let expected_foo = expected("foo", &["foo"]);
        let expected_bar = expected("bar", &["foo", "bar"]);
        let expected_baz = expected("baz", &["foo", "bar", "baz"]);
        let expected_quz = expected("quz", &["foo", "bar", "baz", "quz"]);

        assert_eq!(expected_foo, find_definition("foo_click"));
        assert_eq!(expected_bar, find_definition("bar_click"));
        assert_eq!(expected_baz, find_definition("baz_click"));
        assert_eq!(expected_quz, find_definition("quz_click"));

        Ok(())
    }
}


/*
 * 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 thiserror::Error;

use crate::analysis::types::LintT;
use crate::analysis::types::LintWarning;
use crate::codemap::CodeMap;
use crate::codemap::FileSpan;
use crate::codemap::Span;
use crate::syntax::ast::AstExpr;
use crate::syntax::ast::AstLiteral;
use crate::syntax::ast::AstStmt;
use crate::syntax::ast::Expr;
use crate::syntax::ast::Stmt;
use crate::syntax::AstModule;
use crate::values::num::NumRef;
use crate::values::types::bigint::StarlarkBigInt;
use crate::values::types::int_or_big::StarlarkInt;

#[derive(Error, Debug)]
pub(crate) enum Dubious {
    #[error("Duplicate dictionary key `{0}`, also used at {1}")]
    DuplicateKey(String, FileSpan),
    #[error("Variable `{0}` will either do nothing or fail if uninitialised")]
    IdentifierAsStatement(String),
}

impl LintWarning for Dubious {
    fn is_serious(&self) -> bool {
        true
    }

    fn short_name(&self) -> &'static str {
        match self {
            Dubious::DuplicateKey(..) => "duplicate-key",
            Dubious::IdentifierAsStatement(..) => "ident-as-statement",
        }
    }
}

// Go implementation of Starlark disallows duplicate top-level assignments,
// it's likely that will become Starlark standard sooner or later, so check now.
// The one place we allow it is to export something you grabbed with load.
fn duplicate_dictionary_key(module: &AstModule, res: &mut Vec<LintT<Dubious>>) {
    #[derive(PartialEq, Eq, Hash)]
    enum Key<'a> {
        Int(i32),
        BigInt(&'a StarlarkBigInt),
        Float(u64),
        String(&'a str),
        Identifier(&'a str),
    }

    fn to_key<'a>(x: &'a AstExpr) -> Option<(Key<'a>, Span)> {
        match &**x {
            Expr::Literal(x) => match x {
                AstLiteral::Int(x) => match &x.node.0 {
                    StarlarkInt::Small(i) => Some((Key::Int(i.to_i32()), x.span)),
                    StarlarkInt::Big(i) => Some((Key::BigInt(i), x.span)),
                },
                AstLiteral::Float(x) => {
                    let n = NumRef::from(x.node);
                    if let Some(i) = n.as_int() {
                        // make an integer float always collide with other ints
                        Some((Key::Int(i), x.span))
                    } else {
                        // use bits representation of float to be able to always compare them for equality
                        // First normalise -0.0
                        let v = if x.node == 0.0 { 0.0 } else { x.node };
                        Some((Key::Float(v.to_bits()), x.span))
                    }
                }
                AstLiteral::String(x) => Some((Key::String(&x.node), x.span)),
            },
            Expr::Identifier(x) => Some((Key::Identifier(&x.node.0), x.span)),
            _ => None,
        }
    }

    fn expr<'a>(x: &'a AstExpr, codemap: &CodeMap, res: &mut Vec<LintT<Dubious>>) {
        match &**x {
            Expr::Dict(args) => {
                let mut seen = HashMap::new();
                for (key, _) in args {
                    if let Some((key_id, pos)) = to_key(key) {
                        if let Some(old) = seen.insert(key_id, pos) {
                            res.push(LintT::new(
                                codemap,
                                old,
                                Dubious::DuplicateKey(key.to_string(), codemap.file_span(pos)),
                            ))
                        }
                    }
                }
            }
            _ => {}
        }
        x.visit_expr(|x| expr(x, codemap, res));
    }

    module
        .statement
        .visit_expr(|x| expr(x, &module.codemap, res))
}

fn identifier_as_statement(module: &AstModule, res: &mut Vec<LintT<Dubious>>) {
    fn stmt<'a>(x: &'a AstStmt, codemap: &CodeMap, res: &mut Vec<LintT<Dubious>>) {
        match &**x {
            Stmt::Expression(x) => match &**x {
                Expr::Identifier(x) => res.push(LintT::new(
                    codemap,
                    x.span,
                    Dubious::IdentifierAsStatement(x.node.0.clone()),
                )),
                _ => {}
            },
            _ => x.visit_stmt(|x| stmt(x, codemap, res)),
        }
    }

    stmt(&module.statement, &module.codemap, res)
}

pub(crate) fn lint(module: &AstModule) -> Vec<LintT<Dubious>> {
    let mut res = Vec::new();
    duplicate_dictionary_key(module, &mut res);
    identifier_as_statement(module, &mut res);
    res
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::slice_vec_ext::SliceExt;
    use crate::syntax::Dialect;

    fn module(x: &str) -> AstModule {
        AstModule::parse("X", x.to_owned(), &Dialect::Extended).unwrap()
    }

    impl Dubious {
        fn about(&self) -> &String {
            match self {
                Dubious::DuplicateKey(x, _) => x,
                Dubious::IdentifierAsStatement(x) => x,
            }
        }
    }

    #[test]
    fn test_lint_duplicate_keys() {
        let m = module(
            r#"
{'no1': 1, 'no1': 2}
{42: 1, 78: 9, 'no2': 100, 42: 6, 'no2': 8}
{123.0: "f", 123: "i"}
{0.25: "frac", 25e-2: "exp"}

# Variables can't change as a result of expression evaluation,
# so it's always an error if you see the same expression
{no3: 1, no4: 2, yes: 3, no3: 1, no3: 3, no4: 8}

# Functions can change each time round, so don't lint on them.
{f(): 1, f(): 2}
"#,
        );
        let mut res = Vec::new();
        duplicate_dictionary_key(&m, &mut res);
        assert_eq!(
            res.map(|x| x.problem.about()),
            &[
                "\"no1\"", "42", "\"no2\"", "123", "0.25", "no3", "no3", "no4"
            ]
        );
    }

    #[test]
    fn test_lint_identifier_as_statement() {
        let m = module(
            r#"
no1
def foo():
    f(yes)
    no2
"#,
        );
        let mut res = Vec::new();
        identifier_as_statement(&m, &mut res);
        assert_eq!(res.map(|x| x.problem.about()), &["no1", "no2"]);
    }
}


/*
 * 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 dupe::Dupe;

use crate::codemap::FileSpan;
use crate::collections::SmallMap;
use crate::syntax::ast::AstAssignIdent;
use crate::syntax::ast::DefP;
use crate::syntax::ast::Expr;
use crate::syntax::ast::Stmt;
use crate::syntax::AstModule;

/// The type of an exported symbol.
/// If unknown, will use `Any`.
#[derive(Debug, PartialEq, Eq, Copy, Clone, Dupe, Hash)]
pub enum SymbolKind {
    /// Any kind of symbol.
    Any,
    /// The symbol represents something that can be called, for example
    /// a `def` or a variable assigned to a `lambda`.
    Function,
}

impl SymbolKind {
    pub(crate) fn from_expr(x: &Expr) -> Self {
        match x {
            Expr::Lambda(..) => Self::Function,
            _ => Self::Any,
        }
    }
}

/// A symbol. Returned from [`AstModule::exported_symbols`].
#[derive(Debug, PartialEq, Eq, Clone, Dupe, Hash)]
pub struct Symbol<'a> {
    /// The name of the symbol.
    pub name: &'a str,
    /// The location of its definition.
    pub span: FileSpan,
    /// The type of symbol it represents.
    pub kind: SymbolKind,
}

impl AstModule {
    /// Which symbols are exported by this module. These are the top-level assignments,
    /// including function definitions. Any symbols that start with `_` are not exported.
    pub fn exported_symbols<'a>(&'a self) -> Vec<Symbol<'a>> {
        // Map since we only want to store the first of each export
        // IndexMap since we want the order to match the order they were defined in
        let mut result: SmallMap<&'a str, _> = SmallMap::new();

        fn add<'a>(
            me: &AstModule,
            result: &mut SmallMap<&'a str, Symbol<'a>>,
            name: &'a AstAssignIdent,
            kind: SymbolKind,
        ) {
            if !name.0.starts_with('_') {
                result.entry(&name.0).or_insert(Symbol {
                    name: &name.0,
                    span: me.file_span(name.span),
                    kind,
                });
            }
        }

        for x in self.top_level_statements() {
            match &**x {
                Stmt::Assign(dest, rhs) => {
                    dest.visit_lvalue(|name| {
                        let kind = SymbolKind::from_expr(&rhs.1);
                        add(self, &mut result, name, kind);
                    });
                }
                Stmt::AssignModify(dest, _, _) => {
                    dest.visit_lvalue(|name| {
                        add(self, &mut result, name, SymbolKind::Any);
                    });
                }
                Stmt::Def(DefP { name, .. }) => {
                    add(self, &mut result, name, SymbolKind::Function);
                }
                _ => {}
            }
        }
        result.into_values().collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::slice_vec_ext::SliceExt;
    use crate::syntax::Dialect;

    fn module(x: &str) -> AstModule {
        AstModule::parse("X", x.to_owned(), &Dialect::Extended).unwrap()
    }

    #[test]
    fn test_lint_exported() {
        let modu = module(
            r#"
load("test", "a")
def b(): pass
d = 1
def _e(): pass
d = 2
"#,
        );
        let res = modu.exported_symbols();
        assert_eq!(
            res.map(|symbol| format!("{} {}", symbol.span, symbol.name)),
            &["X:3:5-6 b", "X:4:1-2 d"]
        );
    }
}


/*
 * 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 crate::codemap::ResolvedSpan;
use crate::codemap::Span;
use crate::codemap::Spanned;
use crate::syntax::ast::Argument;
use crate::syntax::ast::AstExpr;
use crate::syntax::ast::AstLiteral;
use crate::syntax::ast::Expr;
use crate::syntax::AstModule;

impl AstModule {
    /// Find the location of a top level function call that has a kwarg "name", and a string value
    /// matching `name`.
    ///
    /// NOTE: If the AST is exposed in the future, this function may be removed and implemented
    ///       by specific programs instead.
    pub fn find_function_call_with_name(&self, name: &str) -> Option<ResolvedSpan> {
        let mut ret = None;

        fn visit_expr(ret: &mut Option<Span>, name: &str, node: &AstExpr) {
            if ret.is_some() {
                return;
            }

            match node {
                Spanned {
                    node: Expr::Call(identifier, arguments),
                    ..
                } => {
                    if let Expr::Identifier(_) = &identifier.node {
                        let found = arguments.iter().find_map(|argument| match &argument.node {
                            Argument::Named(
                                arg_name,
                                Spanned {
                                    node: Expr::Literal(AstLiteral::String(s)),
                                    ..
                                },
                            ) if arg_name.node == "name" && s.node == name => Some(identifier.span),
                            _ => None,
                        });
                        if found.is_some() {
                            *ret = found;
                        }
                    }
                }
                _ => node.visit_expr(|x| visit_expr(ret, name, x)),
            }
        }

        self.statement.visit_expr(|x| visit_expr(&mut ret, name, x));
        ret.map(|span| self.codemap.resolve_span(span))
    }
}

#[cfg(test)]
mod test {

    use crate::codemap::ResolvedSpan;
    use crate::syntax::AstModule;
    use crate::syntax::Dialect;

    #[test]
    fn finds_function_calls_with_name_kwarg() -> anyhow::Result<()> {
        let contents = r#"
foo(name = "foo_name")
bar("bar_name")
baz(name = "baz_name")

def x(name = "foo_name"):
    pass
"#;

        let module = AstModule::parse("foo.star", contents.to_owned(), &Dialect::Extended).unwrap();

        assert_eq!(
            Some(ResolvedSpan {
                begin_line: 1,
                begin_column: 0,
                end_line: 1,
                end_column: 3
            }),
            module.find_function_call_with_name("foo_name")
        );
        assert_eq!(None, module.find_function_call_with_name("bar_name"));
        Ok(())
    }
}