| 1 | //===-- InstrumentorConfigFile.cpp ----------------------------------------===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | // |
| 9 | // The implementation of the utilities for the Instrumentor JSON configuration |
| 10 | // file. |
| 11 | // |
| 12 | //===----------------------------------------------------------------------===// |
| 13 | |
| 14 | #include "llvm/Transforms/IPO/Instrumentor.h" |
| 15 | |
| 16 | #include "llvm/ADT/StringMap.h" |
| 17 | #include "llvm/ADT/StringRef.h" |
| 18 | #include "llvm/ADT/StringSet.h" |
| 19 | #include "llvm/IR/DiagnosticInfo.h" |
| 20 | #include "llvm/IR/LLVMContext.h" |
| 21 | #include "llvm/Support/ErrorHandling.h" |
| 22 | #include "llvm/Support/JSON.h" |
| 23 | #include "llvm/Support/MemoryBuffer.h" |
| 24 | #include "llvm/Support/Path.h" |
| 25 | #include "llvm/Support/StringSaver.h" |
| 26 | #include "llvm/Support/VirtualFileSystem.h" |
| 27 | |
| 28 | #include <string> |
| 29 | |
| 30 | using namespace llvm; |
| 31 | |
| 32 | static Expected<std::unique_ptr<MemoryBuffer>> |
| 33 | setupMemoryBuffer(const Twine &Filename, vfs::FileSystem &FS) { |
| 34 | auto BufferOrErr = Filename.str() == "-" ? MemoryBuffer::getSTDIN() |
| 35 | : FS.getBufferForFile(Name: Filename); |
| 36 | if (std::error_code EC = BufferOrErr.getError()) |
| 37 | return errorCodeToError(EC); |
| 38 | return std::move(BufferOrErr.get()); |
| 39 | } |
| 40 | |
| 41 | namespace llvm { |
| 42 | namespace instrumentor { |
| 43 | |
| 44 | void writeConfigToJSON(InstrumentationConfig &IConf, StringRef OutputFile, |
| 45 | LLVMContext &Ctx) { |
| 46 | if (OutputFile.empty()) |
| 47 | return; |
| 48 | |
| 49 | std::error_code EC; |
| 50 | raw_fd_stream OS(OutputFile, EC); |
| 51 | if (EC) { |
| 52 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 53 | Twine("failed to open instrumentor configuration file for writing: " ) + |
| 54 | EC.message(), |
| 55 | DS_Warning)); |
| 56 | return; |
| 57 | } |
| 58 | |
| 59 | json::OStream J(OS, 2); |
| 60 | J.objectBegin(); |
| 61 | |
| 62 | J.attributeBegin(Key: "configuration" ); |
| 63 | J.objectBegin(); |
| 64 | for (auto *BaseCO : IConf.BaseConfigurationOptions) { |
| 65 | switch (BaseCO->Kind) { |
| 66 | case BaseConfigurationOption::STRING: |
| 67 | J.attribute(Key: BaseCO->Name, Contents: BaseCO->getString()); |
| 68 | break; |
| 69 | case BaseConfigurationOption::BOOLEAN: |
| 70 | J.attribute(Key: BaseCO->Name, Contents: BaseCO->getBool()); |
| 71 | break; |
| 72 | case BaseConfigurationOption::STRING_LIST: |
| 73 | J.attributeArray(Key: BaseCO->Name, Contents: [&] { |
| 74 | for (StringRef Value : BaseCO->getStringList()) |
| 75 | J.value(V: Value); |
| 76 | }); |
| 77 | break; |
| 78 | } |
| 79 | if (!BaseCO->Description.empty()) |
| 80 | J.attribute(Key: std::string(BaseCO->Name) + ".description" , |
| 81 | Contents: BaseCO->Description); |
| 82 | } |
| 83 | J.objectEnd(); |
| 84 | J.attributeEnd(); |
| 85 | |
| 86 | for (unsigned KindVal = 0; KindVal <= InstrumentationLocation::Last; |
| 87 | ++KindVal) { |
| 88 | auto Kind = InstrumentationLocation::KindTy(KindVal); |
| 89 | |
| 90 | auto &KindChoices = IConf.IChoices[Kind]; |
| 91 | if (KindChoices.empty()) |
| 92 | continue; |
| 93 | |
| 94 | J.attributeBegin(Key: InstrumentationLocation::getKindStr(Kind)); |
| 95 | J.objectBegin(); |
| 96 | for (auto &[Name, Choice] : KindChoices) { |
| 97 | J.attributeBegin(Key: Name); |
| 98 | J.objectBegin(); |
| 99 | J.attribute(Key: "enabled" , Contents: Choice->Enabled); |
| 100 | J.attribute(Key: "filter" , Contents: Choice->Filter); |
| 101 | J.attribute(Key: "filter.description" , |
| 102 | Contents: "Static property filter to exclude instrumentation." ); |
| 103 | for (auto &ArgIt : Choice->IRTArgs) { |
| 104 | J.attribute(Key: ArgIt.Name, Contents: ArgIt.Enabled); |
| 105 | if ((ArgIt.Flags & IRTArg::REPLACABLE) || |
| 106 | (ArgIt.Flags & IRTArg::REPLACABLE_CUSTOM)) |
| 107 | J.attribute(Key: std::string(ArgIt.Name) + ".replace" , Contents: true); |
| 108 | if (!ArgIt.Description.empty()) |
| 109 | J.attribute(Key: std::string(ArgIt.Name) + ".description" , |
| 110 | Contents: ArgIt.Description); |
| 111 | } |
| 112 | J.objectEnd(); |
| 113 | J.attributeEnd(); |
| 114 | } |
| 115 | J.objectEnd(); |
| 116 | J.attributeEnd(); |
| 117 | } |
| 118 | |
| 119 | J.objectEnd(); |
| 120 | } |
| 121 | |
| 122 | template <typename Map> |
| 123 | static StringRef closestOption(const Map &Options, StringRef Missing) { |
| 124 | uint32_t MaxEdit = 5; |
| 125 | StringRef Closest; |
| 126 | for (const auto &Key : Options.keys()) { |
| 127 | auto EditDist = Missing.edit_distance_insensitive(Other: Key, AllowReplacements: true, MaxEditDistance: MaxEdit); |
| 128 | if (EditDist < MaxEdit) { |
| 129 | Closest = Key; |
| 130 | MaxEdit = EditDist; |
| 131 | } |
| 132 | } |
| 133 | return Closest; |
| 134 | } |
| 135 | |
| 136 | bool readConfigFromJSON(InstrumentationConfig &IConf, StringRef InputFile, |
| 137 | LLVMContext &Ctx, vfs::FileSystem &FS) { |
| 138 | if (InputFile.empty()) |
| 139 | return true; |
| 140 | |
| 141 | auto BufferOrErr = setupMemoryBuffer(Filename: InputFile, FS); |
| 142 | if (Error E = BufferOrErr.takeError()) { |
| 143 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 144 | Twine("failed to open instrumentor configuration file for reading: " ) + |
| 145 | toString(E: std::move(E)), |
| 146 | DS_Warning)); |
| 147 | return false; |
| 148 | } |
| 149 | auto Buffer = std::move(BufferOrErr.get()); |
| 150 | json::Path::Root NullRoot; |
| 151 | auto Parsed = json::parse(JSON: Buffer->getBuffer()); |
| 152 | if (!Parsed) { |
| 153 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 154 | Twine("failed to parse instrumentor configuration file: " ) + |
| 155 | toString(E: Parsed.takeError()), |
| 156 | DS_Warning)); |
| 157 | return false; |
| 158 | } |
| 159 | auto *Config = Parsed->getAsObject(); |
| 160 | if (!Config) { |
| 161 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 162 | "failed to parse instrumentor configuration file, expected an object " |
| 163 | "'{ ... }'" , |
| 164 | DS_Warning)); |
| 165 | return false; |
| 166 | } |
| 167 | |
| 168 | StringMap<BaseConfigurationOption *> BCOMap; |
| 169 | for (auto *BO : IConf.BaseConfigurationOptions) |
| 170 | BCOMap[BO->Name] = BO; |
| 171 | |
| 172 | SmallPtrSet<InstrumentationOpportunity *, 32> SeenIOs; |
| 173 | for (auto &It : *Config) { |
| 174 | auto *Obj = It.second.getAsObject(); |
| 175 | if (!Obj) { |
| 176 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 177 | "malformed JSON configuration, expected an object" , DS_Warning)); |
| 178 | continue; |
| 179 | } |
| 180 | if (It.first == "configuration" ) { |
| 181 | for (auto &ObjIt : *Obj) { |
| 182 | if (auto *BO = BCOMap.lookup(Key: ObjIt.first)) { |
| 183 | switch (BO->Kind) { |
| 184 | case BaseConfigurationOption::STRING: |
| 185 | if (auto V = ObjIt.second.getAsString()) { |
| 186 | BO->setString(IConf.SS.save(S: *V)); |
| 187 | } else { |
| 188 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 189 | Twine("configuration key '" ) + StringRef(ObjIt.first) + |
| 190 | Twine("' expects a string, value ignored" ), |
| 191 | DS_Warning)); |
| 192 | } |
| 193 | break; |
| 194 | case BaseConfigurationOption::BOOLEAN: |
| 195 | if (auto V = ObjIt.second.getAsBoolean()) |
| 196 | BO->setBool(*V); |
| 197 | else { |
| 198 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 199 | Twine("configuration key '" ) + StringRef(ObjIt.first) + |
| 200 | Twine("' expects a boolean, value ignored" ), |
| 201 | DS_Warning)); |
| 202 | } |
| 203 | break; |
| 204 | case BaseConfigurationOption::STRING_LIST: { |
| 205 | auto *Values = ObjIt.second.getAsArray(); |
| 206 | if (!Values) { |
| 207 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 208 | Twine("configuration key '" ) + StringRef(ObjIt.first) + |
| 209 | Twine("' expects an array of strings, value ignored" ), |
| 210 | DS_Warning)); |
| 211 | break; |
| 212 | } |
| 213 | |
| 214 | SmallVector<StringRef> Strings; |
| 215 | bool AllStrings = true; |
| 216 | for (const json::Value &Value : *Values) { |
| 217 | auto String = Value.getAsString(); |
| 218 | if (!String) { |
| 219 | AllStrings = false; |
| 220 | break; |
| 221 | } |
| 222 | Strings.push_back(Elt: IConf.SS.save(S: *String)); |
| 223 | } |
| 224 | if (AllStrings) |
| 225 | BO->setStringList(Strings); |
| 226 | else |
| 227 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 228 | Twine("configuration key '" ) + StringRef(ObjIt.first) + |
| 229 | Twine("' expects an array of strings, value ignored" ), |
| 230 | DS_Warning)); |
| 231 | break; |
| 232 | } |
| 233 | } |
| 234 | } else if (!StringRef(ObjIt.first).ends_with(Suffix: ".description" )) { |
| 235 | std::string Diag = "configuration key '" + ObjIt.first.str() + |
| 236 | "' not found and ignored" ; |
| 237 | StringRef Closest = closestOption(Options: BCOMap, Missing: ObjIt.first); |
| 238 | if (!Closest.empty()) |
| 239 | Diag += "; did you mean '" + Closest.str() + "'?" ; |
| 240 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation(Diag, DS_Warning)); |
| 241 | } |
| 242 | } |
| 243 | continue; |
| 244 | } |
| 245 | |
| 246 | auto &IChoiceMap = |
| 247 | IConf.IChoices[InstrumentationLocation::getKindFromStr(S: It.first)]; |
| 248 | for (auto &ObjIt : *Obj) { |
| 249 | auto *InnerObj = ObjIt.second.getAsObject(); |
| 250 | if (!InnerObj) { |
| 251 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 252 | "malformed JSON configuration, expected an object" , DS_Warning)); |
| 253 | continue; |
| 254 | } |
| 255 | auto *IO = IChoiceMap.lookup(Key: ObjIt.first); |
| 256 | if (!IO) { |
| 257 | std::string Diag = |
| 258 | "malformed JSON configuration, expected an object matching " |
| 259 | "an instrumentor choice, got '" + |
| 260 | ObjIt.first.str() + "'" ; |
| 261 | StringRef Closest = closestOption(Options: IChoiceMap, Missing: ObjIt.first); |
| 262 | if (!Closest.empty()) |
| 263 | Diag += "; did you mean '" + Closest.str() + "'?" ; |
| 264 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation(Diag, DS_Warning)); |
| 265 | continue; |
| 266 | } |
| 267 | SeenIOs.insert(Ptr: IO); |
| 268 | StringMap<bool> ValueMap, ReplaceMap; |
| 269 | StringRef FilterStr; |
| 270 | StringSet<> IOOpts; |
| 271 | IOOpts.insert(key: "enabled" ); |
| 272 | IOOpts.insert(key: "filter" ); |
| 273 | for (auto &IRArg : IO->IRTArgs) |
| 274 | IOOpts.insert(key: IRArg.Name); |
| 275 | for (auto &InnerObjIt : *InnerObj) { |
| 276 | auto Name = StringRef(InnerObjIt.first); |
| 277 | if (Name == "filter" ) { |
| 278 | if (auto V = InnerObjIt.second.getAsString()) |
| 279 | FilterStr = IConf.SS.save(S: *V); |
| 280 | } else if (Name.consume_back(Suffix: ".replace" )) { |
| 281 | ReplaceMap[Name] = InnerObjIt.second.getAsBoolean().value_or(u: false); |
| 282 | } else { |
| 283 | ValueMap[Name] = InnerObjIt.second.getAsBoolean().value_or(u: false); |
| 284 | } |
| 285 | if (!IOOpts.contains(key: Name)) { |
| 286 | std::string Diag = "unrecognized JSON property '" + Name.str() + |
| 287 | "' in configuration for '" + IO->getName().str() + |
| 288 | "'" ; |
| 289 | StringRef Closest = closestOption(Options: IOOpts, Missing: Name); |
| 290 | if (!Closest.empty()) |
| 291 | Diag += "; did you mean '" + Closest.str() + "'?" ; |
| 292 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation(Diag, DS_Warning)); |
| 293 | } |
| 294 | } |
| 295 | IO->Enabled = ValueMap["enabled" ]; |
| 296 | IO->Filter = FilterStr; |
| 297 | for (auto &IRArg : IO->IRTArgs) { |
| 298 | IRArg.Enabled = ValueMap[IRArg.Name]; |
| 299 | if (!ReplaceMap.lookup(Key: IRArg.Name)) { |
| 300 | IRArg.Flags &= ~IRTArg::REPLACABLE; |
| 301 | IRArg.Flags &= ~IRTArg::REPLACABLE_CUSTOM; |
| 302 | } |
| 303 | } |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | for (auto &IChoiceMap : IConf.IChoices) |
| 308 | for (auto &It : IChoiceMap) |
| 309 | if (!SeenIOs.count(Ptr: It.second)) |
| 310 | It.second->Enabled = false; |
| 311 | |
| 312 | return true; |
| 313 | } |
| 314 | |
| 315 | bool readConfigPathsFile(StringRef InputFile, cl::list<std::string> &Configs, |
| 316 | LLVMContext &Ctx, vfs::FileSystem &FS) { |
| 317 | if (InputFile.empty()) |
| 318 | return true; |
| 319 | |
| 320 | auto BufferOrErr = setupMemoryBuffer(Filename: InputFile, FS); |
| 321 | if (Error E = BufferOrErr.takeError()) { |
| 322 | Ctx.diagnose(DI: DiagnosticInfoInstrumentation( |
| 323 | Twine("failed to open instrumentor configuration paths file for " |
| 324 | "reading: " ) + |
| 325 | toString(E: std::move(E)), |
| 326 | DS_Warning)); |
| 327 | return false; |
| 328 | } |
| 329 | |
| 330 | StringRef InputFilePath(sys::path::parent_path(path: InputFile)); |
| 331 | |
| 332 | auto Buffer = std::move(BufferOrErr.get()); |
| 333 | StringRef Content = Buffer->getBuffer(); |
| 334 | StringRef EOL = Content.detectEOL(); |
| 335 | do { |
| 336 | auto [LHS, RHS] = Content.split(Separator: EOL); |
| 337 | std::string ConfigPath = LHS.trim().str(); |
| 338 | if (!sys::path::is_absolute(path: ConfigPath)) { |
| 339 | SmallString<128> InputFilePathStringVec(InputFilePath); |
| 340 | sys::path::append(path&: InputFilePathStringVec, a: ConfigPath); |
| 341 | ConfigPath = InputFilePathStringVec.c_str(); |
| 342 | } |
| 343 | Configs.push_back(value: ConfigPath); |
| 344 | Content = RHS.trim(); |
| 345 | } while (!Content.empty()); |
| 346 | |
| 347 | return true; |
| 348 | } |
| 349 | |
| 350 | } // end namespace instrumentor |
| 351 | } // end namespace llvm |
| 352 | |