Skip to content

Commit 78f2b5a

Browse files
committed
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...`
1 parent 3b8110f commit 78f2b5a

File tree

3 files changed

+174
-20
lines changed

3 files changed

+174
-20
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,46 @@ void main(string[] args)
4747
}
4848
```
4949

50+
## Linting
51+
52+
Use
53+
54+
```sh
55+
dscanner lint source/
56+
```
57+
58+
to view a human readable list of issues.
59+
60+
For a CLI / tool parsable output use either
61+
62+
```sh
63+
dscanner -S source/
64+
# or
65+
dscanner --report source/
66+
```
67+
68+
You can also specify custom formats using `-f` / `--errorFormat`, where there
69+
are also built-in formats for GitHub Actions:
70+
71+
```sh
72+
# for GitHub actions: (automatically adds annotations to files in PRs)
73+
dscanner -S -f github source/
74+
# custom format:
75+
dscanner -S -f '{filepath}({line}:{column})[{type}]: {message}' source/
76+
```
77+
78+
Diagnostic types can be enabled/disabled using a configuration file, check out
79+
the `--config` argument / `dscanner.ini` file for more info. Tip: some IDEs that
80+
integrate D-Scanner may have helpers to configure the diagnostics or help
81+
generate the dscanner.ini file.
82+
<!--
83+
IDE list for overview:
84+
code-d has an "insert default dscanner.ini content" command + proprietary
85+
disabling per-line (we really need to bring that into standard D-Scanner)
86+
-->
87+
88+
## Other features
89+
5090
### Token Count
5191
The "--tokenCount" or "-t" option prints the number of tokens in the given file
5292

src/dscanner/analysis/run.d

Lines changed: 104 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -106,28 +106,115 @@ string[string] errorFormatMap()
106106
static string[string] ret;
107107
if (ret is null)
108108
ret = [
109-
"github": "::{type2} file={filepath},line={line},endLine={endLine},col={column},endColumn={endColumn},title={Type2} ({name})::{message}"
109+
"github": "::{type2} file={filepath},line={line},endLine={endLine},col={column},endColumn={endColumn},title={Type2} ({name})::{message}",
110+
"pretty": "\x1B[1m{filepath}({line}:{column}): {Type2}: \x1B[0m{message} \x1B[2m({name})\x1B[0m{context}{supplemental}"
110111
];
111112
return ret;
112113
}
113114

114-
void messageFunctionFormat(string format, Message message, bool isError)
115+
private string formatBase(string format, Message.Diagnostic diagnostic, scope const(ubyte)[] code, bool color)
115116
{
116117
auto s = format;
118+
s = s.replace("{filepath}", diagnostic.fileName);
119+
s = s.replace("{line}", to!string(diagnostic.startLine));
120+
s = s.replace("{column}", to!string(diagnostic.startColumn));
121+
s = s.replace("{endLine}", to!string(diagnostic.endLine));
122+
s = s.replace("{endColumn}", to!string(diagnostic.endColumn));
123+
s = s.replace("{message}", diagnostic.message);
124+
s = s.replace("{context}", diagnostic.formatContext(cast(const(char)[]) code, color));
125+
return s;
126+
}
127+
128+
private string formatContext(Message.Diagnostic diagnostic, scope const(char)[] code, bool color)
129+
{
130+
import std.string : indexOf, lastIndexOf;
131+
132+
if (diagnostic.startIndex >= diagnostic.endIndex || diagnostic.endIndex > code.length
133+
|| diagnostic.startColumn >= diagnostic.endColumn || diagnostic.endColumn == 0)
134+
return null;
135+
136+
auto lineStart = code.lastIndexOf('\n', diagnostic.startIndex) + 1;
137+
auto lineEnd = code.indexOf('\n', diagnostic.endIndex);
138+
if (lineEnd == -1)
139+
lineEnd = code.length;
140+
141+
auto ret = appender!string;
142+
ret.reserve((lineEnd - lineStart) + diagnostic.endColumn + (color ? 30 : 10));
143+
ret ~= '\n';
144+
if (color)
145+
ret ~= "\x1B[m"; // reset
146+
ret ~= code[lineStart .. lineEnd].replace('\t', ' ');
147+
ret ~= '\n';
148+
if (color)
149+
ret ~= "\x1B[0;33m"; // reset, yellow
150+
foreach (_; 0 .. diagnostic.startColumn - 1)
151+
ret ~= ' ';
152+
foreach (_; 0 .. diagnostic.endColumn - diagnostic.startColumn)
153+
ret ~= '^';
154+
if (color)
155+
ret ~= "\x1B[m"; // reset
156+
return ret.data;
157+
}
158+
159+
version (Windows)
160+
void enableColoredOutput()
161+
{
162+
import core.sys.windows.windows;
163+
164+
// Set output mode to handle virtual terminal sequences
165+
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
166+
if (hOut == INVALID_HANDLE_VALUE)
167+
return;
168+
169+
DWORD dwMode;
170+
if (!GetConsoleMode(hOut, &dwMode))
171+
return;
172+
173+
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
174+
if (!SetConsoleMode(hOut, dwMode))
175+
return;
176+
}
117177

