From 03d6dd6ca2736993ae905e7471af4a0725d927a3 Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Thu, 14 Sep 2023 15:00:30 +0200 Subject: [PATCH] Improve resolution of relative includes (#489) * Add aeso_utils:canonical_dir/1 * Add current file directory when resolving includes * Add CHANGELOG * Add documentation * Add a test case * Properly keep track of src_dir --- CHANGELOG.md | 2 ++ docs/sophia_features.md | 18 ++++++++++ src/aeso_compiler.erl | 7 ++-- src/aeso_parse_lib.erl | 9 ++++- src/aeso_parser.erl | 54 +++++++++++++++++++++-------- src/aeso_utils.erl | 14 +++++++- test/aeso_compiler_tests.erl | 1 + test/contracts/dir1/bar.aes | 4 +++ test/contracts/dir2/baz.aes | 3 ++ test/contracts/relative_include.aes | 3 ++ 10 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 test/contracts/dir1/bar.aes create mode 100644 test/contracts/dir2/baz.aes create mode 100644 test/contracts/relative_include.aes diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ce4b06..3d90868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added ### Changed +- Improve how includes with relative paths are resolved during parsing/compilation. Relative + include paths are now always relative to the file containing the `include` statement. ### Removed ### Fixed diff --git a/docs/sophia_features.md b/docs/sophia_features.md index e8170c0..3025f82 100644 --- a/docs/sophia_features.md +++ b/docs/sophia_features.md @@ -493,6 +493,24 @@ the file, except that error messages will refer to the original source locations. The language will try to include each file at most one time automatically, so even cyclic includes should be working without any special tinkering. +### Include files using relative paths + +When including code from another file using the `include` statement, the path +is relative to _the file that includes it_. Consider the following file tree: +``` +c1.aes +c3.aes +dir1/c2.aes +dir1/c3.aes +``` + +If `c1.aes` contains `include "c3.aes"` it will include the top level `c3.aes`, +while if `c2.aes` contained the same line it would as expected include +`dir1/c3.aes`. + +Note: Prior to v7.5.0, it would consider the include path relative to _the main +contract file_ (or any explicitly set include path). + ## Standard library Sophia offers [standard library](sophia_stdlib.md) which exposes some diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 1ff6ec3..5968081 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -42,6 +42,7 @@ | {include, {file_system, [string()]} | {explicit_files, #{string() => binary()}}} | {src_file, string()} + | {src_dir, string()} | {aci, aeso_aci:aci_type()}. -type options() :: [option()]. @@ -87,7 +88,9 @@ file(Filename) -> file(File, Options0) -> Options = add_include_path(File, Options0), case read_contract(File) of - {ok, Bin} -> from_string(Bin, [{src_file, File} | Options]); + {ok, Bin} -> + SrcDir = aeso_utils:canonical_dir(filename:dirname(File)), + from_string(Bin, [{src_file, File}, {src_dir, SrcDir} | Options]); {error, Error} -> Msg = lists:flatten([File,": ",file:format_error(Error)]), {error, [aeso_errors:new(file_error, Msg)]} @@ -99,7 +102,7 @@ add_include_path(File, Options) -> false -> Dir = filename:dirname(File), {ok, Cwd} = file:get_cwd(), - [{include, {file_system, [Cwd, Dir]}} | Options] + [{include, {file_system, [Cwd, aeso_utils:canonical_dir(Dir)]}} | Options] end. -spec from_string(binary() | string(), options()) -> {ok, map()} | {error, [aeso_errors:error()]}. diff --git a/src/aeso_parse_lib.erl b/src/aeso_parse_lib.erl index 4c50527..8246ef6 100644 --- a/src/aeso_parse_lib.erl +++ b/src/aeso_parse_lib.erl @@ -15,7 +15,7 @@ many/1, many1/1, sep/2, sep1/2, infixl/2, infixr/2]). --export([current_file/0, set_current_file/1, +-export([current_file/0, set_current_file/1, current_dir/0, set_current_dir/1, current_include_type/0, set_current_include_type/1]). %% -- Types ------------------------------------------------------------------ @@ -480,6 +480,13 @@ current_file() -> set_current_file(File) -> put('$current_file', File). +%% Current source directory +current_dir() -> + get('$current_dir'). + +set_current_dir(File) -> + put('$current_dir', File). + add_current_file({L, C}) -> {current_file(), L, C}; add_current_file(Pos) -> Pos. diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index d1d92cb..3c03bf0 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -19,6 +19,7 @@ -include("aeso_parse_lib.hrl"). -import(aeso_parse_lib, [current_file/0, set_current_file/1, + current_dir/0, set_current_dir/1, current_include_type/0, set_current_include_type/1]). -type parse_result() :: aeso_syntax:ast() | {aeso_syntax:ast(), sets:set(include_hash())} | none(). @@ -58,6 +59,7 @@ run_parser(P, Inp, Opts) -> parse_and_scan(P, S, Opts) -> set_current_file(proplists:get_value(src_file, Opts, no_file)), + set_current_dir(proplists:get_value(src_dir, Opts, no_file)), set_current_include_type(proplists:get_value(include_type, Opts, none)), case aeso_scan:scan(S) of {ok, Tokens} -> aeso_parse_lib:parse(P, Tokens); @@ -556,6 +558,7 @@ bracket_list(P) -> brackets(comma_sep(P)). -spec pos_ann(ann_line(), ann_col()) -> ann(). pos_ann(Line, Col) -> [ {file, current_file()} + , {dir, current_dir()} , {include_type, current_include_type()} , {line, Line} , {col, Col} ]. @@ -696,7 +699,7 @@ expand_includes([], Included, Acc, Opts) -> end; expand_includes([{include, Ann, {string, _SAnn, File}} | AST], Included, Acc, Opts) -> case get_include_code(File, Ann, Opts) of - {ok, Code} -> + {ok, AbsDir, Code} -> Hashed = hash_include(File, Code), case sets:is_element(Hashed, Included) of false -> @@ -706,9 +709,10 @@ expand_includes([{include, Ann, {string, _SAnn, File}} | AST], Included, Acc, Op _ -> indirect end, Opts1 = lists:keystore(src_file, 1, Opts, {src_file, File}), - Opts2 = lists:keystore(include_type, 1, Opts1, {include_type, IncludeType}), + Opts2 = lists:keystore(src_dir, 1, Opts1, {src_dir, AbsDir}), + Opts3 = lists:keystore(include_type, 1, Opts2, {include_type, IncludeType}), Included1 = sets:add_element(Hashed, Included), - case parse_and_scan(file(), Code, Opts2) of + case parse_and_scan(file(), Code, Opts3) of {ok, AST1} -> expand_includes(AST1 ++ AST, Included1, Acc, Opts); Err = {error, _} -> @@ -726,13 +730,12 @@ expand_includes([E | AST], Included, Acc, Opts) -> read_file(File, Opts) -> case proplists:get_value(include, Opts, {explicit_files, #{}}) of {file_system, Paths} -> - CandidateNames = [ filename:join(Dir, File) || Dir <- Paths ], - lists:foldr(fun(F, {error, _}) -> file:read_file(F); - (_F, OK) -> OK end, {error, not_found}, CandidateNames); + lists:foldr(fun(Path, {error, _}) -> read_file_(Path, File); + (_Path, OK) -> OK end, {error, not_found}, Paths); {explicit_files, Files} -> case maps:get(binary_to_list(File), Files, not_found) of not_found -> {error, not_found}; - Src -> {ok, Src} + Src -> {ok, File, Src} end; escript -> try @@ -741,7 +744,7 @@ read_file(File, Opts) -> Archive = proplists:get_value(archive, Sections), FileName = binary_to_list(filename:join([aesophia, priv, stdlib, File])), case zip:extract(Archive, [{file_list, [FileName]}, memory]) of - {ok, [{_, Src}]} -> {ok, Src}; + {ok, [{_, Src}]} -> {ok, escript, Src}; _ -> {error, not_found} end catch _:_ -> @@ -749,6 +752,13 @@ read_file(File, Opts) -> end end. +read_file_(Path, File) -> + AbsFile = filename:join(Path, File), + case file:read_file(AbsFile) of + {ok, Bin} -> {ok, aeso_utils:canonical_dir(filename:dirname(AbsFile)), Bin}; + Err -> Err + end. + stdlib_options() -> StdLibDir = aeso_stdlib:stdlib_include_path(), case filelib:is_dir(StdLibDir) of @@ -757,23 +767,37 @@ stdlib_options() -> end. get_include_code(File, Ann, Opts) -> - case {read_file(File, Opts), read_file(File, stdlib_options())} of - {{ok, Bin}, {ok, _}} -> + %% Temporarily extend include paths with the directory of the current file + Opts1 = include_current_file_dir(Opts, Ann), + case {read_file(File, Opts1), read_file(File, stdlib_options())} of + {{ok, Dir, Bin}, {ok, _}} -> case filename:basename(File) == File of true -> { error , fail( ann_pos(Ann) , "Illegal redefinition of standard library " ++ binary_to_list(File))}; %% If a path is provided then the stdlib takes lower priority - false -> {ok, binary_to_list(Bin)} + false -> {ok, Dir, binary_to_list(Bin)} end; - {_, {ok, Bin}} -> - {ok, binary_to_list(Bin)}; - {{ok, Bin}, _} -> - {ok, binary_to_list(Bin)}; + {_, {ok, _, Bin}} -> + {ok, stdlib, binary_to_list(Bin)}; + {{ok, Dir, Bin}, _} -> + {ok, Dir, binary_to_list(Bin)}; {_, _} -> {error, {ann_pos(Ann), include_error, File}} end. +include_current_file_dir(Opts, Ann) -> + case {proplists:get_value(dir, Ann, undefined), + proplists:get_value(include, Opts, undefined)} of + {undefined, _} -> Opts; + {CurrDir, {file_system, Paths}} -> + case lists:member(CurrDir, Paths) of + false -> [{include, {file_system, [CurrDir | Paths]}} | Opts]; + true -> Opts + end; + {_, _} -> Opts + end. + -spec hash_include(string() | binary(), string()) -> include_hash(). hash_include(File, Code) when is_binary(File) -> hash_include(binary_to_list(File), Code); diff --git a/src/aeso_utils.erl b/src/aeso_utils.erl index e86f9ca..dcf9b7d 100644 --- a/src/aeso_utils.erl +++ b/src/aeso_utils.erl @@ -6,10 +6,22 @@ %%%------------------------------------------------------------------- -module(aeso_utils). --export([scc/1]). +-export([scc/1, canonical_dir/1]). -export_type([graph/1]). +%% -- Simplistic canonical directory +%% Note: no attempts to be 100% complete + +canonical_dir(Dir) -> + {ok, Cwd} = file:get_cwd(), + AbsName = filename:absname(Dir), + RelAbsName = filename:join(tl(filename:split(AbsName))), + case filelib:safe_relative_path(RelAbsName, Cwd) of + unsafe -> AbsName; + Simplified -> filename:absname(Simplified, "") + end. + %% -- Topological sort -type graph(Node) :: #{Node => [Node]}. %% List of incoming edges (dependencies). diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 26c5d64..b0f2008 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -161,6 +161,7 @@ compilable_contracts() -> "state_handling", "events", "include", + "relative_include", "basic_auth", "basic_auth_tx", "bitcoin_auth", diff --git a/test/contracts/dir1/bar.aes b/test/contracts/dir1/bar.aes new file mode 100644 index 0000000..abc4aed --- /dev/null +++ b/test/contracts/dir1/bar.aes @@ -0,0 +1,4 @@ +include "../dir2/baz.aes" +namespace D = + function g() = E.h() + diff --git a/test/contracts/dir2/baz.aes b/test/contracts/dir2/baz.aes new file mode 100644 index 0000000..e14962f --- /dev/null +++ b/test/contracts/dir2/baz.aes @@ -0,0 +1,3 @@ +namespace E = + function h() = 42 + diff --git a/test/contracts/relative_include.aes b/test/contracts/relative_include.aes new file mode 100644 index 0000000..e64ecb1 --- /dev/null +++ b/test/contracts/relative_include.aes @@ -0,0 +1,3 @@ +include "./dir1/bar.aes" +contract C = + entrypoint f() = D.g()