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

use crate::analysis::types::LintT;
use crate::analysis::types::LintWarning;
use crate::codemap::CodeMap;
use crate::codemap::ResolvedFileSpan;
use crate::codemap::Span;
use crate::codemap::Spanned;
use crate::syntax::ast::AstExpr;
use crate::syntax::ast::AstLiteral;
use crate::syntax::ast::AstStmt;
use crate::syntax::ast::AstTypeExpr;
use crate::syntax::ast::DefP;
use crate::syntax::ast::Expr;
use crate::syntax::ast::Stmt;
use crate::syntax::AstModule;

#[derive(Error, Debug)]
pub(crate) enum FlowIssue {
    #[error("`return` lacks expression, but function `{0}` at {1} seems to want one due to {2}")]
    MissingReturnExpression(String, ResolvedFileSpan, ResolvedFileSpan),
    #[error("No `return` at the end, but function `{0}` seems to want one due to {1}")]
    MissingReturn(String, ResolvedFileSpan),
    #[error("Unreachable statement `{0}`")]
    Unreachable(String),
    #[error("Redundant `return` at the end of a function")]
    RedundantReturn,
    #[error("Redundant `continue` at the end of a loop")]
    RedundantContinue,
    #[error("A `load` statement not at the top of the file")]
    MisplacedLoad,
    #[error("Statement at has no effect")]
    NoEffect,
}

impl LintWarning for FlowIssue {
    fn is_serious(&self) -> bool {
        match self {
            // Sometimes people add these to make flow clearer
            FlowIssue::RedundantContinue | FlowIssue::RedundantReturn => false,
            _ => true,
        }
    }

    fn short_name(&self) -> &'static str {
        match self {
            FlowIssue::MissingReturnExpression(..) => "missing-return-expression",
            FlowIssue::MissingReturn(..) => "missing-return",
            FlowIssue::Unreachable(..) => "unreachable",
            FlowIssue::RedundantReturn => "redundant-return",
            FlowIssue::RedundantContinue => "redundant-continue",
            FlowIssue::MisplacedLoad => "misplaced-load",
            FlowIssue::NoEffect => "no-effect",
        }
    }
}

fn returns(x: &AstStmt) -> Vec<(Span, Option<&AstExpr>)> {
    fn f<'a>(x: &'a AstStmt, res: &mut Vec<(Span, Option<&'a AstExpr>)>) {
        match &**x {
            Stmt::Return(ret) => res.push((x.span, ret.as_ref())),
            Stmt::Def(..) => {} // Do not descend
            _ => x.visit_stmt(|x| f(x, res)),
        }
    }

    let mut res = Vec::new();
    f(x, &mut res);
    res
}

// fail is kind of like a return with error
fn is_fail(x: &AstExpr) -> bool {
    match &**x {
        Expr::Call(x, _) => match &***x {
            Expr::Identifier(name) => name.node.0 == "fail",
            _ => false,
        },
        _ => false,
    }
}

fn has_effect(x: &AstExpr) -> bool {
    match &**x {
        Expr::Literal(x) => {
            // String literals have the "effect" of providing documentation
            matches!(x, AstLiteral::String(_))
        }
        Expr::Lambda(_) => false,
        Expr::If(_) | Expr::Tuple(_) | Expr::List(_) | Expr::Dict(_) => {
            let mut res = false;
            x.visit_expr(|x| res = res || has_effect(x));
            res
        }
        _ => true,
    }
}

fn final_return(x: &AstStmt) -> bool {
    match &**x {
        Stmt::Return(_) => true,
        Stmt::Expression(x) if is_fail(x) => true,
        Stmt::Statements(xs) => match xs.last() {
            None => false,
            Some(x) => final_return(x),
        },
        Stmt::IfElse(_, x_y) => {
            let (x, y) = &**x_y;
            final_return(x) && final_return(y)
        }
        _ => false,
    }
}

fn require_return_expression(ret_type: &Option<Box<AstTypeExpr>>) -> Option<Span> {
    match ret_type {
        None => None,
        Some(x) => match &x.node.expr.node {
            Expr::Identifier(x) if x.node.0 == "None" => None,
            _ => Some(x.span),
        },
    }
}

fn check_stmt(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
    match &**x {
        Stmt::Def(DefP {
            name,
            params: _,
            return_type,
            body,
            payload: _,
        }) => {
            let rets = returns(body);

            // Do I require my return statements to have an expression
            let require_expression = require_return_expression(return_type)
                .or_else(|| rets.iter().find(|x| x.1.is_some()).map(|x| x.0));
            if let Some(reason) = require_expression {
                if !final_return(body) {
                    res.push(LintT::new(
                        codemap,
                        x.span,
                        FlowIssue::MissingReturn(
                            // Statements often end with \n, so remove that to fit nicely
                            name.node.0.trim_end().to_owned(),
                            codemap.file_span(reason).resolve(),
                        ),
                    ));
                }
                for (span, ret) in rets {
                    if ret.is_none() {
                        res.push(LintT::new(
                            codemap,
                            span,
                            FlowIssue::MissingReturnExpression(
                                name.0.clone(),
                                codemap.file_span(x.span).resolve(),
                                codemap.file_span(reason).resolve(),
                            ),
                        ))
                    }
                }
            }
        }
        _ => {}
    }
}

fn stmt(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
    check_stmt(codemap, x, res);
    x.visit_stmt(|x| stmt(codemap, x, res));
}

// Returns true if the code aborts this sequence early, due to return, fail, break or continue
fn reachable(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) -> bool {
    match &**x {
        Stmt::Break | Stmt::Continue | Stmt::Return(_) => true,
        Stmt::Expression(x) => is_fail(x),
        Stmt::Statements(xs) => {
            let mut i = xs.iter();
            while let Some(x) = i.next() {
                let aborts = reachable(codemap, x, res);
                if aborts {
                    if let Some(nxt) = i.next() {
                        res.push(LintT::new(
                            codemap,
                            nxt.span,
                            FlowIssue::Unreachable(nxt.node.to_string().trim().to_owned()),
                        ))
                    }
                    // All the remaining statements are totally unreachable, but we declared that once
                    // so don't even bother looking at them
                    return aborts;
                }
            }
            false
        }
        Stmt::IfElse(_, x_y) => {
            let (x, y) = &**x_y;
            let abort1 = reachable(codemap, x, res);
            let abort2 = reachable(codemap, y, res);
            abort1 && abort2
        }
        // For all remaining constructs, visit their children to accumulate errors,
        // but even if they are present with returns, you don't guarantee the code with inner returns
        // gets executed.
        _ => {
            x.visit_stmt(|x| {
                reachable(codemap, x, res);
            });
            false
        }
    }
}

// If you have a definition which ends with return, or a loop which ends with continue
// that is a useless statement that just
fn redundant(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
    fn check(is_loop: bool, codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
        match &**x {
            Stmt::Continue if is_loop => {
                res.push(LintT::new(codemap, x.span, FlowIssue::RedundantContinue))
            }
            Stmt::Return(None) if !is_loop => {
                res.push(LintT::new(codemap, x.span, FlowIssue::RedundantReturn))
            }
            Stmt::Statements(xs) if !xs.is_empty() => {
                check(is_loop, codemap, xs.last().unwrap(), res)
            }
            Stmt::If(_, x) => check(is_loop, codemap, x, res),
            Stmt::IfElse(_, x_y) => {
                let (x, y) = &**x_y;
                check(is_loop, codemap, x, res);
                check(is_loop, codemap, y, res);
            }
            _ => {}
        }
    }

    fn f(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
        match &**x {
            Stmt::For(_, over_body) => {
                let (_over, body) = &**over_body;
                check(true, codemap, body, res)
            }
            Stmt::Def(DefP { body, .. }) => check(false, codemap, body, res),
            _ => {}
        }
        // We always want to look inside everything for other types of violation
        x.visit_stmt(|x| f(codemap, x, res))
    }

    x.visit_stmt(|x| f(codemap, x, res));
}

fn misplaced_load(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
    // accumulate all statements at the top-level
    fn top_statements<'a>(x: &'a AstStmt, stmts: &mut Vec<&'a AstStmt>) {
        match &**x {
            Stmt::Statements(xs) => {
                for x in xs {
                    top_statements(x, stmts);
                }
            }
            _ => stmts.push(x),
        }
    }

    let mut stmts = Vec::new();
    top_statements(x, &mut stmts);

    // We allow loads or documentation strings, but after that, no loads
    let mut allow_loads = true;
    for x in stmts {
        match &**x {
            Stmt::Load(..) => {
                if !allow_loads {
                    res.push(LintT::new(codemap, x.span, FlowIssue::MisplacedLoad))
                }
            }
            Stmt::Expression(Spanned {
                node: Expr::Literal(AstLiteral::String(_)),
                ..
            }) => {
                // Still allow loads after a literal string (probably documentation)
            }
            _ => allow_loads = false,
        }
    }
}

fn no_effect(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
    match &**x {
        Stmt::Expression(x) if !has_effect(x) => {
            res.push(LintT::new(codemap, x.span, FlowIssue::NoEffect))
        }
        _ => x.visit_stmt(|x| no_effect(codemap, x, res)),
    }
}

