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 }