knave-expand 0.14.22

The reading, scoping and expansion stages of knave
Documentation
use super::LibrarySource;
use crate::diagnostic::{Diagnostic, DiagnosticCode, Label, Suggestion};
use crate::language::Language;
use crate::source::Span;

pub struct ProjectProblems;

impl ProjectProblems {
    const PROJECT_EXAMPLE: &str = "(defproject my-program)";

    pub fn missing_project_form(span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::MISSING_PROJECT_FORM,
            format!(
                "`{}` must hold a `defproject` form",
                Language::project_file_name()
            ),
            Label::new(span, "expected `(defproject <name> ...)` here"),
        )
        .with_help(format!("start the file with `{}`", Self::PROJECT_EXAMPLE))
    }

    pub fn extra_project_form(extra: Span, project: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::EXTRA_PROJECT_FORM,
            format!(
                "`{}` holds a form after its `defproject` form",
                Language::project_file_name()
            ),
            Label::new(extra, "this form is outside the `defproject` form"),
        )
        .with_label(Label::new(project, "the `defproject` form ends here"))
        .with_help("move this form inside the `defproject` form, or remove it")
    }

    pub fn invalid_project_name(name: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::INVALID_PROJECT_NAME,
            format!("`{name}` is not a project name"),
            Label::new(span, "expected a kebab-case name"),
        )
        .with_note("the name names the build directory and the program")
        .with_help("use lowercase letters, digits and single dashes, as `my-program`")
    }

    pub fn reserved_library_name(name: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::RESERVED_LIBRARY_NAME,
            format!("`{name}` cannot name a library"),
            Label::new(span, "this name is reserved"),
        )
        .with_note("`prelude`, `kernel` and `code` name the compiler's own modules")
        .with_help("give the library another name")
    }

    pub fn invalid_library_name(name: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::INVALID_LIBRARY_NAME,
            format!("`{name}` is not a library name"),
            Label::new(span, "expected a kebab-case name"),
        )
        .with_note("`use` reads a library's name as the first part of a module path")
        .with_help("use lowercase letters, digits and single dashes, as `my-library`")
    }

    pub fn library_at_two_sources(name: &str, span: Span, first: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::LIBRARY_AT_TWO_SOURCES,
            format!("library `{name}` comes from two sources"),
            Label::new(span, "this source differs"),
        )
        .with_label(Label::new(first, "from the source given here"))
        .with_help(format!("point every `{name}` entry at one source"))
    }

    pub fn library_without_project_file(name: &str, span: Span, project_file: &str) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::LIBRARY_WITHOUT_PROJECT_FILE,
            format!("library `{name}` has no project file at `{project_file}`"),
            Label::new(span, "this library's `:path` names no project"),
        )
        .with_help("point `:path` at the library's directory, which holds its project file")
    }

    pub fn library_without_root_module(
        name: &str,
        span: Span,
        root_file: &str,
        project_file: &str,
    ) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::LIBRARY_WITHOUT_ROOT_MODULE,
            format!("library `{name}` has no root module `{root_file}`"),
            Label::new(span, "this library's `:lib` names no file"),
        )
        .with_help(format!("fix `:lib` in `{project_file}`, or add the file"))
    }

    pub fn unavailable_git_library(name: &str, span: Span, reason: &str) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::GIT_LIBRARY_UNAVAILABLE,
            format!("cannot fetch git library `{name}`"),
            Label::new(span, "this library's commit could not be checked out"),
        )
        .with_note(format!("git says: {reason}"))
        .with_help("check the library's `:git` and `:rev`, and that the repository is reachable")
    }

    pub fn repository_like_an_option(span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::INVALID_GIT_REPOSITORY,
            "`:git` cannot start with `-`",
            Label::new(span, "git would read this as an option"),
        )
        .with_help("write the repository's URL or path, as `https://example.com/keymap.git`")
    }

    pub fn path_outside_checkout(name: &str, key: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::PATH_OUTSIDE_CHECKOUT,
            format!("library `{name}` names a `{key}` outside its checkout"),
            Label::new(span, "this git library's project file reaches outside it"),
        )
        .with_note("a library fetched from git may only name files inside its own checkout")
        .with_help(format!(
            "ask the library's authors to use a relative `{key}` without `..`"
        ))
    }

    pub fn crate_path_outside_checkout(name: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::PATH_OUTSIDE_CHECKOUT,
            format!("crate `{name}` names a `:path` outside its library's checkout"),
            Label::new(span, "this git library's project file reaches outside it"),
        )
        .with_note("a library fetched from git may only name files inside its own checkout")
        .with_help("ask the library's authors to use a relative `:path` without `..`")
    }

    pub fn crate_requests_conflict(name: &str, span: Span, first: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::CRATE_REQUESTS_CONFLICT,
            format!("crate `{name}` has two requests that cannot merge"),
            Label::new(span, "this request"),
        )
        .with_label(Label::new(first, "conflicts with this one"))
        .with_note(
            "the program builds one copy of each crate, and only plain versions that are \
             semver-compatible merge",
        )
        .with_help(format!(
            "make the `{name}` entries ask for compatible plain versions"
        ))
    }

    pub fn crates_unavailable_to(command: &str, span: Span, label: String) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::CRATES_UNAVAILABLE_TO_COMMAND,
            format!("`{command}` cannot use crate dependencies yet"),
            Label::new(span, label),
        )
        .with_help("use `build`, `run` or `test` for a program with crates for now")
    }

    pub fn missing_project_name(span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::MISSING_PROJECT_NAME,
            "the project has no name",
            Label::new(span, "expected a name after `defproject`"),
        )
        .with_help(format!("name the project, as `{}`", Self::PROJECT_EXAMPLE))
    }

    pub fn unknown_key(key: &str, span: Span, closest: Option<&str>) -> Diagnostic {
        let diagnostic = Diagnostic::error(
            DiagnosticCode::UNKNOWN_PROJECT_KEY,
            format!("unknown key `:{key}`"),
            Label::new(span, "not a key of this form"),
        );
        match closest {
            Some(closest) => diagnostic.with_suggestion(
                "a key with a similar name exists",
                Suggestion::new(span, format!(":{closest}")),
            ),
            None => diagnostic,
        }
    }

    pub fn missing_value(key: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::MISSING_PROJECT_VALUE,
            format!("`:{key}` has no value"),
            Label::new(span, "expected a value after this key"),
        )
        .with_help(format!("write the value right after `:{key}`"))
    }

    pub fn mismatched_value(key: &str, expected: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::MISMATCHED_PROJECT_VALUE,
            format!("`:{key}` takes {expected}"),
            Label::new(span, format!("expected {expected}")),
        )
    }

    pub fn unquoted_feature(name: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::MISMATCHED_PROJECT_VALUE,
            "a feature is a string",
            Label::new(span, "expected a string"),
        )
        .with_suggestion(
            "quote the feature's name",
            Suggestion::new(span, format!("\"{name}\"")),
        )
    }

    pub fn unknown_section(section: &str, span: Span, closest: Option<&str>) -> Diagnostic {
        let diagnostic = Diagnostic::error(
            DiagnosticCode::UNKNOWN_PROJECT_SECTION,
            format!("unknown section `{section}`"),
            Label::new(span, "not a section of a project"),
        );
        match closest {
            Some(closest) => diagnostic.with_suggestion(
                "a section with a similar name exists",
                Suggestion::new(span, closest),
            ),
            None => diagnostic
                .with_help("the sections are `libraries`, `dependencies` and `dev-dependencies`"),
        }
    }

    pub fn unexpected_form(span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::UNEXPECTED_PROJECT_FORM,
            "expected a key or a section",
            Label::new(span, "this is neither a `:key value` pair nor a section"),
        )
        .with_help(format!(
            "give a value after a key, as `:entry \"src/main.{}\"`",
            Language::EXTENSION
        ))
    }

    pub fn unexpected_entry_form(span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::UNEXPECTED_PROJECT_FORM,
            "expected a key",
            Label::new(span, "this is not a `:key value` pair"),
        )
        .with_help("an entry is a name, then an optional version, then `:key value` pairs")
    }

    pub fn duplicate_entry(name: &str, span: Span, first: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::DUPLICATE_PROJECT_ENTRY,
            format!("`{name}` is given more than once"),
            Label::new(span, "given again here"),
        )
        .with_label(Label::new(first, "first given here"))
        .with_help("remove one of them")
    }

    pub fn malformed_dependency(span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::MALFORMED_DEPENDENCY,
            "expected a dependency",
            Label::new(span, "a dependency is a list that starts with a name"),
        )
        .with_help("write a dependency as `(serde \"1.0\")` or `(hints :path \"../hints\")`")
    }

    pub fn versioned_library(name: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::VERSIONED_LIBRARY,
            format!("library `{name}` cannot take a version"),
            Label::new(span, "a library is found by its source, not its version"),
        )
        .with_help("remove the version")
    }

    pub fn library_without_source(name: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::LIBRARY_WITHOUT_SOURCE,
            format!("library `{name}` has no source"),
            Label::new(span, "this library needs `:path` or `:git`"),
        )
        .with_help(format!(
            "write `({name} :path \"../{name}\")`, or `:git` with a `:rev`"
        ))
    }

    pub fn conflicting_sources(name: &str, span: Span, other: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::CONFLICTING_LIBRARY_SOURCES,
            format!("library `{name}` has two sources"),
            Label::new(span, "a git source"),
        )
        .with_label(Label::new(other, "conflicts with this one"))
        .with_help("keep `:path` alone, or `:git` with `:rev`")
    }

    pub fn git_without_revision(name: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::GIT_LIBRARY_WITHOUT_REVISION,
            format!("git library `{name}` has no `:rev`"),
            Label::new(span, "this repository needs a commit"),
        )
        .with_help(format!(
            "add `:rev` with the commit's full {}-character hash",
            LibrarySource::FULL_COMMIT_LENGTH
        ))
    }

    pub fn short_revision(span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::SHORT_REVISION,
            "`:rev` must name a full commit",
            Label::new(
                span,
                format!(
                    "expected {} hexadecimal digits",
                    LibrarySource::FULL_COMMIT_LENGTH
                ),
            ),
        )
        .with_note("git fetches a commit only by its full hash")
        .with_help("write the commit's full hash, as `git rev-parse <commit>` prints it")
    }

    pub fn unversioned_crate(name: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::UNVERSIONED_CRATE,
            format!("crate `{name}` has no version"),
            Label::new(span, "this crate needs a version or a `:path`"),
        )
        .with_help(format!(
            "write the version after the name, as `({name} \"1.0\")`"
        ))
    }

    pub fn unknown_edition(written: &str, span: Span) -> Diagnostic {
        Diagnostic::error(
            DiagnosticCode::UNKNOWN_EDITION,
            format!("edition {written} does not exist"),
            Label::new(span, "not an edition of this compiler"),
        )
        .with_suggestion(
            format!("the only edition is {}", Language::EDITION),
            Suggestion::new(span, Language::EDITION.to_string()),
        )
    }
}