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:
parent
3b8110fdfa
commit
78f2b5a420
40
README.md
40
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.
|
||||||
|
<!--
|
||||||
|
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
|
### Token Count
|
||||||
The "--tokenCount" or "-t" option prints the number of tokens in the given file
|
The "--tokenCount" or "-t" option prints the number of tokens in the given file
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,28 +106,115 @@ string[string] errorFormatMap()
|
||||||
static string[string] ret;
|
static string[string] ret;
|
||||||
if (ret is null)
|
if (ret is null)
|
||||||
ret = [
|
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;
|
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;
|
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);
|
private string formatContext(Message.Diagnostic diagnostic, scope const(char)[] code, bool color)
|
||||||
s = s.replace("{line}", to!string(message.startLine));
|
{
|
||||||
s = s.replace("{column}", to!string(message.startColumn));
|
import std.string : indexOf, lastIndexOf;
|
||||||
s = s.replace("{endLine}", to!string(message.endLine));
|
|
||||||
s = s.replace("{endColumn}", to!string(message.endColumn));
|
if (diagnostic.startIndex >= diagnostic.endIndex || diagnostic.endIndex > code.length
|
||||||
s = s.replace("{type}", isError ? "error" : "warn");
|
|| diagnostic.startColumn >= diagnostic.endColumn || diagnostic.endColumn == 0)
|
||||||
s = s.replace("{Type}", isError ? "Error" : "Warn");
|
return null;
|
||||||
s = s.replace("{TYPE}", isError ? "ERROR" : "WARN");
|
|
||||||
s = s.replace("{type2}", isError ? "error" : "warning");
|
auto lineStart = code.lastIndexOf('\n', diagnostic.startIndex) + 1;
|
||||||
s = s.replace("{Type2}", isError ? "Error" : "Warning");
|
auto lineEnd = code.indexOf('\n', diagnostic.endIndex);
|
||||||
s = s.replace("{TYPE2}", isError ? "ERROR" : "WARNING");
|
if (lineEnd == -1)
|
||||||
s = s.replace("{message}", message.message);
|
lineEnd = code.length;
|
||||||
s = s.replace("{name}", message.checkName);
|
|
||||||
|
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);
|
writefln("%s", s);
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +390,7 @@ bool analyze(string[] fileNames, const StaticAnalysisConfig config, string error
|
||||||
foreach (result; results[])
|
foreach (result; results[])
|
||||||
{
|
{
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
messageFunctionFormat(errorFormat, result, false);
|
messageFunctionFormat(errorFormat, result, false, code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hasErrors;
|
return hasErrors;
|
||||||
|
|
@ -334,7 +421,7 @@ const(Module) parseModule(string fileName, ubyte[] code, RollbackAllocator* p,
|
||||||
// TODO: proper index and column ranges
|
// TODO: proper index and column ranges
|
||||||
return messageFunctionFormat(errorFormat,
|
return messageFunctionFormat(errorFormat,
|
||||||
Message(Message.Diagnostic.from(fileName, [0, 0], line, [column, column], message), "dscanner.syntax"),
|
Message(Message.Diagnostic.from(fileName, [0, 0], line, [column, column], message), "dscanner.syntax"),
|
||||||
isError);
|
isError, code);
|
||||||
};
|
};
|
||||||
|
|
||||||
return parseModule(fileName, code, p, cache, tokens,
|
return parseModule(fileName, code, p, cache, tokens,
|
||||||
|
|
|
||||||
|
|
@ -165,10 +165,24 @@ else
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.length > 1 && args[1] == "lint")
|
||||||
|
{
|
||||||
|
args = args[0] ~ args[2 .. $];
|
||||||
|
styleCheck = true;
|
||||||
|
if (!errorFormat.length)
|
||||||
|
errorFormat = "pretty";
|
||||||
|
}
|
||||||
|
|
||||||
if (!errorFormat.length)
|
if (!errorFormat.length)
|
||||||
errorFormat = defaultErrorFormat;
|
errorFormat = defaultErrorFormat;
|
||||||
else if (auto errorFormatSuppl = errorFormat in errorFormatMap)
|
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()
|
const(string[]) absImportPaths = importPaths.map!(a => a.absolutePath()
|
||||||
.buildNormalizedPath()).array();
|
.buildNormalizedPath()).array();
|
||||||
|
|
@ -350,7 +364,14 @@ else
|
||||||
void printHelp(string programName)
|
void printHelp(string programName)
|
||||||
{
|
{
|
||||||
stderr.writefln(`
|
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:
|
Options:
|
||||||
--help, -h
|
--help, -h
|
||||||
|
|
@ -418,11 +439,17 @@ Options:
|
||||||
- {type2}: "error" or "warning", uppercase variants: {Type2}, {TYPE2}
|
- {type2}: "error" or "warning", uppercase variants: {Type2}, {TYPE2}
|
||||||
- {message}: human readable message such as "Variable c is never used."
|
- {message}: human readable message such as "Variable c is never used."
|
||||||
- {name}: D-Scanner check name such as "unused_variable_check"
|
- {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
|
For compatibility with other tools, the following strings may be
|
||||||
specified as shorthand aliases:
|
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>...
|
--ctags <file | directory>..., -c <file | directory>...
|
||||||
Generates ctags information from the given source code file. Note that
|
Generates ctags information from the given source code file. Note that
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue