Cleanup code and fix integration tests (#172)

* Delete libdparse unit test assertion function

* Delete unused libdparse visitors from base.d

* Improve StaticIfElse warning location

* Improve FunctionAttributeCheck warning location

* Switch to DMD flow for listing autofixes

* Extract dmd analyzer selection in a separate function

* Make getName() method in BaseAnalyzerDmd public

* Fix offsets in integration test json

* Improve StyleChecker warning location

* Enable integration tests in CI

* Fix Autofix flow

* Remove & comment dead code

* Remove dead code from autofix unit test

* Remove dead code

* Remove dead code from autofix.d

* Clean up code in helpers.d

* Clean up code in run.d and migrate StatsCollector to dmd

* Fix reading code from stdin

* Return if errors are found in analysis flows

* Remove dead code

* Check for Windows line terminators in integration tests
This commit is contained in:
Vladiwostok 2024-12-10 09:25:16 +02:00 committed by Vladiwostok
parent 930dd525f4
commit 232cd304de
24 changed files with 561 additions and 1158 deletions

View File

@ -168,11 +168,16 @@ jobs:
fi
"./bin/dscanner$EXE" --styleCheck -f "$FORMAT" src
# TODO: fixme
#- name: Integration Tests
#run: ./it.sh
#working-directory: tests
#shell: bash
- name: Integration Tests
# run: ./it.sh
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
./it.sh Windows
else
./it.sh Unix
fi
working-directory: tests
shell: bash
- name: Run style checks
if: ${{ matrix.compiler.dmd == 'dmd' && matrix.build.type == 'make' }}

View File

@ -133,7 +133,7 @@ extern (C++) class AlwaysCurlyCheck(AST) : BaseAnalyzerDmd
unittest
{
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix;
import dscanner.analysis.helpers : assertAnalyzerWarningsDMD;
import std.stdio : stderr;
StaticAnalysisConfig sac = disabledConfig();
@ -240,7 +240,7 @@ unittest
void test() {
if(true) { return; } // fix:0
}
}c, sac, true);
}c, sac);
assertAutoFix(q{
void test() {
@ -250,7 +250,7 @@ unittest
void test() {
foreach(_; 0 .. 10 ) { return; } // fix:0
}
}c, sac, true);
}c, sac);
assertAutoFix(q{
void test() {
@ -260,7 +260,7 @@ unittest
void test() {
for(int i = 0; i < 10; ++i) { return; } // fix:0
}
}c, sac, true);
}c, sac);
assertAutoFix(q{
void test() {
@ -270,7 +270,7 @@ unittest
void test() {
do { return; } while(true); // fix:0
}
}c, sac, true);
}c, sac);
stderr.writeln("Unittest for AutoFix AlwaysCurly passed.");

View File

@ -226,7 +226,7 @@ unittest
@safe void doStuff(){} // fix
@Custom
void doStuff(){} // fix
}c, sac, true);
}c, sac);
stderr.writeln("Unittest for AutoFunctionChecker passed.");
}

View File

