diff --git a/uppsrc/ide/Linter/ClangTidy.cpp b/uppsrc/ide/Linter/ClangTidy.cpp new file mode 100644 index 000000000..a5a93e1ef --- /dev/null +++ b/uppsrc/ide/Linter/ClangTidy.cpp @@ -0,0 +1,173 @@ +#include "ClangTidy.h" + +static String sExeFilePath; + +String ClangTidy::GetConfigFilePath() const +{ + return ConfigFile(AppendFileName("clangtidy", IdeGetCurrentMainPackage() + "-clangtidy.json")); +} + +Value ClangTidy::LoadConfig() +{ + String path = GetConfigFilePath(); + if(!FileExists(path)) + return Null; + return ParseJSON(LoadFile(path)); +} + +void ClangTidy::SaveConfig(const Value& cfg) +{ + String path = GetConfigFilePath(); + RealizePath(path); + SaveChangedFile(path, cfg); +} + +bool ClangTidy::Exists() const +{ + static bool b = false; + ONCELOCK + { +#ifdef flagWIN32 + // FIXME: Check path. + constexpr const char *exe = "C:\\Program Files\\LLVM\\bin\\clang-tidy.exe"; + b = FileExists(exe); + if(b) sExeFilePath << "\"" << exe << "\""; +#else + b = Sys("which clang-tidy", sExeFilePath) == 0; + if(b) sExeFilePath = TrimRight(sExeFilePath); + else sExeFilePath.Clear(); +#endif + } + return b && TheIde(); +} + +void ClangTidy::Settings() +{ + ClangTidyConfigDlg dlg(*this); + dlg.Load(); + if(dlg.ExecuteOK()) + dlg.Save(); +} + +Vector ClangTidy::ResolveProject(const String& ccjpath) +{ + Vector files; + Value ccj = ParseJSON(LoadFile(ccjpath)); + for(int i = 0; i < ccj.GetCount(); i++) + files.Add(ccj[i]["file"]); + return files; +} + +Vector ClangTidy::ResolvePackage(const String& ccjpath, const Vector& paths) +{ + Vector files; + Value ccj = ParseJSON(LoadFile(ccjpath)); + for(int i = 0; i < ccj.GetCount(); i++) { + String file = ccj[i]["file"]; + // Length-check prevents prefix collisions (e.g. /upp/Core vs /upp/Core2) + for(const String& p : paths) { + int pl = p.GetLength(); + if(file.StartsWith(p) && (file.GetLength() == pl || file[pl] == '/' || file[pl] == '\\')) { + files.Add(file); + break; + } + } + } + return files; +} + +Vector ClangTidy::ResolveFiles(Scope sc, const String& ccjpath, const Vector& paths) +{ + bool hasccj = FileExists(ccjpath); + switch(sc) { + case Scope::File: + return clone(paths); + case Scope::Project: + return hasccj ? ResolveProject(ccjpath) : clone(paths); + case Scope::Package: + return hasccj ? ResolvePackage(ccjpath, paths) : clone(paths); + } + return clone(paths); +} + +String ClangTidy::MakeCmdLine(Scope sc, Vector& paths) +{ + Value v = LoadConfig()["ClangTidy"]; + + // Is there a better way to obtain compile_commands.json file? + String ccjdir = GetTempPath(); + String ccjpath = AppendFileName(ccjdir, "compile_commands.json"); + + if(MakeBuild *mb = dynamic_cast(TheIdeContext())) + mb->SaveCCJ(ccjpath, false); + + String path; + for(const String& f : ResolveFiles(sc, ccjpath, paths)) + path << "\"" << f << "\" "; + + if(IsNull(v)) + return sExeFilePath + " --checks=* " + path; + + String checks = Nvl(v["checks"], "*"); + String extraargs = Nvl(v["extra_args"], ""); + String standard = Nvl(v["standard"], "c++14"); + + String s; + s << sExeFilePath << " " + << "--checks=\"" << checks << "\" " + << "--quiet "; + + if(FileExists(ccjpath)) + s << "-p=\"" << ccjdir << "\" "; + + return s + extraargs + (extraargs.GetCount() ? " " : "") + path + + (FileExists(ccjpath) ? "" : "-- -std=" + standard); +} + +void ClangTidy::OnResults(const String& results) +{ + // FIXME: This is fragile, but not all versions of clang have JSON output... + RegExp r("([A-Za-z]?:?[/\\\\][^:]+\\.[ch]pp):(\\d+):(\\d+): (\\w+): (.+?)(\\s+\\[[\\w,.-]+\\])?$"); + + for(const String& line : Split(results, '\n', false)) { + if(!r.Match(line)) + continue; + + Vector v = r.GetStrings(); + if(v.GetCount() < 5) + continue; + + String severity = v.At(3); + if(severity == "note") + continue; + + Ide::ListLineInfo e; + e.file = v.At(0); + e.lineno = StrInt(v.At(1)); + e.linepos = StrInt(v.At(2)); + e.kind = severity == "error" ? 0 : 1; + e.message = v.At(4) + TrimLeft(v.At(5)); + + Image img = decode( + severity, + "error", LinterImg::error(), + "warning", LinterImg::warning(), + LinterImg::warning() + ); + Color paper = HighlightSetup::GetHlStyle(e.kind == 0 ? HighlightSetup::PAPER_ERROR + : HighlightSetup::PAPER_WARNING).color; + int linecy; + AttrText txt(TheIde()->FormatErrorLine(e.message, linecy)); + txt.NormalPaper(paper); + txt.SetImage(img); + + ArrayCtrl& error = TheIde()->error; + error.Add(e.file, e.lineno, txt, RawToValue(e)); + error.SetLineCy(error.GetCount() - 1, linecy); + } +} + +INITIALIZER(ClangTidy) +{ + RegisterLinterModule(Single("ClangTidy")); +} \ No newline at end of file diff --git a/uppsrc/ide/Linter/ClangTidy.h b/uppsrc/ide/Linter/ClangTidy.h new file mode 100644 index 000000000..4ae862736 --- /dev/null +++ b/uppsrc/ide/Linter/ClangTidy.h @@ -0,0 +1,39 @@ +#ifndef _ClangTidy_h_ +#define _ClangTidy_h_ + +#include "Linter.h" + +#define LAYOUTFILE +#include + +class ClangTidy : public Linter { +public: + ClangTidy(const String& name) : Linter(name) {} + + virtual String GetConfigFilePath() const final; + virtual Value LoadConfig() final; + virtual void SaveConfig(const Value& cfg) final; + virtual bool Exists() const final; + virtual void Settings() final; + +private: + Vector ResolveProject(const String& ccjpath); + Vector ResolvePackage(const String& ccjpath, const Vector& paths); + Vector ResolveFiles (Scope sc, const String& ccjpath, const Vector& paths); + + virtual String MakeCmdLine(Scope sc, Vector& paths) final; + virtual void OnResults(const String& results) final; +}; + +INITIALIZE(ClangTidy); + +class ClangTidyConfigDlg : public WithClangTidyConfigLayout, public Linter::Config { +public: + ClangTidyConfigDlg(Linter& l); + + virtual void Load() final; + virtual void Save() final; + virtual void Reset() final; +}; + +#endif \ No newline at end of file diff --git a/uppsrc/ide/Linter/ClangTidyConfig.cpp b/uppsrc/ide/Linter/ClangTidyConfig.cpp new file mode 100644 index 000000000..c4d553d57 --- /dev/null +++ b/uppsrc/ide/Linter/ClangTidyConfig.cpp @@ -0,0 +1,19 @@ +#include "Linter.h" + +ClangTidyConfigDlg::ClangTidyConfigDlg(Linter& l) +: Linter::Config(l) +{ +} + +void ClangTidyConfigDlg::Load() +{ +} + +void ClangTidyConfigDlg::Save() +{ +} + +void ClangTidyConfigDlg::Reset() +{ +} + diff --git a/uppsrc/ide/Linter/ClangTidyConfig.lay b/uppsrc/ide/Linter/ClangTidyConfig.lay new file mode 100644 index 000000000..146432689 --- /dev/null +++ b/uppsrc/ide/Linter/ClangTidyConfig.lay @@ -0,0 +1,3 @@ +LAYOUT(ClangTidyConfigLayout, 400, 200) +END_LAYOUT + diff --git a/uppsrc/ide/Linter/Copying b/uppsrc/ide/Linter/Copying new file mode 100644 index 000000000..74b31bec6 --- /dev/null +++ b/uppsrc/ide/Linter/Copying @@ -0,0 +1,22 @@ +Copyright (c) 1998, 2023, The U++ Project +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/uppsrc/ide/Linter/CppCheck.cpp b/uppsrc/ide/Linter/CppCheck.cpp new file mode 100644 index 000000000..374cab241 --- /dev/null +++ b/uppsrc/ide/Linter/CppCheck.cpp @@ -0,0 +1,167 @@ +#include "CppCheck.h" + +static String sExeFilePath; +static bool sVerboseMode = false; + +String CppCheck::GetConfigFilePath() const +{ + return ConfigFile(AppendFileName("cppcheck", IdeGetCurrentMainPackage() + "-cppcheck.json")); +} + +Value CppCheck::LoadConfig() +{ + String path = GetConfigFilePath(); + if(!FileExists(path)) + return Null; + return ParseJSON(LoadFile(path)); +} + +void CppCheck::SaveConfig(const Value& cfg) +{ + String path = GetConfigFilePath(); + RealizePath(path); + SaveChangedFile(path, cfg); +} + +bool CppCheck::Exists() const +{ + static bool b = false; + ONCELOCK { +#ifdef flagWIN32 + constexpr const char *exe = "C:\\Program Files\\CppCheck\\cppcheck.exe"; + b = FileExists(exe); + if(b) sExeFilePath << "\"" << exe << "\""; +#else + b = Sys("which cppcheck", sExeFilePath) == 0; + if(b) sExeFilePath = TrimRight(sExeFilePath); + else sExeFilePath.Clear(); +#endif + } + return b && TheIde(); +} + +void CppCheck::Settings() +{ + CppCheckConfigDlg dlg(static_cast(*this)); + dlg.Load(); + if(dlg.ExecuteOK()) + dlg.Save(); +} + +String CppCheck::MakeCmdLine(Scope sc, Vector& paths) +{ + String path; + for(const String& s : paths) + path << "\"" << s << "\" "; + + Value v = LoadConfig()["CppCheck"]; + if(IsNull(v)) + return sExeFilePath + + " --language=c++ --std=c++17 --platform=native" + " --enable=all --xml -i *.tpp " + path; + + int depth = v["depth"]; + int jobs = v["jobs"]; + String opts = v["cmdline_options"]; + + Vector severity; + for(const Value& q : v["severity"]) + if(!IsNull(q)) + severity.Add() = q; + + String s; + s << sExeFilePath << " " + << "--force " + << "--xml " + << "--language=" << Nvl(v["language"], "c++") << " " + << "--platform=" << Nvl(v["platform"], "native") << " " + << "--std=" << Nvl(v["standard"], "c++17") << " " + << "--max-ctu-depth=" << AsString(clamp(depth, 1, 6)) << " " + << "-j " << AsString(clamp(jobs, 1, 1024)) << " "; + + if(severity.GetCount()) + s << "--enable=" << Join(severity, ",", true) << " "; + + for(const String& q : v["libraries"]) + if(FileExists(q)) + s << "--library=\"" << q << "\" "; + + for(const String& q : v["addons"]) + if(FileExists(q)) + s << "--plugin=\"" << q << "\" "; + + sVerboseMode = v["verbose_mode"]; + + // Ensure clean spacing between user options and paths + return s + opts + (opts.GetCount() ? " " : "") + path; +} + +void CppCheck::OnResults(const String& results) +{ + DecodeXML(ParseXML(results)["results"]); +} + +void CppCheck::DecodeXML(const XmlNode& results) +{ + if(results.IsTag("results")) { + DecodeXML(results["errors"]); + } + else + if(results.IsTag("errors")) { + for(const XmlNode& node : results) { + if(!node.IsTag("error")) + continue; + + const XmlNode& loc = node["location"]; + String severity = node.Attr("severity"); + Ide::ListLineInfo e; + e.file = loc.Attr("file"); + e.lineno = StrInt(loc.Attr("line")); + e.linepos = StrInt(loc.Attr("column")); + e.kind = severity == "error" ? 0 : 1; + + Image img = decode( + severity, + "warning", LinterImg::warning(), + "style", LinterImg::style(), + "performance", LinterImg::performance(), + "portability", LinterImg::portability(), + "information", LinterImg::information(), + "unusedFunction", LinterImg::unusedfunction(), + "missingInclude", LinterImg::missinginclude(), + LinterImg::error() + ); + + Color paper = HighlightSetup::GetHlStyle(e.kind == 0 ? HighlightSetup::PAPER_ERROR + : HighlightSetup::PAPER_WARNING).color; + + e.message = node.Attr("msg"); + int linecy; + AttrText txt(TheIde()->FormatErrorLine(e.message, linecy)); + txt.NormalPaper(paper); + txt.SetImage(img); + + ArrayCtrl& error = TheIde()->error; + error.Add(e.file, e.lineno, txt, RawToValue(e)); + + ValueArray notes; + if(sVerboseMode) { + e.message = node.Attr("verbose"); + if(e.message.GetCount()) + notes.Add(RawToValue(e)); + } + if(node.Attr("inconclusive") == "true") { + e.message = "[Note that this is an inconclusive result!]"; + notes.Add(RawToValue(e)); + } + + error.Set(error.GetCount() - 1, "NOTES", notes); + error.SetLineCy(error.GetCount() - 1, linecy); + } + } +} + +INITIALIZER(CppCheck) +{ + RegisterLinterModule(Single("CppCheck")); +} \ No newline at end of file diff --git a/uppsrc/ide/Linter/CppCheck.h b/uppsrc/ide/Linter/CppCheck.h new file mode 100644 index 000000000..063a50111 --- /dev/null +++ b/uppsrc/ide/Linter/CppCheck.h @@ -0,0 +1,45 @@ +#ifndef _ide_Linter_CppCheck_h_ +#define _ide_Linter_CppCheck_h_ + +#include "Linter.h" + +#define LAYOUTFILE +#include + +class CppCheck final : public Linter { +public: + CppCheck(const String& name) : Linter(name) {} + + String GetConfigFilePath() const final; + Value LoadConfig() final; + void SaveConfig(const Value& cfg) final; + + bool Exists() const final; + void Settings() final; + +protected: + String MakeCmdLine(Scope sc, Vector& paths) final; + void OnResults(const String& results) final; + +private: + void DecodeXML(const XmlNode& results); +}; + +INITIALIZE(CppCheck); + +struct CppCheckConfigDlg final : Linter::Config, WithCppCheckConfigLayout { + CppCheckConfigDlg(Linter& l); + + void Load() final; + void Save() final; + void Reset() final; + + struct Pane : WithCppCheckConfigPaneLayout { + Pane(); + + void SetData(const Value& data) final; + Value GetData() const final; + void Load(const String& path, const String& ext); + } libs, addons; +}; +#endif diff --git a/uppsrc/ide/Linter/CppCheck.lay b/uppsrc/ide/Linter/CppCheck.lay new file mode 100644 index 000000000..764a95e82 --- /dev/null +++ b/uppsrc/ide/Linter/CppCheck.lay @@ -0,0 +1,20 @@ +LAYOUT(CppCheckSetupLayout, 436, 384) + ITEM(Upp::DropList, language, HCenterPosZ(64, 32).TopPosZ(20, 19)) + ITEM(Upp::DropList, standard, HCenterPosZ(64, 32).TopPosZ(44, 19)) + ITEM(Upp::DropList, platform, HCenterPosZ(64, 32).TopPosZ(68, 19)) + ITEM(Upp::DropList, depth, HCenterPosZ(64, 32).TopPosZ(92, 19)) + ITEM(Upp::EditInt, jobs, Min(1).Max(6).HCenterPosZ(64, 32).TopPosZ(120, 19)) + ITEM(Upp::Option, style, SetLabel(t_("Style")).HCenterPosZ(160, 0).TopPosZ(188, 16)) + ITEM(Upp::Option, performance, SetLabel(t_("Performance")).HCenterPosZ(160, 0).TopPosZ(208, 16)) + ITEM(Upp::Option, portability, SetLabel(t_("Portability")).HCenterPosZ(160, 0).TopPosZ(228, 16)) + ITEM(Upp::Option, information, SetLabel(t_("Information")).HCenterPosZ(160, 0).TopPosZ(248, 16)) + ITEM(Upp::Option, unusedfunction, SetLabel(t_("Unused functions")).HCenterPosZ(160, 0).TopPosZ(268, 16)) + ITEM(Upp::Option, missinginclude, SetLabel(t_("Missing Includes")).HCenterPosZ(160, 0).TopPosZ(288, 16)) + ITEM(Upp::LabelBox, dv___11, SetLabel(t_("Additional Checks")).SetFont(Serif()).HCenterPosZ(188, 0).TopPosZ(160, 160)) + ITEM(Upp::Label, dv___12, SetLabel(t_("Threads")).HCenterPosZ(56, -36).TopPosZ(120, 19)) + ITEM(Upp::Label, dv___13, SetLabel(t_("Language")).HCenterPosZ(56, -36).TopPosZ(20, 19)) + ITEM(Upp::Label, dv___14, SetLabel(t_("Standard")).HCenterPosZ(56, -36).TopPosZ(44, 19)) + ITEM(Upp::Label, dv___15, SetLabel(t_("Depth")).HCenterPosZ(56, -36).TopPosZ(92, 19)) + ITEM(Upp::Label, dv___16, SetLabel(t_("Platform")).HCenterPosZ(56, -36).TopPosZ(68, 19)) +END_LAYOUT + diff --git a/uppsrc/ide/Linter/CppCheckConfig.cpp b/uppsrc/ide/Linter/CppCheckConfig.cpp new file mode 100644 index 000000000..10c933b2e --- /dev/null +++ b/uppsrc/ide/Linter/CppCheckConfig.cpp @@ -0,0 +1,205 @@ +#include "CppCheck.h" + +CppCheckConfigDlg::CppCheckConfigDlg(Linter& l) +: Linter::Config(l) +{ + CtrlLayoutOKCancel(*this, "CppCheck Settings"); + + for(const Value& v : { "c", "c++"}) + language.Add(v); + + for(const Value& v : { "c89", "c99", "c11", "c++03", "c++11", "c++14", "c++17", "c++20"}) + standard.Add(v); + + for(const Value& v : { "native", "unix32", "unix64", "win32A", "win32W", "win64"}) + platform.Add(v); + + for(int i = 0; i < 6; i++) + depth.Add(i); + + CtrlLayout(libs); + CtrlLayout(addons); + + libs.dirpath.WhenAction = [this] { libs.Load(SelectDirectory(), "*.cfg"); }; + addons.dirpath.WhenAction = [this] { addons.Load(SelectDirectory(), "*.py"); }; + + tabs.Add(libs.SizePos(), "Libraries"); + tabs.Add(addons.SizePos(), "Addons"); + + defaults.WhenAction = [this] { Reset(); }; + + Reset(); +} + +void CppCheckConfigDlg::Reset() +{ + language.SetIndex(1); + standard.SetIndex(5); + platform.SetIndex(0); + depth.SetIndex(1); + jobs.MinMax(1, CPU_Cores()) <<= CPU_Cores(); + warning = false; + style = false; + performance = false; + portability = false; + information = false; + unusedfunction = false; + missinginclude = false; + verbose = false; + options <<= "-isrc.tpp -isrcdoc.tpp"; + +#ifdef flagWIN32 + constexpr const char *deflibrarypath = "C:\\Program Files\\CppCheck\\cfg"; + constexpr const char *defpluginspath = "C:\\Program Files\\CppCheck\\addons"; +#else + constexpr const char *deflibrarypath = "/usr/share/cppcheck/cfg"; + constexpr const char *defpluginspath = "/usr/share/cppcheck/addons"; +#endif + + libs.Load(deflibrarypath, "*.cfg"); + addons.Load(defpluginspath, "*.py"); + + Title("CppCheck Configuration [" + IdeGetCurrentMainPackage() + "]"); +} + +void CppCheckConfigDlg::Load() +{ + Reset(); + + try + { + Value v = linter.LoadConfig()["CppCheck"]; + if(IsNull(v)) + return; + + auto LoadList = [this, &v](DropList& lst, const String& id, const Value& def) + { + int i = lst.FindValue(v[id]); + lst.SetIndex(i >= 0 ? i : lst.FindValue(def)); + }; + + LoadList(language, "language", "c++"); + LoadList(standard, "standard", "c++14"); + LoadList(platform, "platform", "native"); + LoadList(depth, "depth", 2); + + jobs <<= clamp((int) v["jobs"], 1, INT_MAX); + + options <<= v["cmdline_options"]; + + for(const Value& q : v["severity"]) { + if(q == "warning") + warning = true; + else + if(q == "style") + style = true; + else + if(q == "performance") + performance = true; + else + if(q == "portability") + portability = true; + else + if(q == "information") + information = true; + else + if(q == "unusedFunction") + unusedfunction = true; + else + if(q == "missingInclude") // FIXED TYPO: Was "missingInglude" + missinginclude = true; + } + + libs <<= v["libraries"]; + addons <<= v["addons"]; + + verbose <<= v["verbose_mode"]; + } + catch(...) + { + Reset(); + } +} + +void CppCheckConfigDlg::Save() +{ + JsonArray jl, jq; + for(const String& s : ~libs) jl << s; + for(const String& s : ~addons) jq << s; + + JsonArray ja; + + if(~warning) ja << "warning"; + if(~style) ja << "style"; + if(~performance) ja << "performance"; + if(~portability) ja << "portability"; + if(~information) ja << "information"; + if(~unusedfunction) ja << "unusedFunction"; + if(~missinginclude) ja << "missingInclude"; + + Json j; + + j("language", language.GetValue()); + j("standard", standard.GetValue()); + j("platform", platform.GetValue()); + j("depth", depth.GetValue()); + j("jobs", ~jobs); + j("severity", ja); + j("libraries", jl); + j("addons", jq); + j("cmdline_options", ~options); + j("verbose_mode", ~verbose); + + linter.SaveConfig(Json("CppCheck", j).ToString()); +} + +CppCheckConfigDlg::Pane::Pane() +{ + struct NameDisplay : Display + { + void Paint(Draw& w, const Rect& r, const Value& q, Color ink, Color paper, dword style) const override + { + StdDisplay().Paint(w, r, GetFileTitle(q.To()), ink, paper, style); + }; + }; + + list.AddColumn("Enable").Ctrls