Skip to content

Commit

Permalink
Merge pull request #6506 from roboteng/ensure-roc-files
Browse files Browse the repository at this point in the history
Helpful error message on non roc files
  • Loading branch information
Anton-4 authored Feb 9, 2024
2 parents 61d630f + d08bda6 commit e4a7f11
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 25 deletions.
31 changes: 31 additions & 0 deletions crates/compiler/load_internal/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3838,6 +3838,35 @@ struct HeaderOutput<'a> {
opt_platform_shorthand: Option<&'a str>,
}

fn ensure_roc_file<'a>(filename: &Path, src_bytes: &[u8]) -> Result<(), LoadingProblem<'a>> {
match filename.extension() {
Some(ext) => {
if ext != ROC_FILE_EXTENSION {
return Err(LoadingProblem::FileProblem {
filename: filename.to_path_buf(),
error: io::ErrorKind::Unsupported,
});
}
}
None => {
let index = src_bytes
.iter()
.position(|a| *a == b'\n')
.unwrap_or(src_bytes.len());
let frist_line_bytes = src_bytes[0..index].to_vec();
if let Ok(first_line) = String::from_utf8(frist_line_bytes) {
if !(first_line.starts_with("#!") && first_line.contains("roc")) {
return Err(LoadingProblem::FileProblem {
filename: filename.to_path_buf(),
error: std::io::ErrorKind::Unsupported,
});
}
}
}
}
Ok(())
}

fn parse_header<'a>(
arena: &'a Bump,
read_file_duration: Duration,
Expand All @@ -3856,6 +3885,8 @@ fn parse_header<'a>(
let parsed = roc_parse::module::parse_header(arena, parse_state.clone());
let parse_header_duration = parse_start.elapsed();

ensure_roc_file(&filename, src_bytes)?;

// Insert the first entries for this module's timings
let mut module_timing = ModuleTiming::new(start_time);

