1 /**
2 Copyright: Copyright (c) 2015-2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 */
10 module cpptooling.testdouble.header_filter;
11 
12 import std.regex : Regex;
13 import logger = std.experimental.logger;
14 
15 enum LocationType {
16     Root,
17     Leaf
18 }
19 
20 enum DummyPayload {
21     none
22 }
23 
24 /** Includes intended for the test double.
25  *
26  * Filtered according to the user.
27  *
28  * State diagram in plantuml:
29  * @startuml
30  * [*] -> Waiting
31  * Waiting: Initialize pool
32  *
33  * Waiting -> SymbolInclude: Add
34  * Waiting --> RootInclude: Add root
35  * Waiting --> UserInclude: Add user
36  * Waiting --> Finalize: Done
37  *
38  * UserInclude: Static pool from CLI
39  *
40  * SymbolInclude: Temp pool of includes
41  * SymbolInclude --> SymbolInclude: Add
42  * SymbolInclude --> Process: Done
43  * SymbolInclude --> RootInclude: Clear pool, add root
44  *
45  * RootInclude: Temp pool of includes
46  * RootInclude --> RootInclude: Add
47  * RootInclude --> Process: Done
48  *
49  * Process: Temp pool to permanent pool
50  * Process --> Waiting
51  *
52  * Finalize: strip includes
53  * Finalize: Data ready to be used
54  * @enduml
55  *
56  * Params:
57  *  PayloadT = extra data associated with a particulare file such as the language.
58  */
59 struct GenericTestDoubleIncludes(PayloadT) {
60     import std.regex : Regex;
61     import std.container : RedBlackTree;
62 
63     private enum State {
64         waiting,
65         symbolInclude,
66         rootInclude,
67         process,
68         finalize,
69         forceInclude
70     }
71 
72     struct Include {
73         string filename;
74         PayloadT payload;
75         alias filename this;
76     }
77 
78     private {
79         RedBlackTree!Include permanent_pool;
80         Include[] work_pool;
81 
82         State st;
83         Regex!char strip_incl;
84         string[] unstripped_incls;
85     }
86 
87     @disable this();
88 
89     this(Regex!char strip_incl) {
90         import std.container : make;
91 
92         this.strip_incl = strip_incl;
93         this.permanent_pool = make!(typeof(this.permanent_pool));
94     }
95 
96     auto includes() @safe pure nothrow @nogc
97     in {
98         assert(st == State.finalize);
99     }
100     do {
101         import std.algorithm : map;
102 
103         return permanent_pool[].map!(a => a.filename);
104     }
105 
106     auto includesWithPayload() @safe pure nothrow @nogc
107     in {
108         assert(st == State.finalize);
109     }
110     do {
111         return permanent_pool[];
112     }
113 
114     /** Replace buffer of includes with argument.
115      *
116      * See description of states to understand what UserDefined entitles.
117      *
118      * TODO this contains a possible bug because the kind is not provided.
119      */
120     void forceIncludes(string[] in_incls) {
121         import std.algorithm : each;
122 
123         st = State.forceInclude;
124 
125         /// Assuming user defined includes are good as they are so no stripping.
126         () @trusted { in_incls.each!(a => permanent_pool.insert(Include(a))); }();
127     }
128 
129     void finalize() @safe pure nothrow @nogc
130     in {
131         import std.algorithm : among;
132 
133         assert(st.among(State.waiting, State.forceInclude));
134     }
135     do {
136         st = State.finalize;
137     }
138 
139     void process() @safe
140     in {
141         import std.algorithm : among;
142 
143         assert(st.among(State.waiting, State.rootInclude, State.symbolInclude, State.forceInclude));
144     }
145     do {
146         import std.algorithm : each, map;
147         import std.array : array;
148 
149         if (st == State.forceInclude)
150             return;
151 
152         st = State.waiting;
153 
154         // no paths added, nothing to do
155         if (work_pool.length == 0)
156             return;
157 
158         () @trusted {
159             stripIncl(work_pool.map!(a => a.filename).array, strip_incl).each!(
160                     a => permanent_pool.insert(Include(a)));
161         }();
162         work_pool = null;
163     }
164 
165     void put(string fname, LocationType type, PayloadT kind = PayloadT.init) @safe
166     in {
167         import std.utf : validate;
168 
169         validate((cast(string) fname));
170     }
171     do {
172         auto val = Include(fname, kind);
173 
174         switch (st) with (State) {
175         case waiting:
176             work_pool ~= val;
177             if (type == LocationType.Root) {
178                 st = rootInclude;
179             } else {
180                 st = symbolInclude;
181             }
182             break;
183 
184         case rootInclude:
185             st = rootInclude;
186             if (type == LocationType.Root) {
187                 work_pool ~= val;
188             }
189             break;
190 
191         case symbolInclude:
192             if (type == LocationType.Root) {
193                 work_pool = [val]; // root override previous pool values
194                 st = rootInclude;
195             } else {
196                 work_pool ~= val;
197             }
198             break;
199 
200         case forceInclude:
201             // ignore
202             break;
203 
204         default:
205             assert(0);
206         }
207     }
208 
209     string toString() @safe const {
210         import std.exception : assumeUnique;
211 
212         char[] buf;
213         buf.reserve(100);
214         this.toString((const(char)[] s) { buf ~= s; });
215         auto trustedUnique(T)(T t) @trusted {
216             return assumeUnique(t);
217         }
218 
219         return trustedUnique(buf);
220     }
221 
222     void toString(Writer)(scope Writer w) const {
223         import std.algorithm : copy, joiner, map;
224         import std.ascii : newline;
225         import std.conv : to;
226         import std.range : chain, only;
227         import std.range.primitives : put;
228 
229         put(w, st.to!string());
230         put(w, newline);
231         // dfmt off
232         chain(work_pool.map!(a => cast(string) a),
233               permanent_pool[].map!(a => cast(string) a))
234             .joiner(newline)
235             .copy(w);
236         // dfmt on
237     }
238 }
239 
240 /// Use when no extra payload is needed.
241 alias TestDoubleIncludes = GenericTestDoubleIncludes!(DummyPayload);
242 
243 /** Strip the filename with the regexp or if that fails use the input filename as is.
244  */
245 string stripFile(string fname, Regex!char re) @trusted {
246     import std.array : appender;
247     import std.algorithm : copy;
248     import std.range : dropOne;
249     import std.regex : matchFirst;
250 
251     if (re.empty) {
252         return fname;
253     }
254 
255     auto c = matchFirst(fname, re);
256     auto rval = fname;
257 
258     debug logger.tracef("input is '%s'. After strip: %s", fname, c);
259 
260     if (!c.empty) {
261         auto app = appender!string();
262         c.dropOne.copy(app);
263         rval = app.data;
264     }
265 
266     return rval;
267 }
268 
269 /** Fixup the includes to be ready for usage as #include.
270  *
271  * Deduplicate.
272  * Strip the includes according to the user supplied configuration.
273  */
274 private string[] stripIncl(string[] incls, Regex!char re) {
275     import std.array : array;
276     import std.algorithm : cache, map, filter;
277     import cpptooling.utility : dedup;
278 
279     // dfmt off
280     auto r = dedup(incls)
281         .map!(a => stripFile(a, re))
282         .filter!(a => a.length > 0)
283         .array();
284     // dfmt on
285 
286     return r;
287 }