@ -2,124 +2,23 @@ module dscanner.analysis.autofix;
import std.algorithm : filter, findSplit;
import std.conv : to;
import std.file : exists, remove;
import std.functional : toDelegate;
import std.stdio;
import dparse.lexer;
import dparse.rollback_allocator;
import dparse.ast : Module;
import dsymbol.modulecache : ModuleCache;
import dscanner.analysis.base : AutoFix, AutoFixFormatting, BaseAnalyzer, Message;
import dscanner.analysis.base : AutoFix, AutoFixFormatting, BaseAnalyzer, BaseAnalyzerDmd, Message;
import dscanner.analysis.config : StaticAnalysisConfig;
import dscanner.analysis.run : analyze, doNothing;
import dscanner.utils : readFile, readStdin;
private void resolveAutoFixes(
ref Message message,
string fileName,
ref ModuleCache moduleCache,
scope const(Token)[] tokens,
const Module m,
const StaticAnalysisConfig analysisConfig,
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid
)
{
resolveAutoFixes(message.checkName, message.autofixes, fileName, moduleCache,
tokens, m, analysisConfig, overrideFormattingConfig);
}
private void resolveAutoFixes(string messageCheckName, AutoFix[] autofixes, string fileName,
ref ModuleCache moduleCache,
scope const(Token)[] tokens, const Module m,
const StaticAnalysisConfig analysisConfig,
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
{
import core.memory : GC;
import dsymbol.conversion.first : FirstPass;
import dsymbol.conversion.second : secondPass;
import dsymbol.scope_ : Scope;
import dsymbol.semantic : SemanticSymbol;
import dsymbol.string_interning : internString;
import dsymbol.symbol : DSymbol;
import dscanner.analysis.run : getAnalyzersForModuleAndConfig;
const(AutoFixFormatting) formattingConfig =
overrideFormattingConfig is AutoFixFormatting.invalid
? analysisConfig.getAutoFixFormattingConfig()
: overrideFormattingConfig;
scope first = new FirstPass(m, internString(fileName), &moduleCache, null);
first.run();
secondPass(first.rootSymbol, first.moduleScope, moduleCache);
auto moduleScope = first.moduleScope;
scope(exit) typeid(DSymbol).destroy(first.rootSymbol.acSymbol);
scope(exit) typeid(SemanticSymbol).destroy(first.rootSymbol);
scope(exit) typeid(Scope).destroy(first.moduleScope);
GC.disable;
scope (exit)
GC.enable;
foreach (BaseAnalyzer check; getAnalyzersForModuleAndConfig(fileName, tokens, m, analysisConfig, moduleScope))
{
if (check.getName() == messageCheckName)
{
foreach (ref autofix; autofixes)
autofix.resolveAutoFixFromCheck(check, m, tokens, formattingConfig);
return;
}
}
throw new Exception("Cannot find analyzer " ~ messageCheckName
~ " to resolve autofix with.");
}
void resolveAutoFixFromCheck(
ref AutoFix autofix,
BaseAnalyzer check,
const Module m,
scope const(Token)[] tokens,
const AutoFixFormatting formattingConfig
)
{
import std.sumtype : match;
autofix.replacements.match!(
(AutoFix.ResolveContext context) {
autofix.replacements = check.resolveAutoFix(m, tokens, context, formattingConfig);
},
(_) {}
);
}
private AutoFix.CodeReplacement[] resolveAutoFix(string messageCheckName, AutoFix.ResolveContext context,
string fileName,
ref ModuleCache moduleCache,
scope const(Token)[] tokens, const Module m,
const StaticAnalysisConfig analysisConfig,
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
{
AutoFix temp;
temp.replacements = context;
resolveAutoFixes(messageCheckName, (&temp)[0 .. 1], fileName, moduleCache,
tokens, m, analysisConfig, overrideFormattingConfig);
return temp.expectReplacements("resolving didn't work?!");
}
import dscanner.analysis.rundmd;
import dscanner.utils : getModuleName, readFile, readStdin;
void listAutofixes(
StaticAnalysisConfig config,
string resolveMessage,
bool usingStdin,
string fileName,
StringCache* cache,
ref ModuleCache moduleCache
string fileName
)
{
import dparse.parser : parseModule;
import dscanner.analysis.base : Message;
import std.format : format;
import std.json : JSONValue;
@ -145,30 +44,35 @@ void listAutofixes(
bool matchesCursor(Message m)
{
return isBytes
? req.bytes >= m.startIndex && req.bytes <= m.endIndex
: req.line >= m.startLine && req.line <= m.endLine
&& (req.line > m.startLine || req.column >= m.startColumn)
&& (req.line < m.endLine || req.column <= m.endColumn);
return isBytes ? req.bytes >= m.startIndex && req.bytes <= m.endIndex
: req.line >= m.startLine && req.line <= m.endLine
&& (req.line > m.startLine || req.column >= m.startColumn)
&& (req.line < m.endLine || req.column <= m.endColumn);
}
RollbackAllocator rba;
LexerConfig lexerConfig;
lexerConfig.fileName = fileName;
lexerConfig.stringBehavior = StringBehavior.source;
auto tokens = getTokensForParser(usingStdin ? readStdin()
: readFile(fileName), lexerConfig, cache);
auto mod = parseModule(tokens, fileName, &rba, toDelegate(&doNothing));
ubyte[] code;
if (usingStdin)
{
code = readStdin();
fileName = "stdin.d";
File f = File(fileName, "w");
f.rawWrite(code);
f.close();
}
else
{
code = readFile(fileName);
}
auto messages = analyze(fileName, mod, config, moduleCache, tokens);
auto dmdModule = parseDmdModule(fileName, cast(string) code);
auto moduleName = getModuleName(dmdModule.md);
auto messages = analyzeDmd(fileName, dmdModule, moduleName, config);
with (stdout.lockingTextWriter)
{
put("[");
foreach (message; messages[].filter!matchesCursor)
{
resolveAutoFixes(message, fileName, moduleCache, tokens, mod, config);
foreach (i, autofix; message.autofixes)
{
put(i == 0 ? "\n" : ",\n");
@ -191,6 +95,12 @@ void listAutofixes(
put("\n]");
}
stdout.flush();
if (usingStdin)
{
assert(exists(fileName));
remove(fileName);
}
}
void improveAutoFixWhitespace(scope const(char)[] code, AutoFix.CodeReplacement[] replacements)
@ -227,7 +137,8 @@ void improveAutoFixWhitespace(scope const(char)[] code, AutoFix.CodeReplacement[
{
assert(replacement.range[0] >= 0 && replacement.range[0] < code.length
&& replacement.range[1] >= 0 && replacement.range[1] < code.length
&& replacement.range[0] <= replacement.range[1], "trying to autofix whitespace on code that doesn't match with what the replacements were generated for");
&& replacement.range[0] <= replacement.range[1],
"trying to autofix whitespace on code that doesn't match with what the replacements were generated for");
void growRight()
{

View File

@ -82,31 +82,6 @@ struct AutoFix
return ret;
}
static AutoFix replacement(const Token token, string newText, string name = null)
{
if (!name.length)
{
auto text = token.text.length ? token.text : str(token.type);
if (newText.length)
name = "Replace `" ~ text ~ "` with `" ~ newText ~ "`";
else
name = "Remove `" ~ text ~ "`";
}
return replacement([token], newText, name);
}
static AutoFix replacement(const BaseNode node, string newText, string name)
{
return replacement(node.tokens, newText, name);
}
static AutoFix replacement(const Token[] tokens, string newText, string name)
in(tokens.length > 0, "must provide at least one token")
{
auto end = tokens[$ - 1].text.length ? tokens[$ - 1].text : str(tokens[$ - 1].type);
return replacement([tokens[0].index, tokens[$ - 1].index + end.length], newText, name);
}
static AutoFix replacement(size_t[2] range, string newText, string name)
{
AutoFix ret;
@ -117,17 +92,6 @@ struct AutoFix
return ret;
}
static AutoFix insertionBefore(const Token token, string content, string name = null)
{
return insertionAt(token.index, content, name);
}
static AutoFix insertionAfter(const Token token, string content, string name = null)
{
auto tokenText = token.text.length ? token.text : str(token.type);
return insertionAt(token.index + tokenText.length, content, name);
}
static AutoFix insertionAt(size_t index, string content, string name = null)
{
assert(content.length > 0, "generated auto fix inserting text without content");
@ -143,24 +107,6 @@ struct AutoFix
return ret;
}
static AutoFix indentLines(scope const(Token)[] tokens, const AutoFixFormatting formatting, string name = "Indent code")
{
CodeReplacement[] inserts;
size_t line = -1;
foreach (token; tokens)
{
if (line != token.line)
{
line = token.line;
inserts ~= CodeReplacement([token.index, token.index], formatting.indentation);
}
}
AutoFix ret;
ret.name = name;
ret.replacements = inserts;
return ret;
}
AutoFix concat(AutoFix other) const
{
import std.algorithm : sort;
@ -291,33 +237,6 @@ struct Message
deprecated("Use startLine instead") alias line = startLine;
deprecated("Use startColumn instead") alias column = startColumn;
static Diagnostic from(string fileName, const BaseNode node, string message)
{
return from(fileName, node !is null ? node.tokens : [], message);
}
static Diagnostic from(string fileName, const Token token, string message)
{
auto text = token.text.length ? token.text : str(token.type);
return from(fileName,
[token.index, token.index + text.length],
token.line,
[token.column, token.column + text.length],
message);
}
static Diagnostic from(string fileName, const Token[] tokens, string message)
{
auto start = tokens.length ? tokens[0] : Token.init;
auto end = tokens.length ? tokens[$ - 1] : Token.init;
auto endText = end.text.length ? end.text : str(end.type);
return from(fileName,
[start.index, end.index + endText.length],
[start.line, end.line],
[start.column, end.column + endText.length],
message);
}
static Diagnostic from(string fileName, size_t[2] index, size_t line, size_t[2] columns, string message)
{
return Message.Diagnostic(fileName, index[0], index[1], line, line, columns[0], columns[1], message);
@ -342,7 +261,7 @@ struct Message
/// the `BaseAnalyzer.resolveAutoFix` method with.
AutoFix[] autofixes;
deprecated this(string fileName, size_t line, size_t column, string key = null, string message = null, string checkName = null)
this(string fileName, size_t line, size_t column, string key = null, string message = null, string checkName = null)
{
diagnostic.fileName = fileName;
diagnostic.startLine = diagnostic.endLine = line;
@ -395,7 +314,7 @@ mixin template AnalyzerInfo(string checkName)
{
enum string name = checkName;
extern(D) override protected string getName()
extern (D) override protected string getName()
{
return name;
}
@ -503,48 +422,6 @@ protected:
}
}
deprecated("Use the overload taking start and end locations or a Node instead")
void addErrorMessage(size_t line, size_t column, string key, string message)
{
_messages.insert(Message(fileName, line, column, key, message, getName()));
}
void addErrorMessage(const BaseNode node, string key, string message, AutoFix[] autofixes = null)
{
addErrorMessage(Message.Diagnostic.from(fileName, node, message), key, autofixes);
}
void addErrorMessage(const Token token, string key, string message, AutoFix[] autofixes = null)
{
addErrorMessage(Message.Diagnostic.from(fileName, token, message), key, autofixes);
}
void addErrorMessage(const Token[] tokens, string key, string message, AutoFix[] autofixes = null)
{
addErrorMessage(Message.Diagnostic.from(fileName, tokens, message), key, autofixes);
}
void addErrorMessage(size_t[2] index, size_t line, size_t[2] columns, string key, string message, AutoFix[] autofixes = null)
{
addErrorMessage(index, [line, line], columns, key, message, autofixes);
}
void addErrorMessage(size_t[2] index, size_t[2] lines, size_t[2] columns, string key, string message, AutoFix[] autofixes = null)
{
auto d = Message.Diagnostic.from(fileName, index, lines, columns, message);
_messages.insert(Message(d, key, getName(), autofixes));
}
void addErrorMessage(Message.Diagnostic diagnostic, string key, AutoFix[] autofixes = null)
{
_messages.insert(Message(diagnostic, key, getName(), autofixes));
}
void addErrorMessage(Message.Diagnostic diagnostic, Message.Diagnostic[] supplemental, string key, AutoFix[] autofixes = null)
{
_messages.insert(Message(diagnostic, supplemental, key, getName(), autofixes));
}
/**
* The file name
*/
@ -555,343 +432,6 @@ protected:
MessageSet _messages;
}
/// Find the token with the given type, otherwise returns the whole range or a user-specified fallback, if set.
const(Token)[] findTokenForDisplay(const BaseNode node, IdType type, const(Token)[] fallback = null)
{
return node.tokens.findTokenForDisplay(type, fallback);
}
/// ditto
const(Token)[] findTokenForDisplay(const Token[] tokens, IdType type, const(Token)[] fallback = null)
{
foreach (i, token; tokens)
if (token.type == type)
return tokens[i .. i + 1];
return fallback is null ? tokens : fallback;
}
abstract class ScopedBaseAnalyzer : BaseAnalyzer
{
public:
this(BaseAnalyzerArguments args)
{
super(args);
}
template ScopedVisit(NodeType)
{
override void visit(const NodeType n)
{
pushScopeImpl();
scope (exit)
popScopeImpl();
n.accept(this);
}
}
alias visit = BaseAnalyzer.visit;
mixin ScopedVisit!BlockStatement;
mixin ScopedVisit!ForeachStatement;
mixin ScopedVisit!ForStatement;
mixin ScopedVisit!Module;
mixin ScopedVisit!StructBody;
mixin ScopedVisit!TemplateDeclaration;
mixin ScopedVisit!WithStatement;
mixin ScopedVisit!WhileStatement;
mixin ScopedVisit!DoStatement;
// mixin ScopedVisit!SpecifiedFunctionBody; // covered by BlockStatement
mixin ScopedVisit!ShortenedFunctionBody;
override void visit(const SwitchStatement switchStatement)
{
switchStack.length++;
scope (exit)
switchStack.length--;
switchStatement.accept(this);
}
override void visit(const IfStatement ifStatement)
{
pushScopeImpl();
if (ifStatement.condition)
ifStatement.condition.accept(this);
if (ifStatement.thenStatement)
ifStatement.thenStatement.accept(this);
popScopeImpl();
if (ifStatement.elseStatement)
{
pushScopeImpl();
ifStatement.elseStatement.accept(this);
popScopeImpl();
}
}
static foreach (T; AliasSeq!(CaseStatement, DefaultStatement, CaseRangeStatement))
override void visit(const T stmt)
{
// case and default statements always open new scopes and close
// previous case scopes
bool close = switchStack.length && switchStack[$ - 1].inCase;
bool b = switchStack[$ - 1].inCase;
switchStack[$ - 1].inCase = true;
scope (exit)
switchStack[$ - 1].inCase = b;
if (close)
{
popScope();
pushScope();
stmt.accept(this);
}
else
{
pushScope();
stmt.accept(this);
popScope();
}
}
protected:
/// Called on new scopes, which includes for example:
///
/// - `module m; /* here, entire file */`
/// - `{ /* here */ }`
/// - `if () { /* here */ } else { /* here */ }`
/// - `foreach (...) { /* here */ }`
/// - `case 1: /* here */ break;`
/// - `case 1: /* here, up to next case */ goto case; case 2: /* here 2 */ break;`
/// - `default: /* here */ break;`
/// - `struct S { /* here */ }`
///
/// But doesn't include:
///
/// - `static if (x) { /* not a separate scope */ }` (use `mixin ScopedVisit!ConditionalDeclaration;`)
///
/// You can `mixin ScopedVisit!NodeType` to automatically call push/popScope
/// on occurences of that NodeType.
abstract void pushScope();
/// ditto
abstract void popScope();
void pushScopeImpl()
{
if (switchStack.length)
switchStack[$ - 1].scopeDepth++;
pushScope();
}
void popScopeImpl()
{
if (switchStack.length)
switchStack[$ - 1].scopeDepth--;
popScope();
}
struct SwitchStack
{
int scopeDepth;
bool inCase;
}
SwitchStack[] switchStack;
}
unittest
{
import core.exception : AssertError;
import dparse.lexer : getTokensForParser, LexerConfig, StringCache;
import dparse.parser : parseModule;
import dparse.rollback_allocator : RollbackAllocator;
import std.conv : to;
import std.exception : assertThrown;
// test where we can:
// call `depth(1);` to check that the scope depth is at 1
// if calls are syntactically not valid, define `auto depth = 1;`
//
// call `isNewScope();` to check that the scope hasn't been checked with isNewScope before
// if calls are syntactically not valid, define `auto isNewScope = void;`
//
// call `isOldScope();` to check that the scope has already been checked with isNewScope
// if calls are syntactically not valid, define `auto isOldScope = void;`
class TestScopedAnalyzer : ScopedBaseAnalyzer
{
this(size_t codeLine)
{
super(BaseAnalyzerArguments("stdin"));
this.codeLine = codeLine;
}
override void visit(const FunctionCallExpression f)
{
int depth = cast(int) stack.length;
if (f.unaryExpression && f.unaryExpression.primaryExpression
&& f.unaryExpression.primaryExpression.identifierOrTemplateInstance)
{
auto fname = f.unaryExpression.primaryExpression.identifierOrTemplateInstance.identifier.text;
if (fname == "depth")
{
assert(f.arguments.tokens.length == 3);
auto expected = f.arguments.tokens[1].text.to!int;
assert(expected == depth, "Expected depth="
~ expected.to!string ~ " in line " ~ (codeLine + f.tokens[0].line).to!string
~ ", but got depth=" ~ depth.to!string);
}
else if (fname == "isNewScope")
{
assert(!stack[$ - 1]);
stack[$ - 1] = true;
}
else if (fname == "isOldScope")
{
assert(stack[$ - 1]);
}
}
}
override void visit(const AutoDeclarationPart p)
{
int depth = cast(int) stack.length;
if (p.identifier.text == "depth")
{
assert(p.initializer.tokens.length == 1);
auto expected = p.initializer.tokens[0].text.to!int;
assert(expected == depth, "Expected depth="
~ expected.to!string ~ " in line " ~ (codeLine + p.tokens[0].line).to!string
~ ", but got depth=" ~ depth.to!string);
}
else if (p.identifier.text == "isNewScope")
{
assert(!stack[$ - 1]);
stack[$ - 1] = true;
}
else if (p.identifier.text == "isOldScope")
{
assert(stack[$ - 1]);
}
}
override void pushScope()
{
stack.length++;
}
override void popScope()
{
stack.length--;
}
alias visit = ScopedBaseAnalyzer.visit;
bool[] stack;
size_t codeLine;
}
void testScopes(string code, size_t codeLine = __LINE__ - 1)
{
StringCache cache = StringCache(4096);
LexerConfig config;
RollbackAllocator rba;
auto tokens = getTokensForParser(code, config, &cache);
Module m = parseModule(tokens, "stdin", &rba);
auto analyzer = new TestScopedAnalyzer(codeLine);
analyzer.visit(m);
}
testScopes(q{
auto isNewScope = void;
auto depth = 1;
auto isOldScope = void;
});
assertThrown!AssertError(testScopes(q{
auto isNewScope = void;
auto isNewScope = void;
}));
assertThrown!AssertError(testScopes(q{
auto isOldScope = void;
}));
assertThrown!AssertError(testScopes(q{
auto depth = 2;
}));
testScopes(q{
auto isNewScope = void;
auto depth = 1;
void foo() {
isNewScope();
isOldScope();
depth(2);
switch (a)
{
case 1:
isNewScope();
depth(4);
break;
depth(4);
isOldScope();
case 2:
isNewScope();
depth(4);
if (a)
{
isNewScope();
depth(6);
default:
isNewScope();
depth(6); // since cases/default opens new scope
break;
case 3:
isNewScope();
depth(6); // since cases/default opens new scope
break;
default:
isNewScope();
depth(6); // since cases/default opens new scope
break;
}
break;
depth(4);
default:
isNewScope();
depth(4);
break;
depth(4);
}
isOldScope();
depth(2);
switch (a)
{
isNewScope();
depth(3);
isOldScope();
default:
isNewScope();
depth(4);
break;
isOldScope();
case 1:
isNewScope();
depth(4);
break;
isOldScope();
}
}
auto isOldScope = void;
});
}
/**
* Visitor that implements the AST traversal logic.
* Supports collecting error messages
@ -911,7 +451,7 @@ extern(C++) class BaseAnalyzerDmd : SemanticTimeTransitiveVisitor
* Ensures that template AnalyzerInfo is instantiated in all classes
* deriving from this class
*/
extern(D) protected string getName()
extern(D) string getName()
{
assert(0);
}
@ -940,6 +480,19 @@ protected:
_messages.insert(Message(fileName, line, column, key, message, getName(), autofixes));
}
extern (D) void addErrorMessage(size_t[2] index, size_t[2] lines, size_t[2] columns, string key, string message)
{
auto diag = Message.Diagnostic.from(fileName, index, lines, columns, message);
_messages.insert(Message(diag, key, getName()));
}
extern (D) void addErrorMessage(size_t[2] index, size_t[2] lines, size_t[2] columns,
string key, string message, AutoFix[] autofixes)
{
auto diag = Message.Diagnostic.from(fileName, index, lines, columns, message);
_messages.insert(Message(diag, key, getName(), autofixes));
}
extern (D) bool skipTests;
/**

View File

@ -5,9 +5,7 @@
module dscanner.analysis.del;
import std.stdio;
import dscanner.analysis.base;
import dscanner.analysis.helpers;
/**
* Checks for use of the deprecated 'delete' keyword
@ -45,7 +43,8 @@ extern(C++) class DeleteCheck(AST) : BaseAnalyzerDmd
unittest
{
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix;
import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix;
import std.stdio : stderr;
StaticAnalysisConfig sac = disabledConfig();
sac.delete_check = Check.enabled;
@ -79,7 +78,7 @@ unittest
auto a = new Class();
destroy(a); // fix
}
}c, sac, true);
}c, sac);
stderr.writeln("Unittest for DeleteCheck passed.");
}

View File

@ -58,7 +58,7 @@ unittest
enum x = [1, 2, 3]; // fix
}c, q{
static immutable x = [1, 2, 3]; // fix
}c, sac, true);
}c, sac);
stderr.writeln("Unittest for EnumArrayLiteralCheck passed.");
}

View File

@ -5,15 +5,14 @@
module dscanner.analysis.explicitly_annotated_unittests;
import dscanner.analysis.base;
import dscanner.analysis.helpers;
/**
* Requires unittests to be explicitly annotated with either @safe or @system
*/
extern(C++) class ExplicitlyAnnotatedUnittestCheck(AST) : BaseAnalyzerDmd
extern (C++) class ExplicitlyAnnotatedUnittestCheck(AST) : BaseAnalyzerDmd
{
mixin AnalyzerInfo!"explicitly_annotated_unittests";
alias visit = BaseAnalyzerDmd.visit;
mixin AnalyzerInfo!"explicitly_annotated_unittests";
extern(D) this(string fileName)
{
@ -46,10 +45,10 @@ private:
unittest
{
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix;
import std.stdio : stderr;
import std.format : format;
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings;
StaticAnalysisConfig sac = disabledConfig();
sac.explicitly_annotated_unittests = Check.enabled;
@ -78,7 +77,7 @@ unittest
}
}c, sac);
//// nested
// nested
assertAutoFix(q{
unittest {} // fix:0
pure nothrow @nogc unittest {} // fix:0
@ -97,7 +96,7 @@ unittest
@system unittest {} // fix:1
pure nothrow @nogc @system unittest {} // fix:1
}
}c, sac, true);
}c, sac);
stderr.writeln("Unittest for ExplicitlyAnnotatedUnittestCheck passed.");
}

View File

@ -487,7 +487,7 @@ extern (C++) class FinalAttributeChecker(AST) : BaseAnalyzerDmd
static{void foo(){}} // fix
void foo(){}
}
}, sac, true);
}, sac);
stderr.writeln("Unittest for FinalAttributeChecker passed.");
}