pub(crate) fn lint(module: &AstModule) -> Vec<LintT<FlowIssue>> {
    let mut res = Vec::new();
    stmt(&module.codemap, &module.statement, &mut res);
    reachable(&module.codemap, &module.statement, &mut res);
    redundant(&module.codemap, &module.statement, &mut res);
    misplaced_load(&module.codemap, &module.statement, &mut res);
    no_effect(&module.codemap, &module.statement, &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 FlowIssue {
        fn about(&self) -> &String {
            match self {
                FlowIssue::MissingReturnExpression(x, _, _) => x,
                FlowIssue::MissingReturn(x, _) => x,
                FlowIssue::Unreachable(x) => x,
                _ => panic!("Should not be used on such issues in test code"),
            }
        }
    }

    #[test]
    fn test_lint_returns() {
        let m = module(
            r#"
def no1() -> "string":
    pass
def no2():
    if x:
        return 1
def no3():
    if x:
        return
    return 1
def ok():
    def no4() -> "int":
        no4()
    pass
def yes1():
    pass
def yes2() -> "string":
    yes1()
    if x:
        return "x"
    else:
        return "y"
def yes3():
    if x:
        return
    pass
def yes4() -> "string":
    fail("die")
"#,
        );
        let mut res = Vec::new();
        stmt(&m.codemap, &m.statement, &mut res);
        assert_eq!(
            res.map(|x| x.problem.about()),
            &["no1", "no2", "no3", "no4"]
        );
    }

    #[test]
    fn test_lint_unreachable() {
        let m = module(
            r#"
def test():
    return 1
    no1
def test2():
    if x:
        return 1
    yes
def test3():
    if x:
        return
    else:
        return
    no2
    ignored
def test4():
    for x in xs:
        continue
        no3
    reachable
def test5():
    for x in xs:
        if test:
            continue
        reachable
        return
    reachable
def test6():
    fail(1)
    no4
def f():
    def g():
        return 5
    reachable
"#,
        );
        let mut res = Vec::new();
        reachable(&m.codemap, &m.statement, &mut res);
        assert_eq!(
            res.map(|x| x.problem.about()),
            &["no1", "no2", "no3", "no4"]
        );
    }

    #[test]
    fn test_lint_redundant() {
        let m = module(
            r#"
def test(): # 1
    foo
    return # bad: 3
def test2(): # 4
    return
    foo
def test3(): # 7
    if x:
        return # bad: 9
    else:
        y + 1
def test4(): # 12
    def test5():
        for x in xs:
            test
            if x:
                return
            else:
                continue # bad: 19
    test5()
def test6():
    if x:
        return
    y + 1
def test7():
    for x in xs:
        if x:
            continue
        return
"#,
        );
        let mut res = Vec::new();
        redundant(&m.codemap, &m.statement, &mut res);
        assert_eq!(
            res.map(|x| x.location.resolve_span().begin_line),
            &[3, 9, 19]
        );
    }

    #[test]
    fn test_lint_misplaced_load() {
        let m = module(
            r#"
load("a", "a")
"""
this is some comment
over multiple lines
"""
load("b", "b")

x = 1
load("c", "b")
"#,
        );
        let mut res = Vec::new();
        misplaced_load(&m.codemap, &m.statement, &mut res);
        assert_eq!(res.len(), 1);
    }

    #[test]
    fn test_lint_no_effect() {
        let src = r#"
"""
a doc block
"""
load("b", "b")

x = 1
7 ## BAD
def foo():
    [18] ## BAD
1 + 2
"#;

        let m = module(src);
        let bad = src
            .lines()
            .enumerate()
            .filter_map(|(i, x)| x.contains("## BAD").then_some(i))
            .collect::<Vec<_>>();
        let mut res = Vec::new();
        no_effect(&m.codemap, &m.statement, &mut res);
        assert_eq!(res.map(|x| x.location.resolve_span().begin_line), bad);
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::collections::HashMap;
use std::collections::HashSet;

use maplit::hashmap;
use once_cell::sync::Lazy;
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::Assign;
use crate::syntax::ast::AstAssignIdent;
use crate::syntax::ast::AstExpr;
use crate::syntax::ast::AstStmt;
use crate::syntax::ast::BinOp;
use crate::syntax::ast::DefP;
use crate::syntax::ast::Expr;
use crate::syntax::ast::Stmt;
use crate::syntax::AstModule;

#[derive(Error, Debug)]
pub(crate) enum Incompatibility {
    #[error("Type check `{0}` should be written `{1}`")]
    IncompatibleTypeCheck(String, String),
    #[error("Duplicate top-level assignment of `{0}`, first defined at {1}")]
    DuplicateTopLevelAssign(String, FileSpan),
}

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

    fn short_name(&self) -> &'static str {
        match self {
            Incompatibility::IncompatibleTypeCheck(..) => "incompatible-type-check",
            Incompatibility::DuplicateTopLevelAssign(..) => "duplicate-top-level-assign",
        }
    }
}

static TYPES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
    hashmap![
        "bool" => "True",
        "tuple" => "()",
        "str" => "\"\"",
        "list" => "[]",
        "int" => "0"
    ]
});

fn match_bad_type_equality(
    codemap: &CodeMap,
    x: &AstExpr,
    types: &HashMap<&str, &str>,
    res: &mut Vec<LintT<Incompatibility>>,
) {
    fn lookup_type<'a>(x: &AstExpr, types: &HashMap<&str, &'a str>) -> Option<&'a str> {
        match &**x {
            Expr::Identifier(name) => types.get(name.node.0.as_str()).copied(),
            _ => None,
        }
    }

    // Return true if this expression matches `type($x)`
    fn is_type_call(x: &AstExpr) -> bool {
        match &**x {
            Expr::Call(fun, args) if args.len() == 1 => match &***fun {
                Expr::Identifier(x) => x.node.0 == "type",
                _ => false,
            },
            _ => false,
        }
    }

    // If we see type(x) == y (or negated), where y is in our types table, suggest a replacement
    match &**x {
        Expr::Op(lhs, op, rhs)
            if (*op == BinOp::Equal || *op == BinOp::NotEqual) && is_type_call(lhs) =>
        {
            if let Some(replacement) = lookup_type(rhs, types) {
                res.push(LintT::new(
                    codemap,
                    x.span,
                    Incompatibility::IncompatibleTypeCheck(
                        x.to_string(),
                        format!("{}{}type({})", lhs.node, op, replacement),
                    ),
                ))
            }
        }
        _ => {}
    }
}

fn bad_type_equality(module: &AstModule, res: &mut Vec<LintT<Incompatibility>>) {
    let types = Lazy::force(&TYPES);
    fn check(
        codemap: &CodeMap,
        x: &AstExpr,
        types: &HashMap<&str, &str>,
        res: &mut Vec<LintT<Incompatibility>>,
    ) {
        match_bad_type_equality(codemap, x, types, res);
        x.visit_expr(|x| check(codemap, x, types, res));
    }
    module
        .statement
        .visit_expr(|x| check(&module.codemap, x, types, res));
}

// 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_top_level_assignment(module: &AstModule, res: &mut Vec<LintT<Incompatibility>>) {
    let mut defined = HashMap::new(); //(name, (location, is_load))
    let mut exported = HashSet::new(); // name's already exported by is_load

    fn ident<'a>(
        x: &'a AstAssignIdent,
        is_load: bool,
        codemap: &CodeMap,
        defined: &mut HashMap<&'a str, (Span, bool)>,
        res: &mut Vec<LintT<Incompatibility>>,
    ) {
        if let Some((old, _)) = defined.get(x.0.as_str()) {
            res.push(LintT::new(
                codemap,
                x.span,
                Incompatibility::DuplicateTopLevelAssign(x.0.clone(), codemap.file_span(*old)),
            ));
        } else {
            defined.insert(&x.0, (x.span, is_load));
        }
    }

    fn stmt<'a>(
        x: &'a AstStmt,
        codemap: &CodeMap,
        defined: &mut HashMap<&'a str, (Span, bool)>,
        exported: &mut HashSet<&'a str>,
        res: &mut Vec<LintT<Incompatibility>>,
    ) {
        match &**x {
            Stmt::Assign(lhs, op_rhs) => match (&**lhs, &(**op_rhs).1.node) {
                (Assign::Identifier(x), Expr::Identifier(y))
                    if x.node.0 == y.node.0
                        && defined.get(x.node.0.as_str()).map_or(false, |x| x.1)
                        && !exported.contains(x.node.0.as_str()) =>
                {
                    // Normally this would be an error, but if we load()'d it, this is how we'd reexport through Starlark.
                    // But only allow one export
                    exported.insert(x.node.0.as_str());
                }
                _ => lhs.visit_lvalue(|x| ident(x, false, codemap, defined, res)),
            },
            Stmt::AssignModify(lhs, _, _) => {
                lhs.visit_lvalue(|x| ident(x, false, codemap, defined, res))
            }
            Stmt::Def(DefP { name, .. }) => ident(name, false, codemap, defined, res),
            Stmt::Load(load) => {
                for (name, _) in &load.args {
                    ident(name, true, codemap, defined, res)
                }
            }
            // Visit statements, but don't descend under def - only top-level statements are interesting
            _ => x.visit_stmt(|x| stmt(x, codemap, defined, exported, res)),
        }
    }

    stmt(
        &module.statement,
        &module.codemap,
        &mut defined,
        &mut exported,
        res,
    )
}

pub(crate) fn lint(module: &AstModule) -> Vec<LintT<Incompatibility>> {
    let mut res = Vec::new();
    bad_type_equality(module, &mut res);
    duplicate_top_level_assignment(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("bad.py", x.to_owned(), &Dialect::Extended).unwrap()
    }

    #[test]
    fn test_lint_incompatible() {
        let mut res = Vec::new();
        bad_type_equality(
            &module(
                r#"
def foo():
    if type(x) == str and type(y) == type(list) and type(z) == foobar:
        pass
"#,
            ),
            &mut res,
        );
        assert_eq!(
            res.map(|x| x.to_string()),
            &[
                "bad.py:3:8-22: Type check `(type(x) == str)` should be written `type(x) == type(\"\")`"
            ]
        );
    }

    #[test]
    fn test_lint_duplicate_top_level_assign() {
        let m = module(
            r#"
load("file", "foo", "no3", "no4")
no1 = 1
no1 = 4
no1 += 8
foo = foo # Starlark reexport
no3 = no3
no3 = no3
no4 = no4 + 1
def no2(): pass
def no2():
    x = 1
    x += 1
    return x
"#,
        );
        let mut res = Vec::new();
        duplicate_top_level_assignment(&m, &mut res);
        let mut res = res.map(|x| match &x.problem {
            Incompatibility::DuplicateTopLevelAssign(x, _) => x,
            _ => panic!("Unexpected lint"),
        });
        res.sort();
        assert_eq!(res, &["no1", "no1", "no2", "no3", "no4"])
    }
}


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

pub use types::EvalMessage;
pub use types::EvalSeverity;
pub use types::Lint;

use crate::analysis::types::LintT;
use crate::syntax::AstModule;

mod bind;
pub(crate) mod definition;
mod dubious;
pub(crate) mod exported;
mod find_call_name;
mod flow;
mod incompatible;
mod names;
mod performance;
mod types;
mod underscore;

impl AstModule {
    /// Run a static linter over the module. If the complete set of global variables are known
    /// they can be passed as the `globals` argument, resulting in name-resolution lint errors.
    /// The precise checks run by the linter are not considered stable between versions.
    pub fn lint(&self, globals: Option<&HashSet<String>>) -> Vec<Lint> {
        let mut res = Vec::new();
        res.extend(flow::lint(self).into_iter().map(LintT::erase));
        res.extend(incompatible::lint(self).into_iter().map(LintT::erase));
        res.extend(dubious::lint(self).into_iter().map(LintT::erase));
        res.extend(names::lint(self, globals).into_iter().map(LintT::erase));
        res.extend(underscore::lint(self).into_iter().map(LintT::erase));
        res.extend(performance::lint(self).into_iter().map(LintT::erase));
        res
    }
}


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

// There are two improvements that we could make:
//
// 1. Use the existing name resolution code, so we don't end up duplicating that logic.
// 2. Extract a flow graph rather than doing a fixed point.
//
// But it does as things stand.

use std::cmp;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::collections::HashSet;
use std::mem;

use dupe::Dupe;
use maplit::hashset;
use thiserror::Error;

use crate::analysis::types::LintT;
use crate::analysis::types::LintWarning;
use crate::codemap::CodeMap;
use crate::codemap::Span;
use crate::codemap::Spanned;
use crate::syntax::ast::AstAssign;
use crate::syntax::ast::AstAssignIdent;
use crate::syntax::ast::AstExpr;
use crate::syntax::ast::AstIdent;
use crate::syntax::ast::AstStmt;
use crate::syntax::ast::AstTypeExpr;
use crate::syntax::ast::Clause;
use crate::syntax::ast::Expr;
use crate::syntax::ast::ForClause;
use crate::syntax::ast::Stmt;
use crate::syntax::AstModule;

