diff --git a/README.md b/README.md index 94d22e1..f40319a 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,46 @@ void main(string[] args) } ``` +## Linting + +Use + +```sh +dscanner lint source/ +``` + +to view a human readable list of issues. + +For a CLI / tool parsable output use either + +```sh +dscanner -S source/ +# or +dscanner --report source/ +``` + +You can also specify custom formats using `-f` / `--errorFormat`, where there +are also built-in formats for GitHub Actions: + +```sh +# for GitHub actions: (automatically adds annotations to files in PRs) +dscanner -S -f github source/ +# custom format: +dscanner -S -f '{filepath}({line}:{column})[{type}]: {message}' source/ +``` + +Diagnostic types can be enabled/disabled using a configuration file, check out +the `--config` argument / `dscanner.ini` file for more info. Tip: some IDEs that +integrate D-Scanner may have helpers to configure the diagnostics or help +generate the dscanner.ini file. + + +## Other features + ### Token Count The "--tokenCount" or "-t" option prints the number of tokens in the given file diff --git a/src/dscanner/analysis/run.d b/src/dscanner/analysis/run.d index 11b2fca..dec4215 100644 --- a/src/dscanner/analysis/run.d +++ b/src/dscanner/analysis/run.d @@ -106,28 +106,115 @@ string[string] errorFormatMap() static string[string] ret; if (ret is null) ret = [ - "github": "::{type2} file={filepath},line={line},endLine={endLine},col={column},endColumn={endColumn},title={Type2} ({name})::{message}" + "github": "::{type2} file={filepath},line={line},endLine={endLine},col={column},endColumn={endColumn},title={Type2} ({name})::{message}", + "pretty": "\x1B[1m{filepath}({line}:{column}): {Type2}: \x1B[0m{message} \x1B[2m({name})\x1B[0m{context}{supplemental}" ]; return ret; } -void messageFunctionFormat(string format, Message message, bool isError) +private string formatBase(string format, Message.Diagnostic diagnostic, scope const(ubyte)[] code, bool color) { auto s = format; + s = s.replace("{filepath}", diagnostic.fileName); + s = s.replace("{line}", to!string(diagnostic.startLine)); + s = s.replace("{column}", to!string(diagnostic.startColumn)); + s = s.replace("{endLine}", to!string(diagnostic.endLine)); + s = s.replace("{endColumn}", to!string(diagnostic.endColumn)); + s = s.replace("{message}", diagnostic.message); + s = s.replace("{context}", diagnostic.formatContext(cast(const(char)[]) code, color)); + return s; +} - s = s.replace("{filepath}", message.fileName); - s = s.replace("{line}", to!string(message.startLine)); - s = s.replace("{column}", to!string(message.startColumn)); - s = s.replace("{endLine}", to!string(message.endLine)); - s = s.replace("{endColumn}", to!string(message.endColumn)); - s = s.replace("{type}", isError ? "error" : "warn"); - s = s.replace("{Type}", isError ? "Error" : "Warn"); - s = s.replace("{TYPE}", isError ? "ERROR" : "WARN"); - s = s.replace("{type2}", isError ? "error" : "warning"); - s = s.replace("{Type2}", isError ? "Error" : "Warning"); - s = s.replace("{TYPE2}", isError ? "ERROR" : "WARNING"); - s = s.replace("{message}", message.message); - s = s.replace("{name}", message.checkName); +private string formatContext(Message.Diagnostic diagnostic, scope const(char)[] code, bool color) +{ + import std.string : indexOf, lastIndexOf; + + if (diagnostic.startIndex >= diagnostic.endIndex || diagnostic.endIndex > code.length + || diagnostic.startColumn >= diagnostic.endColumn || diagnostic.endColumn == 0) + return null; + + auto lineStart = code.lastIndexOf('\n', diagnostic.startIndex) + 1; + auto lineEnd = code.indexOf('\n', diagnostic.endIndex); + if (lineEnd == -1) + lineEnd = code.length; + + auto ret = appender!string; + ret.reserve((lineEnd - lineStart) + diagnostic.endColumn + (color ? 30 : 10)); + ret ~= '\n'; + if (color) + ret ~= "\x1B[m"; // reset + ret ~= code[lineStart .. lineEnd].replace('\t', ' '); + ret ~= '\n'; + if (color) + ret ~= "\x1B[0;33m"; // reset, yellow + foreach (_; 0 .. diagnostic.startColumn - 1) + ret ~= ' '; + foreach (_; 0 .. diagnostic.endColumn - diagnostic.startColumn) + ret ~= '^'; + if (color) + ret ~= "\x1B[m"; // reset + return ret.data; +} + +version (Windows) +void enableColoredOutput() +{ + import core.sys.windows.windows; + + // Set output mode to handle virtual terminal sequences + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hOut == INVALID_HANDLE_VALUE) + return; + + DWORD dwMode; + if (!GetConsoleMode(hOut, &dwMode)) + return; + + dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if (!SetConsoleMode(hOut, dwMode)) + return; +} + +void messageFunctionFormat(string format, Message message, bool isError, scope const(ubyte)[] code = null) +{ + bool color = format.canFind("\x1B["); + if (color) + { + version (Windows) + enableColoredOutput(); + } + + auto s = format.formatBase(message.diagnostic, code, color); + + string formatType(string s, string type, string colorCode) + { + import std.ascii : toUpper; + import std.string : representation; + + string upperFirst(string s) { return s[0].toUpper ~ s[1 .. $]; } + string upper(string s) { return s.representation.map!(a => toUpper(cast(char) a)).array; } + + string type2 = type; + if (type2 == "warn") + type2 = "warning"; + + s = s.replace("{type}", color ? (colorCode ~ type ~ "\x1B[m") : type); + s = s.replace("{Type}", color ? (colorCode ~ upperFirst(type) ~ "\x1B[m") : upperFirst(type)); + s = s.replace("{TYPE}", color ? (colorCode ~ upper(type) ~ "\x1B[m") : upper(type)); + s = s.replace("{type2}", color ? (colorCode ~ type2 ~ "\x1B[m") : type2); + s = s.replace("{Type2}", color ? (colorCode ~ upperFirst(type2) ~ "\x1B[m") : upperFirst(type2)); + s = s.replace("{TYPE2}", color ? (colorCode ~ upper(type2) ~ "\x1B[m") : upper(type2)); + + return s; + } + + s = formatType(s, isError ? "error" : "warn", isError ? "\x1B[31m" : "\x1B[33m"); + s = s.replace("{name}", message.checkName); + s = s.replace("{supplemental}", message.supplemental.map!(a => "\n\t" + ~ formatType(format.formatBase(a, code, color), "hint", "\x1B[35m") + .replace("{name}", "").replace("{supplemental}", "") + .replace("\n", "\n\t")) + .join()); writefln("%s", s); } @@ -303,7 +390,7 @@ bool analyze(string[] fileNames, const StaticAnalysisConfig config, string error foreach (result; results[]) { hasErrors = true; - messageFunctionFormat(errorFormat, result, false); + messageFunctionFormat(errorFormat, result, false, code); } } return hasErrors; @@ -334,7 +421,7 @@ const(Module) parseModule(string fileName, ubyte[] code, RollbackAllocator* p, // TODO: proper index and column ranges return messageFunctionFormat(errorFormat, Message(Message.Diagnostic.from(fileName, [0, 0], line, [column, column], message), "dscanner.syntax"), - isError); + isError, code); }; return parseModule(fileName, code, p, cache, tokens, diff --git a/src/dscanner/main.d b/src/dscanner/main.d index e1b1bc6..ab2eaba 100644 --- a/src/dscanner/main.d +++ b/src/dscanner/main.d @@ -165,10 +165,24 @@ else return 0; } + if (args.length > 1 && args[1] == "lint") + { + args = args[0] ~ args[2 .. $]; + styleCheck = true; + if (!errorFormat.length) + errorFormat = "pretty"; + } + if (!errorFormat.length) errorFormat = defaultErrorFormat; else if (auto errorFormatSuppl = errorFormat in errorFormatMap) - errorFormat = *errorFormatSuppl; + errorFormat = (*errorFormatSuppl) + // support some basic formatting things so it's easier for the user to type these + .replace("\\x1B", "\x1B") + .replace("\\033", "\x1B") + .replace("\\r", "\r") + .replace("\\n", "\n") + .replace("\\t", "\t"); const(string[]) absImportPaths = importPaths.map!(a => a.absolutePath() .buildNormalizedPath()).array(); @@ -350,7 +364,14 @@ else void printHelp(string programName) { stderr.writefln(` - Usage: %s + Usage: %1$s + +Human-readable output: + %1$s lint + +Parsable outputs: + %1$s -S + %1$s --report Options: --help, -h @@ -418,11 +439,17 @@ Options: - {type2}: "error" or "warning", uppercase variants: {Type2}, {TYPE2} - {message}: human readable message such as "Variable c is never used." - {name}: D-Scanner check name such as "unused_variable_check" + - {context}: "\n\n ^^^^^ here" + - {supplemental}: for supplemental messages, each one formatted using + this same format string, tab indented, type = "hint". For compatibility with other tools, the following strings may be specified as shorthand aliases: - %3$(-f %1$s -> %2$s\n %) + %3$(-f %1$s -> %2$s` ~ '\n' ~ ` %) + + When calling "%1$s lint" for human readable output, "pretty" + is used by default. --ctags ..., -c ... Generates ctags information from the given source code file. Note that