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