#[derive(Error, Debug)]
pub(crate) enum NameWarning {
    #[error("Unused `load` of `{0}`")]
    UnusedLoad(String),
    #[error("Unused assignment of `{0}`")]
    UnusedAssign(String),
    #[error("Unused argument `{0}`")]
    UnusedArgument(String),
    #[error("Use of unassigned variable `{0}`")]
    UsingUnassigned(String),
    #[error("Use of undefined variable `{0}`")]
    UsingUndefined(String),
    #[error("Use of potentially undefined variable `{0}`")]
    UsingMaybeUndefined(String),
}

impl LintWarning for NameWarning {
    fn is_serious(&self) -> bool {
        match self {
            Self::UsingUnassigned(..) => true,
            _ => false,
        }
    }

    fn short_name(&self) -> &'static str {
        match self {
            Self::UnusedLoad(..) => "unused-load",
            Self::UnusedAssign(..) => "unused-assign",
            Self::UnusedArgument(..) => "unused-argument",
            Self::UsingUnassigned(..) => "using-unassigned",
            Self::UsingUndefined(..) => "using-undefined",
            Self::UsingMaybeUndefined(..) => "using-maybe-undefined",
        }
    }
}

#[derive(Debug, Copy, Dupe, Clone, PartialEq, Eq)]
enum Kind {
    Load,
    Argument,
    Assign,
}

impl Kind {
    fn unused(self, name: String) -> NameWarning {
        match self {
            Kind::Load => NameWarning::UnusedLoad(name),
            Kind::Argument => NameWarning::UnusedArgument(name),
            Kind::Assign => NameWarning::UnusedAssign(name),
        }
    }
}

#[derive(Debug, Copy, Dupe, Clone, PartialEq, Eq)]
enum Assigned {
    Definitely,
    Maybe,
}

type AstStr<'a> = Spanned<&'a str>;

impl<'a> AstStr<'a> {
    fn new(span: Span, str: &'a str) -> Self {
        Spanned { span, node: str }
    }

    fn ident(x: &'a AstIdent) -> Self {
        Self::new(x.span, x.node.0.as_str())
    }

    fn assign_ident(x: &'a AstAssignIdent) -> Self {
        Self::new(x.span, x.node.0.as_str())
    }
}

/// When we see a control flow operator, how far does it apply?
#[derive(Copy, Dupe, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Abort {
    /// Abort the loop (e.g. `continue` or `break`)
    Loop,
    /// Abort the function (e.g. `return`)
    Function,
}

/// Is this function the global `fail` function.
/// Can technically be shadowed, but if you shadow it with something that doesn't fail
/// you're going to confuse users, so not too problematic.
fn is_fail(x: &AstExpr) -> bool {
    if let Expr::Call(x, _) = &**x {
        if let Expr::Identifier(x) = &***x {
            return x.0.as_str() == "fail";
        }
    }
    false
}

/// A combination of the scope information (what is set) and state information as we
/// step through the function.
#[derive(Debug, Default)]
struct ScopeState<'a> {
    /// Those identifiers that weren't set at that point, so we went to the parent.
    /// If these are set later on, then it is a unassigned usage error.
    cant_set: Vec<AstStr<'a>>,
    /// Those identifiers that I couldn't find at the point I saw them.
    /// Since the child runs in a different order with respect to the parent, they might resolve later.
    unbound: Vec<AstStr<'a>>,
    /// Those definition sites that we have ended up using.
    used: HashSet<AstStr<'a>>,
    /// Those definitions that we have set.
    set: Vec<(AstStr<'a>, Kind)>,
    /// The last location/locations where I was set.
    /// The assigned is whether I am always set or not
    last_set: HashMap<&'a str, (Assigned, HashSet<Span>)>,
    /// Whether I can be reached
    abort: Option<Abort>,
}

/// The state we use when scanning the variables
struct State<'a> {
    codemap: &'a CodeMap,
    /// Those that are set in the global scope.
    /// If None then assume anything might be set at the global scope.
    globals: Option<&'a HashSet<String>>,
    /// These are the various scopes - one for the module, one for each def.
    scopes: Vec<ScopeState<'a>>,
    /// The current list of warnings.
    warnings: Vec<LintT<NameWarning>>,
    /// Thing things we have already warned about (no duplicates due to running loops twice)
    warned: HashSet<AstStr<'a>>,
    /// How many nested loops we are in
    loop_depth: usize,
}

impl<'a> State<'a> {
    fn add_warning(&mut self, ident: AstStr<'a>, ctor: impl Fn(String) -> NameWarning) {
        if self.warned.insert(ident) {
            self.warnings.push(LintT::new(
                self.codemap,
                ident.span,
                ctor(ident.node.to_owned()),
            ))
        }
    }

    // Scope stuff

    fn exit_scope(&mut self) {
        // The scope only collects things that could have been assigned, so should always be
        let scope = self.scopes.pop().unwrap();
        // Variables defined in the local scope
        let local: HashSet<&str> = scope.set.iter().map(|x| x.0.node).collect();

        // unset & set => we thought it was in the parent scope, but was actually undefined
        for x in &scope.cant_set {
            if local.contains(x.node) {
                self.add_warning(*x, NameWarning::UsingUnassigned);
            }
        }

        // some of our unbound variables were from our children
        // these might use any random variable at any point - impossible to know
        // so for those, just assume they are used
        let (unbound_defined, unbound_undefined) = scope
            .unbound
            .iter()
            .partition::<Vec<AstStr>, _>(|x| local.contains(x.node));
        let unbound_defined: HashSet<&str> = unbound_defined.iter().map(|x| x.node).collect();

        // set & !used => we set it but never used it in any branch
        // if we are in unbound,
        let top = self.scopes.is_empty();
        for (ident, kind) in scope.set {
            if !scope.used.contains(&ident) && !unbound_defined.contains(ident.node) {
                let underscore = ident.node.starts_with('_');
                let exported = top
                    && !underscore
                    && kind == Kind::Assign // Assume loads don't automatically export
                    && scope
                        .last_set
                        .get(ident.node)
                        .map_or(false, |x| x.1.contains(&ident.span));
                let ignored = !top && underscore;

                if !exported && !ignored {
                    self.add_warning(ident, |s| kind.unused(s));
                }
            }
        }

        // unbound & not defined => move to the parents unbound
        // unbound & no parent => undefined
        match self.scopes.last_mut() {
            None => {
                if let Some(globals) = self.globals {
                    for x in unbound_undefined {
                        if !globals.contains(x.node) {
                            self.add_warning(x, NameWarning::UsingUndefined)
                        }
                    }
                }
            }
            Some(parent) => {
                // these things were unbound, but perhaps in a child, and perhaps we defined it later
                // as we can use a variable in a child before we define it, since the child is a def,
                // so runs later.
                for x in unbound_undefined {
                    if !local.contains(x.node) {
                        parent.unbound.push(x);
                    }
                }
            }
        }
    }

    fn enter_scope(&mut self) {
        self.scopes.push(ScopeState::default());
    }

    // Nest stuff

    fn set_abort(&mut self, abort: Abort) {
        self.scopes.last_mut().unwrap().abort = Some(abort);
    }

