1 /**
2 Copyright: Copyright (c) 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 dextool.plugin.cpptestdouble.frontend.raw_args;
11 
12 import std.typecons : Nullable;
13 
14 import logger = std.experimental.logger;
15 
16 static import dextool.xml;
17 
18 /// Represent a yes/no configuration option.
19 /// Using an explicit name so the help text is improved in such a way that the
20 /// user understand that the choices are between yes/no.
21 enum Config_YesNo {
22     no,
23     yes
24 }
25 
26 struct RawConfiguration {
27     import std.conv : ConvException;
28     import std.getopt : GetoptResult, getopt, defaultGetoptPrinter;
29     import dextool.type : Path;
30 
31     Nullable!XmlConfig xmlConfig;
32 
33     Config_YesNo gtestPODPrettyPrint = Config_YesNo.yes;
34     Path[] inFiles;
35     bool doFreeFuncs;
36     bool genPostInclude;
37     bool generatePreInclude;
38     bool gmock;
39     bool help;
40     bool shortPluginHelp;
41     string config;
42     string header;
43     string headerFile;
44     string mainFileName = "test_double";
45     string mainName = "TestDouble";
46     string out_;
47     string prefix = "Test_";
48     string stripInclude;
49     string systemCompiler;
50     string[] cflags;
51     string[] compileDb;
52     string[] fileExclude;
53     string[] fileInclude;
54     string[] testDoubleInclude;
55 
56     string[] originalFlags;
57 
58     private GetoptResult help_info;
59 
60     void parse(string[] args) {
61         import std.traits : EnumMembers;
62         import std.format : format;
63 
64         static import std.getopt;
65 
66         originalFlags = args.dup;
67         string[] input;
68 
69         try {
70             // dfmt off
71             // sort alphabetic
72             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
73                    "compile-db", "Retrieve compilation parameters from the file", &compileDb,
74                    "config", "Use configuration file", &config,
75                    "file-exclude", "Exclude files from generation matching the regex", &fileExclude,
76                    "file-include", "Include the scope of the test double to those files matching the regex.", &fileInclude,
77                    "free-func", "Generate test doubles of free functions", &doFreeFuncs,
78                    "gen-post-incl", "Generate a post include header file if it doesn't exist and use it", &genPostInclude,
79                    "gen-pre-incl", "Generate a pre include header file if it doesn't exist and use it", &generatePreInclude,
80                    "gmock", "Generate a gmock implementation of test double interface", &gmock,
81                    "gtest-pp", "Generate pretty printer of POD's public members for gtest " ~ format("[%(%s|%)]", [EnumMembers!Config_YesNo]), &gtestPODPrettyPrint,
82                    "header", "Prepends generated files with the string", &header,
83                    "header-file", "Prepend generated files with the header read from the file", &headerFile,
84                    "in", "Input file to parse (at least one)", &input,
85                    "main", "Used as part of interface, namespace etc [default: TestDouble]", &mainName,
86                    "main-fname", "Used as part of filename for generated files [default: test_double]", &mainFileName,
87                    "out", "directory for generated files [default: ./]", &out_,
88                    "prefix", "Prefix used when generating test artifacts [default: Test_]", &prefix,
89                    "short-plugin-help", "short description of the plugin",  &shortPluginHelp,
90                    "strip-incl", "A regex used to strip the include paths", &stripInclude,
91                    "system-compiler", "Derive the system include paths from this compiler [default: use from compile_commands.json]", &systemCompiler,
92                    "td-include", "User supplied includes used instead of those found", &testDoubleInclude,
93                    );
94             // dfmt on
95             help = help_info.helpWanted;
96         } catch (ConvException e) {
97             logger.error(e.msg);
98             logger.errorf("%s possible values: %(%s|%)", Config_YesNo.stringof,
99                     [EnumMembers!Config_YesNo]);
100             help = true;
101         } catch (std.getopt.GetOptException ex) {
102             logger.error(ex.msg);
103             help = true;
104         } catch (Exception ex) {
105             logger.error(ex.msg);
106             help = true;
107         }
108 
109         // default arguments
110         if (stripInclude.length == 0) {
111             stripInclude = r".*/(.*)";
112         }
113 
114         if (config.length != 0) {
115             xmlConfig = readRawConfig(Path(config));
116             if (xmlConfig.isNull) {
117                 help = true;
118             }
119         }
120 
121         import std.algorithm : find, map;
122         import std.array : array;
123         import std.range : drop;
124 
125         inFiles = input.map!(a => Path(a)).array;
126 
127         // at this point args contain "what is left". What is interesting then is those after "--".
128         cflags = args.find("--").drop(1).array();
129     }
130 
131     void printHelp() {
132         import std.stdio : writeln;
133 
134         defaultGetoptPrinter("Usage: dextool cpptestdouble [options] [--in=] [-- CFLAGS...]",
135                 help_info.options);
136 
137         writeln("
138 REGEX
139 The regex syntax is found at http://dlang.org/phobos/std_regex.html
140 
141 Information about --strip-incl.
142   Default regexp is: .*/(.*)
143 
144   To allow the user to selectively extract parts of the include path dextool
145   applies the regex and then concatenates all the matcher groups found.  It is
146   turned into the replacement include path.
147 
148   Important to remember then is that this approach requires that at least one
149   matcher group exists.
150 
151 Information about --file-exclude.
152   The regex must fully match the filename the AST node is located in.
153   If it matches all data from the file is excluded from the generated code.
154 
155 Information about --file-include.
156   The regex must fully match the filename the AST node is located in.
157   Only symbols from files matching the include affect the generated test double.
158 
159 Information about --in.
160   When it is used in conjuction with a compile commands database it is used to also find the flags.
161   For each entry in the database the argument to --in is matched against the file or output JSON attribute.
162   If either of them match the compiler flags for that file are used.
163   The argument can be either the absolute path, the exact file or output attribute or a file glob pattern.
164   A glob pattern is such as `ls *.cpp`.
165 ");
166     }
167 }
168 
169 /** Extracted configuration data from an XML file.
170  *
171  * It is not inteded to be used as is but rather further processed.
172  */
173 struct XmlConfig {
174     import dextool.type : DextoolVersion, FilterClangFlag;
175 
176     DextoolVersion version_;
177     int skipCompilerArgs;
178     RawCliArguments command;
179     FilterClangFlag[] filterClangFlags;
180 }
181 
182 auto parseRawConfig(T)(T xml) @trusted {
183     import std.conv : to, ConvException;
184     import std.xml;
185     import dextool.utility : DextoolVersion;
186     import dextool.type : FilterClangFlag;
187 
188     DextoolVersion version_;
189     int skip_flags = 1;
190     RawCliArguments command;
191     FilterClangFlag[] filter_clang_flags;
192 
193     if (auto tag = "version" in xml.tag.attr) {
194         version_ = DextoolVersion(*tag);
195     }
196 
197     // dfmt off
198     xml.onStartTag["compiler_flag_filter"] = (ElementParser filter_flags) {
199         if (auto tag = "skip_compiler_args" in xml.tag.attr) {
200             try {
201                 skip_flags = (*tag).to!int;
202             }
203             catch (ConvException ex) {
204                 logger.info(ex.msg);
205                 logger.info("   using fallback '1'");
206             }
207         }
208 
209         xml.onEndTag["exclude"] = (const Element e) { filter_clang_flags ~= FilterClangFlag(e.text()); };
210     };
211     // dfmt on
212     xml.parse();
213 
214     return XmlConfig(version_, skip_flags, command, filter_clang_flags);
215 }
216 
217 alias readRawConfig = dextool.xml.readRawConfig!(XmlConfig, parseRawConfig);
218 
219 /// The raw arguments from the command line.
220 struct RawCliArguments {
221     string[] payload;
222     alias payload this;
223 }