%% ===================================================================== %% This library is free software; you can redistribute it and/or modify %% it under the terms of the GNU Lesser General Public License as %% published by the Free Software Foundation; either version 2 of the %% License, or (at your option) any later version. %% %% This library is distributed in the hope that it will be useful, but %% WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %% Lesser General Public License for more details. %% %% You should have received a copy of the GNU Lesser General Public %% License along with this library; if not, write to the Free Software %% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 %% USA %% %% @copyright 1997-2006 Richard Carlsson %% @author Richard Carlsson <carlsson.richard@gmail.com> %% @end %% ===================================================================== %% @doc Inserting comments into abstract Erlang syntax trees %% %% <p>This module contains functions for inserting comments, described %% by position, indentation and text, as attachments on an abstract %% syntax tree, at the correct places.</p> -module(erl_recomment). -export([recomment_forms/2, quick_recomment_forms/2, recomment_tree/2]). %% ===================================================================== %% @spec quick_recomment_forms(Forms, Comments::[Comment]) -> %% syntaxTree() %% %% Forms = syntaxTree() | [syntaxTree()] %% Comment = {Line, Column, Indentation, Text} %% Line = integer() %% Column = integer() %% Indentation = integer() %% Text = [string()] %% %% @doc Like {@link recomment_forms/2}, but only inserts top-level %% comments. Comments within function definitions or declarations %% ("forms") are simply ignored. -spec quick_recomment_forms(erl_syntax:forms(), [erl_comment_scan:comment()]) -> erl_syntax:syntaxTree(). quick_recomment_forms(Tree, Cs) -> recomment_forms(Tree, Cs, false). %% ===================================================================== %% @spec recomment_forms(Forms, Comments::[Comment]) -> syntaxTree() %% %% syntaxTree() = erl_syntax:syntaxTree() %% Forms = syntaxTree() | [syntaxTree()] %% Comment = {Line, Column, Indentation, Text} %% Line = integer() %% Column = integer() %% Indentation = integer() %% Text = [string()] %% %% @doc Attaches comments to the syntax tree/trees representing a %% program. The given <code>Forms</code> should be a single syntax tree %% of type <code>form_list</code>, or a list of syntax trees %% representing "program forms". The syntax trees must contain valid %% position information (for details, see %% <code>recomment_tree/2</code>). The result is a corresponding syntax %% tree of type <code>form_list</code> in which all comments in the list %% <code>Comments</code> have been attached at the proper places. %% %% <p>Assuming <code>Forms</code> represents a program (or any sequence %% of "program forms"), any comments whose first lines are not directly %% associated with a specific program form will become standalone %% comments inserted between the neighbouring program forms. %% Furthermore, comments whose column position is less than or equal to %% one will not be attached to a program form that begins at a %% conflicting line number (this can happen with preprocessor-generated %% <code>line</code>-attributes).</p> %% %% <p>If <code>Forms</code> is a syntax tree of some other type than %% <code>form_list</code>, the comments will be inserted directly using %% <code>recomment_tree/2</code>, and any comments left over from that %% process are added as postcomments on the result.</p> %% %% <p>Entries in <code>Comments</code> represent multi-line comments. %% For each entry, <code>Line</code> is the line number and %% <code>Column</code> the left column of the comment (the column of the %% first comment-introducing "<code>%</code>" character). %% <code>Indentation</code> is the number of character positions between %% the last non-whitespace character before the comment (or the left %% margin) and the left column of the comment. <code>Text</code> is a %% list of strings representing the consecutive comment lines in %% top-down order, where each string contains all characters following %% (but not including) the comment-introducing "<code>%</code>" and up %% to (but not including) the terminating newline. (Cf. module %% <code>erl_comment_scan</code>.)</p> %% %% <p>Evaluation exits with reason <code>{bad_position, Pos}</code> if %% the associated position information <code>Pos</code> of some subtree %% in the input does not have a recognizable format, or with reason %% <code>{bad_tree, L, C}</code> if insertion of a comment at line %% <code>L</code>, column <code>C</code>, fails because the tree %% structure is ill-formed.</p> %% %% @see erl_comment_scan %% @see recomment_tree/2 %% @see quick_recomment_forms/2 -spec recomment_forms(erl_syntax:forms(), [erl_comment_scan:comment()]) -> erl_syntax:syntaxTree(). recomment_forms(Tree, Cs) -> recomment_forms(Tree, Cs, true). recomment_forms(Tree, Cs, Insert) when is_list(Tree) -> recomment_forms(erl_syntax:form_list(Tree), Cs, Insert); recomment_forms(Tree, Cs, Insert) -> case erl_syntax:type(Tree) of form_list -> Tree1 = erl_syntax:flatten_form_list(Tree), Node = build_tree(Tree1), %% Here we make a small assumption about the substructure of %% a `form_list' tree: it has exactly one group of subtrees. [Node1] = node_subtrees(Node), List = filter_forms(node_subtrees(Node1)), List1 = recomment_forms_1(Cs, List, Insert), revert_tree(set_node_subtrees(Node, [set_node_subtrees(Node1, List1)])); _ -> %% Not a form list - just call `recomment_tree' and %% append any leftover comments. {Tree1, Cs1} = recomment_tree(Tree, Cs), revert_tree(append_comments(Cs1, Tree1)) end. append_comments([C | Cs], Tree) -> append_comments(Cs, node_add_postcomment(C, Tree)); append_comments([], Tree) -> Tree. %% This part goes over each comment in turn and inserts it into the %% proper place in the given list of program forms: recomment_forms_1([C | Cs], Ns, Insert) -> Ns1 = recomment_forms_2(C, Ns, Insert), recomment_forms_1(Cs, Ns1, Insert); recomment_forms_1([], Ns, _Insert) -> Ns. recomment_forms_2(C, [N | Ns] = Nodes, Insert) -> {L, Col, Ind, Text} = C, Min = node_min(N), Max = node_max(N), Delta = comment_delta(Text), Trailing = case Ns of [] -> true; [Next | _] -> L + Delta < node_min(Next) - 2 end, if L > Max + 1 ; L =:= Max + 1, not Trailing -> [N | recomment_forms_2(C, Ns, Insert)]; L + Delta < Min - 1 -> %% At least one empty line between the current form %% and the comment, so we make it a standalone. [standalone_comment(C) | Nodes]; L < Min -> %% The comment line should be above this node. %% (This duplicates what insert/5 would have done.) [node_add_precomment(C, N) | Ns]; Col =< 1, L =< Min, L + Delta >= Min -> %% This is a conflict - the "first" token of the node %% overlaps with some comment line, but the comment %% started at column 1. N1 = standalone_comment(C), if L < Min -> [N1 | Nodes]; true -> [N, N1 | Ns] end; Insert =:= true -> [insert(N, L, Col, Ind, C) | Ns]; true -> Nodes % skipping non-toplevel comment end; recomment_forms_2(C, [], _Top) -> [standalone_comment(C)]. %% Creating a leaf node for a standalone comment. Note that we try to %% preserve the original starting column rather than the indentation. standalone_comment({L, Col, _Ind, Text}) -> leaf_node(L, L + comment_delta(Text), erl_syntax:set_pos(erl_syntax:comment(Col - 1, Text), L)). %% Compute delta between first and last line of a comment, given %% the lines of text. comment_delta(Text) -> case length(Text) of N when N > 0 -> N - 1; _ -> 0 % avoid negative delta end. %% This kills line information for program forms that do not come from %% the source file itself, but have been included by preprocessing. This %% way, comments will not be inserted into such parts by mistake. -record(filter, {file = undefined :: file:filename() | 'undefined', line = 0 :: integer()}). filter_forms(Fs) -> filter_forms(Fs, false, #filter{}). filter_forms([F | Fs], Kill, S) -> case check_file_attr(F) of {true, A1, A2} -> S1 = case S#filter.file of undefined -> S#filter{file = A1, line = A2}; _ -> S end, if S1#filter.file =:= A1, S1#filter.line =< A2 -> [F | filter_forms(Fs, false, S1#filter{line = A2})]; Kill =:= true -> [node_kill_range(F) | filter_forms(Fs, true, S1)]; true -> [F | filter_forms(Fs, true, S1)] end; false -> case Kill of true -> [node_kill_range(F) | filter_forms(Fs, Kill, S)]; false -> [F | filter_forms(Fs, Kill, S)] end end; filter_forms([], _, _) -> []. %% This structure matching gets a bit painful... check_file_attr(F) -> case node_type(F) of tree_node -> case tree_node_type(F) of attribute -> case node_subtrees(F) of [L1, L2 | _] -> check_file_attr_1(L1, L2); _ -> false end; _ -> false end; _ -> false end. check_file_attr_1(L1, L2) -> case node_subtrees(L1) of [N1 | _] -> N2 = leaf_node_value(N1), case erl_syntax:type(N2) of atom -> case erl_syntax:atom_value(N2) of file -> check_file_attr_2(L2); _ -> false end; _ -> false end; _ -> false end. check_file_attr_2(L) -> case node_subtrees(L) of [N1, N2 | _] -> T1 = erl_syntax:concrete(revert_tree(N1)), T2 = erl_syntax:concrete(revert_tree(N2)), {true, T1, T2}; _ -> false end. %% ===================================================================== %% @spec recomment_tree(Tree::syntaxTree(), Comments::[Comment]) -> %% {syntaxTree(), [Comment]} %% %% Comment = {Line, Column, Indentation, Text} %% Line = integer() %% Column = integer() %% Indentation = integer() %% Text = [string()] %% %% @doc Attaches comments to a syntax tree. The result is a pair %% <code>{NewTree, Remainder}</code> where <code>NewTree</code> is the %% given <code>Tree</code> where comments from the list %% <code>Comments</code> have been attached at the proper places. %% <code>Remainder</code> is the list of entries in %% <code>Comments</code> which have not been inserted, because their %% line numbers are greater than those of any node in the tree. The %% entries in <code>Comments</code> are inserted in order; if two %% comments become attached to the same node, they will appear in the %% same order in the program text. %% %% <p>The nodes of the syntax tree must contain valid position %% information. This can be single integers, assumed to represent a line %% number, or 2- or 3-tuples where the first or second element is an %% integer, in which case the leftmost integer element is assumed to %% represent the line number. Line numbers less than one are ignored %% (usually, the default line number for newly created nodes is %% zero).</p> %% %% <p>For details on the <code>Line</code>, <code>Column</code> and %% <code>Indentation</code> fields, and the behaviour in case of errors, %% see <code>recomment_forms/2</code>.</p> %% %% @see recomment_forms/2 -spec recomment_tree(erl_syntax:syntaxTree(), [erl_comment_scan:comment()]) -> {erl_syntax:syntaxTree(), [erl_comment_scan:comment()]}. recomment_tree(Tree, Cs) -> {Tree1, Cs1} = insert_comments(Cs, build_tree(Tree)), {revert_tree(Tree1), Cs1}. %% Comments are inserted in the tree one at a time. Note that this %% part makes no assumptions about how tree nodes and list nodes %% are nested; only `build_tree' and `revert_tree' knows about %% such things. insert_comments(Cs, Node) -> insert_comments(Cs, Node, []). insert_comments([C | Cs], Node, Cs1) -> {L, Col, Ind, _Text} = C, Max = node_max(Node), if L =< Max -> insert_comments(Cs, insert(Node, L, Col, Ind, C), Cs1); true -> insert_comments(Cs, Node, [C | Cs1]) end; insert_comments([], Node, Cs) -> {Node, lists:reverse(Cs)}. %% Here, we assume that the comment is located on some line not %% below the last element of the given node. insert(Node, L, Col, Ind, C) -> case node_type(Node) of list_node -> %% We cannot attach comments directly to a list node. set_node_subtrees(Node, insert_in_list(node_subtrees(Node), L, Col, Ind, C)); _ -> %% We check if the comment belongs before, or inside %% the range of the current node. Min = node_min(Node), Max = node_max(Node), if L < Min -> %% The comment line should be above this node. node_add_precomment(C, Node); Min =:= Max -> %% The whole node is on a single line (this %% should usually catch all leaf nodes), so we %% postfix the comment. node_add_postcomment(C, Node); true -> %% The comment should be inserted in the %% subrange of the node, i.e., attached either %% to the node itself, or to one of its %% subtrees. insert_1(Node, L, Col, Ind, C) end end. insert_1(Node, L, Col, Ind, C) -> case node_type(Node) of tree_node -> %% Insert in one of the subtrees. set_node_subtrees(Node, insert_in_list(node_subtrees(Node), L, Col, Ind, C)); leaf_node -> %% Odd case: no components, but not on a single line. %% (Never mind anyway - just postfix the comment.) node_add_postcomment(C, Node) end. %% We assume that there exists at least one tree node in some tree %% in the list; since we have decided to insert here, we're %% screwed if there isn't one. insert_in_list([Node | Ns], L, Col, Ind, C) -> Max = node_max(Node), %% Get the `Min' of the next node that follows in the %% flattened left-to-right order, or -1 (minus one) if no such %% tree node exists. NextMin = next_min_in_list(Ns), %% `NextMin' could be less than `Max', in inconsistent trees. if NextMin < 0 -> %% There is no following leaf/tree node, so we try %% to insert at this node. insert_here(Node, L, Col, Ind, C, Ns); L >= NextMin, NextMin >= Max -> %% Tend to select the later node, in case the next %% node should also match. insert_later(Node, L, Col, Ind, C, Ns); L =< Max -> insert_here(Node, L, Col, Ind, C, Ns); true -> insert_later(Node, L, Col, Ind, C, Ns) end; insert_in_list([], L, Col, _, _) -> exit({bad_tree, L, Col}). %% The comment belongs to the current subrange insert_here(Node, L, Col, Ind, C, Ns) -> [insert(Node, L, Col, Ind, C) | Ns]. %% The comment should be inserted later insert_later(Node, L, Col, Ind, C, Ns) -> [Node | insert_in_list(Ns, L, Col, Ind, C)]. %% `next_min_in_list' returns the `Min' field of the leftmost tree %% or leaf node in the given node list, or the integer -1 (minus %% one) if no such element exists. next_min_in_list(Ts) -> next_min_in_list(Ts, []). next_min_in_list([T | Ts], Ack) -> next_min_in_node(T, [Ts | Ack]); next_min_in_list([], [T | Ts]) -> next_min_in_list(T, Ts); next_min_in_list([], []) -> -1. next_min_in_node(Node, Ack) -> case node_type(Node) of leaf_node -> node_min(Node); tree_node -> node_min(Node); list_node -> next_min_in_list(node_subtrees(Node), Ack) end. %% Building an extended syntax tree from an `erl_syntax' abstract %% syntax tree. build_tree(Node) -> L = get_line(Node), case erl_syntax:subtrees(Node) of [] -> %% This guarantees that Min =< Max for the base case. leaf_node(L, L, Node); Ts -> %% `Ts' is a list of lists of abstract terms. {Subtrees, Min, Max} = build_list_list(Ts), %% Include L, while preserving Min =< Max. tree_node(minpos(L, Min), erlang:max(L, Max), erl_syntax:type(Node), erl_syntax:get_attrs(Node), Subtrees) end. %% Since `erl_syntax:subtrees' yields the components in %% left-to-right textual order, the line numbers should grow %% monotonically as the list is traversed, and the maximum line %% number of the list should therefore be the dito of the last %% component. However, we do not want to make such a strong %% assumption about the consistency of the line numbering, so we %% take the trouble to find the maximum line number in the subtree %% taken over all its elements. build_list(Ts) -> build_list(Ts, 0, 0, []). build_list([T | Ts], Min, Max, Ack) -> Node = build_tree(T), Min1 = minpos(node_min(Node), Min), Max1 = erlang:max(node_max(Node), Max), build_list(Ts, Min1, Max1, [Node | Ack]); build_list([], Min, Max, Ack) -> list_node(Min, Max, lists:reverse(Ack)). build_list_list(Ls) -> build_list_list(Ls, 0, 0, []). build_list_list([L | Ls], Min, Max, Ack) -> Node = build_list(L), Min1 = minpos(node_min(Node), Min), Max1 = erlang:max(node_max(Node), Max), build_list_list(Ls, Min1, Max1, [Node | Ack]); build_list_list([], Min, Max, Ack) -> {lists:reverse(Ack), Min, Max}. %% Reverting to an abstract syntax tree from the extended form. %% Note that the new comments are inserted after the original %% attributes are restored. revert_tree(Node) -> case node_type(Node) of leaf_node -> add_comments(Node, leaf_node_value(Node)); tree_node -> add_comments(Node, erl_syntax:set_attrs( erl_syntax:make_tree( tree_node_type(Node), revert_list(node_subtrees(Node))), tree_node_attrs(Node))); list_node -> revert_list(node_subtrees(Node)) end. revert_list([T | Ts]) -> [revert_tree(T) | revert_list(Ts)]; revert_list([]) -> []. add_comments(Node, Tree) -> case node_precomments(Node) of [] -> add_comments_1(Node, Tree); Cs -> Cs1 = lists:reverse(expand_comments(Cs)), add_comments_1(Node, erl_syntax:add_precomments(Cs1, Tree)) end. add_comments_1(Node, Tree) -> case node_postcomments(Node) of [] -> Tree; Cs -> Cs1 = lists:reverse(expand_comments(Cs)), erl_syntax:add_postcomments(Cs1, Tree) end. expand_comments([C | Cs]) -> [expand_comment(C) | expand_comments(Cs)]; expand_comments([]) -> []. expand_comment(C) -> {L, _Col, Ind, Text} = C, erl_syntax:set_pos(erl_syntax:comment(Ind, Text), L). %% ===================================================================== %% Abstract data type for extended syntax trees. %% %% These explicitly distinguish between leaf and tree nodes, both %% corresponding to a single abstract syntax tree, and list nodes, %% corresponding to a left-to-right ordered sequence of such trees. %% %% All nodes have `min' and `max' fields, containing the first and last %% source lines, respectively, over which the tree extends. %% %% Tree nodes and list nodes have a `subtrees' field, containing the %% (extended) subtrees of the node. Tree nodes also have a `type' field, %% containing the atom returned by `erl_syntax:type' for the %% corresponding abstract syntax tree, and an `attrs' field, containing %% the value of `erl_syntax:get_attrs' for the abstract syntax tree. %% %% Leaf nodes and tree nodes also have `precomments' and `postcomments' %% fields. The comment fields are lists of comment structures (in %% top-down order); the representation of comments has no consequence to %% the tree representation. %% %% Leaf nodes, lastly, have a `value' field containing the abstract %% syntax tree for any such tree that can have no subtrees, i.e., such %% that `erl_syntax:is_leaf' yields `true'. -record(leaf, {min = 0 :: integer(), max = 0 :: integer(), precomments = [] :: [erl_syntax:syntaxTree()], postcomments = [] :: [erl_syntax:syntaxTree()], value :: erl_syntax:syntaxTree()}). -record(tree, {min = 0 :: integer(), max = 0 :: integer(), type :: atom(), attrs :: erl_syntax:syntaxTreeAttributes(), precomments = [] :: [erl_syntax:syntaxTree()], postcomments = [] :: [erl_syntax:syntaxTree()], subtrees = [] :: [erl_syntax:syntaxTree()]}). -record(list, {min = 0 :: integer(), max = 0 :: integer(), subtrees = [] :: [erl_syntax:syntaxTree()]}). leaf_node(Min, Max, Value) -> #leaf{min = Min, max = Max, value = Value}. tree_node(Min, Max, Type, Attrs, Subtrees) -> #tree{min = Min, max = Max, type = Type, attrs = Attrs, subtrees = Subtrees}. list_node(Min, Max, Subtrees) -> #list{min = Min, max = Max, subtrees = Subtrees}. node_type(#leaf{}) -> leaf_node; node_type(#tree{}) -> tree_node; node_type(#list{}) -> list_node. node_min(#leaf{min = Min}) -> Min; node_min(#tree{min = Min}) -> Min; node_min(#list{min = Min}) -> Min. node_max(#leaf{max = Max}) -> Max; node_max(#tree{max = Max}) -> Max; node_max(#list{max = Max}) -> Max. node_kill_range(Node) -> case Node of #leaf{} -> Node#leaf{min = -1, max = -1}; #tree{} -> Node#tree{min = -1, max = -1}; #list{} -> Node#list{min = -1, max = -1} end. node_precomments(#leaf{precomments = Cs}) -> Cs; node_precomments(#tree{precomments = Cs}) -> Cs. node_add_precomment(C, Node) -> case Node of #leaf{} -> Node#leaf{precomments = [C | Node#leaf.precomments]}; #tree{} -> Node#tree{precomments = [C | Node#tree.precomments]} end. node_postcomments(#leaf{postcomments = Cs}) -> Cs; node_postcomments(#tree{postcomments = Cs}) -> Cs. node_add_postcomment(C, Node) -> case Node of #leaf{} -> Node#leaf{postcomments = [C | Node#leaf.postcomments]}; #tree{} -> Node#tree{postcomments = [C | Node#tree.postcomments]} end. node_subtrees(#tree{subtrees = Subtrees}) -> Subtrees; node_subtrees(#list{subtrees = Subtrees}) -> Subtrees. leaf_node_value(#leaf{value = Value}) -> Value. tree_node_type(#tree{type = Type}) -> Type. set_node_subtrees(Node, Subtrees) -> case Node of #tree{} -> Node#tree{subtrees = Subtrees}; #list{} -> Node#list{subtrees = Subtrees} end. tree_node_attrs(#tree{attrs = Attrs}) -> Attrs. %% ===================================================================== %% General utility functions %% Return the least positive integer of X and Y, or zero if none of them %% are positive. (This is necessary for computing minimum source line %% numbers, since zero (or negative) numbers may occur, but they %% represent the "undefined" line number.) minpos(X, Y) when X < Y -> minpos1(X, Y); minpos(X, Y) -> minpos1(Y, X). minpos1(X, Y) when X < 1 -> minpos2(Y); minpos1(X, _) -> X. minpos2(X) when X < 1 -> 0; minpos2(X) -> X. get_line(Node) -> case erl_syntax:get_pos(Node) of L when is_integer(L) -> L; {L, _} when is_integer(L) -> L; {_, L} when is_integer(L) -> L; {L, _, _} when is_integer(L) -> L; {_, L, _} when is_integer(L) -> L; Pos -> exit({bad_position, Pos}) end. %% =====================================================================