    fn loops(&mut self, inner: impl Fn(&mut State<'a>)) {
        self.loop_depth += 1;
        // We run the loop twice since it might set a variable that is only used in subsequent iterations of the loop.
        // That means n nested loops are now O(2^n), but if n > 5, just give up and only iterate once
        // (a few too many warnings, bounded runtime).
        for _ in 0..(if self.loop_depth > 5 { 1 } else { 2 }) {
            self.branch(&inner, |_| ());
            let scope = self.scopes.last_mut().unwrap();
            if scope.abort == Some(Abort::Loop) {
                scope.abort = None;
            }
        }
        self.loop_depth -= 1;
    }

    fn branch(&mut self, opt1: impl Fn(&mut State<'a>), opt2: impl Fn(&mut State<'a>)) {
        let original_abort = self.scopes.last().unwrap().abort;
        let mut other = self.scopes.last().unwrap().last_set.clone();
        opt1(self);
        let other_abort = self.scopes.last().unwrap().abort;
        mem::swap(&mut other, &mut self.scopes.last_mut().unwrap().last_set);
        self.scopes.last_mut().unwrap().abort = original_abort;
        opt2(self);
        let current_abort = self.scopes.last().unwrap().abort;

        // now need to merge `current` with `other`
        self.scopes.last_mut().unwrap().abort = cmp::min(other_abort, current_abort);
        let current = &mut self.scopes.last_mut().unwrap().last_set;

        let join_assigned = |c, o| {
            match (current_abort.is_some(), other_abort.is_some()) {
                (true, true) => Assigned::Definitely, // Probably irrelevant
                (true, false) => o,
                (false, true) => c,
                (false, false) => {
                    if c == Assigned::Maybe {
                        Assigned::Maybe
                    } else {
                        o
                    }
                }
            }
        };

        let mut keys: HashSet<&str> = current.keys().copied().collect();
        keys.extend(other.keys().copied());
        for k in keys {
            match current.entry(k) {
                Entry::Vacant(e) => {
                    if let Some(x) = other.remove(k) {
                        e.insert((join_assigned(Assigned::Maybe, x.0), x.1));
                    }
                }
                Entry::Occupied(mut e) => match other.remove(k) {
                    None => {
                        e.get_mut().0 = join_assigned(e.get().0, Assigned::Maybe);
                    }
                    Some((assigned, spans)) => {
                        e.get_mut().0 = join_assigned(e.get().0, assigned);
                        for span in spans {
                            e.get_mut().1.insert(span);
                        }
                    }
                },
            }
        }
    }

    // Actual operations

    fn use_ident(&mut self, ident: AstStr<'a>) {
        for (depth, scope) in self.scopes.iter_mut().rev().enumerate() {
            match scope.last_set.get(ident.node) {
                None => {
                    if depth == 0 {
                        scope.cant_set.push(ident)
                    }
                }
                Some((assigned, spans)) => {
                    for span in spans {
                        scope.used.insert(AstStr::new(*span, ident.node));
                    }
                    if *assigned == Assigned::Maybe {
                        self.add_warning(ident, NameWarning::UsingMaybeUndefined)
                    }
                    return;
                }
            }
        }
        self.scopes.last_mut().unwrap().unbound.push(ident);
    }

    fn set_ident(&mut self, ident: &'a AstAssignIdent, kind: Kind) {
        let ident = AstStr::assign_ident(ident);
        let scope = self.scopes.last_mut().unwrap();
        scope.set.push((ident, kind));
        scope
            .last_set
            .insert(ident.node, (Assigned::Definitely, hashset![ident.span]));
    }

    // Traverse the syntax tree

    fn assign(&mut self, assign: &'a AstAssign) {
        assign.visit_expr(|x| self.expr(x));
        assign.visit_lvalue(|x| self.set_ident(x, Kind::Assign))
    }

    fn comprehension(
        &mut self,
        res1: &'a AstExpr,
        res2: Option<&'a AstExpr>,
        for_: &'a ForClause,
        clauses: &'a [Clause],
    ) {
        self.expr(&for_.over);
        self.enter_scope();
        // This isn't quite right, as we assume the comprehensions are always evaluated (e.g. no zero arrays)
        // and the assign is always hit, which isn't true.
        // But it is a close enough approximation, and comprehensions tend not to have variable assignment issues.
        self.assign(&for_.var);
        for clause in clauses {
            match clause {
                Clause::For(ForClause { var, over }) => {
                    self.expr(over);
                    self.assign(var);
                }
                Clause::If(x) => self.expr(x),
            }
        }
        self.expr(res1);
        self.expr_opt(res2);
        self.exit_scope();
    }

    fn expr(&mut self, expr: &'a AstExpr) {
        match &**expr {
            Expr::Identifier(ident) => self.use_ident(AstStr::ident(ident)),
            Expr::Lambda(x) => {
                for p in &x.params {
                    p.node.visit_expr(|e| self.expr(e));
                }
                self.enter_scope();
                for p in &x.params {
                    if let Some(pname) = p.node.ident() {
                        self.set_ident(pname, Kind::Argument);
                    }
                }
                self.expr(&x.body);
                self.exit_scope();
            }
            Expr::ListComprehension(a, b, c) => self.comprehension(a, None, b, c),
            Expr::DictComprehension(a, b, c) => self.comprehension(&a.0, Some(&a.1), b, c),
            _ => expr.visit_expr(|x| self.expr(x)),
        }
    }

    fn expr_opt(&mut self, expr: Option<&'a AstExpr>) {
        if let Some(x) = expr {
            self.expr(x)
        }
    }

    fn typ_opt(&mut self, ty: Option<&'a AstTypeExpr>) {
        if let Some(x) = ty {
            self.typ(x)
        }
    }

    fn typ(&mut self, ty: &'a AstTypeExpr) {
        self.expr(&ty.expr)
    }

    fn assign_as_expr(&mut self, assign: &'a AstAssign) {
        assign.visit_expr(|x| self.expr(x));
        assign.visit_lvalue(|x| self.use_ident(AstStr::assign_ident(x)));
    }

    fn stmt(&mut self, stmt: &'a AstStmt) {
        match &**stmt {
            Stmt::Expression(x) => {
                self.expr(x);
                if is_fail(x) {
                    self.set_abort(Abort::Function);
                }
            }
            Stmt::Return(x) => {
                self.expr_opt(x.as_ref());
                self.set_abort(Abort::Function);
            }
            Stmt::Assign(lhs, type_rhs) => {
                self.typ_opt(type_rhs.0.as_ref());
                self.expr(&type_rhs.1);
                self.assign(lhs);
            }
            Stmt::AssignModify(lhs, _, rhs) => {
                self.expr(rhs);
                self.assign_as_expr(lhs);
                self.assign(lhs);
            }
            Stmt::Statements(xs) => {
                for x in xs {
                    self.stmt(x)
                }
            }
            Stmt::If(cond, t) => {
                self.expr(cond);
                self.branch(|me| me.stmt(t), |_| ());
            }
            Stmt::IfElse(cond, t_f) => {
                self.expr(cond);
                self.branch(|me| me.stmt(&t_f.0), |me| me.stmt(&t_f.1));
            }
            Stmt::For(var, iter_body) => {
                self.expr(&iter_body.0);
                // Note this isn't 100% correct, as a for loop may set something the next iteration consumes
                self.loops(|me| {
                    me.assign(var);
                    me.stmt(&iter_body.1);
                });
            }
            Stmt::Def(x) => {
                for p in &x.params {
                    p.node.visit_expr(|e| self.expr(e));
                }
                self.typ_opt(x.return_type.as_deref());
                self.set_ident(&x.name, Kind::Assign);
                self.enter_scope();
                for p in &x.params {
                    if let Some(pname) = p.node.ident() {
                        self.set_ident(pname, Kind::Argument);
                    }
                }
                self.stmt(&x.body);
                self.exit_scope();
            }
            // These were handled by collecting the scopes
            Stmt::Load(x) => {
                for (ident, _) in &x.args {
                    self.set_ident(ident, Kind::Load)
                }
            }
            // These control flow operators can be ignored - either the code after is fine (no problem)
            // or in error (in which case you have useless code after flow control)
            Stmt::Break | Stmt::Continue => {
                self.set_abort(Abort::Loop);
            }
            Stmt::Pass => {}
        }
    }

    fn module(&mut self, module: &'a AstModule) {
        self.enter_scope();
        self.stmt(&module.statement);
        self.exit_scope();
    }
}

pub(crate) fn lint(
    module: &AstModule,
    globals: Option<&HashSet<String>>,
) -> Vec<LintT<NameWarning>> {
    let mut state = State {
        codemap: &module.codemap,
        globals,
        scopes: Vec::new(),
        warnings: Vec::new(),
        warned: HashSet::new(),
        loop_depth: 0,
    };
    state.module(module);
    state.warnings
}

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

    impl NameWarning {
        fn about(&self) -> &String {
            match self {
                NameWarning::UnusedLoad(x) => x,
                NameWarning::UnusedAssign(x) => x,
                NameWarning::UnusedArgument(x) => x,
                NameWarning::UsingUnassigned(x) => x,
                NameWarning::UsingUndefined(x) => x,
                NameWarning::UsingMaybeUndefined(x) => x,
            }
        }
    }

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

    #[test]
    fn test_lint_unused() {
        // unused kind arg is fine if it starts with _
        // allowed to ignore things which are

        let m = module(
            r#"
load("test", "no1", "a")
b = a
_c = 18
_no2 = 17
d = _c
_e = 9
_no4 = 1
def magic(no3, f, _allowed):
    _ignore = f
    _no4 = f # shadows out _no4, which is thus unused
    no5 = 10
    (a, _) = f()
    (a2, _ignored) = f()
    f.go(_e, a, a2, _no4)
def uses_h():
    _h
_h = []
def dots(f, g):
    (d_a, d_b, d_c, d_d, d_e, d_f, d_g, d_h, d_i, d_j) = (20, 21, 22, 23, 24, 25, 26, 27, 28, 29)
    # Make sure that expressions on dotted accesses are accounted for.
    # Ignore that the integers referenced aren't functions. They're just names.
    f.foo[d_a].bar
    f.foo[d_b].bar[d_c]
    f.baz(g(["ignore", d_d]))
    f.foobar.baz(d_e, [d_f], d_g([d_h]))
    d_i(d_j).blah
    pass
def _no6(): pass
def foo():
    array = [1,2,3]
    x = 1
    array[x] = 9
    return array
"#,
        );
        let res = lint(&m, None);
        let mut res = res.map(|x| x.problem.about());
        res.sort();
        assert_eq!(res, &["_no2", "_no4", "_no6", "no1", "no3", "no5"]);
    }

    #[test]
    fn test_lint_duplicate_assign() {
        let m = module(
            r#"
load("test", "no1", "a")
no1 = 12
b = a
no2 = 8
no2 = 9
c = 8
c += 1
def foo():
    no3 = 1
    no3 = 1
    _ignore = a
    _ignore = a
    if foo2:
        d = 1
    else:
        d = 2
    _use = (no3, d)
no2 = 10
_no4 = 1
_no4 = 2
export = _no4

def capture_e(): print(e)
e = 8
capture_e()
e = 10
capture_e()
"#,
        );
        let res = lint(&m, None);
        let mut res = res.map(|x| x.problem.about());
        res.sort();
        assert_eq!(res, &["_no4", "no1", "no2", "no2", "no3"]);
    }

    #[test]
    fn test_lint_unassigned() {
        let m = module(
            r#"
a = no1
no1 = 9
test = no3()
def foo():
    b = no2 + undefined_variable
    for x in xs:
        no2 = 18 + x + b
        _use = no2
def no3(): pass
def uses_h():
    _h
_h = []
"#,
        );
        let res = lint(&m, None);
        let mut res = res.map(|x| x.problem.about());
        res.sort();
        assert_eq!(res, &["no1", "no2", "no3"]);
    }

    #[test]
    fn test_lint_undefined() {
        let globals = HashSet::from(["True".to_owned(), "fail".to_owned()]);

        let m = module(
            r#"
load("test", imported = "more")
a = True + imported + no1
def foo():
    fail("test") + no2(t = 3)
def bar(ctx):
    ctx.attrs.dep[fail.index].default_outputs[0]
    ctx[0].default_outputs
"#,
        );
        let res = lint(&m, Some(&globals));
        let mut res = res.map(|x| x.problem.about());
        res.sort();
        assert_eq!(res, &["no1", "no2"])
    }

    #[test]
    fn test_early_fail() {
        let m = module(
            r#"
def foo(x):
    if x == 1:
        ok1 = 2
    elif x == 3:
        ok1 = 7
    else:
        fail("bad")
    return ok1

def bar(xs):
    for x in xs:
        if x == 1:
            ok2 = 2
        else:
            continue
        print(ok2)
"#,
        );
        let res = lint(&m, None);
        let res = res.map(|x| x.problem.about());
        assert_eq!(res, Vec::<&String>::new())
    }

    #[test]
    fn test_assign_for_next() {
        let m = module(
            r#"
def foo(xs):
    counter = 0
    reached = None
    for x in xs:
        if counter == 7:
            reached = x
        counter += 1
    return reached
"#,
        );
        let res = lint(&m, None);
        let res = res.map(|x| x.problem.about());
        assert_eq!(res, Vec::<&String>::new())
    }

    #[test]
    fn test_flow_control() {
        let m = module(
            r#"
def foo(b, xs):
    if b:
        no1 = xs
    for x in no1:
        return x
    for no2 in xs:
        print(no2)
    print(no2)
    if b:
        yes = xs
    else:
        yes = []
    return yes
"#,
        );
        let res = lint(&m, None);
        let res = res.map(|x| x.problem.about());
        assert_eq!(res, &["no1", "no2"])
    }

    #[test]
    fn test_lambda_capture() {
        let m = module(
            r#"
def foo():
    lam = lambda: print(x)
    x = 1
    lam()
    x = 2
    lam()
"#,
        );
        let res = lint(&m, None);
        assert_eq!(res.len(), 0);
    }

    #[test]
    fn test_global_defined_later() {
        let m = module(
            r#"
def foo():
    _bar()
def _bar():
    pass
"#,
        );
        let res = lint(&m, Some(&HashSet::new()));
        assert_eq!(res.len(), 0);
    }
}


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

use crate::analysis::types::LintT;
use crate::analysis::types::LintWarning;
use crate::codemap::CodeMap;
use crate::syntax::ast::Argument;
use crate::syntax::ast::AstExpr;
use crate::syntax::ast::Expr;
use crate::syntax::AstModule;

