add colored output option

also adds a simpler way to invoke D-Scanner for users that uses this new
UI by default: `dscanner lint FILES...`
This commit is contained in:
WebFreak001 2023-06-29 18:12:47 +02:00 committed by Jan Jurzitza
parent 3b8110fdfa
commit 78f2b5a420
3 changed files with 174 additions and 20 deletions

View file

@ -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.
<!--
IDE list for overview:
code-d has an "insert default dscanner.ini content" command + proprietary
disabling per-line (we really need to bring that into standard D-Scanner)
-->
## Other features
### Token Count
The "--tokenCount" or "-t" option prints the number of tokens in the given file

View file

@ -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);
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,

View file

@ -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 <options>
Usage: %1$s <options>
Human-readable output:
%1$s lint <options> <files...>
Parsable outputs:
%1$s -S <options> <files...>
%1$s --report <options> <files...>
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<source code>\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 <file | directory>..., -c <file | directory>...
Generates ctags information from the given source code file. Note that