View File

@ -98,8 +98,13 @@ extern (C++) class FunctionAttributeCheck(AST) : BaseAnalyzerDmd
if (fd.type is null)
return;
immutable ulong lineNum = cast(ulong) fd.loc.linnum;
immutable ulong charNum = cast(ulong) fd.loc.charnum;
string funcName = fd.ident is null ? "" : cast(string) fd.ident.toString();
ulong fileOffset = cast(ulong) fd.loc.fileOffset;
ulong lineNum = cast(ulong) fd.loc.linnum;
ulong charNum = cast(ulong) fd.loc.charnum;
ulong[2] index = [fileOffset, fileOffset + funcName.length];
ulong[2] lines = [lineNum, lineNum];
ulong[2] columns = [charNum, charNum + funcName.length];
if (inInterface)
{
@ -114,7 +119,7 @@ extern (C++) class FunctionAttributeCheck(AST) : BaseAnalyzerDmd
.front.loc.fileOffset;
addErrorMessage(
lineNum, charNum, KEY, ABSTRACT_MSG,
index, lines, columns, KEY, ABSTRACT_MSG,
[AutoFix.replacement(offset, offset + 8, "", "Remove `abstract` attribute")]
);
@ -146,7 +151,7 @@ extern (C++) class FunctionAttributeCheck(AST) : BaseAnalyzerDmd
if (!isStatic && isZeroParamProperty && !propertyRange.empty)
addErrorMessage(
lineNum, charNum, KEY, CONST_MSG,
index, lines, columns, KEY, CONST_MSG,
[
AutoFix.insertionAt(propertyRange.front.loc.fileOffset, "const "),
AutoFix.insertionAt(propertyRange.front.loc.fileOffset, "inout "),
@ -192,7 +197,7 @@ extern (C++) class FunctionAttributeCheck(AST) : BaseAnalyzerDmd
constLikeToken.loc.fileOffset + modifier.length, "(", "Make return type" ~ modifier)
.concat(AutoFix.insertionAt(parensToken.loc.fileOffset - 1, ")"));
addErrorMessage(lineNum, charNum, KEY, RETURN_MSG.format(storageTok), [fix1, fix2]);
addErrorMessage(index, lines, columns, KEY, RETURN_MSG.format(storageTok), [fix1, fix2]);
}
}
}
@ -316,7 +321,7 @@ unittest
int method(); // fix
}
}c, sac, true);
}c, sac);
stderr.writeln("Unittest for ObjectConstCheck passed.");
}