#[derive(Error, Debug)]
pub(crate) enum Performance {
    #[error("Dict copy `{0}` is more efficient as `{1}`")]
    DictWithoutStarStar(String, String),

    #[error(
        "`{0}` eagerly evaluates all items in the iterable, and allocates an array for the results. Prefer using a for-loop."
    )]
    EagerAndInefficientBoolCheck(String),

    #[error("`{0}` allocates a new {1} for the results. Prefer using a for-loop.")]
    InefficientBoolCheck(String, String),
}

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

    fn short_name(&self) -> &'static str {
        match self {
            Performance::DictWithoutStarStar(..) => "dict-without-star-star",
            Performance::EagerAndInefficientBoolCheck(..) => "eager-and-inefficient-bool-check",
            Performance::InefficientBoolCheck(..) => "inefficient-bool-check",
        }
    }
}

fn match_dict_copy(codemap: &CodeMap, x: &AstExpr, res: &mut Vec<LintT<Performance>>) {
    // If we see `dict(**x)` suggest `dict(x)`
    match &**x {
        Expr::Call(fun, args) if args.len() == 1 => match (&***fun, &*args[0]) {
            (Expr::Identifier(f), Argument::KwArgs(arg)) if f.node.0 == "dict" => {
                res.push(LintT::new(
                    codemap,
                    x.span,
                    Performance::DictWithoutStarStar(x.to_string(), format!("dict({})", arg.node)),
                ))
            }
            _ => {}
        },
        _ => {}
    }
}

fn match_inefficient_bool_check(codemap: &CodeMap, x: &AstExpr, res: &mut Vec<LintT<Performance>>) {
    match &**x {
        Expr::Call(fun, args) if args.len() == 1 => match (&***fun, &*args[0]) {
            (Expr::Identifier(f), Argument::Positional(arg))
                if f.node.0 == "any" || f.node.0 == "all" =>
            {
                match &**arg {
                    // any([blah for blah in blahs])
                    Expr::ListComprehension(_, _, _) | Expr::DictComprehension(_, _, _) => res
                        .push(LintT::new(
                            codemap,
                            x.span,
                            Performance::EagerAndInefficientBoolCheck(f.node.0.clone()),
                        )),
                    // any(list(_get_some_dict()))
                    Expr::Call(any_call, _) => match &***any_call {
                        Expr::Identifier(any_id)
                            if any_id.node.0 == "dict" || any_id.node.0 == "list" =>
                        {
                            res.push(LintT::new(
                                codemap,
                                x.span,
                                Performance::InefficientBoolCheck(
                                    x.to_string(),
                                    any_id.node.0.clone(),
                                ),
                            ))
                        }
                        _ => {}
                    },
                    _ => {}
                }
            }
            _ => {}
        },
        _ => {}
    }
}

fn check_call_expr(module: &AstModule, res: &mut Vec<LintT<Performance>>) {
    fn check(codemap: &CodeMap, x: &AstExpr, res: &mut Vec<LintT<Performance>>) {
        match_dict_copy(codemap, x, res);
        match_inefficient_bool_check(codemap, x, res);
        x.visit_expr(|x| check(codemap, x, res));
    }
    module
        .statement
        .visit_expr(|x| check(&module.codemap, x, res));
}

pub(crate) fn lint(module: &AstModule) -> Vec<LintT<Performance>> {
    let mut res = Vec::new();
    check_call_expr(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("bad.bzl", x.to_owned(), &Dialect::Extended).unwrap()
    }

    #[test]
    fn test_lint_matches_dict_issue() {
        let mut res = Vec::new();
        check_call_expr(
            &module(
                r#"
def foo(extra, **kwargs):
    x = dict(**kwargs)
    y = dict(extra)
    return (x,y)
"#,
            ),
            &mut res,
        );
        assert_eq!(
            res.map(|x| x.to_string()),
            &["bad.bzl:3:9-23: Dict copy `dict(**kwargs)` is more efficient as `dict(kwargs)`"]
        );
    }

    #[test]
    fn test_lint_matches_any_function() {
        let mut res = Vec::new();
        check_call_expr(
            &module(
                r#"
def foo(items):
    a = all(items)
    b = all([item for item in items])
    c = any([item for item in items])
    d = all({"a": a for a in []})
    e = any(list({}))
    f = all(dict([]))
    return (a,b,c,d,e,f)
"#,
            ),
            &mut res,
        );
        assert_eq!(
            res.map(|x| x.to_string()),
            &[
                "bad.bzl:4:9-38: `all` eagerly evaluates all items in the iterable, and allocates an array for the results. Prefer using a for-loop.",
                "bad.bzl:5:9-38: `any` eagerly evaluates all items in the iterable, and allocates an array for the results. Prefer using a for-loop.",
                "bad.bzl:6:9-34: `all` eagerly evaluates all items in the iterable, and allocates an array for the results. Prefer using a for-loop.",
                "bad.bzl:7:9-22: `any(list({}))` allocates a new list for the results. Prefer using a for-loop.",
                "bad.bzl:8:9-22: `all(dict([]))` allocates a new dict for the results. Prefer using a for-loop."
            ]
        );
    }
}


