1 /**
2 Copyright: Copyright (c) 2016-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 Analyze C/C++ source code to generate a GraphML of the relations.
11 */
12 module dextool.plugin.frontend.graphml;
13 
14 import std.typecons : Flag;
15 
16 import logger = std.experimental.logger;
17 
18 import dextool.compilation_db;
19 import dextool.type;
20 
21 import dextool.plugin.types;
22 import dextool.plugin.graphml.backend : Controller, Parameters, Products;
23 import dextool.plugin.graphml.frontend.argsparser;
24 
25 class GraphMLFrontend : Controller, Parameters, Products {
26     import std.typecons : Tuple;
27     import std.regex : regex, Regex;
28     import dextool.type : FileName, DirName;
29 
30     private {
31         static struct FileData {
32             FileName filename;
33             string data;
34         }
35 
36         static enum fileExt = ".graphml";
37 
38         immutable Flag!"genClassMethod" gen_class_method;
39         immutable Flag!"genClassParamDependency" gen_class_param_dep;
40         immutable Flag!"genClassInheritDependency" gen_class_inherit_dep;
41         immutable Flag!"genClassMemberDependency" gen_class_member_dep;
42 
43         immutable FilePrefix file_prefix;
44         immutable DirName output_dir;
45 
46         Regex!char[] exclude;
47         Regex!char[] restrict;
48     }
49 
50     immutable FileName toFile;
51 
52     /// Data produced by the generatore intented to be written to specified file.
53     FileData[] fileData;
54 
55     static auto makeVariant(ref RawConfiguration parsed) {
56         import std.algorithm : map;
57         import std.array : array;
58 
59         Regex!char[] exclude = parsed.fileExclude.map!(a => regex(a)).array();
60         Regex!char[] restrict = parsed.fileRestrict.map!(a => regex(a)).array();
61 
62         auto gen_class_method = cast(Flag!"genClassMethod") parsed.classMethod;
63         auto gen_class_param_dep = cast(Flag!"genClassParamDependency") parsed.classParamDep;
64         auto gen_class_inherit_dep = cast(Flag!"genClassInheritDependency") parsed.classInheritDep;
65         auto gen_class_member_dep = cast(Flag!"genClassMemberDependency") parsed.classMemberDep;
66 
67         auto variant = new GraphMLFrontend(FilePrefix(parsed.filePrefix), DirName(parsed.out_),
68                 gen_class_method, gen_class_param_dep, gen_class_inherit_dep,
69                 gen_class_member_dep);
70 
71         variant.exclude = exclude;
72         variant.restrict = restrict;
73 
74         return variant;
75     }
76 
77     this(FilePrefix file_prefix, DirName output_dir, Flag!"genClassMethod" class_method,
78             Flag!"genClassParamDependency" class_param_dep,
79             Flag!"genClassInheritDependency" class_inherit_dep,
80             Flag!"genClassMemberDependency" class_member_dep) {
81 
82         this.file_prefix = file_prefix;
83         this.output_dir = output_dir;
84         this.gen_class_method = class_method;
85         this.gen_class_param_dep = class_param_dep;
86         this.gen_class_inherit_dep = class_inherit_dep;
87         this.gen_class_member_dep = class_member_dep;
88 
89         import std.path : baseName, buildPath, relativePath, stripExtension;
90 
91         this.toFile = FileName(buildPath(cast(string) output_dir,
92                 cast(string) file_prefix ~ "raw" ~ fileExt));
93     }
94 
95     // -- Controller --
96     override bool doFile(const string filename) {
97         import dextool.plugin.regex_matchers : matchAny;
98 
99         bool restrict_pass = true;
100         bool exclude_pass = true;
101 
102         if (restrict.length > 0) {
103             restrict_pass = matchAny(filename, restrict);
104             debug {
105                 logger.tracef(!restrict_pass, "--file-restrict skipping %s", filename);
106             }
107         }
108 
109         if (exclude.length > 0) {
110             exclude_pass = !matchAny(filename, exclude);
111             debug {
112                 logger.tracef(!exclude_pass, "--file-exclude skipping %s", filename);
113             }
114         }
115 
116         return restrict_pass && exclude_pass;
117     }
118 }
119 
120 @safe struct XmlStream {
121     import dextool.type : FileName;
122     import std.stdio : File;
123 
124     private File fout;
125 
126     static auto make(FileName fname) {
127         auto fout = File(cast(string) fname, "w");
128         return XmlStream(fout);
129     }
130 
131     @disable this(this);
132 
133     void put(const(char)[] v) {
134         fout.write(v);
135     }
136 }
137 
138 unittest {
139     import std.range.primitives : isOutputRange;
140 
141     static assert(isOutputRange!(XmlStream, char), "Should be an output range");
142 }
143 
144 struct Lookup {
145     import cpptooling.data.symbol : Container, USRType;
146     import cpptooling.data : Location, LocationTag, TypeKind;
147 
148     private Container* container;
149 
150     auto kind(USRType usr) @safe {
151         return container.find!TypeKind(usr);
152     }
153 
154     auto location(USRType usr) @safe {
155         return container.find!LocationTag(usr);
156     }
157 }
158 
159 /// TODO cleaner split between frontend and backend is needed. Move most of the
160 /// logic to the backend and leave the error handling in the frontend. E.g. by
161 /// using callbacks.
162 ExitStatusType pluginMain(GraphMLFrontend variant, const string[] in_cflags,
163         CompileCommandDB compile_db, InFiles in_files, Flag!"skipFileError" skipFileError) {
164     import std.algorithm : map;
165     import std.conv : text;
166     import std.path : buildNormalizedPath, asAbsolutePath;
167     import std.range : enumerate;
168     import std.typecons : TypedefType, Yes, NullableRef;
169 
170     import cpptooling.analyzer.clang.context : ClangContext;
171     import cpptooling.data.symbol : Container;
172     import cpptooling.utility.virtualfilesystem : vfsFileName = FileName,
173         vfsMode = Mode;
174     import dextool.plugin.graphml.backend : GraphMLAnalyzer,
175         TransformToXmlStream;
176     import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile;
177 
178     const auto user_cflags = prependDefaultFlags(in_cflags, PreferLang.none);
179 
180     Container container;
181     auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
182 
183     auto xml_stream = XmlStream.make(variant.toFile);
184 
185     auto transform_to_file = new TransformToXmlStream!(XmlStream, Lookup)(xml_stream,
186             Lookup(&container));
187 
188     auto visitor = new GraphMLAnalyzer!(typeof(transform_to_file))(transform_to_file,
189             variant, variant, variant, container);
190 
191     ExitStatusType analyze(T, U)(ref T in_file, U idx, U total_files) {
192         logger.infof("File %d/%d ", idx + 1, total_files);
193         string[] use_cflags;
194         AbsolutePath abs_in_file;
195 
196         if (compile_db.length > 0) {
197             auto db_search_result = compile_db.appendOrError(user_cflags, in_file);
198             if (db_search_result.isNull) {
199                 return ExitStatusType.Errors;
200             }
201             use_cflags = db_search_result.get.cflags;
202             abs_in_file = db_search_result.get.absoluteFile;
203         } else {
204             use_cflags = user_cflags.dup;
205             abs_in_file = AbsolutePath(FileName(in_file));
206         }
207 
208         if (analyzeFile(abs_in_file, use_cflags, visitor, ctx) == ExitStatusType.Errors) {
209             return ExitStatusType.Errors;
210         }
211 
212         return ExitStatusType.Ok;
213     }
214 
215     ExitStatusType analyzeFiles(T)(ref T files, const size_t total_files, ref string[] skipped_files) {
216 
217         foreach (idx, file; files) {
218             auto status = analyze(file, idx, total_files);
219             if (status == ExitStatusType.Errors && skipFileError) {
220                 skipped_files ~= file;
221             } else if (status == ExitStatusType.Errors) {
222                 return ExitStatusType.Errors;
223             }
224         }
225 
226         return ExitStatusType.Ok;
227     }
228 
229     import dextool.plugin.graphml.backend : xmlHeader, xmlFooter;
230 
231     string[] skipped_files;
232     ExitStatusType exit_status;
233 
234     auto stream = NullableRef!XmlStream(&xml_stream);
235     xmlHeader(stream);
236     scope (success)
237         xmlFooter(stream);
238 
239     if (in_files.length == 0) {
240         auto range = compile_db.map!(a => a.absoluteFile).enumerate;
241         exit_status = analyzeFiles(range, compile_db.length, skipped_files);
242     } else {
243         auto range = cast(TypedefType!InFiles) in_files;
244         exit_status = analyzeFiles(range, in_files.length, skipped_files);
245     }
246 
247     transform_to_file.finalize();
248 
249     if (skipped_files.length != 0) {
250         logger.error("Skipped the following files due to errors:");
251         foreach (f; skipped_files) {
252             logger.error("  ", f);
253         }
254     }
255 
256     debug {
257         logger.trace(visitor);
258     }
259 
260     return exit_status;
261 }