View File

@ -6,6 +6,8 @@
module dscanner.analysis.helpers;
import core.exception : AssertError;
import std.file : exists, remove;
import std.path : dirName;
import std.stdio;
import std.string;
import std.traits;
@ -13,12 +15,12 @@ import std.traits;
import dparse.ast;
import dparse.lexer : tok, Token;
import dparse.rollback_allocator;
import dscanner.analysis.base;
import dscanner.analysis.config;
import dscanner.analysis.run;
import dsymbol.modulecache : ModuleCache;
import std.experimental.allocator;
import std.experimental.allocator.mallocator;
import dscanner.analysis.rundmd;
import dscanner.utils : getModuleName;
import dmd.astbase : ASTBase;
import dmd.astcodegen;
@ -46,179 +48,6 @@ S after(S)(S value, S separator) if (isSomeString!S)
return value[i + separator.length .. $];
}
string getLineIndentation(scope const(Token)[] tokens, size_t line, const AutoFixFormatting formatting)
{
import std.algorithm : countUntil;
import std.array : array;
import std.range : repeat;
import std.string : lastIndexOfAny;
auto idx = tokens.countUntil!(a => a.line == line);
if (idx == -1 || tokens[idx].column <= 1 || !formatting.indentation.length)
return "";
auto indent = tokens[idx].column - 1;
if (formatting.indentation[0] == '\t')
return (cast(immutable)'\t').repeat(indent).array;
else
return (cast(immutable)' ').repeat(indent).array;
}
/**
* This assert function will analyze the passed in code, get the warnings,
* and make sure they match the warnings in the comments. Warnings are
* marked like so if range doesn't matter: // [warn]: Failed to do somethings.
*
* To test for start and end column, mark warnings as multi-line comments like
* this: /+
* ^^^^^ [warn]: Failed to do somethings. +/
*/
void assertAnalyzerWarnings(string code, const StaticAnalysisConfig config,
string file = __FILE__, size_t line = __LINE__)
{
import dscanner.analysis.run : parseModule;
import dparse.lexer : StringCache, Token;
StringCache cache = StringCache(StringCache.defaultBucketCount);
RollbackAllocator r;
const(Token)[] tokens;
const(Module) m = parseModule(file, cast(ubyte[]) code, &r, defaultErrorFormat, cache, false, tokens);
ModuleCache moduleCache;
// Run the code and get any warnings
MessageSet rawWarnings = analyze("test", m, config, moduleCache, tokens);
string[] codeLines = code.splitLines();
struct FoundWarning
{
string msg;
size_t startColumn, endColumn;
}
// Get the warnings ordered by line
FoundWarning[size_t] warnings;
foreach (rawWarning; rawWarnings[])
{
// Skip the warning if it is on line zero
immutable size_t rawLine = rawWarning.endLine;
if (rawLine == 0)
{
stderr.writefln("!!! Skipping warning because it is on line zero:\n%s",
rawWarning.message);
continue;
}
size_t warnLine = line - 1 + rawLine;
warnings[warnLine] = FoundWarning(
format("[warn]: %s", rawWarning.message),
rawWarning.startLine != rawWarning.endLine ? 1 : rawWarning.startColumn,
rawWarning.endColumn,
);
}
// Get all the messages from the comments in the code
FoundWarning[size_t] messages;
bool lastLineStartedComment = false;
foreach (i, codeLine; codeLines)
{
scope (exit)
lastLineStartedComment = codeLine.stripRight.endsWith("/+", "/*") > 0;
// Get the line of this code line
size_t lineNo = i + line;
if (codeLine.stripLeft.startsWith("^") && lastLineStartedComment)
{
auto start = codeLine.indexOf("^") + 1;
assert(start != 0);
auto end = codeLine.indexOfNeither("^", start) + 1;
assert(end != 0);
auto warn = codeLine.indexOf("[warn]:");
assert(warn != -1, "malformed line, expected `[warn]: text` after `^^^^^` part");
auto message = codeLine[warn .. $].stripRight;
if (message.endsWith("+/", "*/"))
message = message[0 .. $ - 2].stripRight;
messages[lineNo - 1] = FoundWarning(message, start, end);
}
// Skip if no [warn] comment
else if (codeLine.indexOf("// [warn]:") != -1)
{
// Skip if there is no comment or code
immutable string codePart = codeLine.before("// ");
immutable string commentPart = codeLine.after("// ");
if (!codePart.length || !commentPart.length)
continue;
// Get the message
messages[lineNo] = FoundWarning(commentPart);
}
}
// Throw an assert error if any messages are not listed in the warnings
foreach (lineNo, message; messages)
{
// No warning
if (lineNo !in warnings)
{
immutable string errors = "Expected warning:\n%s\nFrom source code at (%s:?):\n%s".format(messages[lineNo],
lineNo, codeLines[lineNo - line]);
throw new AssertError(errors, file, lineNo);
}
// Different warning
else if (warnings[lineNo].msg != messages[lineNo].msg)
{
immutable string errors = "Expected warning:\n%s\nBut was:\n%s\nFrom source code at (%s:?):\n%s".format(
messages[lineNo], warnings[lineNo], lineNo, codeLines[lineNo - line]);
throw new AssertError(errors, file, lineNo);
}
// specified column range
if ((message.startColumn || message.endColumn)
&& warnings[lineNo] != message)
{
import std.algorithm : max;
import std.array : array;
import std.range : repeat;
import std.string : replace;
const(char)[] expectedRange = ' '.repeat(max(0, cast(int)message.startColumn - 1)).array
~ '^'.repeat(max(0, cast(int)(message.endColumn - message.startColumn))).array;
const(char)[] actualRange;
if (!warnings[lineNo].startColumn || warnings[lineNo].startColumn == warnings[lineNo].endColumn)
actualRange = "no column range defined!";
else
actualRange = ' '.repeat(max(0, cast(int)warnings[lineNo].startColumn - 1)).array
~ '^'.repeat(max(0, cast(int)(warnings[lineNo].endColumn - warnings[lineNo].startColumn))).array;
size_t paddingWidth = max(expectedRange.length, actualRange.length);
immutable string errors = "Wrong warning range: expected %s, but was %s\nFrom source code at (%s:?):\n%s\n%-*s <-- expected\n%-*s <-- actual".format(
[message.startColumn, message.endColumn],
[warnings[lineNo].startColumn, warnings[lineNo].endColumn],
lineNo, codeLines[lineNo - line].replace("\t", " "),
paddingWidth, expectedRange,
paddingWidth, actualRange);
throw new AssertError(errors, file, lineNo);
}
}
// Throw an assert error if there were any warnings that were not expected
string[] unexpectedWarnings;
foreach (lineNo, warning; warnings)
{
// Unexpected warning
if (lineNo !in messages)
{
unexpectedWarnings ~= "%s\nFrom source code at (%s:?):\n%s".format(warning,
lineNo, codeLines[lineNo - line]);
}
}
if (unexpectedWarnings.length)
{
immutable string message = "Unexpected warnings:\n" ~ unexpectedWarnings.join("\n");
throw new AssertError(message, file, line);
}
}
/// EOL inside this project, for tests
private static immutable fileEol = q{
};
@ -233,52 +62,29 @@ private static immutable fileEol = q{
* comment. Alternatively you can also just write `// fix` to apply the only
* available suggestion.
*/
void assertAutoFix(string before, string after, const StaticAnalysisConfig config, bool useDmd = false,
const AutoFixFormatting formattingConfig = AutoFixFormatting(AutoFixFormatting.BraceStyle.otbs, "\t", 4, fileEol),
string file = __FILE__, size_t line = __LINE__)
void assertAutoFix(string before, string after, const StaticAnalysisConfig config,
string file = __FILE__, size_t line = __LINE__)
{
import dparse.lexer : StringCache, Token;
import dscanner.analysis.autofix : improveAutoFixWhitespace;
import dscanner.analysis.run : parseModule;
import std.algorithm : canFind, findSplit, map, sort;
import std.conv : to;
import std.sumtype : match;
import std.typecons : tuple, Tuple;
import std.file : exists, remove;
import std.path : dirName;
import std.stdio : File;
import dscanner.analysis.rundmd : analyzeDmd, parseDmdModule;
import dscanner.utils : getModuleName;
import dscanner.analysis.autofix : improveAutoFixWhitespace;
MessageSet rawWarnings;
if (useDmd)
auto testFileName = "test.d";
File f = File(testFileName, "w");
scope(exit)
{
auto testFileName = "test.d";
File f = File(testFileName, "w");
scope(exit)
{
assert(exists(testFileName));
remove(testFileName);
}
f.rawWrite(before);
f.close();
auto dmdModule = parseDmdModule(file, before);
rawWarnings = analyzeDmd(testFileName, dmdModule, getModuleName(dmdModule.md), config);
assert(exists(testFileName));
remove(testFileName);
}
else
{
StringCache cache = StringCache(StringCache.defaultBucketCount);
RollbackAllocator r;
const(Token)[] tokens;
const(Module) m = parseModule(file, cast(ubyte[]) before, &r, defaultErrorFormat, cache, false, tokens);
ModuleCache moduleCache;
f.rawWrite(before);
f.close();
rawWarnings = analyze("test", m, config, moduleCache, tokens, true, true, formattingConfig);
}
auto dmdModule = parseDmdModule(file, before);
rawWarnings = analyzeDmd(testFileName, dmdModule, getModuleName(dmdModule.md), config);
string[] codeLines = before.splitLines();
Tuple!(Message, int)[] toApply;
@ -301,8 +107,7 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi
immutable size_t rawLine = rawWarning.endLine;
if (rawLine == 0)
{
stderr.writefln("!!! Skipping warning because it is on line zero:\n%s",
rawWarning.message);
stderr.writefln("!!! Skipping warning because it is on line zero:\n%s", rawWarning.message);
continue;
}
@ -316,27 +121,22 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi
assert(i >= 0, "can't use negative autofix indices");
if (i >= rawWarning.autofixes.length)
throw new AssertError("autofix index out of range, diagnostic only has %s autofixes (%s)."
.format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"),
file, rawLine + line);
.format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"),file, rawLine + line);
toApply ~= tuple(rawWarning, i);
}
else
{
if (rawWarning.autofixes.length != 1)
throw new AssertError("diagnostic has %s autofixes (%s), but expected exactly one."
.format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"),
file, rawLine + line);
.format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"), file, rawLine + line);
toApply ~= tuple(rawWarning, 0);
}
}
}
foreach (i, codeLine; codeLines)
{
if (!applyLines.canFind(i) && codeLine.canFind("// fix"))
throw new AssertError("Missing expected warning for autofix on line %s"
.format(i + line), file, i + line);
}
throw new AssertError("Missing expected warning for autofix on line %s".format(i + line), file, i + line);
AutoFix.CodeReplacement[] replacements;
@ -353,10 +153,7 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi
string newCode = before;
foreach_reverse (replacement; replacements)
{
newCode = newCode[0 .. replacement.range[0]] ~ replacement.newText
~ newCode[replacement.range[1] .. $];
}
newCode = newCode[0 .. replacement.range[0]] ~ replacement.newText ~ newCode[replacement.range[1] .. $];
if (newCode != after)
{
@ -385,11 +182,6 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi
void assertAnalyzerWarningsDMD(string code, const StaticAnalysisConfig config, bool semantic = false,
string file = __FILE__, size_t line = __LINE__)
{
import std.file : exists, remove;
import std.path : dirName;
import std.stdio : File;
import dscanner.analysis.rundmd : analyzeDmd, parseDmdModule;
import dscanner.utils : getModuleName;
import dmd.globals : global;
auto testFileName = "test.d";
@ -483,6 +275,7 @@ void assertAnalyzerWarningsDMD(string code, const StaticAnalysisConfig config, b
lineNo, codeLines[lineNo - line]);
}
}
if (unexpectedWarnings.length)
{
immutable string message = "Unexpected warnings:\n" ~ unexpectedWarnings.join("\n");

View File

@ -139,7 +139,7 @@ unittest
pragma(msg, typeof((a) { return a; })); // fix:0
pragma(msg, typeof((a) => () { return a; })); // fix:1
}
}c, sac, true);
}c, sac);
stderr.writeln("Unittest for LambdaReturnCheck passed.");
}

