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         }
94         catch (ConvException e) {
95             logger.error(e.msg);
96             logger.errorf("%s possible values: %(%s|%)", Config_YesNo.stringof,
97                     [EnumMembers!Config_YesNo]);
98             help = true;
99         }
100         catch (std.getopt.GetOptException ex) {
101             logger.error(ex.msg);
102             help = true;
103         }
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(FileName(config));
116             if (xmlConfig.isNull) {
117                 help = true;
118             }
119         }
120 
121         import std.algorithm : find;
122         import std.array : array;
123         import std.range : drop;
124 
125         // at this point args contain "what is left". What is interesting then is those after "--".
126         cflags = args.find("--").drop(1).array();
127     }
128 
129     void printHelp() {
130         import std.stdio : writeln;
131 
132         defaultGetoptPrinter("Usage: dextool cpptestdouble [options] [--in=] [-- CFLAGS...]",
133                 help_info.options);
134 
135         writeln("
136 REGEX
137 The regex syntax is found at http://dlang.org/phobos/std_regex.html
138 
139 Information about --strip-incl.
140   Default regexp is: .*/(.*)
141 
142   To allow the user to selectively extract parts of the include path dextool
143   applies the regex and then concatenates all the matcher groups found.  It is
144   turned into the replacement include path.
145 
146   Important to remember then is that this approach requires that at least one
147   matcher group exists.
148 
149 Information about --file-exclude.
150   The regex must fully match the filename the AST node is located in.
151   If it matches all data from the file is excluded from the generated code.
152 
153 Information about --file-restrict.
154   The regex must fully match the filename the AST node is located in.
155   Only symbols from files matching the restrict affect the generated test double.
156 
157 Information about --in.
158   When it is used in conjuction with a compile commands database it is used to also find the flags.
159   For each entry in the database the argument to --in is matched against the file or output JSON attribute.
160   If either of them match the compiler flags for that file are used.
161   The argument can be either the absolute path, the exact file or output attribute or a file glob pattern.
162   A glob pattern is such as `ls *.cpp`.
163 ");
164     }
165 }
166 
167 /** Extracted configuration data from an XML file.
168  *
169  * It is not inteded to be used as is but rather further processed.
170  */
171 struct XmlConfig {
172     import dextool.type : DextoolVersion, RawCliArguments, FilterClangFlag;
173 
174     DextoolVersion version_;
175     int skipCompilerArgs;
176     RawCliArguments command;
177     FilterClangFlag[] filterClangFlags;
178 }
179 
180 auto parseRawConfig(T)(T xml) @trusted {
181     import std.conv : to, ConvException;
182     import std.xml;
183     import dextool.utility : DextoolVersion;
184     import dextool.type : RawCliArguments, FilterClangFlag;
185 
186     DextoolVersion version_;
187     int skip_flags = 1;
188     RawCliArguments command;
189     FilterClangFlag[] filter_clang_flags;
190 
191     if (auto tag = "version" in xml.tag.attr) {
192         version_ = *tag;
193     }
194 
195     // dfmt off
196     xml.onStartTag["compiler_flag_filter"] = (ElementParser filter_flags) {
197         if (auto tag = "skip_compiler_args" in xml.tag.attr) {
198             try {
199                 skip_flags = (*tag).to!int;
200             }
201             catch (ConvException ex) {
202                 logger.info(ex.msg);
203                 logger.info("   using fallback '1'");
204             }
205         }
206 
207         xml.onEndTag["exclude"] = (const Element e) { filter_clang_flags ~= FilterClangFlag(e.text()); };
208     };
209     // dfmt on
210     xml.parse();
211 
212     return XmlConfig(version_, skip_flags, command, filter_clang_flags);
213 }
214 
215 alias readRawConfig = dextool.xml.readRawConfig!(XmlConfig, parseRawConfig);