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     body {
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     body {
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     body {
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     body {
146         import std.algorithm : each;
147 
148         if (st == State.forceInclude)
149             return;
150 
151         st = State.waiting;
152 
153         // no paths added, nothing to do
154         if (work_pool.length == 0)
155             return;
156 
157         () @trusted{
158             stripIncl(work_pool, strip_incl).each!(a => permanent_pool.insert(a));
159         }();
160         work_pool.length = 0;
161     }
162 
163     void put(string fname, LocationType type, PayloadT kind = PayloadT.init) @safe
164     in {
165         import std.utf : validate;
166 
167         validate((cast(string) fname));
168     }
169     body {
170         auto val = Include(fname, kind);
171 
172         switch (st) with (State) {
173         case waiting:
174             work_pool ~= val;
175             if (type == LocationType.Root) {
176                 st = rootInclude;
177             } else {
178                 st = symbolInclude;
179             }
180             break;
181 
182         case rootInclude:
183             st = rootInclude;
184             if (type == LocationType.Root) {
185                 work_pool ~= val;
186             }
187             break;
188 
189         case symbolInclude:
190             if (type == LocationType.Root) {
191                 work_pool = [val]; // root override previous pool values
192                 st = rootInclude;
193             } else {
194                 work_pool ~= val;
195             }
196             break;
197 
198         case forceInclude:
199             // ignore
200             break;
201 
202         default:
203             assert(0);
204         }
205     }
206 
207     string toString() @safe const {
208         import std.exception : assumeUnique;
209 
210         char[] buf;
211         buf.reserve(100);
212         this.toString((const(char)[] s) { buf ~= s; });
213         auto trustedUnique(T)(T t) @trusted {
214             return assumeUnique(t);
215         }
216 
217         return trustedUnique(buf);
218     }
219 
220     void toString(Writer)(scope Writer w) const {
221         import std.algorithm : copy, joiner, map;
222         import std.ascii : newline;
223         import std.conv : to;
224         import std.range : chain, only;
225         import std.range.primitives : put;
226 
227         put(w, st.to!string());
228         put(w, newline);
229         // dfmt off
230         chain(work_pool.map!(a => cast(string) a),
231               permanent_pool[].map!(a => cast(string) a))
232             .joiner(newline)
233             .copy(w);
234         // dfmt on
235     }
236 }
237 
238 /// Use when no extra payload is needed.
239 alias TestDoubleIncludes = GenericTestDoubleIncludes!(DummyPayload);
240 
241 /** Strip the filename with the regexp or if that fails use the input filename as is.
242  */
243 T stripFile(T)(T fname, Regex!char re) @trusted {
244     import std.array : appender;
245     import std.algorithm : copy;
246     import std.range : dropOne;
247     import std.regex : matchFirst;
248 
249     if (re.empty) {
250         return fname;
251     }
252 
253     auto c = matchFirst(cast(string) fname, re);
254     auto rval = fname;
255 
256     debug logger.tracef("input is '%s'. After strip: %s", fname, c);
257 
258     if (!c.empty) {
259         auto app = appender!string();
260         c.dropOne.copy(app);
261         rval = app.data;
262     }
263 
264     return rval;
265 }
266 
267 /** Fixup the includes to be ready for usage as #include.
268  *
269  * Deduplicate.
270  * Strip the includes according to the user supplied configuration.
271  */
272 private T[] stripIncl(T)(ref T[] incls, Regex!char re) {
273     import std.array : array;
274     import std.algorithm : cache, map, filter;
275     import cpptooling.utility : dedup;
276 
277     // dfmt off
278     auto r = dedup(incls)
279         .map!(a => stripFile(a, re))
280         .filter!(a => a.length > 0)
281         .array();
282     // dfmt on
283 
284     return r;
285 }