/*
 * 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::fmt;
use std::fmt::Display;
use std::path::Path;

use dupe::Dupe;
use lsp_types::DiagnosticSeverity;
use lsp_types::NumberOrString;
use lsp_types::Range;
use serde::Serialize;

use crate::codemap::CodeMap;
use crate::codemap::FileSpan;
use crate::codemap::ResolvedSpan;
use crate::codemap::Span;
use crate::errors::Diagnostic;

pub(crate) trait LintWarning: Display {
    fn is_serious(&self) -> bool;
    fn short_name(&self) -> &'static str;
}

/// A private version of lint without the inner trait erased, useful so we can test
/// using full matching, but then erase the internal details when exporting to users.
#[derive(Debug)]
pub(crate) struct LintT<T> {
    pub location: FileSpan,
    pub original: String,
    pub problem: T,
}

/// A lint produced by [`AstModule::lint`](crate::syntax::AstModule::lint).
#[derive(Debug)]
pub struct Lint {
    /// Which code location does this lint refer to.
    pub location: FileSpan,
    /// kebab-case constant describing this issue, e.g. `missing-return`.
    pub short_name: String,
    /// Is this code highly-likely to be wrong, rather
    /// than merely stylistically non-ideal.
    pub serious: bool,
    /// A description of the underlying problem.
    pub problem: String,
    /// The source code at [`location`](Lint::location).
    pub original: String,
}

impl Display for Lint {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.location, self.problem)
    }
}

impl<T: Display> Display for LintT<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.location, self.problem)
    }
}

impl<T: LintWarning> LintT<T> {
    pub(crate) fn new(codemap: &CodeMap, span: Span, problem: T) -> Self {
        let location = codemap.file_span(span);
        Self {
            original: location.file.source_span(span).to_owned(),
            location,
            problem,
        }
    }

    pub(crate) fn erase(self) -> Lint {
        Lint {
            location: self.location,
            short_name: self.problem.short_name().to_owned(),
            serious: self.problem.is_serious(),
            problem: self.problem.to_string(),
            original: self.original,
        }
    }
}

/// A standardised set of severities.
#[derive(Debug, Serialize, Dupe, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum EvalSeverity {
    /// An error while the program was being parsed.
    Error,
    /// The program parsed, but might not execute without error.
    Warning,
    /// This could be changed, but is not a blocking problem.
    Advice,
    /// This should be ignored
    Disabled,
}

impl Display for EvalSeverity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            EvalSeverity::Error => "Error",
            EvalSeverity::Warning => "Warning",
            EvalSeverity::Advice => "Advice",
            EvalSeverity::Disabled => "Disabled",
        })
    }
}

impl From<EvalSeverity> for DiagnosticSeverity {
    fn from(s: EvalSeverity) -> Self {
        match s {
            EvalSeverity::Error => DiagnosticSeverity::ERROR,
            EvalSeverity::Warning => DiagnosticSeverity::WARNING,
            EvalSeverity::Advice => DiagnosticSeverity::HINT,
            EvalSeverity::Disabled => DiagnosticSeverity::INFORMATION,
        }
    }
}

#[derive(Debug, Clone)]
/// Potential problems that occurred while parsing a starlark program.
pub struct EvalMessage {
    /// The path to the starlark program
    pub path: String,
    /// If present, where in the program the problem occurred.
    pub span: Option<ResolvedSpan>,
    /// How severed the problem is.
    pub severity: EvalSeverity,
    /// The general name of the issue.
    pub name: String,
    /// The details of the issue, generally displayed to the user.
    pub description: String,
    /// The full error details.
    pub full_error_with_span: Option<String>,
    /// The text referred to by `.span`
    pub original: Option<String>,
}

impl Display for EvalMessage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}:", self.severity, self.path)?;
        if let Some(span) = self.span {
            write!(f, "{}", span)?;
        }
        write!(f, " {}", self.description)
    }
}

impl EvalMessage {
    /// Convert from an `anyhow::Error`, including some type checking, to an `EvalMessage`
    pub fn from_anyhow(file: &Path, x: &anyhow::Error) -> Self {
        match x.downcast_ref::<Diagnostic>() {
            Some(
                d @ Diagnostic {
                    message,
                    span: Some(span),
                    ..
                },
            ) => {
                let original = span.source_span().to_owned();
                let resolved_span = span.resolve_span();
                Self {
                    path: span.filename().to_owned(),
                    span: Some(resolved_span),
                    severity: EvalSeverity::Error,
                    name: "error".to_owned(),
                    description: format!("{:#}", message),
                    full_error_with_span: Some(d.to_string()),
                    original: Some(original),
                }
            }
            _ => Self {
                path: file.display().to_string(),
                span: None,
                severity: EvalSeverity::Error,
                name: "error".to_owned(),
                description: format!("{:#}", x),
                full_error_with_span: None,
                original: None,
            },
        }
    }
}

impl From<Lint> for EvalMessage {
    fn from(x: Lint) -> Self {
        Self {
            path: x.location.filename().to_owned(),
            span: Some(x.location.resolve_span()),
            severity: if x.serious {
                EvalSeverity::Warning
            } else {
                // Start with all non-serious errors disabled, and ramp up from there
                EvalSeverity::Disabled
            },
            name: x.short_name,
            description: x.problem,
            full_error_with_span: None,
            original: Some(x.original),
        }
    }
}

impl From<EvalMessage> for lsp_types::Diagnostic {
    fn from(x: EvalMessage) -> Self {
        let range = match x.span {
            Some(s) => s.into(),
            _ => Range::default(),
        };
        lsp_types::Diagnostic::new(
            range,
            Some(x.severity.into()),
            Some(NumberOrString::String(x.name)),
            None,
            x.description,
            None,
            None,
        )
    }
}


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

use thiserror::Error;

use crate::analysis::types::LintT;
use crate::analysis::types::LintWarning;
use crate::codemap::CodeMap;
use crate::syntax::ast::Assign;
use crate::syntax::ast::AstExpr;
use crate::syntax::ast::AstStmt;
use crate::syntax::ast::DefP;
use crate::syntax::ast::Expr;
use crate::syntax::ast::Stmt;
use crate::syntax::AstModule;

#[derive(Error, Debug)]
pub(crate) enum UnderscoreWarning {
    #[error("Underscore definitions should be simple `{0}`")]
    UnderscoreDefinition(String),
    #[error("Used ignored variable `{0}`")]
    UsingIgnored(String),
}

impl LintWarning for UnderscoreWarning {
    fn is_serious(&self) -> bool {
        false
    }

    fn short_name(&self) -> &'static str {
        match self {
            UnderscoreWarning::UnderscoreDefinition(..) => "underscore-definition",
            UnderscoreWarning::UsingIgnored(..) => "using-ignored",
        }
    }
}

pub(crate) fn lint(module: &AstModule) -> Vec<LintT<UnderscoreWarning>> {
    let mut res = Vec::new();
    inappropriate_underscore(&module.codemap, &module.statement, true, &mut res);
    use_ignored(&module.codemap, &module.statement, &mut res);
    res
}

// There's no reason to make a def or lambda and give it an underscore name not at the top level
fn inappropriate_underscore(
    codemap: &CodeMap,
    x: &AstStmt,
    top: bool,
    res: &mut Vec<LintT<UnderscoreWarning>>,
) {
    // Is this value allowed as an assignment to a boring identifier - just tuple of vars and var
    fn is_allowed(x: &AstExpr) -> bool {
        match &**x {
            Expr::Tuple(xs) if !xs.is_empty() => {
                xs.iter().all(|x| matches!(&**x, Expr::Identifier(..)))
            }
            Expr::Identifier(..) => true,
            _ => false,
        }
    }

    match &**x {
        Stmt::Def(DefP { name, body, .. }) => {
            if !top && name.0.starts_with('_') {
                res.push(LintT::new(
                    codemap,
                    name.span,
                    UnderscoreWarning::UnderscoreDefinition(name.0.clone()),
                ))
            }
            inappropriate_underscore(codemap, body, false, res)
        }
        Stmt::Assign(lhs, type_rhs) if !top => {
            if let Assign::Identifier(name) = &**lhs {
                if name.0.starts_with('_') && !is_allowed(&type_rhs.1) {
                    res.push(LintT::new(
                        codemap,
                        name.span,
                        UnderscoreWarning::UnderscoreDefinition(name.node.0.clone()),
                    ))
                }
            }
        }
        _ => x.visit_stmt(|x| inappropriate_underscore(codemap, x, top, res)),
    }
}

// Don't want to use a variable that has been defined to be ignored
fn use_ignored(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<UnderscoreWarning>>) {
    // we are ok with using things that were defined at the top level, but not nested
    fn root_definitions<'a>(x: &'a AstStmt, res: &mut HashSet<&'a str>) {
        match &**x {
            Stmt::Assign(x, _) | Stmt::AssignModify(x, _, _) => {
                x.visit_lvalue(|x| {
                    res.insert(x.0.as_str());
                });
            }
            Stmt::Def(x) => {
                res.insert(x.name.0.as_str());
            }
            Stmt::Load(xs) => {
                for x in &xs.args {
                    res.insert(x.0.0.as_str());
                }
            }
            _ => x.visit_stmt(|x| root_definitions(x, res)),
        }
    }

    fn is_ignored(x: &str) -> bool {
        // we want things like __internal__ for builtin things to expose themselves quietly
        x.starts_with('_') && !(x.starts_with("__") && x.ends_with("__"))
    }

    fn check_expr(
        codemap: &CodeMap,
        x: &AstExpr,
        roots: &HashSet<&str>,
        res: &mut Vec<LintT<UnderscoreWarning>>,
    ) {
        match &**x {
            Expr::Identifier(x) => {
                if is_ignored(x.0.as_str()) && !roots.contains(x.0.as_str()) {
                    res.push(LintT::new(
                        codemap,
                        x.span,
                        UnderscoreWarning::UsingIgnored(x.node.0.clone()),
                    ));
                }
            }
            _ => x.visit_expr(|x| check_expr(codemap, x, roots, res)),
        }
    }

    let mut roots = HashSet::new();
    root_definitions(x, &mut roots);
    x.visit_expr(|x| check_expr(codemap, x, &roots, res));
}

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

    impl UnderscoreWarning {
        fn about(&self) -> &String {
            match self {
                UnderscoreWarning::UnderscoreDefinition(x) => x,
                UnderscoreWarning::UsingIgnored(x) => x,
            }
        }
    }

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

    #[test]
    fn test_lint_inappropriate_underscore() {
        let m = module(
            r#"
def _ok():
    def _no1(foo):
        _no2 = lambda x: x
        _no3 = 8
        _unused = foo
"#,
        );
        let mut res = Vec::new();
        inappropriate_underscore(&m.codemap, &m.statement, true, &mut res);
        let mut res = res.map(|x| x.problem.about());
        res.sort();
        assert_eq!(res, &["_no1", "_no2", "_no3"])
    }

    #[test]
    fn test_lint_use_ignored() {
        let m = module(
            r#"
def _foo(): pass
_allowed = 1
_missed = 1
def bar():
    def _no1(): pass
    _foo()
    _no1()
    _no2 = 1
    print(_no2)
    _no3 = 1
    _missed = 7
    # Could argue that missed should be an error, since it shadows
    print(_missed)
    print(_allowed)
    def deeper():
        print(_no3)
        _foo(__internal__)
"#,
        );
        let mut res = Vec::new();
        use_ignored(&m.codemap, &m.statement, &mut res);
        let mut res = res.map(|x| x.problem.about());
        res.sort();
        assert_eq!(res, &["_no1", "_no2", "_no3"])
    }
}


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

//! Utilities to test Starlark code execution.

// `if_then_panic` is only in newer clippy, delete this in future.
#![allow(unknown_lints)]
// We want to carefully control the panic message.
#![allow(clippy::if_then_panic)]

use std::collections::HashMap;

use dupe::Dupe;
use maplit::hashmap;
use once_cell::sync::Lazy;
use starlark_derive::starlark_module;

use crate as starlark;
use crate::codemap::CodeMap;
use crate::codemap::FileSpanRef;
use crate::codemap::Pos;
use crate::codemap::Span;
use crate::environment::FrozenModule;
use crate::environment::Globals;
use crate::environment::GlobalsBuilder;
use crate::environment::Module;
use crate::errors::Diagnostic;
use crate::eval::Evaluator;
use crate::eval::ReturnFileLoader;
use crate::slice_vec_ext::SliceExt;
use crate::stdlib::PrintHandler;
use crate::syntax::lexer::Lexer;
use crate::syntax::lexer::Token;
use crate::syntax::AstModule;
use crate::syntax::Dialect;
use crate::values::none::NoneType;
use crate::values::structs::AllocStruct;
use crate::values::typing::TypeCompiled;
use crate::values::AllocValue;
use crate::values::Heap;
use crate::values::OwnedFrozenValue;
use crate::values::Value;

fn mk_environment() -> GlobalsBuilder {
    GlobalsBuilder::extended().with(test_functions)
}

static GLOBALS: Lazy<Globals> = Lazy::new(|| mk_environment().build());

static ASSERTS_STAR: Lazy<FrozenModule> = Lazy::new(|| {
    let g = GlobalsBuilder::new()
        .with_struct("asserts", asserts_star)
        .build();
    let m = Module::new();
    m.frozen_heap().add_reference(g.heap());
    let asserts = g.get("asserts").unwrap();
    m.set("asserts", asserts);
    m.set(
        "freeze",
        asserts.get_attr("freeze", m.heap()).unwrap().unwrap(),
    );
    m.freeze().unwrap()
});

fn assert_equals<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
    if !a.equals(b)? {
        Err(anyhow::anyhow!("assert_eq: expected {}, got {}", a, b))
    } else {
        Ok(NoneType)
    }
}

fn assert_different<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
    if a.equals(b)? {
        Err(anyhow::anyhow!("assert_ne: but {} == {}", a, b))
    } else {
        Ok(NoneType)
    }
}

fn assert_less_than<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
    if a.compare(b)? != std::cmp::Ordering::Less {
        Err(anyhow::anyhow!("assert_lt: but {} >= {}", a, b))
    } else {
        Ok(NoneType)
    }
}

/// How often we garbage collection _should_ be transparent to the tests,
/// so we run each test in three configurations.
#[derive(Clone, Copy, Dupe, Debug)]
enum GcStrategy {
    Never,  // Disable GC
    Auto,   // Use the automatic heuristics (in practice, this does almost no GC)
    Always, // GC as aggressively as we can
}

/// Definitions to support assert.star as used by the Go test suite
#[starlark_module]
// Deliberately qualify the GlobalsBuild type to test that we can
fn asserts_star(builder: &mut crate::environment::GlobalsBuilder) {
    fn eq<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
        assert_equals(a, b)
    }

    fn ne<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
        assert_different(a, b)
    }

    fn lt<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
        assert_less_than(a, b)
    }

    fn contains<'v>(xs: Value<'v>, x: Value<'v>) -> anyhow::Result<NoneType> {
        if !xs.is_in(x)? {
            Err(anyhow::anyhow!(
                "assert.contains: expected {} to be in {}",
                x,
                xs
            ))
        } else {
            Ok(NoneType)
        }
    }

    fn r#true(x: Value) -> anyhow::Result<NoneType> {
        assert_equals(Value::new_bool(x.to_bool()), Value::new_bool(true))
    }

    // We don't allow this at runtime - just to be compatible with the Go Starlark test suite
    fn freeze<'v>(x: Value<'v>) -> anyhow::Result<Value<'v>> {
        Ok(x)
    }

    fn fails<'v>(
        f: Value<'v>,
        msg: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<NoneType> {
        let _ = msg;
        match f.invoke_pos(&[], eval) {
            Err(_e) => Ok(NoneType), // We don't actually check the message
            Ok(_) => Err(anyhow::anyhow!("assert.fails: didn't fail")),
        }
    }
}

#[starlark_module]
pub(crate) fn test_functions(builder: &mut GlobalsBuilder) {
    // Used by one of the test methods in Go
    const fibonacci: Vec<i32> = vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89];

    // Approximate version of a method used by the Go test suite
    fn hasfields<'v>() -> anyhow::Result<impl AllocValue<'v>> {
        Ok(AllocStruct::EMPTY)
    }

    // Approximate version of a method used by the Go test suite
    fn set<'v>(xs: Value<'v>) -> anyhow::Result<Value<'v>> {
        Ok(xs)
    }

    fn assert_eq<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
        assert_equals(a, b)
    }

    fn assert_ne<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
        assert_different(a, b)
    }

    fn assert_lt<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
        assert_less_than(a, b)
    }

    fn assert_true(a: Value) -> anyhow::Result<NoneType> {
        if !a.to_bool() {
            Err(anyhow::anyhow!("assertion failed"))
        } else {
            Ok(NoneType)
        }
    }

    fn assert_false(a: Value) -> anyhow::Result<NoneType> {
        if a.to_bool() {
            Err(anyhow::anyhow!("assertion failed"))
        } else {
            Ok(NoneType)
        }
    }

    // This is only safe to call at the top-level of a Starlark module
    fn garbage_collect(eval: &mut Evaluator) -> anyhow::Result<NoneType> {
        eval.trigger_gc();
        Ok(NoneType)
    }

    fn assert_type<'v>(v: Value<'v>, ty: Value<'v>, heap: &'v Heap) -> anyhow::Result<NoneType> {
        TypeCompiled::new(ty, heap)?.check_type(v, Some("v"))?;
        Ok(NoneType)
    }

    fn is_type<'v>(v: Value<'v>, ty: Value<'v>, heap: &'v Heap) -> anyhow::Result<bool> {
        Ok(TypeCompiled::new(ty, heap)?.matches(v))
    }

    /// Function which consumes arguments and that's it.
    ///
    /// This function is unknown to optimizer, so it can be used in optimizer tests.
    fn noop<'v>(
        #[starlark(args)] args: Vec<Value<'v>>,
        #[starlark(kwargs)] kwargs: Value<'v>,
    ) -> anyhow::Result<Value<'v>> {
        let _ = kwargs;
        Ok(args.into_iter().next().unwrap_or(Value::new_none()))
    }
}

/// Environment in which to run assertion tests.
pub struct Assert<'a> {
    dialect: Dialect,
    modules: HashMap<String, FrozenModule>,
    globals: Globals,
    gc_strategy: Option<GcStrategy>,
    setup_eval: Box<dyn Fn(&mut Evaluator)>,
    // Ideally `print_handler` should be set up in `setup_eval`
    // but if you know how to do it, show me how.
    print_handler: Option<&'a (dyn PrintHandler + 'a)>,
    static_typechecking: bool,
}

/// Construction and state management.
impl<'a> Assert<'a> {
    /// Create a new assert object, which will by default use
    /// [`Dialect::Extended`] and [`Globals::extended()`],
    /// plus some additional global functions like `assert_eq`.
    /// The usual pattern is to create a `mut` `Assert`, modify some properties
    /// and then execute some tests.
    pub fn new() -> Self {
        Self {
            dialect: Dialect::Extended,
            modules: hashmap!["asserts.star".to_owned() => Lazy::force(&ASSERTS_STAR).dupe()],
            globals: Lazy::force(&GLOBALS).dupe(),
            gc_strategy: None,
            setup_eval: Box::new(|_| ()),
            print_handler: None,
            static_typechecking: true,
        }
    }

    /// Disable garbage collection on the tests.
    pub fn disable_gc(&mut self) {
        self.gc_strategy = Some(GcStrategy::Never)
    }

    /// Configure a callback which is used to setup evaluator before each evaluation.
    pub fn setup_eval(&mut self, setup: impl Fn(&mut Evaluator) + 'static) {
        self.setup_eval = Box::new(setup);
    }

    /// Configure the handler for `print` function.
    pub fn set_print_handler(&mut self, handler: &'a (dyn PrintHandler + 'a)) {
        self.print_handler = Some(handler);
    }

    pub(crate) fn disable_static_typechecking(&mut self) {
        self.static_typechecking = false;
    }

    fn with_gc<A>(&self, f: impl Fn(GcStrategy) -> A) -> A {
        match self.gc_strategy {
            None => {
                // We want to run with Auto first, and use that as the result, because that's the default
                let res = f(GcStrategy::Auto);
                f(GcStrategy::Never);
                f(GcStrategy::Always);
                res
            }
            Some(x) => f(x),
        }
    }

    fn execute<'v>(
        &self,
        path: &str,
        program: &str,
        module: &'v Module,
        gc: GcStrategy,
    ) -> anyhow::Result<Value<'v>> {
        let mut modules = HashMap::with_capacity(self.modules.len());
        for (k, v) in &self.modules {
            modules.insert(k.as_str(), v);
        }
        let loader = ReturnFileLoader { modules: &modules };
        let ast = AstModule::parse(path, program.to_owned(), &self.dialect)?;
        let gc_always = |_span: FileSpanRef, eval: &mut Evaluator| {
            eval.trigger_gc();
        };
        let mut eval = Evaluator::new(module);
        eval.enable_static_typechecking(self.static_typechecking);
        (self.setup_eval)(&mut eval);
        if let Some(print_handler) = self.print_handler {
            eval.set_print_handler(print_handler);
        }

        match gc {
            GcStrategy::Never => eval.disable_gc(),
            GcStrategy::Auto => {}
            GcStrategy::Always => eval.before_stmt_fn(&gc_always),
        }
        eval.set_loader(&loader);
        eval.eval_module(ast, &self.globals)
    }

    fn execute_fail<'v>(
        &self,
        func: &str,
        program: &str,
        module: &'v Module,
        gc: GcStrategy,
    ) -> anyhow::Error {
        match self.execute("assert.bzl", program, module, gc) {
            Ok(v) => panic!(
                "starlark::assert::{}, didn't fail!\nCode:\n{}\nResult:\n{}\n",
                func, program, v
            ),
            Err(e) => e,
        }
    }

    fn execute_unwrap<'v>(
        &self,
        func: &str,
        path: &str,
        program: &str,
        module: &'v Module,
        gc: GcStrategy,
    ) -> Value<'v> {
        match self.execute(path, program, module, gc) {
            Ok(v) => v,
            Err(err) => {
                Diagnostic::eprint(&err);
                panic!(
                    "starlark::assert::{}, failed to execute!\nCode:\n{}\nGot error: {}",
                    func, program, err
                );
            }
        }
    }

    fn execute_unwrap_true<'v>(
        &self,
        func: &str,
        program: &str,
        module: &'v Module,
        gc: GcStrategy,
    ) {
        let v = self.execute_unwrap(func, "assert.bzl", program, module, gc);
        match v.unpack_bool() {
            Some(true) => {}
            Some(false) => panic!("starlark::assert::{}, got false!\nCode:\n{}", func, program),
            None => panic!(
                "starlark::assert::{}, not a bool!\nCode:\n{}\nResult\n{}",
                func, program, v
            ),
        }
    }

    /// Set the [`Dialect`] that future tests will use.
    pub fn dialect(&mut self, x: &Dialect) {
        self.dialect = x.clone();
    }

    /// Set specific fields in the [`Dialect`] that future tests will use.
    pub fn dialect_set(&mut self, f: impl FnOnce(&mut Dialect)) {
        f(&mut self.dialect)
    }

    /// Add a [`FrozenModule`] to the environment that future tests can access via
    /// `load`. To construct the [`FrozenModule`] automatically use [`module`](Assert::module).
    pub fn module_add(&mut self, name: &str, module: FrozenModule) {
        self.modules.insert(name.to_owned(), module);
    }

    /// Add a module to the environment that future tests can access.
    ///
    /// ```
    /// # use starlark::assert::Assert;
    /// let mut a = Assert::new();
    /// a.module("hello.star", "hello = 'world'");
    /// a.is_true("load('hello.star', 'hello'); hello == 'world'");
    /// ```
    pub fn module(&mut self, name: &str, program: &str) -> FrozenModule {
        let module = self
            .with_gc(|gc| {
                let module = Module::new();
                self.execute_unwrap("module", &format!("{}.bzl", name), program, &module, gc);
                module.freeze()
            })
            .expect("error freezing module");
        self.module_add(name, module.dupe());
        module
    }

    /// Set the [`Globals`] that future tests have access to.
    pub fn globals(&mut self, x: Globals) {
        self.globals = x;
    }

    /// Modify the [`Globals`] that future tests have access to.
    /// Note that this method will start from the default environment for [`Assert`],
    /// ignoring any previous [`globals`](Assert::globals) or [`globals_add`](Assert::globals_add) calls.
    pub fn globals_add(&mut self, f: impl FnOnce(&mut GlobalsBuilder)) {
        self.globals(mk_environment().with(f).build())
    }

    fn fails_with_name(&self, func: &str, program: &str, msgs: &[&str]) -> anyhow::Error {
        self.with_gc(|gc| {
            let module_env = Module::new();
            let original = self.execute_fail(func, program, &module_env, gc);
            // We really want to check the error message, but if in our doc tests we do:
            // fail("bad") # error: magic
            // Then when we print the source code, magic is contained in the error message.
            // Therefore, find the internals.
            let inner = original
                .downcast_ref::<Diagnostic>()
                .map_or(&original, |d| &d.message);
            let err_msg = format!("{:#}", inner);
            for msg in msgs {
                if !err_msg.contains(msg) {
                    Diagnostic::eprint(&original);
                    panic!(
                    "starlark::assert::{}, failed with the wrong message!\nCode:\n{}\nError:\n{}\nMissing:\n{}\nExpected:\n{:?}",
                    func, program, inner, msg, msgs
                )
                }
            }
            original
        })
    }
}

/// Execution tests.
impl<'a> Assert<'a> {
    /// A program that must fail with an error message that contains a specific
    /// string. Remember that the purpose of `fail` is to ensure you get
    /// the right error, not to fully specify the error - usually only one or
    /// two words will be sufficient to ensure that.
    ///
    /// ```
    /// # use starlark::assert::Assert;
    /// Assert::new().fail("fail('hello')", "ello");
    /// ```
    pub fn fail(&self, program: &str, msg: &str) -> anyhow::Error {
        self.fails_with_name("fail", program, &[msg])
    }

    /// A program that must fail with an error message that contains a specific
    /// set of strings. Remember that the purpose of `fail` is to ensure you get
    /// the right error, not to fully specify the error - usually only one or
    /// two words will be sufficient to ensure that. The words do not have to be
    /// in order.
    ///
    /// ```
    /// # use starlark::assert::Assert;
    /// Assert::new().fails("fail('hello')", &["fail", "ello"]);
    /// ```
    pub fn fails(&self, program: &str, msgs: &[&str]) -> anyhow::Error {
        self.fails_with_name("fails", program, msgs)
    }

    /// A program that must execute successfully without an exception. Often uses
    /// assert_eq. Returns the resulting value.
    ///
    /// ```
    /// # use starlark::assert::Assert;
    /// Assert::new().pass("assert_eq(1, 1)");
    /// ```
    pub fn pass(&self, program: &str) -> OwnedFrozenValue {
        self.with_gc(|gc| {
            let env = Module::new();
            let res = self.execute_unwrap("pass", "assert.bzl", program, &env, gc);
            env.set("_", res);
            env.freeze()
                .expect("error freezing module")
                .get("_")
                .unwrap()
        })
    }

    /// A program that must execute successfully without an exception. Returns the frozen module
    /// that `program` was evaluated in.
    pub fn pass_module(&self, program: &str) -> FrozenModule {
        self.with_gc(|gc| {
            let env = Module::new();
            self.execute_unwrap("pass", "assert.bzl", program, &env, gc);
            env.freeze().expect("error freezing module")
        })
    }

    /// A program that must evaluate to `True`.
    ///
    /// ```
    /// # use starlark::assert::Assert;
    /// Assert::new().is_true(r#"
    /// x = 1 + 1
    /// x == 2
    /// "#);
    /// ```
    pub fn is_true(&self, program: &str) {
        self.with_gc(|gc| {
            let env = Module::new();
            self.execute_unwrap_true("is_true", program, &env, gc);
        })
    }

    /// Many lines, each of which must individually evaluate to `True` (or be blank lines).
    ///
    /// ```
    /// # use starlark::assert::Assert;
    /// Assert::new().all_true(r#"
    /// 1 == 1
    ///
    /// 2 == 1 + 1
    /// "#);
    /// ```
    pub fn all_true(&self, program: &str) {
        self.with_gc(|gc| {
            for s in program.lines() {
                if s == "" {
                    continue;
                }
                let env = Module::new();
                self.execute_unwrap_true("all_true", s, &env, gc);
            }
        })
    }

    /// Two programs that must evaluate to the same (non-error) result.
    ///
    /// ```
    /// # use starlark::assert::Assert;
    /// Assert::new().eq("1 + 1", "2");
    /// ```
    pub fn eq(&self, lhs: &str, rhs: &str) {
        self.with_gc(|gc| {
            let lhs_m = Module::new();
            let rhs_m = Module::new();
            let lhs_v = self.execute_unwrap("eq", "lhs.bzl", lhs, &lhs_m, gc);
            let rhs_v = self.execute_unwrap("eq", "rhs.bzl", rhs, &rhs_m, gc);
            if lhs_v != rhs_v {
                panic!(
                "starlark::assert::eq, values differ!\nCode 1:\n{}\nCode 2:\n{}\nValue 1:\n{}\nValue 2\n{}",
                lhs, rhs, lhs_v, rhs_v
            );
            }
        })
    }

    /// Parse some text and return the AST. Fails if the program does not parse.
    pub fn parse_ast(&self, program: &str) -> AstModule {
        match AstModule::parse("assert.bzl", program.to_owned(), &self.dialect) {
            Ok(x) => x,
            Err(e) => {
                panic!(
                    "starlark::assert::parse_ast, expected parse success but failed\nCode: {}\nError: {}",
                    program, e
                );
            }
        }
    }

    /// Parse some text and pretty-print it using enough brackets etc. to avoid
    /// ambiguity. Fails if the program does not parse.
    /// The precise form of the pretty-printed output is not stable over time.
    pub fn parse(&self, program: &str) -> String {
        self.parse_ast(program).statement.to_string()
    }

    /// Restricted to crate because 'Lexeme' isn't a public type.
    pub(crate) fn lex_tokens(&self, program: &str) -> Vec<(usize, Token, usize)> {
        fn tokens(dialect: &Dialect, program: &str) -> Vec<(usize, Token, usize)> {
            let codemap = CodeMap::new("assert.bzl".to_owned(), program.to_owned());
            Lexer::new(program, dialect, codemap.dupe())
                .collect::<Result<Vec<_>, _>>()
                .unwrap_or_else(|e|
                    panic!(
                        "starlark::assert::lex_tokens, expected lex success but failed\nCode: {}\nError: {}",
                        program, e
                    )
                )
        }

        // Check the invariant that each token position can't be before the previous one
        fn check_spans(tokens: &[(usize, Token, usize)]) {
            let mut pos = 0;
            for (i, t, j) in tokens {
                let span_incorrect = format!("Span of {:?} incorrect", t);
                assert!(pos <= *i, "{}: {} > {}", span_incorrect, pos, i);
                assert!(i <= j, "{}: {} > {}", span_incorrect, i, j);
                pos = *j;
            }
        }

        let orig = tokens(&self.dialect, program);
        check_spans(&orig);

        // In Starlark Windows newline characters shouldn't change the lex tokens (only the positions), so run that test too.
        // First convert \r\n to \n, in case we started with Windows newlines, so we don't get \r\r\n.
        let with_r = tokens(
            &self.dialect,
            &program.replace("\r\n", "\n").replace('\n', "\r\n"),
        );
        check_spans(&with_r);
        assert_eq!(
            orig.map(|x| &x.1),
            with_r.map(|x| &x.1),
            "starlark::assert::lex_tokens, difference using CRLF newlines\nCode: {}",
            program,
        );

        orig
    }

    /// Lex some text and pretty-print it using enough whitespace etc. to avoid
    /// ambiguity. Fails if the program does not lex.
    /// The precise form of the pretty-printed output is not stable over time.
    pub fn lex(&self, program: &str) -> String {
        self.lex_tokens(program).map(|x| x.1.unlex()).join(" ")
    }

    /// Parse some text which must fail to parse. Two exclamation marks should be
    /// placed around the span which is reported by the error.
    ///
    /// ```
    /// # use starlark::assert::Assert;
    /// Assert::new().parse_fail("!nonlocal! = 1");
    /// ```
    pub fn parse_fail(&self, contents: &str) -> anyhow::Error {
        let rest = contents.replace('!', "");
        assert!(
            rest.len() + 2 == contents.len(),
            "Must be exactly 2 ! marks around the parse error location"
        );

        let begin = contents.find('!').unwrap();
        let end = contents[begin + 1..].find('!').unwrap() + begin;

        match AstModule::parse("assert.bzl", rest, &self.dialect) {
            Ok(ast) => panic!(
                "Expected parse failure, but succeeded:\nContents: {}\nGot: {:?}",
                contents, ast
            ),
            Err(e) => {
                if let Some(d) = e.downcast_ref::<Diagnostic>() {
                    if let Some(span) = &d.span {
                        let want_span = Span::new(Pos::new(begin as u32), Pos::new(end as u32));
                        if span.span == want_span {
                            return e; // Success
                        }
                    }
                }
                panic!(
                    "Expected diagnostic with span information, but didn't get a good span:\nContents: {}\nGot: {:?}\nWanted: {:?}",
                    contents,
                    e,
                    (begin, end)
                )
            }
        }
    }
}

/// See [`Assert::eq`].
pub fn eq(lhs: &str, rhs: &str) {
    Assert::new().eq(lhs, rhs)
}

/// See [`Assert::fail`].
pub fn fail(program: &str, msg: &str) -> anyhow::Error {
    Assert::new().fail(program, msg)
}

#[cfg(test)]
pub(crate) fn fail_skip_typecheck(program: &str, msg: &str) -> anyhow::Error {
    let mut a = Assert::new();
    a.disable_static_typechecking();
    a.fail(program, msg)
}

/// See [`Assert::fails`].
pub fn fails(program: &str, msgs: &[&str]) -> anyhow::Error {
    Assert::new().fails(program, msgs)
}

/// See [`Assert::is_true`].
pub fn is_true(program: &str) {
    Assert::new().is_true(program)
}

/// See [`Assert::all_true`].
pub fn all_true(expressions: &str) {
    let mut a = Assert::new();
    // TODO(nga): fix and enable.
    a.disable_static_typechecking();
    a.all_true(expressions)
}

/// See [`Assert::pass`].
pub fn pass(program: &str) -> OwnedFrozenValue {
    Assert::new().pass(program)
}

/// See [`Assert::pass_module`].
pub fn pass_module(program: &str) -> FrozenModule {
    Assert::new().pass_module(program)
}

/// See [`Assert::parse`].
pub fn parse(program: &str) -> String {
    Assert::new().parse(program)
}

/// See [`Assert::parse_ast`].
pub fn parse_ast(program: &str) -> AstModule {
    Assert::new().parse_ast(program)
}

/// Lex some text and return the tokens. Fails if the program does not parse.
/// Only available inside the crate because the Token type is not exported.
#[cfg(test)]
pub(crate) fn lex_tokens(program: &str) -> Vec<(usize, Token, usize)> {
    Assert::new().lex_tokens(program)
}

/// See [`Assert::lex`].
pub fn lex(program: &str) -> String {
    Assert::new().lex(program)
}

/// See [`Assert::parse_fail`].
pub fn parse_fail(program: &str) -> anyhow::Error {
    Assert::new().parse_fail(program)
}


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

//! Run conformance tests, which are used by the Go starlark.
//! e.g. <https://github.com/google/skylark/tree/master/testdata>

// `if_then_panic` is only in newer clippy, delete this in future.
#![allow(unknown_lints)]
// We want to carefully control the panic message.
#![allow(clippy::if_then_panic)]

use itertools::Itertools;

use crate::assert::assert::Assert;
use crate::errors::Diagnostic;

impl<'a> Assert<'a> {
    /// Run a conformance test, e.g. the Go Starlark tests
    pub fn conformance(&self, code: &str) {
        self.conformance_except(code, &[])
    }

    /// Run a conformance test, but where some test cases are allowed to fail.
    /// The `except` argument represents a list of these permissible failures,
    /// in the order they occur in the conformance test set,
    /// identified by a substring that occurs in the test.
    pub fn conformance_except(&self, code: &str, except: &[&str]) {
        let mut except = except.iter().peekable();

        'next: for x in ConformanceTest::parse(code) {
            if let Some(e) = except.peek() {
                if x.code.contains(**e) {
                    except.next();
                    continue 'next;
                }
            }

            x.test(self)
        }

        if let Some(missed) = except.next() {
            panic!("Exception given but not used, `{}`", missed);
        }
    }
}

/// Describe a conformance test
struct ConformanceTest {
    /// The code of the test
    code: String,
    /// If this might throw an error, what is it
    error: Option<(usize, String)>,
}

impl ConformanceTest {
    fn parse(code: &str) -> Vec<Self> {
        // First split on "---"
        code.lines()
            .collect::<Vec<_>>()
            .split(|x| *x == "---")
            .map(|xs| Self {
                code: xs.join("\n"),
                error: xs
                    .iter()
                    .find_position(|x| x.contains("###"))
                    .map(|(i, x)| {
                        (
                            i + 1,
                            (**x).split_once("###").unwrap().1.trim_start().to_owned(),
                        )
                    }),
            })
            .collect()
    }

    fn test(&self, assert: &Assert) {
        fn get_line(err: &anyhow::Error) -> Option<usize> {
            match err.downcast_ref::<Diagnostic>() {
                Some(Diagnostic {
                    span: Some(span), ..
                }) => Some(span.resolve_span().begin_line + 1),
                _ => None,
            }
        }

        match &self.error {
            None => {
                assert.pass(&self.code);
            }
            Some((line, _msg)) => {
                // We don't actually check error messages, since these tests were taken from upstream
                // and our error messages are different
                let err = assert.fail(&self.code, "");
                let got = get_line(&err);
                if got != Some(*line) {
                    panic!(
                        "starlark::assert::conformance, failed at wrong line!\nCode:\n{}\nError:\n{}\nExpected: {}\nGot: {:?}\n",
                        &self.code, &err, line, got
                    );
                }
            }
        }
    }
}