1 /**
2  * Copyright: Copyright (c) 2016 Wojciech Szęszoł. All rights reserved.
3  * Authors: Wojciech Szęszoł
4  * Version: Initial created: May 19, 2016
5  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
6  */
7 module dstep.translator.CommentIndex;
8 
9 import std.range;
10 
11 import clang.c.Index;
12 import clang.Token;
13 import clang.SourceLocation;
14 import clang.SourceRange;
15 import clang.TranslationUnit;
16 
17 class CommentIndex
18 {
19     public struct Comment
20     {
21         string content;
22         SourceRange extent;
23         uint line;
24         uint column;
25         uint offset;
26 
27         this(Token token)
28         {
29             auto location = token.location;
30             content = normalize(token.spelling);
31             extent = token.extent;
32             line = location.line;
33             column = location.column;
34             offset = location.offset;
35         }
36 
37         size_t indentAmount() const
38         {
39             import std..string;
40             import std.format;
41             import std.algorithm.comparison;
42             import std.algorithm.searching;
43 
44             size_t amount = this.column - 1;
45             auto lines = lineSplitter(content);
46 
47             if (!lines.empty)
48             {
49                 lines.popFront();
50 
51                 foreach (line; lines)
52                 {
53                     amount = min(
54                         amount,
55                         cast(size_t) countUntil!(a => a != ' ')(line));
56                 }
57             }
58 
59             return amount;
60         }
61 
62         int opCmp(ref const Comment s) const
63         {
64             return offset < s.offset ? -1 : (offset == s.offset ? 0 : 1);
65         }
66 
67         int opCmp(uint s) const
68         {
69             return offset < s ? -1 : (offset == s ? 0 : 1);
70         }
71 
72         private static bool isNormalized(string content)
73         {
74             import std.ascii : isWhite;
75             import std.range : iota;
76             import std.algorithm : canFind;
77 
78             return !iota(0, content.length).canFind!(
79                 i => content[i] == '\n' && content[i - 1].isWhite);
80         }
81 
82         private static string normalize(string content)
83         {
84             import std.algorithm : map, splitter;
85             import std..string : stripRight;
86 
87             if (isNormalized(content))
88                 return content;
89             else
90                 return content.splitter("\n").map!(stripRight).join("\n");
91         }
92 
93         unittest
94         {
95             assert(normalize("") == "");
96             assert(normalize("foo") == "foo");
97             assert(normalize("foo \n") == "foo\n");
98             assert(normalize("foo \n  ") == "foo\n");
99             assert(normalize("foo \n  bar \n  ") == "foo\n  bar\n");
100             assert(normalize("foo \n  bar \n\n  ") == "foo\n  bar\n\n");
101         }
102     }
103 
104     private TranslationUnit translUnit;
105     private Comment[] comments;
106     private SourceRange lastTokenRange;
107     private SourceLocation includeGuardLocation;
108     private bool hasIncludeGuard = false;
109 
110     this(TranslationUnit translUnit)
111     {
112         import std.algorithm.iteration : filter, map;
113 
114         this.translUnit = translUnit;
115         lastTokenRange = translUnit.cursor.extent;
116 
117         auto tokens = translUnit.cursor.tokens;
118 
119         if (!tokens.empty)
120             lastTokenRange = tokens.back.extent;
121 
122         comments = tokens
123             .filter!(token =>
124                 token.kind == TokenKind.comment &&
125                 token.location.isFromMainFile)
126             .map!(token => Comment(token)).array;
127     }
128 
129     this (TranslationUnit translUnit, SourceLocation includeGuardLocation)
130     {
131         this (translUnit);
132 
133         this.includeGuardLocation = includeGuardLocation;
134         this.hasIncludeGuard = true;
135     }
136 
137     auto queryComments(uint begin, uint end)
138     {
139         auto sorted = assumeSorted(comments);
140         auto lower = sorted.lowerBound(end);
141 
142         if (lower.empty || begin == 0)
143             return lower;
144         else
145             return lower.upperBound(begin - 1);
146     }
147 
148     SourceLocation queryLastLocation()
149     {
150         return lastTokenRange.end;
151     }
152 
153     /**
154       * Returns true if a header comment is present.
155       *
156       * If include guard is present the header comment consists of all of the
157       * comments before the header guard. Otherwise, the header comment is a
158       * comment placed at the very beginning of the file, specifically it cannot
159       * have any white-spaces before it.
160       */
161     bool isHeaderCommentPresent()
162     {
163         if (hasIncludeGuard)
164             return !comments.empty &&
165                 comments.front.extent.end.offset < includeGuardLocation.offset;
166         else
167             return !comments.empty && comments.front.offset == 0;
168     }
169 
170     SourceRange queryHeaderCommentExtent()
171     {
172         if (hasIncludeGuard)
173         {
174             import std.algorithm.searching : find;
175 
176             auto offset = includeGuardLocation.offset;
177             auto prv = comments.front;
178 
179             foreach (itr; comments)
180             {
181                 if (itr.offset >= offset)
182                     break;
183 
184                 prv = itr;
185             }
186 
187             return translUnit.extent(
188                 comments.front.offset,
189                 prv.extent.end.offset);
190         }
191         else
192         {
193             return translUnit.extent(
194                 comments.front.offset,
195                 comments.front.offset + cast(uint) comments.front.content.length);
196         }
197     }
198 }