118-
s = s.replace("{filepath}", message.fileName);
119-
s = s.replace("{line}", to!string(message.startLine));
120-
s = s.replace("{column}", to!string(message.startColumn));
121-
s = s.replace("{endLine}", to!string(message.endLine));
122-
s = s.replace("{endColumn}", to!string(message.endColumn));
123-
s = s.replace("{type}", isError ? "error" : "warn");
124-
s = s.replace("{Type}", isError ? "Error" : "Warn");
125-
s = s.replace("{TYPE}", isError ? "ERROR" : "WARN");
126-
s = s.replace("{type2}", isError ? "error" : "warning");
127-
s = s.replace("{Type2}", isError ? "Error" : "Warning");
128-
s = s.replace("{TYPE2}", isError ? "ERROR" : "WARNING");
129-
s = s.replace("{message}", message.message);
130-
s = s.replace("{name}", message.checkName);
178+
void messageFunctionFormat(string format, Message message, bool isError, scope const(ubyte)[] code = null)
179+
{
180+
bool color = format.canFind("\x1B[");
181+
if (color)
182+
{
183+
version (Windows)
184+
enableColoredOutput();
185+
}
186+
187+
auto s = format.formatBase(message.diagnostic, code, color);
188+
189+
string formatType(string s, string type, string colorCode)
190+
{
191+
import std.ascii : toUpper;
192+
import std.string : representation;
193+
194+
string upperFirst(string s) { return s[0].toUpper ~ s[1 .. $]; }
195+
string upper(string s) { return s.representation.map!(a => toUpper(cast(char) a)).array; }
196+
197+
string type2 = type;
198+
if (type2 == "warn")
199+
type2 = "warning";
200+
201+
s = s.replace("{type}", color ? (colorCode ~ type ~ "\x1B[m") : type);
202+
s = s.replace("{Type}", color ? (colorCode ~ upperFirst(type) ~ "\x1B[m") : upperFirst(type));
203+
s = s.replace("{TYPE}", color ? (colorCode ~ upper(type) ~ "\x1B[m") : upper(type));
204+
s = s.replace("{type2}", color ? (colorCode ~ type2 ~ "\x1B[m") : type2);
205+
s = s.replace("{Type2}", color ? (colorCode ~ upperFirst(type2) ~ "\x1B[m") : upperFirst(type2));
206+
s = s.replace("{TYPE2}", color ? (colorCode ~ upper(type2) ~ "\x1B[m") : upper(type2));
207+
208+
return s;
209+
}
210+
211+
s = formatType(s, isError ? "error" : "warn", isError ? "\x1B[31m" : "\x1B[33m");
212+
s = s.replace("{name}", message.checkName);
213+
s = s.replace("{supplemental}", message.supplemental.map!(a => "\n\t"
214+
~ formatType(format.formatBase(a, code, color), "hint", "\x1B[35m")
215+
.replace("{name}", "").replace("{supplemental}", "")
216+
.replace("\n", "\n\t"))
217+
.join());
131218

132219
writefln("%s", s);
133220
}
@@ -303,7 +390,7 @@ bool analyze(string[] fileNames, const StaticAnalysisConfig config, string error
303390
foreach (result; results[])
304391
{
305392
hasErrors = true;
306-
messageFunctionFormat(errorFormat, result, false);
393+
messageFunctionFormat(errorFormat, result, false, code);
307394
}
308395
}
309396
return hasErrors;
@@ -334,7 +421,7 @@ const(Module) parseModule(string fileName, ubyte[] code, RollbackAllocator* p,
334421
// TODO: proper index and column ranges
335422
return messageFunctionFormat(errorFormat,
336423
Message(Message.Diagnostic.from(fileName, [0, 0], line, [column, column], message), "dscanner.syntax"),
337-
isError);
424+
isError, code);
338425
};
339426

340427
return parseModule(fileName, code, p, cache, tokens,

src/dscanner/main.d

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,24 @@ else
165165
return 0;
166166
}
167167

168+
if (args.length > 1 && args[1] == "lint")
169+
{
170+
args = args[0] ~ args[2 .. $];
171+
styleCheck = true;
172+
if (!errorFormat.length)
173+
errorFormat = "pretty";
174+
}
175+
168176
if (!errorFormat.length)
169177
errorFormat = defaultErrorFormat;
170178
else if (auto errorFormatSuppl = errorFormat in errorFormatMap)
171-
errorFormat = *errorFormatSuppl;
179+
errorFormat = (*errorFormatSuppl)
180+
// support some basic formatting things so it's easier for the user to type these
181+
.replace("\\x1B", "\x1B")
182+
.replace("\\033", "\x1B")
183+
.replace("\\r", "\r")
184+
.replace("\\n", "\n")
185+
.replace("\\t", "\t");
172186

173187
const(string[]) absImportPaths = importPaths.map!(a => a.absolutePath()
174188
.buildNormalizedPath()).array();
@@ -350,7 +364,14 @@ else
350364
void printHelp(string programName)
351365
{
352366
stderr.writefln(`
353-
Usage: %s <options>
367+
Usage: %1$s <options>
368+
369+
Human-readable output:
370+
%1$s lint <options> <files...>
371+
372+
Parsable outputs:
373+
%1$s -S <options> <files...>
374+
%1$s --report <options> <files...>
354375
355376
Options:
356377
--help, -h
@@ -418,11 +439,17 @@ Options:
418439
- {type2}: "error" or "warning", uppercase variants: {Type2}, {TYPE2}
419440
- {message}: human readable message such as "Variable c is never used."
420441
- {name}: D-Scanner check name such as "unused_variable_check"
442+
- {context}: "\n<source code>\n ^^^^^ here"
443+
- {supplemental}: for supplemental messages, each one formatted using
444+
this same format string, tab indented, type = "hint".
421445
422446
For compatibility with other tools, the following strings may be
423447
specified as shorthand aliases:
424448
425-
%3$(-f %1$s -> %2$s\n %)
449+
%3$(-f %1$s -> %2$s` ~ '\n' ~ ` %)
450+
451+
When calling "%1$s lint" for human readable output, "pretty"
452+
is used by default.
426453
427454
--ctags <file | directory>..., -c <file | directory>...
428455
Generates ctags information from the given source code file. Note that

0 commit comments

Comments
 (0)