Expand Down
113 changes: 88 additions & 25 deletions crates/compiler/load_internal/tests/test_load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ fn import_transitive_alias() {
// with variables in the importee
let modules = vec![
(
"RBTree",
"RBTree.roc",
indoc!(
r"
interface RBTree exposes [RedBlackTree, empty] imports []
Expand All @@ -341,7 +341,7 @@ fn import_transitive_alias() {
),
),
(
"Other",
"Other.roc",
indoc!(
r"
interface Other exposes [empty] imports [RBTree]
Expand Down Expand Up @@ -626,7 +626,7 @@ fn ingested_file_bytes() {
#[test]
fn parse_problem() {
let modules = vec![(
"Main",
"Main.roc",
indoc!(
r"
interface Main exposes [main] imports []
Expand All @@ -641,7 +641,7 @@ fn parse_problem() {
report,
indoc!(
"
── UNFINISHED LIST in tmp/parse_problem/Main ───────────────────────────────────
── UNFINISHED LIST in tmp/parse_problem/Main.roc ───────────────────────────────
I am partway through started parsing a list, but I got stuck here:
Expand Down Expand Up @@ -707,7 +707,7 @@ fn ingested_file_not_found() {
#[test]
fn platform_does_not_exist() {
let modules = vec![(
"Main",
"main.roc",
indoc!(
r#"
app "example"
Expand Down Expand Up @@ -753,7 +753,7 @@ fn platform_parse_error() {
),
),
(
"Main",
"main.roc",
indoc!(
r#"
app "hello-world"
Expand Down Expand Up @@ -797,7 +797,7 @@ fn platform_exposes_main_return_by_pointer_issue() {
),
),
(
"Main",
"main.roc",
indoc!(
r#"
app "hello-world"
Expand All @@ -818,7 +818,7 @@ fn platform_exposes_main_return_by_pointer_issue() {
fn opaque_wrapped_unwrapped_outside_defining_module() {
let modules = vec![
(
"Age",
"Age.roc",
indoc!(
r"
interface Age exposes [Age] imports []
Expand All @@ -828,7 +828,7 @@ fn opaque_wrapped_unwrapped_outside_defining_module() {
),
),
(
"Main",
"Main.roc",
indoc!(
r"
interface Main exposes [twenty, readAge] imports [Age.{ Age }]
Expand All @@ -847,7 +847,7 @@ fn opaque_wrapped_unwrapped_outside_defining_module() {
err,
indoc!(
r"
── OPAQUE TYPE DECLARED OUTSIDE SCOPE in ...apped_outside_defining_module/Main ─
── OPAQUE TYPE DECLARED OUTSIDE SCOPE in ...d_outside_defining_module/Main.roc
The unwrapped opaque type Age referenced here:
Expand All @@ -861,7 +861,7 @@ fn opaque_wrapped_unwrapped_outside_defining_module() {
Note: Opaque types can only be wrapped and unwrapped in the module they are defined in!
── OPAQUE TYPE DECLARED OUTSIDE SCOPE in ...apped_outside_defining_module/Main ─
── OPAQUE TYPE DECLARED OUTSIDE SCOPE in ...d_outside_defining_module/Main.roc
The unwrapped opaque type Age referenced here:
Expand All @@ -875,7 +875,7 @@ fn opaque_wrapped_unwrapped_outside_defining_module() {
Note: Opaque types can only be wrapped and unwrapped in the module they are defined in!
── UNUSED IMPORT in tmp/opaque_wrapped_unwrapped_outside_defining_module/Main
── UNUSED IMPORT in ...aque_wrapped_unwrapped_outside_defining_module/Main.roc
Nothing from Age is used in this module.
Expand Down Expand Up @@ -910,7 +910,7 @@ fn issue_2863_module_type_does_not_exist() {
),
),
(
"Main",
"main.roc",
indoc!(
r#"
app "test"
Expand All @@ -930,7 +930,7 @@ fn issue_2863_module_type_does_not_exist() {
report,
indoc!(
"
── UNRECOGNIZED NAME in tmp/issue_2863_module_type_does_not_exist/Main ─────────
── UNRECOGNIZED NAME in tmp/issue_2863_module_type_does_not_exist/main.roc ─────
Nothing is named `DoesNotExist` in this scope.
Expand Down Expand Up @@ -971,7 +971,7 @@ fn import_builtin_in_platform_and_check_app() {
),
),
(
"Main",
"main.roc",
indoc!(
r#"
app "test"
Expand All @@ -991,7 +991,7 @@ fn import_builtin_in_platform_and_check_app() {
#[test]
fn module_doesnt_match_file_path() {
let modules = vec![(
"Age",
"Age.roc",
indoc!(
r"
interface NotAge exposes [Age] imports []
Expand All @@ -1006,7 +1006,7 @@ fn module_doesnt_match_file_path() {
err,
indoc!(
r"
── WEIRD MODULE NAME in tmp/module_doesnt_match_file_path/Age ──────────────────
── WEIRD MODULE NAME in tmp/module_doesnt_match_file_path/Age.roc ──────────────
This module name does not correspond with the file path it is defined
in:
Expand All @@ -1026,7 +1026,7 @@ fn module_doesnt_match_file_path() {
#[test]
fn module_cyclic_import_itself() {
let modules = vec![(
"Age",
"Age.roc",
indoc!(
r"
interface Age exposes [] imports [Age]
Expand All @@ -1039,7 +1039,7 @@ fn module_cyclic_import_itself() {
err,
indoc!(
r"
── IMPORT CYCLE in tmp/module_cyclic_import_itself/Age ─────────────────────────
── IMPORT CYCLE in tmp/module_cyclic_import_itself/Age.roc ─────────────────────
I can't compile Age because it depends on itself through the following
chain of module imports:
Expand All @@ -1062,15 +1062,15 @@ fn module_cyclic_import_itself() {
fn module_cyclic_import_transitive() {
let modules = vec![
(
"Age",
"Age.roc",
indoc!(
r"
interface Age exposes [] imports [Person]
"
),
),
(
"Person",
"Person.roc",
indoc!(
r"
interface Person exposes [] imports [Age]
Expand Down Expand Up @@ -1150,7 +1150,7 @@ fn nested_module_has_incorrect_name() {
#[test]
fn module_interface_with_qualified_import() {
let modules = vec![(
"A",
"A.roc",
indoc!(
r"
interface A exposes [] imports [b.T]
Expand All @@ -1163,7 +1163,7 @@ fn module_interface_with_qualified_import() {
err,
indoc!(
r#"
The package shorthand 'b' that you are using in the 'imports' section of the header of module 'tmp/module_interface_with_qualified_import/A' doesn't exist.
The package shorthand 'b' that you are using in the 'imports' section of the header of module 'tmp/module_interface_with_qualified_import/A.roc' doesn't exist.
Check that package shorthand is correct or reference the package in an 'app' or 'package' header.
This module is an interface, because of a bug in the compiler we are unable to directly typecheck interface modules with package imports so this error may not be correct. Please start checking at an app, package or platform file that imports this file."#
),
Expand All @@ -1174,7 +1174,7 @@ fn module_interface_with_qualified_import() {
#[test]
fn app_missing_package_import() {
let modules = vec![(
"Main",
"main.roc",
indoc!(
r#"
app "example"
Expand All @@ -1192,10 +1192,73 @@ fn app_missing_package_import() {
err,
indoc!(
r#"
The package shorthand 'notpack' that you are using in the 'imports' section of the header of module 'tmp/app_missing_package_import/Main' doesn't exist.
The package shorthand 'notpack' that you are using in the 'imports' section of the header of module 'tmp/app_missing_package_import/main.roc' doesn't exist.
Check that package shorthand is correct or reference the package in an 'app' or 'package' header."#
),
"\n{}",
err
);
}

#[test]
fn non_roc_file_extension() {
let modules = vec![(
"main.md",
indoc!(
r"
# Not a roc file
"
),
)];

let expected = indoc!(
r"
── NOT A ROC FILE in tmp/non_roc_file_extension/main.md ────────────────────────
I expected a file with extension `.roc` or without extension.
Instead I received a file with extension `.md`."
);
let color_start = String::from_utf8(vec![27, 91, 51, 54, 109]).unwrap();
let color_end = String::from_utf8(vec![27, 91, 48, 109]).unwrap();
let err = multiple_modules("non_roc_file_extension", modules).unwrap_err();
let err = err.replace(&color_start, "");
let err = err.replace(&color_end, "");
assert_eq!(err, expected, "\n{}", err);
}

#[test]
fn roc_file_no_extension() {
let modules = vec![(
"main",
indoc!(
r#"
app "helloWorld"
packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.8.1/x8URkvfyi9I0QhmVG98roKBUs_AZRkLFwFJVJ3942YA.tar.br" }
imports [pf.Stdout]
provides [main] to pf
main =
Stdout.line "Hello, World!"
"#
),
)];

let expected = indoc!(
r"
── NOT A ROC FILE in tmp/roc_file_no_extension/main ────────────────────────────
I expected a file with either:
- extension `.roc`
- no extension and a roc shebang as the first line, e.g.
`#!/home/username/bin/roc_nightly/roc`
The provided file did not start with a shebang `#!` containing the
string `roc`. Is tmp/roc_file_no_extension/main a Roc file?"
);
let color_start = String::from_utf8(vec![27, 91, 51, 54, 109]).unwrap();
let color_end = String::from_utf8(vec![27, 91, 48, 109]).unwrap();
let err = multiple_modules("roc_file_no_extension", modules).unwrap_err();
let err = err.replace(&color_start, "");
let err = err.replace(&color_end, "");
assert_eq!(err, expected, "\n{}", err);
}
36 changes: 36 additions & 0 deletions crates/reporting/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1652,6 +1652,42 @@ pub fn to_file_problem_report<'b>(
severity: Severity::Fatal,
}
}
io::ErrorKind::Unsupported => {
let doc = match filename.extension() {
Some(ext) => alloc.concat(vec![
alloc.reflow(r"I expected a file with extension `.roc` or without extension."),
alloc.hardline(),
alloc.reflow(r"Instead I received a file with extension `."),
alloc.as_string(ext.to_string_lossy()),
alloc.as_string("`."),
]),
None => {
alloc.stack(vec![
alloc.vcat(vec![
alloc.reflow(r"I expected a file with either:"),
alloc.reflow("- extension `.roc`"),
alloc.intersperse(
"- no extension and a roc shebang as the first line, e.g. `#!/home/username/bin/roc_nightly/roc`"
.split(char::is_whitespace),
alloc.concat(vec![ alloc.hardline(), alloc.text(" ")]).flat_alt(alloc.space()).group()
),
]),
alloc.concat(vec![
alloc.reflow("The provided file did not start with a shebang `#!` containing the string `roc`. Is "),
alloc.as_string(filename.to_string_lossy()),
alloc.reflow(" a Roc file?"),
])
])
}
};

Report {
filename,
doc,
title: "NOT A ROC FILE".to_string(),
severity: Severity::Fatal,
}
}
_ => {
let error = std::io::Error::from(error);
let formatted = format!("{error}");
Expand Down

0 comments on commit e4a7f11

Please sign in to comment.