View File

@ -68,7 +68,7 @@ unittest
if (i < cast(ptrdiff_t) a.length - 1) // fix
writeln("something");
}
}c, sac, true);
}c, sac);
stderr.writeln("Unittest for IfElseSameCheck passed.");
}

View File

@ -22,14 +22,10 @@ import std.range;
import std.stdio;
import std.typecons : scoped;
import std.experimental.allocator : CAllocatorImpl;
import std.experimental.allocator.mallocator : Mallocator;
import std.experimental.allocator.building_blocks.region : Region;
import std.experimental.allocator.building_blocks.allocator_list : AllocatorList;
import dscanner.analysis.autofix : improveAutoFixWhitespace;
import dscanner.analysis.config;
import dscanner.analysis.base;
import dscanner.analysis.rundmd;
import dscanner.analysis.style;
import dscanner.analysis.enumarrayliteral;
import dscanner.analysis.pokemon;
@ -79,23 +75,14 @@ import dscanner.analysis.redundant_storage_class;
import dscanner.analysis.unused_result;
import dscanner.analysis.cyclomatic_complexity;
import dscanner.analysis.body_on_disabled_funcs;
import dsymbol.string_interning : internString;
import dsymbol.scope_;
import dsymbol.semantic;
import dsymbol.conversion;
import dsymbol.conversion.first;
import dsymbol.conversion.second;
import dsymbol.modulecache : ModuleCache;
import dscanner.utils;
import dscanner.reports : DScannerJsonReporter, SonarQubeGenericIssueDataReporter;
import dscanner.utils;
import dmd.astbase : ASTBase;
import dmd.parse : Parser;
import dmd.frontend;
import dmd.astcodegen;
import dmd.frontend;
import dmd.globals : global;
import dmd.parse : Parser;
bool first = true;
@ -108,9 +95,6 @@ void doNothing(string, size_t, size_t, string, bool)
{
}
private alias ASTAllocator = CAllocatorImpl!(
AllocatorList!(n => Region!Mallocator(1024 * 128), Mallocator));
immutable string defaultErrorFormat = "{filepath}({line}:{column})[{type}]: {message}";
string[string] errorFormatMap()
@ -261,9 +245,7 @@ void writeJSON(Message message)
writeln(" {");
writeln(` "key": "`, message.key, `",`);
if (message.checkName !is null)
{
writeln(` "name": "`, message.checkName, `",`);
}
writeln(` "fileName": "`, message.fileName.replace("\\", "\\\\").replace(`"`, `\"`), `",`);
writeln(` "line": `, message.startLine, `,`);
writeln(` "column": `, message.startColumn, `,`);
@ -299,42 +281,32 @@ void writeJSON(Message message)
write(" }");
}
bool syntaxCheck(string[] fileNames, string errorFormat, ref StringCache stringCache, ref ModuleCache moduleCache)
bool syntaxCheck(string[] fileNames, string errorFormat)
{
StaticAnalysisConfig config = defaultStaticAnalysisConfig();
return analyze(fileNames, config, errorFormat, stringCache, moduleCache, false);
return analyze(fileNames, config, errorFormat);
}
void generateReport(string[] fileNames, const StaticAnalysisConfig config,
ref StringCache cache, ref ModuleCache moduleCache, string reportFile = "")
void generateReport(string[] fileNames, const StaticAnalysisConfig config, string reportFile = "")
{
auto reporter = new DScannerJsonReporter();
auto writeMessages = delegate void(string fileName, size_t line, size_t column, string message, bool isError){
// TODO: proper index and column ranges
reporter.addMessage(
Message(Message.Diagnostic.from(fileName, [0, 0], line, [column, column], message), "dscanner.syntax"),
isError);
};
first = true;
StatsCollector stats = new StatsCollector(BaseAnalyzerArguments.init);
auto statsCollector = new StatsCollector!ASTCodegen();
ulong lineOfCodeCount;
foreach (fileName; fileNames)
{
auto code = readFile(fileName);
// Skip files that could not be read and continue with the rest
if (code.length == 0)
continue;
RollbackAllocator r;
const(Token)[] tokens;
const Module m = parseModule(fileName, code, &r, cache, tokens, writeMessages, &lineOfCodeCount, null, null);
stats.visit(m);
MessageSet messageSet = analyze(fileName, m, config, moduleCache, tokens, true);
auto dmdModule = parseDmdModule(fileName, cast(string) code);
dmdModule.accept(statsCollector);
MessageSet messageSet = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
reporter.addMessageSet(messageSet);
}
string reportFileContent = reporter.getContent(stats, lineOfCodeCount);
string reportFileContent = reporter.getContent(statsCollector, lineOfCodeCount);
if (reportFile == "")
{
writeln(reportFileContent);
@ -347,27 +319,18 @@ void generateReport(string[] fileNames, const StaticAnalysisConfig config,
}
void generateSonarQubeGenericIssueDataReport(string[] fileNames, const StaticAnalysisConfig config,
ref StringCache cache, ref ModuleCache moduleCache, string reportFile = "")
string reportFile = "")
{
auto reporter = new SonarQubeGenericIssueDataReporter();
auto writeMessages = delegate void(string fileName, size_t line, size_t column, string message, bool isError){
// TODO: proper index and column ranges
reporter.addMessage(
Message(Message.Diagnostic.from(fileName, [0, 0], line, [column, column], message), "dscanner.syntax"),
isError);
};
foreach (fileName; fileNames)
{
auto code = readFile(fileName);
// Skip files that could not be read and continue with the rest
if (code.length == 0)
continue;
RollbackAllocator r;
const(Token)[] tokens;
const Module m = parseModule(fileName, code, &r, cache, tokens, writeMessages, null, null, null);
MessageSet messageSet = analyze(fileName, m, config, moduleCache, tokens, true);
auto dmdModule = parseDmdModule(fileName, cast(string) code);
MessageSet messageSet = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
reporter.addMessageSet(messageSet);
}
@ -388,45 +351,49 @@ void generateSonarQubeGenericIssueDataReport(string[] fileNames, const StaticAna
*
* Returns: true if there were errors or if there were warnings and `staticAnalyze` was true.
*/
bool analyze(string[] fileNames, const StaticAnalysisConfig config, string errorFormat,
ref StringCache cache, ref ModuleCache moduleCache, bool staticAnalyze = true)
bool analyze(string[] fileNames, const StaticAnalysisConfig config, string errorFormat)
{
import std.string : toStringz;
import dscanner.analysis.rundmd : parseDmdModule;
import dscanner.analysis.rundmd : analyzeDmd;
import std.file : exists, remove;
bool hasErrors;
foreach (fileName; fileNames)
{
auto code = readFile(fileName);
bool isStdin;
ubyte[] code;
if (fileName == "stdin")
{
code = readStdin();
fileName = "stdin.d";
File f = File(fileName, "w");
f.rawWrite(code);
f.close();
isStdin = true;
}
else
{
code = readFile(fileName);
}
// Skip files that could not be read and continue with the rest
if (code.length == 0)
continue;
auto dmdModule = parseDmdModule(fileName, cast(string) code);
RollbackAllocator r;
uint errorCount;
uint warningCount;
const(Token)[] tokens;
const Module m = parseModule(fileName, code, &r, errorFormat, cache, false, tokens,
null, &errorCount, &warningCount);
assert(m);
if (errorCount > 0 || (staticAnalyze && warningCount > 0))
if (global.errors > 0 || global.warnings > 0)
hasErrors = true;
MessageSet results = analyze(fileName, m, config, moduleCache, tokens, staticAnalyze);
MessageSet resultsDmd = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
foreach (result; resultsDmd[])
{
results.insert(result);
}
MessageSet results = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
if (results is null)
continue;
hasErrors = !results.empty;
foreach (result; results[])
{
hasErrors = true;
messageFunctionFormat(errorFormat, result, false, code);
if (isStdin)
{
assert(exists(fileName));
remove(fileName);
}
}
return hasErrors;
@ -437,9 +404,7 @@ bool analyze(string[] fileNames, const StaticAnalysisConfig config, string error
*
* Returns: true if there were parse errors.
*/
bool autofix(string[] fileNames, const StaticAnalysisConfig config, string errorFormat,
ref StringCache cache, ref ModuleCache moduleCache, bool autoApplySingle,
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
bool autofix(string[] fileNames, const StaticAnalysisConfig config, string errorFormat, bool autoApplySingle)
{
import std.format : format;
@ -450,16 +415,11 @@ bool autofix(string[] fileNames, const StaticAnalysisConfig config, string error
// Skip files that could not be read and continue with the rest
if (code.length == 0)
continue;
RollbackAllocator r;
uint errorCount;
uint warningCount;
const(Token)[] tokens;
const Module m = parseModule(fileName, code, &r, errorFormat, cache, false, tokens,
null, &errorCount, &warningCount);
assert(m);
if (errorCount > 0)
auto dmdModule = parseDmdModule(fileName, cast(string) code);
if (global.errors > 0)
hasErrors = true;
MessageSet results = analyze(fileName, m, config, moduleCache, tokens, true, true, overrideFormattingConfig);
MessageSet results = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
if (results is null)
continue;
@ -599,115 +559,6 @@ const(Module) parseModule(string fileName, ubyte[] code, RollbackAllocator* p,
linesOfCode, errorCount, warningCount);
}
/**
Checks whether a module is part of a user-specified include/exclude list.
The user can specify a comma-separated list of filters, everyone needs to start with
either a '+' (inclusion) or '-' (exclusion).
If no includes are specified, all modules are included.
*/
bool shouldRun(check : BaseAnalyzer)(string moduleName, const ref StaticAnalysisConfig config)
{
enum string a = check.name;
if (mixin("config." ~ a) == Check.disabled)
return false;
// By default, run the check
if (!moduleName.length)
return true;
auto filters = mixin("config.filters." ~ a);
// Check if there are filters are defined
// filters starting with a comma are invalid
if (filters.length == 0 || filters[0].length == 0)
return true;
auto includers = filters.filter!(f => f[0] == '+').map!(f => f[1..$]);
auto excluders = filters.filter!(f => f[0] == '-').map!(f => f[1..$]);
// exclusion has preference over inclusion
if (!excluders.empty && excluders.any!(s => moduleName.canFind(s)))
return false;
if (!includers.empty)
return includers.any!(s => moduleName.canFind(s));
// by default: include all modules
return true;
}
BaseAnalyzer[] getAnalyzersForModuleAndConfig(string fileName,
const(Token)[] tokens, const Module m,
const StaticAnalysisConfig analysisConfig, const Scope* moduleScope)
{
BaseAnalyzer[] checks;
string moduleName;
if (m !is null && m.moduleDeclaration !is null &&
m.moduleDeclaration.moduleName !is null &&
m.moduleDeclaration.moduleName.identifiers !is null)
moduleName = m.moduleDeclaration.moduleName.identifiers.map!(e => e.text).join(".");
BaseAnalyzerArguments args = BaseAnalyzerArguments(
fileName,
tokens,
moduleScope
);
// Add those lines to suppress warnings about unused variables until cleanup is complete
bool ignoreVar = analysisConfig.if_constraints_indent == Check.skipTests;
bool ignoreVar2 = args.skipTests;
ignoreVar = ignoreVar || ignoreVar2;
return checks;
}
MessageSet analyze(string fileName, const Module m, const StaticAnalysisConfig analysisConfig,
ref ModuleCache moduleCache, const(Token)[] tokens, bool staticAnalyze = true,
bool resolveAutoFixes = false,
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
{
import dsymbol.symbol : DSymbol;
import dscanner.analysis.autofix : resolveAutoFixFromCheck;
if (!staticAnalyze)
return null;
const(AutoFixFormatting) formattingConfig =
(resolveAutoFixes && overrideFormattingConfig is AutoFixFormatting.invalid)
? analysisConfig.getAutoFixFormattingConfig()
: overrideFormattingConfig;
scope first = new FirstPass(m, internString(fileName), &moduleCache, null);
first.run();
secondPass(first.rootSymbol, first.moduleScope, moduleCache);
auto moduleScope = first.moduleScope;
scope(exit) typeid(DSymbol).destroy(first.rootSymbol.acSymbol);
scope(exit) typeid(SemanticSymbol).destroy(first.rootSymbol);
scope(exit) typeid(Scope).destroy(first.moduleScope);
GC.disable;
scope (exit)
GC.enable;
MessageSet set = new MessageSet;
foreach (BaseAnalyzer check; getAnalyzersForModuleAndConfig(fileName, tokens, m, analysisConfig, moduleScope))
{
check.visit(m);
foreach (message; check.messages)
{
if (resolveAutoFixes)
foreach (ref autofix; message.autofixes)
autofix.resolveAutoFixFromCheck(check, m, tokens, formattingConfig);
set.insert(message);
}
}
return set;
}
version (unittest)
{
shared static this()

View File

@ -97,6 +97,22 @@ private void setupDmd()
MessageSet analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleName, const StaticAnalysisConfig config)
{
MessageSet set = new MessageSet;
auto visitors = getDmdAnalyzersForModuleAndConfig(fileName, config, moduleName);
foreach (visitor; visitors)
{
m.accept(visitor);
foreach (message; visitor.messages)
set.insert(message);
}
return set;
}
BaseAnalyzerDmd[] getDmdAnalyzersForModuleAndConfig(string fileName, const StaticAnalysisConfig config,
const char[] moduleName)
{
BaseAnalyzerDmd[] visitors;
if (moduleName.shouldRunDmd!(ObjectConstCheck!ASTCodegen)(config))
@ -322,7 +338,7 @@ MessageSet analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleN
fileName,
config.vcall_in_ctor == Check.skipTests && !ut
);
if (moduleName.shouldRunDmd!AllManCheck(config))
visitors ~= new AllManCheck(
fileName,
@ -353,15 +369,7 @@ MessageSet analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleN
config.function_attribute_check == Check.skipTests && !ut
);
foreach (visitor; visitors)
{
m.accept(visitor);
foreach (message; visitor.messages)
set.insert(message);
}
return set;
return visitors;
}
/**

View File

@ -120,7 +120,8 @@ extern (C++) class StaticIfElse(AST) : BaseAnalyzerDmd
.map!(t => t.loc.fileOffset + 1)
.array;
AutoFix autofix2 = AutoFix.insertionAt(ifStmt.endloc.fileOffset, braceEnd);
AutoFix autofix2 =
AutoFix.insertionAt(ifStmt.endloc.fileOffset, braceEnd, "Wrap '{}' block around 'if'");
foreach (fileOffset; fileOffsets)
autofix2 = autofix2.concat(AutoFix.insertionAt(fileOffset, "\t"));
autofix2 = autofix2.concat(AutoFix.insertionAt(ifStmt.loc.fileOffset, braceStart));
@ -139,13 +140,12 @@ extern (C++) class StaticIfElse(AST) : BaseAnalyzerDmd
autofix2 = autofix2.concat(AutoFix.insertionAt(ifStmt.ifbody.loc.fileOffset, "\t"));
}
ulong[2] index = [cast(ulong) s.elsebody.loc.fileOffset - 5, cast(ulong) ifStmt.loc.fileOffset + 2];
ulong[2] lines = [cast(ulong) s.elsebody.loc.linnum, cast(ulong) ifStmt.loc.linnum];
ulong[2] columns = [cast(ulong) s.elsebody.loc.charnum, cast(ulong) ifStmt.loc.charnum + 2];
addErrorMessage(
cast(ulong) ifStmt.loc.linnum, cast(ulong) s.elsebody.loc.charnum, KEY, MESSAGE,
[
AutoFix.insertionAt(ifStmt.loc.fileOffset, "static "),
autofix2
]
index, lines, columns, KEY, MESSAGE,
[AutoFix.insertionAt(ifStmt.loc.fileOffset, "static "), autofix2]
);
}
@ -227,7 +227,7 @@ unittest
}
}
}
}c, sac, true);
}c, sac);
stderr.writeln("Unittest for StaticIfElse passed.");
}

View File

@ -5,62 +5,154 @@
module dscanner.analysis.stats_collector;
import dparse.ast;
import dparse.lexer;
import dscanner.analysis.base;
final class StatsCollector : BaseAnalyzer
extern (C++) class StatsCollector(AST) : BaseAnalyzerDmd
{
alias visit = ASTVisitor.visit;
alias visit = BaseAnalyzerDmd.visit;
mixin AnalyzerInfo!"stats_collector";
this(BaseAnalyzerArguments args)
public uint interfaceCount;
public uint classCount;
public uint functionCount;
public uint templateCount;
public uint structCount;
public uint statementCount;
// TODO: Count lines of code
public uint lineOfCodeCount;
// TODO: Count undocumented public symbols
public uint undocumentedPublicSymbols;
extern (D) this(string fileName = "", bool skipTests = false)
{
args.skipTests = false; // old behavior compatibility
super(args);
super(fileName, skipTests);
}
override void visit(const Statement statement)
{
statementCount++;
statement.accept(this);
}
override void visit(const ClassDeclaration classDeclaration)
{
classCount++;
classDeclaration.accept(this);
}
override void visit(const InterfaceDeclaration interfaceDeclaration)
override void visit(AST.InterfaceDeclaration interfaceDecl)
{
interfaceCount++;
interfaceDeclaration.accept(this);
super.visit(interfaceDecl);
}
override void visit(const FunctionDeclaration functionDeclaration)
override void visit(AST.ClassDeclaration classDecl)
{
classCount++;
super.visit(classDecl);
}
override void visit(AST.FuncDeclaration funcDecl)
{
functionCount++;
functionDeclaration.accept(this);
super.visit(funcDecl);
}
override void visit(const StructDeclaration structDeclaration)
{
structCount++;
structDeclaration.accept(this);
}
override void visit(const TemplateDeclaration templateDeclaration)
override void visit(AST.TemplateDeclaration templateDecl)
{
templateCount++;
templateDeclaration.accept(this);
super.visit(templateDecl);
}
uint interfaceCount;
uint classCount;
uint functionCount;
uint templateCount;
uint structCount;
uint statementCount;
uint lineOfCodeCount;
uint undocumentedPublicSymbols;
override void visit(AST.StructDeclaration structDecl)
{
structCount++;
super.visit(structDecl);
}
mixin VisitStatement!(AST.ErrorStatement);
mixin VisitStatement!(AST.PeelStatement);
mixin VisitStatement!(AST.ScopeStatement);
mixin VisitStatement!(AST.ExpStatement);
mixin VisitStatement!(AST.ReturnStatement);
mixin VisitStatement!(AST.IfStatement);
mixin VisitStatement!(AST.CaseStatement);
mixin VisitStatement!(AST.DefaultStatement);
mixin VisitStatement!(AST.LabelStatement);
mixin VisitStatement!(AST.GotoStatement);
mixin VisitStatement!(AST.GotoDefaultStatement);
mixin VisitStatement!(AST.GotoCaseStatement);
mixin VisitStatement!(AST.BreakStatement);
mixin VisitStatement!(AST.DtorExpStatement);
mixin VisitStatement!(AST.MixinStatement);
mixin VisitStatement!(AST.ForwardingStatement);
mixin VisitStatement!(AST.DoStatement);
mixin VisitStatement!(AST.WhileStatement);
mixin VisitStatement!(AST.ForStatement);
mixin VisitStatement!(AST.ForeachStatement);
mixin VisitStatement!(AST.SwitchStatement);
mixin VisitStatement!(AST.ContinueStatement);
mixin VisitStatement!(AST.WithStatement);
mixin VisitStatement!(AST.TryCatchStatement);
mixin VisitStatement!(AST.ThrowStatement);
mixin VisitStatement!(AST.DebugStatement);
mixin VisitStatement!(AST.TryFinallyStatement);
mixin VisitStatement!(AST.ScopeGuardStatement);
mixin VisitStatement!(AST.SwitchErrorStatement);
mixin VisitStatement!(AST.UnrolledLoopStatement);
mixin VisitStatement!(AST.ForeachRangeStatement);
mixin VisitStatement!(AST.CompoundDeclarationStatement);
mixin VisitStatement!(AST.CompoundAsmStatement);
mixin VisitStatement!(AST.StaticAssertStatement);
mixin VisitStatement!(AST.CaseRangeStatement);
mixin VisitStatement!(AST.SynchronizedStatement);
mixin VisitStatement!(AST.AsmStatement);
mixin VisitStatement!(AST.InlineAsmStatement);
mixin VisitStatement!(AST.GccAsmStatement);
mixin VisitStatement!(AST.ImportStatement);
private template VisitStatement(NodeType)
{
override void visit(NodeType node)
{
statementCount++;
super.visit(node);
}
}
}
unittest
{
import std.file : exists, remove;
import std.path : dirName;
import std.stdio : File, stderr;
import dscanner.analysis.rundmd : parseDmdModule;
import dmd.astcodegen : ASTCodegen;
string code = q{
interface I {}
class C {}
void f() {}
template T() {}
struct S {}
void funcWithStatements()
{
int a = 1;
if (a == 1)
a = 2;
a++;
}
}c;
auto testFileName = "test.d";
File f = File(testFileName, "w");
scope(exit)
{
assert(exists(testFileName));
remove(testFileName);
}
f.rawWrite(code);
f.close();
auto dmdModule = parseDmdModule(testFileName, code);
auto collector = new StatsCollector!ASTCodegen();
dmdModule.accept(collector);
assert(collector.interfaceCount == 1);
assert(collector.classCount == 1);
assert(collector.functionCount == 2);
assert(collector.templateCount == 1);
assert(collector.structCount == 1);
assert(collector.statementCount == 4);
stderr.writeln("Unittest for StatsCollector passed.");
}

View File

@ -7,6 +7,7 @@ module dscanner.analysis.style;
import dscanner.analysis.base;
import dmd.astenums : LINK;
import dmd.location : Loc;
import std.conv : to;
import std.format : format;
import std.regex;
@ -39,19 +40,17 @@ extern (C++) class StyleChecker(AST) : BaseAnalyzerDmd
return;
auto moduleDecl = *moduleNode.md;
auto lineNum = cast(ulong) moduleDecl.loc.linnum;
auto charNum = cast(ulong) moduleDecl.loc.charnum;
auto moduleName = moduleDecl.id.toString();
auto moduleName = cast(string) moduleDecl.id.toString();
if (moduleName.matchFirst(moduleNameRegex).length == 0)
addErrorMessage(lineNum, charNum, KEY, MSG.format("Module/package", moduleName));
addError(moduleDecl.loc, "Module/package", moduleName);
foreach (pkg; moduleDecl.packages)
{
auto pkgName = pkg.toString();
if (pkgName.matchFirst(moduleNameRegex).length == 0)
addErrorMessage(lineNum, charNum, KEY, MSG.format("Module/package", pkgName));
addError(moduleDecl.loc, "Module/package", moduleName);
}
}
@ -80,15 +79,10 @@ extern (C++) class StyleChecker(AST) : BaseAnalyzerDmd
if (varDeclaration.storage_class & STC.manifest || varDeclaration.ident is null)
return;
auto varName = varDeclaration.ident.toString();
auto varName = cast(string) varDeclaration.ident.toString();
if (varName.matchFirst(varFunNameRegex).length == 0)
{
auto msg = MSG.format("Variable", varName);
auto lineNum = cast(ulong) varDeclaration.loc.linnum;
auto charNum = cast(ulong) varDeclaration.loc.charnum;
addErrorMessage(lineNum, charNum, KEY, msg);
}
addError(varDeclaration.loc, "Variable", varName);
}
mixin VisitNode!(AST.ClassDeclaration, "Class", aggregateNameRegex);
@ -108,17 +102,23 @@ extern (C++) class StyleChecker(AST) : BaseAnalyzerDmd
if (node.ident is null)
return;
auto nodeSymbolName = node.ident.toString();
auto nodeSymbolName = cast(string) node.ident.toString();
if (nodeSymbolName.matchFirst(regex).length == 0)
{
auto msg = MSG.format(nodeName, nodeSymbolName);
auto lineNum = cast(ulong) node.loc.linnum;
auto charNum = cast(ulong) node.loc.charnum;
addErrorMessage(lineNum, charNum, KEY, msg);
}
addError(node.loc, nodeName, nodeSymbolName);
}
}
private extern (D) void addError(Loc loc, string nodeType, string nodeName)
{
auto fileOffset = cast(ulong) loc.fileOffset;
auto lineNum = cast(ulong) loc.linnum;
auto charNum = cast(ulong) loc.charnum;
ulong[2] index = [fileOffset, fileOffset + nodeName.length];
ulong[2] lines = [lineNum, lineNum];
ulong[2] columns = [charNum, charNum + nodeName.length];
addErrorMessage(index, lines, columns, KEY, MSG.format(nodeType, nodeName));
}
}
unittest

View File

@ -325,11 +325,11 @@ else
if (autofix)
{
return .autofix(expandedArgs, config, errorFormat, cache, moduleCache, applySingleFixes) ? 1 : 0;
return .autofix(expandedArgs, config, errorFormat, applySingleFixes) ? 1 : 0;
}
else if (resolveMessage.length)
{
listAutofixes(config, resolveMessage, usingStdin, usingStdin ? "stdin" : args[1], &cache, moduleCache);
listAutofixes(config, resolveMessage, usingStdin, usingStdin ? "stdin" : args[1]);
return 0;
}
else if (report)
@ -341,19 +341,19 @@ else
goto case;
case "":
case "dscanner":
generateReport(expandedArgs, config, cache, moduleCache, reportFile);
generateReport(expandedArgs, config, reportFile);
break;
case "sonarQubeGenericIssueData":
generateSonarQubeGenericIssueDataReport(expandedArgs, config, cache, moduleCache, reportFile);
generateSonarQubeGenericIssueDataReport(expandedArgs, config, reportFile);
break;
}
}
else
return analyze(expandedArgs, config, errorFormat, cache, moduleCache, true) ? 1 : 0;
return analyze(expandedArgs, config, errorFormat) ? 1 : 0;
}
else if (syntaxCheck)
{
return .syntaxCheck(usingStdin ? ["stdin"] : expandedArgs, errorFormat, cache, moduleCache) ? 1 : 0;
return .syntaxCheck(usingStdin ? ["stdin"] : expandedArgs, errorFormat) ? 1 : 0;
}
else
{

View File

@ -37,7 +37,7 @@ class DScannerJsonReporter
_issues ~= toIssue(message, isError);
}
string getContent(StatsCollector stats, ulong lineOfCodeCount)
string getContent(AST)(StatsCollector!AST stats, ulong lineOfCodeCount)
{
JSONValue result = [
"issues" : JSONValue(_issues.data.map!(e => toJson(e)).array),

View File

@ -41,7 +41,12 @@ cd "$DSCANNER_DIR/tests"
# IDE APIs
# --------
# checking that reporting format stays consistent or only gets extended
diff <(../bin/dscanner --report it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.report.json)
if [[ $1 == "Windows" ]]; then
diff <(../bin/dscanner --report it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix_windows.report.json)
else
diff <(../bin/dscanner --report it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.report.json)
fi
diff <(../bin/dscanner --resolveMessage b16 it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.autofix.json)
# CLI tests

View File

@ -1,36 +1,36 @@
[
{
"name": "Mark function `const`",
"name": "Insert `const`",
"replacements": [
{
"newText": " const",
"newText": "const ",
"range": [
24,
24
25,
25
]
}
]
},
{
"name": "Mark function `inout`",
"name": "Insert `inout`",
"replacements": [
{
"newText": " inout",
"newText": "inout ",
"range": [
24,
24
25,
25
]
}
]
},
{
"name": "Mark function `immutable`",
"name": "Insert `immutable`",
"replacements": [
{
"newText": " immutable",
"newText": "immutable ",
"range": [
24,
24
25,
25
]
}
]

View File

@ -18,37 +18,37 @@
"type": "warn",
"autofixes": [
{
"name": "Mark function `const`",
"name": "Insert `const`",
"replacements": [
{
"newText": " const",
"newText": "const ",
"range": [
24,
24
25,
25
]
}
]
},
{
"name": "Mark function `inout`",
"name": "Insert `inout`",
"replacements": [
{
"newText": " inout",
"newText": "inout ",
"range": [
24,
24
25,
25
]
}
]
},
{
"name": "Mark function `immutable`",
"name": "Insert `immutable`",
"replacements": [
{
"newText": " immutable",
"newText": "immutable ",
"range": [
24,
24
25,
25
]
}
]
@ -71,10 +71,53 @@
},
{
"name": "Wrap '{}' block around 'if'",
"replacements": "resolvable"
"replacements": [
{
"newText": " {\n\t\t\t",
"range": [
69,
69
]
},
{
"newText": "\t",
"range": [
76,
76
]
},
{
"newText": "\t",
"range": [
80,
80
]
},
{
"newText": "\t",
"range": [
82,
82
]
},
{
"newText": "\t",
"range": [
84,
84
]
},
{
"newText": "}\n\t",
"range": [
85,
85
]
}
]
}
],
"column": 3,
"column": 8,
"endColumn": 10,
"endIndex": 71,
"endLine": 8,
@ -88,8 +131,8 @@
"type": "warn"
}
],
"lineOfCodeCount": 3,
"statementCount": 4,
"lineOfCodeCount": 0,
"statementCount": 2,
"structCount": 1,
"templateCount": 0,
"undocumentedPublicSymbols": 0

View File

@ -0,0 +1,139 @@
{
"classCount": 0,
"functionCount": 1,
"interfaceCount": 0,
"issues": [
{
"column": 6,
"endColumn": 12,
"endIndex": 22,
"endLine": 3,
"fileName": "it/autofix_ide/source_autofix.d",
"index": 16,
"key": "dscanner.confusing.function_attributes",
"line": 3,
"message": "Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'.",
"name": "function_attribute_check",
"supplemental": [],
"type": "warn",
"autofixes": [
{
"name": "Insert `const`",
"replacements": [
{
"newText": "const ",
"range": [
25,
25
]
}
]
},
{
"name": "Insert `inout`",
"replacements": [
{
"newText": "inout ",
"range": [
25,
25
]
}
]
},
{
"name": "Insert `immutable`",
"replacements": [
{
"newText": "immutable ",
"range": [
25,
25
]
}
]
}
]
},
{
"autofixes": [
{
"name": "Insert `static`",
"replacements": [
{
"newText": "static ",
"range": [
69,
69
]
}
]
},
{
"name": "Wrap '{}' block around 'if'",
"replacements": [
{
"newText": " {\r\n\t\t\t",
"range": [
69,
69
]
},
{
"newText": "\t",
"range": [
76,
76
]
},
{
"newText": "\t",
"range": [
80,
80
]
},
{
"newText": "\t",
"range": [
82,
82
]
},
{
"newText": "\t",
"range": [
84,
84
]
},
{
"newText": "}\r\n\t",
"range": [
85,
85
]
}
]
}
],
"column": 8,
"endColumn": 10,
"endIndex": 71,
"endLine": 8,
"fileName": "it/autofix_ide/source_autofix.d",
"index": 64,
"key": "dscanner.suspicious.static_if_else",
"line": 8,
"message": "Mismatched static if. Use 'else static if' here.",
"name": "static_if_else_check",
"supplemental": [],
"type": "warn"
}
],
"lineOfCodeCount": 0,
"statementCount": 2,
"structCount": 1,
"templateCount": 0,
"undocumentedPublicSymbols": 0
}