mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-30 07:14:46 +00:00
665 lines
22 KiB
Rust
665 lines
22 KiB
Rust
use crate::report::ReportText::{BinOp, Concat, Module, Region, Value};
|
|
use roc_module::symbol::{Interns, ModuleId, Symbol};
|
|
use roc_problem::can::PrecedenceProblem::BothNonAssociative;
|
|
use roc_problem::can::Problem;
|
|
use roc_types::pretty_print::content_to_string;
|
|
use roc_types::subs::{Content, Subs};
|
|
use roc_types::types::{write_error_type, ErrorType};
|
|
use std::path::PathBuf;
|
|
|
|
use std::fmt;
|
|
use ven_pretty::{BoxAllocator, DocAllocator, DocBuilder, Render, RenderAnnotated};
|
|
|
|
/// A textual report.
|
|
pub struct Report {
|
|
pub filename: PathBuf,
|
|
pub text: ReportText,
|
|
}
|
|
|
|
pub struct Palette<'a> {
|
|
pub primary: &'a str,
|
|
pub code_block: &'a str,
|
|
pub variable: &'a str,
|
|
pub type_variable: &'a str,
|
|
pub structure: &'a str,
|
|
pub alias: &'a str,
|
|
pub error: &'a str,
|
|
pub line_number: &'a str,
|
|
pub gutter_bar: &'a str,
|
|
pub module_name: &'a str,
|
|
pub binop: &'a str,
|
|
}
|
|
|
|
pub const TEST_PALETTE: Palette = Palette {
|
|
primary: WHITE_CODE,
|
|
code_block: WHITE_CODE,
|
|
variable: BLUE_CODE,
|
|
type_variable: YELLOW_CODE,
|
|
structure: GREEN_CODE,
|
|
alias: YELLOW_CODE,
|
|
error: RED_CODE,
|
|
line_number: CYAN_CODE,
|
|
gutter_bar: MAGENTA_CODE,
|
|
module_name: GREEN_CODE,
|
|
binop: GREEN_CODE,
|
|
};
|
|
|
|
pub fn can_problem(filename: PathBuf, problem: Problem) -> Report {
|
|
let mut texts = Vec::new();
|
|
|
|
match problem {
|
|
Problem::UnusedDef(symbol, region) => {
|
|
texts.push(Value(symbol));
|
|
texts.push(plain_text(" is not used anywhere in your code."));
|
|
texts.push(Region(region));
|
|
texts.push(plain_text("If you didn't intend on using "));
|
|
texts.push(Value(symbol));
|
|
texts.push(plain_text(
|
|
" then remove it so future readers of your code don't wonder why it is there.",
|
|
));
|
|
}
|
|
Problem::UnusedImport(module_id, region) => {
|
|
texts.push(plain_text("Nothing from "));
|
|
texts.push(Module(module_id));
|
|
texts.push(plain_text(" is used in this module."));
|
|
texts.push(Region(region));
|
|
texts.push(plain_text("Since "));
|
|
texts.push(Module(module_id));
|
|
texts.push(plain_text(" isn't used, you don't need to import it."));
|
|
}
|
|
Problem::UnusedArgument(closure_symbol, argument_symbol, region) => {
|
|
texts.push(Value(closure_symbol));
|
|
texts.push(plain_text(" doesn't use "));
|
|
texts.push(Value(argument_symbol));
|
|
texts.push(plain_text("."));
|
|
texts.push(Region(region));
|
|
texts.push(plain_text("If you don't need "));
|
|
texts.push(Value(argument_symbol));
|
|
texts.push(plain_text(
|
|
", then you can just remove it. However, if you really do need ",
|
|
));
|
|
texts.push(Value(argument_symbol));
|
|
texts.push(plain_text(" as an argument of "));
|
|
texts.push(Value(closure_symbol));
|
|
texts.push(plain_text(", prefix it with an underscore, like this: \"_"));
|
|
texts.push(Value(argument_symbol));
|
|
texts.push(plain_text("\". Adding an underscore at the start of a variable name is a way of saying that the variable is not used."));
|
|
}
|
|
Problem::PrecedenceProblem(BothNonAssociative(region, left_bin_op, right_bin_op)) => {
|
|
if left_bin_op.value == right_bin_op.value {
|
|
texts.push(plain_text("Using more than one "));
|
|
texts.push(BinOp(left_bin_op.value));
|
|
texts.push(plain_text(
|
|
" like this requires parentheses, to clarify how things should be grouped.",
|
|
))
|
|
} else {
|
|
texts.push(plain_text("Using "));
|
|
texts.push(BinOp(left_bin_op.value));
|
|
texts.push(plain_text(" and "));
|
|
texts.push(BinOp(right_bin_op.value));
|
|
texts.push(plain_text(
|
|
" together requires parentheses, to clarify how they should be grouped.",
|
|
))
|
|
}
|
|
texts.push(Region(region));
|
|
}
|
|
Problem::UnsupportedPattern(_pattern_type, _region) => {
|
|
panic!("TODO implement unsupported pattern report")
|
|
}
|
|
Problem::ShadowingInAnnotation {
|
|
original_region,
|
|
shadow,
|
|
} => {
|
|
// v-- just to satisfy clippy
|
|
let _a = original_region;
|
|
let _b = shadow;
|
|
panic!("TODO implement shadow report");
|
|
}
|
|
Problem::RuntimeError(_runtime_error) => {
|
|
panic!("TODO implement run time error report");
|
|
}
|
|
};
|
|
|
|
Report {
|
|
filename,
|
|
text: Concat(texts),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ReportText {
|
|
/// A value. Render it qualified unless it was defined in the current module.
|
|
Value(Symbol),
|
|
|
|
/// A module,
|
|
Module(ModuleId),
|
|
|
|
/// A type. Render it using roc_types::pretty_print for now, but maybe
|
|
/// do something fancier later.
|
|
Type(Content),
|
|
ErrorType(ErrorType),
|
|
|
|
/// Plain text
|
|
Plain(Box<str>),
|
|
|
|
/// Emphasized text (might be bold, italics, a different color, etc)
|
|
EmText(Box<str>),
|
|
|
|
/// A global tag rendered as code (e.g. a monospace font, or with backticks around it).
|
|
GlobalTag(Box<str>),
|
|
|
|
/// A private tag rendered as code (e.g. a monospace font, or with backticks around it).
|
|
PrivateTag(Symbol),
|
|
|
|
/// A record field name rendered as code (e.g. a monospace font, or with backticks around it).
|
|
RecordField(Box<str>),
|
|
|
|
/// A language keyword like `if`, rendered as code (e.g. a monospace font, or with backticks around it).
|
|
Keyword(Box<str>),
|
|
|
|
/// A region in the original source
|
|
Region(roc_region::all::Region),
|
|
|
|
/// A URL, which should be rendered as a hyperlink.
|
|
Url(Box<str>),
|
|
|
|
/// The documentation for this symbol.
|
|
Docs(Symbol),
|
|
|
|
BinOp(roc_parse::operator::BinOp),
|
|
|
|
/// Many ReportText that should be concatenated together.
|
|
Concat(Vec<ReportText>),
|
|
|
|
/// Many ReportText that each get separate lines
|
|
Stack(Vec<ReportText>),
|
|
|
|
Indent(usize, Box<ReportText>),
|
|
}
|
|
|
|
pub fn plain_text(str: &str) -> ReportText {
|
|
ReportText::Plain(Box::from(str))
|
|
}
|
|
|
|
pub fn em_text(str: &str) -> ReportText {
|
|
ReportText::EmText(Box::from(str))
|
|
}
|
|
|
|
pub fn private_tag_text(symbol: Symbol) -> ReportText {
|
|
ReportText::PrivateTag(symbol)
|
|
}
|
|
|
|
pub fn global_tag_text(str: &str) -> ReportText {
|
|
ReportText::GlobalTag(Box::from(str))
|
|
}
|
|
|
|
pub fn record_field_text(str: &str) -> ReportText {
|
|
ReportText::RecordField(Box::from(str))
|
|
}
|
|
|
|
pub fn keyword_text(str: &str) -> ReportText {
|
|
ReportText::Keyword(Box::from(str))
|
|
}
|
|
|
|
pub fn url(str: &str) -> ReportText {
|
|
ReportText::Url(Box::from(str))
|
|
}
|
|
|
|
pub const RED_CODE: &str = "\u{001b}[31m";
|
|
pub const WHITE_CODE: &str = "\u{001b}[37m";
|
|
pub const BLUE_CODE: &str = "\u{001b}[34m";
|
|
pub const YELLOW_CODE: &str = "\u{001b}[33m";
|
|
pub const GREEN_CODE: &str = "\u{001b}[42m";
|
|
pub const CYAN_CODE: &str = "\u{001b}[36m";
|
|
pub const MAGENTA_CODE: &str = "\u{001b}[35m";
|
|
|
|
pub const BOLD_CODE: &str = "\u{001b}[1m";
|
|
|
|
pub const UNDERLINE_CODE: &str = "\u{001b}[4m";
|
|
|
|
pub const RESET_CODE: &str = "\u{001b}[0m";
|
|
|
|
pub struct CiWrite<W> {
|
|
style_stack: Vec<Annotation>,
|
|
upstream: W,
|
|
}
|
|
|
|
impl<W> CiWrite<W> {
|
|
pub fn new(upstream: W) -> CiWrite<W> {
|
|
CiWrite {
|
|
style_stack: vec![],
|
|
upstream,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ColorWrite<'a, W> {
|
|
style_stack: Vec<Annotation>,
|
|
palette: &'a Palette<'a>,
|
|
upstream: W,
|
|
}
|
|
|
|
impl<'a, W> ColorWrite<'a, W> {
|
|
pub fn new(palette: &'a Palette, upstream: W) -> ColorWrite<'a, W> {
|
|
ColorWrite {
|
|
style_stack: vec![],
|
|
palette,
|
|
upstream,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
pub enum Annotation {
|
|
Emphasized,
|
|
Url,
|
|
Keyword,
|
|
GlobalTag,
|
|
PrivateTag,
|
|
RecordField,
|
|
TypeVariable,
|
|
Alias,
|
|
Structure,
|
|
Symbol,
|
|
BinOp,
|
|
Error,
|
|
GutterBar,
|
|
LineNumber,
|
|
PlainText,
|
|
CodeBlock,
|
|
Module,
|
|
}
|
|
|
|
impl<W> Render for CiWrite<W>
|
|
where
|
|
W: fmt::Write,
|
|
{
|
|
type Error = fmt::Error;
|
|
|
|
fn write_str(&mut self, s: &str) -> Result<usize, fmt::Error> {
|
|
self.write_str_all(s).map(|_| s.len())
|
|
}
|
|
|
|
fn write_str_all(&mut self, s: &str) -> fmt::Result {
|
|
self.upstream.write_str(s)
|
|
}
|
|
}
|
|
|
|
impl<W> RenderAnnotated<Annotation> for CiWrite<W>
|
|
where
|
|
W: fmt::Write,
|
|
{
|
|
fn push_annotation(&mut self, annotation: &Annotation) -> Result<(), Self::Error> {
|
|
use Annotation::*;
|
|
match annotation {
|
|
Emphasized => {
|
|
self.write_str("*")?;
|
|
}
|
|
Url => {
|
|
self.write_str("<")?;
|
|
}
|
|
GlobalTag | PrivateTag | RecordField | Keyword => {
|
|
self.write_str("`")?;
|
|
}
|
|
CodeBlock | PlainText | LineNumber | Error | GutterBar | TypeVariable | Alias
|
|
| Module | Structure | Symbol | BinOp => {}
|
|
}
|
|
self.style_stack.push(*annotation);
|
|
Ok(())
|
|
}
|
|
|
|
fn pop_annotation(&mut self) -> Result<(), Self::Error> {
|
|
use Annotation::*;
|
|
|
|
match self.style_stack.pop() {
|
|
None => {}
|
|
Some(annotation) => match annotation {
|
|
Emphasized => {
|
|
self.write_str("*")?;
|
|
}
|
|
Url => {
|
|
self.write_str(">")?;
|
|
}
|
|
GlobalTag | PrivateTag | RecordField | Keyword => {
|
|
self.write_str("`")?;
|
|
}
|
|
CodeBlock | PlainText | LineNumber | Error | GutterBar | TypeVariable | Alias
|
|
| Module | Structure | Symbol | BinOp => {}
|
|
},
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<'a, W> Render for ColorWrite<'a, W>
|
|
where
|
|
W: fmt::Write,
|
|
{
|
|
type Error = fmt::Error;
|
|
|
|
fn write_str(&mut self, s: &str) -> Result<usize, fmt::Error> {
|
|
self.write_str_all(s).map(|_| s.len())
|
|
}
|
|
|
|
fn write_str_all(&mut self, s: &str) -> fmt::Result {
|
|
self.upstream.write_str(s)
|
|
}
|
|
}
|
|
|
|
impl<'a, W> RenderAnnotated<Annotation> for ColorWrite<'a, W>
|
|
where
|
|
W: fmt::Write,
|
|
{
|
|
fn push_annotation(&mut self, annotation: &Annotation) -> Result<(), Self::Error> {
|
|
use Annotation::*;
|
|
match annotation {
|
|
Emphasized => {
|
|
self.write_str(BOLD_CODE)?;
|
|
}
|
|
Url => {
|
|
self.write_str(UNDERLINE_CODE)?;
|
|
}
|
|
PlainText => {
|
|
self.write_str(self.palette.primary)?;
|
|
}
|
|
CodeBlock => {
|
|
self.write_str(self.palette.code_block)?;
|
|
}
|
|
TypeVariable => {
|
|
self.write_str(self.palette.type_variable)?;
|
|
}
|
|
Alias => {
|
|
self.write_str(self.palette.alias)?;
|
|
}
|
|
BinOp => {
|
|
self.write_str(self.palette.alias)?;
|
|
}
|
|
Symbol => {
|
|
self.write_str(self.palette.variable)?;
|
|
}
|
|
GutterBar => {
|
|
self.write_str(self.palette.gutter_bar)?;
|
|
}
|
|
Error => {
|
|
self.write_str(self.palette.error)?;
|
|
}
|
|
LineNumber => {
|
|
self.write_str(self.palette.line_number)?;
|
|
}
|
|
Structure => {
|
|
self.write_str(self.palette.structure)?;
|
|
}
|
|
Module => {
|
|
self.write_str(self.palette.module_name)?;
|
|
}
|
|
GlobalTag | PrivateTag | RecordField | Keyword => {
|
|
self.write_str("`")?;
|
|
}
|
|
}
|
|
self.style_stack.push(*annotation);
|
|
Ok(())
|
|
}
|
|
|
|
fn pop_annotation(&mut self) -> Result<(), Self::Error> {
|
|
use Annotation::*;
|
|
|
|
match self.style_stack.pop() {
|
|
None => {}
|
|
Some(annotation) => match annotation {
|
|
Emphasized | Url | TypeVariable | Alias | Symbol | BinOp | Error | GutterBar
|
|
| Structure | CodeBlock | PlainText | LineNumber | Module => {
|
|
self.write_str(RESET_CODE)?;
|
|
}
|
|
|
|
GlobalTag | PrivateTag | RecordField | Keyword => {
|
|
self.write_str("`")?;
|
|
}
|
|
},
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl ReportText {
|
|
/// Render to CI console output, where no colors are available.
|
|
pub fn render_ci(
|
|
self,
|
|
buf: &mut String,
|
|
subs: &mut Subs,
|
|
home: ModuleId,
|
|
src_lines: &[&str],
|
|
interns: &Interns,
|
|
) {
|
|
let alloc = BoxAllocator;
|
|
|
|
let err_msg = "<buffer is not a utf-8 encoded string>";
|
|
|
|
self.pretty::<_>(&alloc, subs, home, src_lines, interns)
|
|
.1
|
|
.render_raw(70, &mut CiWrite::new(buf))
|
|
.expect(err_msg);
|
|
}
|
|
|
|
/// Render to a color terminal using ANSI escape sequences
|
|
pub fn render_color_terminal(
|
|
self,
|
|
buf: &mut String,
|
|
subs: &mut Subs,
|
|
home: ModuleId,
|
|
src_lines: &[&str],
|
|
interns: &Interns,
|
|
palette: &Palette,
|
|
) {
|
|
let alloc = BoxAllocator;
|
|
|
|
let err_msg = "<buffer is not a utf-8 encoded string>";
|
|
|
|
self.pretty::<_>(&alloc, subs, home, src_lines, interns)
|
|
.1
|
|
.render_raw(70, &mut ColorWrite::new(palette, buf))
|
|
.expect(err_msg);
|
|
}
|
|
|
|
/// General idea: this function puts all the characters in. Any styling (emphasis, colors,
|
|
/// monospace font, etc) is done in the CiWrite and ColorWrite `RenderAnnotated` instances.
|
|
pub fn pretty<'b, D>(
|
|
self,
|
|
alloc: &'b D,
|
|
subs: &mut Subs,
|
|
home: ModuleId,
|
|
src_lines: &'b [&'b str],
|
|
interns: &Interns,
|
|
) -> DocBuilder<'b, D, Annotation>
|
|
where
|
|
D: DocAllocator<'b, Annotation>,
|
|
D::Doc: Clone,
|
|
{
|
|
use ReportText::*;
|
|
|
|
match self {
|
|
Plain(string) => alloc
|
|
.text(format!("{}", string))
|
|
.annotate(Annotation::PlainText),
|
|
EmText(string) => alloc
|
|
.text(format!("{}", string))
|
|
.annotate(Annotation::Emphasized),
|
|
Url(url) => alloc.text(format!("{}", url)).annotate(Annotation::Url),
|
|
Keyword(string) => alloc
|
|
.text(format!("{}", string))
|
|
.annotate(Annotation::Keyword),
|
|
GlobalTag(string) => alloc
|
|
.text(format!("{}", string))
|
|
.annotate(Annotation::GlobalTag),
|
|
RecordField(string) => alloc
|
|
.text(format!(".{}", string))
|
|
.annotate(Annotation::RecordField),
|
|
PrivateTag(symbol) => {
|
|
if symbol.module_id() == home {
|
|
// Render it unqualified if it's in the current module.
|
|
alloc
|
|
.text(format!("{}", symbol.ident_string(interns)))
|
|
.annotate(Annotation::PrivateTag)
|
|
} else {
|
|
alloc
|
|
.text(format!(
|
|
"{}.{}",
|
|
symbol.module_string(interns),
|
|
symbol.ident_string(interns),
|
|
))
|
|
.annotate(Annotation::PrivateTag)
|
|
}
|
|
}
|
|
Value(symbol) => {
|
|
if symbol.module_id() == home {
|
|
// Render it unqualified if it's in the current module.
|
|
alloc
|
|
.text(format!("{}", symbol.ident_string(interns)))
|
|
.annotate(Annotation::Symbol)
|
|
} else {
|
|
alloc
|
|
.text(format!(
|
|
"{}.{}",
|
|
symbol.module_string(interns),
|
|
symbol.ident_string(interns),
|
|
))
|
|
.annotate(Annotation::Symbol)
|
|
}
|
|
}
|
|
|
|
Module(module_id) => alloc
|
|
.text(format!("{}", interns.module_name(module_id)))
|
|
.annotate(Annotation::Module),
|
|
Type(content) => match content {
|
|
Content::FlexVar(_) | Content::RigidVar(_) => alloc
|
|
.text(content_to_string(content, subs, home, interns))
|
|
.annotate(Annotation::TypeVariable),
|
|
|
|
Content::Structure(_) => alloc
|
|
.text(content_to_string(content, subs, home, interns))
|
|
.annotate(Annotation::Structure),
|
|
|
|
Content::Alias(_, _, _) => alloc
|
|
.text(content_to_string(content, subs, home, interns))
|
|
.annotate(Annotation::Alias),
|
|
|
|
Content::Error => alloc.text(content_to_string(content, subs, home, interns)),
|
|
},
|
|
ErrorType(error_type) => alloc
|
|
.nil()
|
|
.append(alloc.hardline())
|
|
.append(
|
|
alloc
|
|
.text(write_error_type(home, interns, error_type))
|
|
.indent(4),
|
|
)
|
|
.append(alloc.hardline()),
|
|
|
|
Indent(n, nested) => {
|
|
let rest = nested.pretty(alloc, subs, home, src_lines, interns);
|
|
alloc.nil().append(rest).indent(n)
|
|
}
|
|
Docs(_) => {
|
|
panic!("TODO implment docs");
|
|
}
|
|
Concat(report_texts) => alloc.concat(
|
|
report_texts
|
|
.into_iter()
|
|
.map(|rep| rep.pretty(alloc, subs, home, src_lines, interns)),
|
|
),
|
|
Stack(report_texts) => alloc.intersperse(
|
|
report_texts
|
|
.into_iter()
|
|
.map(|rep| (rep.pretty(alloc, subs, home, src_lines, interns))),
|
|
alloc.hardline(),
|
|
),
|
|
BinOp(bin_op) => alloc.text(bin_op.to_string()).annotate(Annotation::BinOp),
|
|
Region(region) => {
|
|
let max_line_number_length = (region.end_line + 1).to_string().len();
|
|
let indent = 2;
|
|
|
|
let body = if region.start_line == region.end_line {
|
|
let i = region.start_line;
|
|
|
|
let line_number_string = (i + 1).to_string();
|
|
let line_number = line_number_string;
|
|
let this_line_number_length = line_number.len();
|
|
|
|
let line = src_lines[i as usize];
|
|
let rest_of_line = if line.trim().is_empty() {
|
|
alloc.nil()
|
|
} else {
|
|
alloc
|
|
.nil()
|
|
.append(alloc.text(line).indent(2))
|
|
.annotate(Annotation::CodeBlock)
|
|
};
|
|
|
|
let source_line = alloc
|
|
.line()
|
|
.append(
|
|
alloc
|
|
.text(" ".repeat(max_line_number_length - this_line_number_length)),
|
|
)
|
|
.append(alloc.text(line_number).annotate(Annotation::LineNumber))
|
|
.append(alloc.text(" ┆").annotate(Annotation::GutterBar))
|
|
.append(rest_of_line);
|
|
|
|
let highlight_line = alloc
|
|
.line()
|
|
.append(alloc.text(" ".repeat(max_line_number_length)))
|
|
.append(alloc.text(" ┆").annotate(Annotation::GutterBar))
|
|
.append(
|
|
alloc
|
|
.text(" ".repeat(region.start_col as usize))
|
|
.indent(indent),
|
|
)
|
|
.append(
|
|
alloc
|
|
.text("^".repeat((region.end_col - region.start_col) as usize))
|
|
.annotate(Annotation::Error),
|
|
);
|
|
|
|
source_line.append(highlight_line)
|
|
} else {
|
|
let mut result = alloc.nil();
|
|
for i in region.start_line..=region.end_line {
|
|
let line_number_string = (i + 1).to_string();
|
|
let line_number = line_number_string;
|
|
let this_line_number_length = line_number.len();
|
|
|
|
let line = src_lines[i as usize];
|
|
let rest_of_line = if !line.trim().is_empty() {
|
|
alloc
|
|
.text(line)
|
|
.annotate(Annotation::CodeBlock)
|
|
.indent(indent)
|
|
} else {
|
|
alloc.nil()
|
|
};
|
|
|
|
let source_line =
|
|
alloc
|
|
.line()
|
|
.append(alloc.text(
|
|
" ".repeat(max_line_number_length - this_line_number_length),
|
|
))
|
|
.append(alloc.text(line_number).annotate(Annotation::LineNumber))
|
|
.append(alloc.text(" ┆").annotate(Annotation::GutterBar))
|
|
.append(alloc.text(">").annotate(Annotation::Error))
|
|
.append(rest_of_line);
|
|
|
|
result = result.append(source_line);
|
|
}
|
|
|
|
result
|
|
};
|
|
alloc
|
|
.nil()
|
|
.append(alloc.line())
|
|
.append(body)
|
|
.append(alloc.line())
|
|
.append(alloc.line())
|
|
}
|
|
}
|
|
}
|
|
}
|