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
This commit is contained in:
Hans Svensson 2023-09-14 15:00:30 +02:00 committed by GitHub
parent 33229c3513
commit 03d6dd6ca2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 96 additions and 19 deletions

View File

@ -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

View File

@ -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

View File

@ -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()]}.

View File

@ -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.

View File

@ -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);

View File

@ -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).

View File

@ -161,6 +161,7 @@ compilable_contracts() ->
"state_handling",
"events",
"include",
"relative_include",
"basic_auth",
"basic_auth_tx",
"bitcoin_auth",

View File

@ -0,0 +1,4 @@
include "../dir2/baz.aes"
namespace D =
function g() = E.h()

View File

@ -0,0 +1,3 @@
namespace E =
function h() = 42

View File

@ -0,0 +1,3 @@
include "./dir1/bar.aes"
contract C =
entrypoint f() = D.g()