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

View File

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

View File

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

View File

@ -2,124 +2,23 @@ module dscanner.analysis.autofix;
import std.algorithm : filter, findSplit; import std.algorithm : filter, findSplit;
import std.conv : to; import std.conv : to;
import std.file : exists, remove;
import std.functional : toDelegate; import std.functional : toDelegate;
import std.stdio; import std.stdio;
import dparse.lexer; import dscanner.analysis.base : AutoFix, AutoFixFormatting, BaseAnalyzer, BaseAnalyzerDmd, Message;
import dparse.rollback_allocator;
import dparse.ast : Module;
import dsymbol.modulecache : ModuleCache;
import dscanner.analysis.base : AutoFix, AutoFixFormatting, BaseAnalyzer, Message;
import dscanner.analysis.config : StaticAnalysisConfig; import dscanner.analysis.config : StaticAnalysisConfig;
import dscanner.analysis.run : analyze, doNothing; import dscanner.analysis.run : analyze, doNothing;
import dscanner.utils : readFile, readStdin; import dscanner.analysis.rundmd;
import dscanner.utils : getModuleName, 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?!");
}
void listAutofixes( void listAutofixes(
StaticAnalysisConfig config, StaticAnalysisConfig config,
string resolveMessage, string resolveMessage,
bool usingStdin, bool usingStdin,
string fileName, string fileName
StringCache* cache,
ref ModuleCache moduleCache
) )
{ {
import dparse.parser : parseModule;
import dscanner.analysis.base : Message;
import std.format : format; import std.format : format;
import std.json : JSONValue; import std.json : JSONValue;
@ -145,30 +44,35 @@ void listAutofixes(
bool matchesCursor(Message m) bool matchesCursor(Message m)
{ {
return isBytes return isBytes ? req.bytes >= m.startIndex && req.bytes <= m.endIndex
? req.bytes >= m.startIndex && req.bytes <= m.endIndex
: req.line >= m.startLine && req.line <= m.endLine : req.line >= m.startLine && req.line <= m.endLine
&& (req.line > m.startLine || req.column >= m.startColumn) && (req.line > m.startLine || req.column >= m.startColumn)
&& (req.line < m.endLine || req.column <= m.endColumn); && (req.line < m.endLine || req.column <= m.endColumn);
} }
RollbackAllocator rba; ubyte[] code;
LexerConfig lexerConfig; if (usingStdin)
lexerConfig.fileName = fileName; {
lexerConfig.stringBehavior = StringBehavior.source; code = readStdin();
auto tokens = getTokensForParser(usingStdin ? readStdin() fileName = "stdin.d";
: readFile(fileName), lexerConfig, cache); File f = File(fileName, "w");
auto mod = parseModule(tokens, fileName, &rba, toDelegate(&doNothing)); 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) with (stdout.lockingTextWriter)
{ {
put("["); put("[");
foreach (message; messages[].filter!matchesCursor) foreach (message; messages[].filter!matchesCursor)
{ {
resolveAutoFixes(message, fileName, moduleCache, tokens, mod, config);
foreach (i, autofix; message.autofixes) foreach (i, autofix; message.autofixes)
{ {
put(i == 0 ? "\n" : ",\n"); put(i == 0 ? "\n" : ",\n");
@ -191,6 +95,12 @@ void listAutofixes(
put("\n]"); put("\n]");
} }
stdout.flush(); stdout.flush();
if (usingStdin)
{
assert(exists(fileName));
remove(fileName);
}
} }
void improveAutoFixWhitespace(scope const(char)[] code, AutoFix.CodeReplacement[] replacements) 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 assert(replacement.range[0] >= 0 && replacement.range[0] < code.length
&& replacement.range[1] >= 0 && replacement.range[1] < 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() void growRight()
{ {

View File

@ -82,31 +82,6 @@ struct AutoFix
return ret; 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) static AutoFix replacement(size_t[2] range, string newText, string name)
{ {
AutoFix ret; AutoFix ret;
@ -117,17 +92,6 @@ struct AutoFix
return ret; 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) static AutoFix insertionAt(size_t index, string content, string name = null)
{ {
assert(content.length > 0, "generated auto fix inserting text without content"); assert(content.length > 0, "generated auto fix inserting text without content");
@ -143,24 +107,6 @@ struct AutoFix
return ret; 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 AutoFix concat(AutoFix other) const
{ {
import std.algorithm : sort; import std.algorithm : sort;
@ -291,33 +237,6 @@ struct Message
deprecated("Use startLine instead") alias line = startLine; deprecated("Use startLine instead") alias line = startLine;
deprecated("Use startColumn instead") alias column = startColumn; 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) 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); 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. /// the `BaseAnalyzer.resolveAutoFix` method with.
AutoFix[] autofixes; 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.fileName = fileName;
diagnostic.startLine = diagnostic.endLine = line; diagnostic.startLine = diagnostic.endLine = line;
@ -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 * The file name
*/ */
@ -555,343 +432,6 @@ protected:
MessageSet _messages; 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. * Visitor that implements the AST traversal logic.
* Supports collecting error messages * Supports collecting error messages
@ -911,7 +451,7 @@ extern(C++) class BaseAnalyzerDmd : SemanticTimeTransitiveVisitor
* Ensures that template AnalyzerInfo is instantiated in all classes * Ensures that template AnalyzerInfo is instantiated in all classes
* deriving from this class * deriving from this class
*/ */
extern(D) protected string getName() extern(D) string getName()
{ {
assert(0); assert(0);
} }
@ -940,6 +480,19 @@ protected:
_messages.insert(Message(fileName, line, column, key, message, getName(), autofixes)); _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; extern (D) bool skipTests;
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,8 @@
module dscanner.analysis.helpers; module dscanner.analysis.helpers;
import core.exception : AssertError; import core.exception : AssertError;
import std.file : exists, remove;
import std.path : dirName;
import std.stdio; import std.stdio;
import std.string; import std.string;
import std.traits; import std.traits;
@ -13,12 +15,12 @@ import std.traits;
import dparse.ast; import dparse.ast;
import dparse.lexer : tok, Token; import dparse.lexer : tok, Token;
import dparse.rollback_allocator; import dparse.rollback_allocator;
import dscanner.analysis.base; import dscanner.analysis.base;
import dscanner.analysis.config; import dscanner.analysis.config;
import dscanner.analysis.run; import dscanner.analysis.run;
import dsymbol.modulecache : ModuleCache; import dscanner.analysis.rundmd;
import std.experimental.allocator; import dscanner.utils : getModuleName;
import std.experimental.allocator.mallocator;
import dmd.astbase : ASTBase; import dmd.astbase : ASTBase;
import dmd.astcodegen; import dmd.astcodegen;
@ -46,179 +48,6 @@ S after(S)(S value, S separator) if (isSomeString!S)
return value[i + separator.length .. $]; 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 /// EOL inside this project, for tests
private static immutable fileEol = q{ private static immutable fileEol = q{
}; };
@ -233,27 +62,16 @@ private static immutable fileEol = q{
* comment. Alternatively you can also just write `// fix` to apply the only * comment. Alternatively you can also just write `// fix` to apply the only
* available suggestion. * available suggestion.
*/ */
void assertAutoFix(string before, string after, const StaticAnalysisConfig config, bool useDmd = false, void assertAutoFix(string before, string after, const StaticAnalysisConfig config,
const AutoFixFormatting formattingConfig = AutoFixFormatting(AutoFixFormatting.BraceStyle.otbs, "\t", 4, fileEol),
string file = __FILE__, size_t line = __LINE__) 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.algorithm : canFind, findSplit, map, sort;
import std.conv : to; import std.conv : to;
import std.sumtype : match; import std.sumtype : match;
import std.typecons : tuple, Tuple; import std.typecons : tuple, Tuple;
import std.file : exists, remove; import dscanner.analysis.autofix : improveAutoFixWhitespace;
import std.path : dirName;
import std.stdio : File;
import dscanner.analysis.rundmd : analyzeDmd, parseDmdModule;
import dscanner.utils : getModuleName;
MessageSet rawWarnings; MessageSet rawWarnings;
if (useDmd)
{
auto testFileName = "test.d"; auto testFileName = "test.d";
File f = File(testFileName, "w"); File f = File(testFileName, "w");
scope(exit) scope(exit)
@ -267,18 +85,6 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi
auto dmdModule = parseDmdModule(file, before); auto dmdModule = parseDmdModule(file, before);
rawWarnings = analyzeDmd(testFileName, dmdModule, getModuleName(dmdModule.md), config); rawWarnings = analyzeDmd(testFileName, dmdModule, getModuleName(dmdModule.md), config);
}
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;
rawWarnings = analyze("test", m, config, moduleCache, tokens, true, true, formattingConfig);
}
string[] codeLines = before.splitLines(); string[] codeLines = before.splitLines();
Tuple!(Message, int)[] toApply; Tuple!(Message, int)[] toApply;
@ -301,8 +107,7 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi
immutable size_t rawLine = rawWarning.endLine; immutable size_t rawLine = rawWarning.endLine;
if (rawLine == 0) if (rawLine == 0)
{ {
stderr.writefln("!!! Skipping warning because it is on line zero:\n%s", stderr.writefln("!!! Skipping warning because it is on line zero:\n%s", rawWarning.message);
rawWarning.message);
continue; continue;
} }
@ -316,27 +121,22 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi
assert(i >= 0, "can't use negative autofix indices"); assert(i >= 0, "can't use negative autofix indices");
if (i >= rawWarning.autofixes.length) if (i >= rawWarning.autofixes.length)
throw new AssertError("autofix index out of range, diagnostic only has %s autofixes (%s)." throw new AssertError("autofix index out of range, diagnostic only has %s autofixes (%s)."
.format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"), .format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"),file, rawLine + line);
file, rawLine + line);
toApply ~= tuple(rawWarning, i); toApply ~= tuple(rawWarning, i);
} }
else else
{ {
if (rawWarning.autofixes.length != 1) if (rawWarning.autofixes.length != 1)
throw new AssertError("diagnostic has %s autofixes (%s), but expected exactly one." throw new AssertError("diagnostic has %s autofixes (%s), but expected exactly one."
.format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"), .format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"), file, rawLine + line);
file, rawLine + line);
toApply ~= tuple(rawWarning, 0); toApply ~= tuple(rawWarning, 0);
} }
} }
} }
foreach (i, codeLine; codeLines) foreach (i, codeLine; codeLines)
{
if (!applyLines.canFind(i) && codeLine.canFind("// fix")) if (!applyLines.canFind(i) && codeLine.canFind("// fix"))
throw new AssertError("Missing expected warning for autofix on line %s" throw new AssertError("Missing expected warning for autofix on line %s".format(i + line), file, i + line);
.format(i + line), file, i + line);
}
AutoFix.CodeReplacement[] replacements; AutoFix.CodeReplacement[] replacements;
@ -353,10 +153,7 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi
string newCode = before; string newCode = before;
foreach_reverse (replacement; replacements) 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) 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, void assertAnalyzerWarningsDMD(string code, const StaticAnalysisConfig config, bool semantic = false,
string file = __FILE__, size_t line = __LINE__) 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; import dmd.globals : global;
auto testFileName = "test.d"; auto testFileName = "test.d";
@ -483,6 +275,7 @@ void assertAnalyzerWarningsDMD(string code, const StaticAnalysisConfig config, b
lineNo, codeLines[lineNo - line]); lineNo, codeLines[lineNo - line]);
} }
} }
if (unexpectedWarnings.length) if (unexpectedWarnings.length)
{ {
immutable string message = "Unexpected warnings:\n" ~ unexpectedWarnings.join("\n"); 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:0
pragma(msg, typeof((a) => () { return a; })); // fix:1 pragma(msg, typeof((a) => () { return a; })); // fix:1
} }
}c, sac, true); }c, sac);
stderr.writeln("Unittest for LambdaReturnCheck passed."); stderr.writeln("Unittest for LambdaReturnCheck passed.");
} }

View File

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

View File

@ -22,14 +22,10 @@ import std.range;
import std.stdio; import std.stdio;
import std.typecons : scoped; 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.autofix : improveAutoFixWhitespace;
import dscanner.analysis.config; import dscanner.analysis.config;
import dscanner.analysis.base; import dscanner.analysis.base;
import dscanner.analysis.rundmd;
import dscanner.analysis.style; import dscanner.analysis.style;
import dscanner.analysis.enumarrayliteral; import dscanner.analysis.enumarrayliteral;
import dscanner.analysis.pokemon; import dscanner.analysis.pokemon;
@ -79,23 +75,14 @@ import dscanner.analysis.redundant_storage_class;
import dscanner.analysis.unused_result; import dscanner.analysis.unused_result;
import dscanner.analysis.cyclomatic_complexity; import dscanner.analysis.cyclomatic_complexity;
import dscanner.analysis.body_on_disabled_funcs; 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.reports : DScannerJsonReporter, SonarQubeGenericIssueDataReporter;
import dscanner.utils;
import dmd.astbase : ASTBase; import dmd.astbase : ASTBase;
import dmd.parse : Parser;
import dmd.frontend;
import dmd.astcodegen; import dmd.astcodegen;
import dmd.frontend;
import dmd.globals : global;
import dmd.parse : Parser;
bool first = true; 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}"; immutable string defaultErrorFormat = "{filepath}({line}:{column})[{type}]: {message}";
string[string] errorFormatMap() string[string] errorFormatMap()
@ -261,9 +245,7 @@ void writeJSON(Message message)
writeln(" {"); writeln(" {");
writeln(` "key": "`, message.key, `",`); writeln(` "key": "`, message.key, `",`);
if (message.checkName !is null) if (message.checkName !is null)
{
writeln(` "name": "`, message.checkName, `",`); writeln(` "name": "`, message.checkName, `",`);
}
writeln(` "fileName": "`, message.fileName.replace("\\", "\\\\").replace(`"`, `\"`), `",`); writeln(` "fileName": "`, message.fileName.replace("\\", "\\\\").replace(`"`, `\"`), `",`);
writeln(` "line": `, message.startLine, `,`); writeln(` "line": `, message.startLine, `,`);
writeln(` "column": `, message.startColumn, `,`); writeln(` "column": `, message.startColumn, `,`);
@ -299,42 +281,32 @@ void writeJSON(Message message)
write(" }"); write(" }");
} }
bool syntaxCheck(string[] fileNames, string errorFormat, ref StringCache stringCache, ref ModuleCache moduleCache) bool syntaxCheck(string[] fileNames, string errorFormat)
{ {
StaticAnalysisConfig config = defaultStaticAnalysisConfig(); StaticAnalysisConfig config = defaultStaticAnalysisConfig();
return analyze(fileNames, config, errorFormat, stringCache, moduleCache, false); return analyze(fileNames, config, errorFormat);
} }
void generateReport(string[] fileNames, const StaticAnalysisConfig config, void generateReport(string[] fileNames, const StaticAnalysisConfig config, string reportFile = "")
ref StringCache cache, ref ModuleCache moduleCache, string reportFile = "")
{ {
auto reporter = new DScannerJsonReporter(); 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; first = true;
StatsCollector stats = new StatsCollector(BaseAnalyzerArguments.init); auto statsCollector = new StatsCollector!ASTCodegen();
ulong lineOfCodeCount; ulong lineOfCodeCount;
foreach (fileName; fileNames) foreach (fileName; fileNames)
{ {
auto code = readFile(fileName); auto code = readFile(fileName);
// Skip files that could not be read and continue with the rest // Skip files that could not be read and continue with the rest
if (code.length == 0) if (code.length == 0)
continue; continue;
RollbackAllocator r; auto dmdModule = parseDmdModule(fileName, cast(string) code);
const(Token)[] tokens; dmdModule.accept(statsCollector);
const Module m = parseModule(fileName, code, &r, cache, tokens, writeMessages, &lineOfCodeCount, null, null); MessageSet messageSet = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
stats.visit(m);
MessageSet messageSet = analyze(fileName, m, config, moduleCache, tokens, true);
reporter.addMessageSet(messageSet); reporter.addMessageSet(messageSet);
} }
string reportFileContent = reporter.getContent(stats, lineOfCodeCount); string reportFileContent = reporter.getContent(statsCollector, lineOfCodeCount);
if (reportFile == "") if (reportFile == "")
{ {
writeln(reportFileContent); writeln(reportFileContent);
@ -347,27 +319,18 @@ void generateReport(string[] fileNames, const StaticAnalysisConfig config,
} }
void generateSonarQubeGenericIssueDataReport(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 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) foreach (fileName; fileNames)
{ {
auto code = readFile(fileName); auto code = readFile(fileName);
// Skip files that could not be read and continue with the rest // Skip files that could not be read and continue with the rest
if (code.length == 0) if (code.length == 0)
continue; continue;
RollbackAllocator r; auto dmdModule = parseDmdModule(fileName, cast(string) code);
const(Token)[] tokens; MessageSet messageSet = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
const Module m = parseModule(fileName, code, &r, cache, tokens, writeMessages, null, null, null);
MessageSet messageSet = analyze(fileName, m, config, moduleCache, tokens, true);
reporter.addMessageSet(messageSet); 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. * Returns: true if there were errors or if there were warnings and `staticAnalyze` was true.
*/ */
bool analyze(string[] fileNames, const StaticAnalysisConfig config, string errorFormat, bool analyze(string[] fileNames, const StaticAnalysisConfig config, string errorFormat)
ref StringCache cache, ref ModuleCache moduleCache, bool staticAnalyze = true)
{ {
import std.string : toStringz; import std.file : exists, remove;
import dscanner.analysis.rundmd : parseDmdModule;
import dscanner.analysis.rundmd : analyzeDmd;
bool hasErrors; bool hasErrors;
foreach (fileName; fileNames) 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 // Skip files that could not be read and continue with the rest
if (code.length == 0) if (code.length == 0)
continue; continue;
auto dmdModule = parseDmdModule(fileName, cast(string) code); auto dmdModule = parseDmdModule(fileName, cast(string) code);
if (global.errors > 0 || global.warnings > 0)
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))
hasErrors = true; hasErrors = true;
MessageSet results = analyze(fileName, m, config, moduleCache, tokens, staticAnalyze); MessageSet results = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
MessageSet resultsDmd = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
foreach (result; resultsDmd[])
{
results.insert(result);
}
if (results is null) if (results is null)
continue; continue;
hasErrors = !results.empty;
foreach (result; results[]) foreach (result; results[])
{
hasErrors = true;
messageFunctionFormat(errorFormat, result, false, code); messageFunctionFormat(errorFormat, result, false, code);
if (isStdin)
{
assert(exists(fileName));
remove(fileName);
} }
} }
return hasErrors; return hasErrors;
@ -437,9 +404,7 @@ bool analyze(string[] fileNames, const StaticAnalysisConfig config, string error
* *
* Returns: true if there were parse errors. * Returns: true if there were parse errors.
*/ */
bool autofix(string[] fileNames, const StaticAnalysisConfig config, string errorFormat, bool autofix(string[] fileNames, const StaticAnalysisConfig config, string errorFormat, bool autoApplySingle)
ref StringCache cache, ref ModuleCache moduleCache, bool autoApplySingle,
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
{ {
import std.format : format; 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 // Skip files that could not be read and continue with the rest
if (code.length == 0) if (code.length == 0)
continue; continue;
RollbackAllocator r; auto dmdModule = parseDmdModule(fileName, cast(string) code);
uint errorCount; if (global.errors > 0)
uint warningCount;
const(Token)[] tokens;
const Module m = parseModule(fileName, code, &r, errorFormat, cache, false, tokens,
null, &errorCount, &warningCount);
assert(m);
if (errorCount > 0)
hasErrors = true; 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) if (results is null)
continue; continue;
@ -599,115 +559,6 @@ const(Module) parseModule(string fileName, ubyte[] code, RollbackAllocator* p,
linesOfCode, errorCount, warningCount); 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) version (unittest)
{ {
shared static this() 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 analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleName, const StaticAnalysisConfig config)
{ {
MessageSet set = new MessageSet; 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; BaseAnalyzerDmd[] visitors;
if (moduleName.shouldRunDmd!(ObjectConstCheck!ASTCodegen)(config)) if (moduleName.shouldRunDmd!(ObjectConstCheck!ASTCodegen)(config))
@ -353,15 +369,7 @@ MessageSet analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleN
config.function_attribute_check == Check.skipTests && !ut config.function_attribute_check == Check.skipTests && !ut
); );
foreach (visitor; visitors) return visitors;
{
m.accept(visitor);
foreach (message; visitor.messages)
set.insert(message);
}
return set;
} }
/** /**

View File

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

View File

@ -5,62 +5,154 @@
module dscanner.analysis.stats_collector; module dscanner.analysis.stats_collector;
import dparse.ast;
import dparse.lexer;
import dscanner.analysis.base; 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(fileName, skipTests);
super(args);
} }
override void visit(const Statement statement) override void visit(AST.InterfaceDeclaration interfaceDecl)
{
statementCount++;
statement.accept(this);
}
override void visit(const ClassDeclaration classDeclaration)
{
classCount++;
classDeclaration.accept(this);
}
override void visit(const InterfaceDeclaration interfaceDeclaration)
{ {
interfaceCount++; 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++; functionCount++;
functionDeclaration.accept(this); super.visit(funcDecl);
} }
override void visit(const StructDeclaration structDeclaration) override void visit(AST.TemplateDeclaration templateDecl)
{
structCount++;
structDeclaration.accept(this);
}
override void visit(const TemplateDeclaration templateDeclaration)
{ {
templateCount++; templateCount++;
templateDeclaration.accept(this); super.visit(templateDecl);
} }
uint interfaceCount; override void visit(AST.StructDeclaration structDecl)
uint classCount; {
uint functionCount; structCount++;
uint templateCount; super.visit(structDecl);
uint structCount; }
uint statementCount;
uint lineOfCodeCount; mixin VisitStatement!(AST.ErrorStatement);
uint undocumentedPublicSymbols; 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 dscanner.analysis.base;
import dmd.astenums : LINK; import dmd.astenums : LINK;
import dmd.location : Loc;
import std.conv : to; import std.conv : to;
import std.format : format; import std.format : format;
import std.regex; import std.regex;
@ -39,19 +40,17 @@ extern (C++) class StyleChecker(AST) : BaseAnalyzerDmd
return; return;
auto moduleDecl = *moduleNode.md; auto moduleDecl = *moduleNode.md;
auto lineNum = cast(ulong) moduleDecl.loc.linnum; auto moduleName = cast(string) moduleDecl.id.toString();
auto charNum = cast(ulong) moduleDecl.loc.charnum;
auto moduleName = moduleDecl.id.toString();
if (moduleName.matchFirst(moduleNameRegex).length == 0) 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) foreach (pkg; moduleDecl.packages)
{ {
auto pkgName = pkg.toString(); auto pkgName = pkg.toString();
if (pkgName.matchFirst(moduleNameRegex).length == 0) 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) if (varDeclaration.storage_class & STC.manifest || varDeclaration.ident is null)
return; return;
auto varName = varDeclaration.ident.toString(); auto varName = cast(string) varDeclaration.ident.toString();
if (varName.matchFirst(varFunNameRegex).length == 0) if (varName.matchFirst(varFunNameRegex).length == 0)
{ addError(varDeclaration.loc, "Variable", varName);
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);
}
} }
mixin VisitNode!(AST.ClassDeclaration, "Class", aggregateNameRegex); mixin VisitNode!(AST.ClassDeclaration, "Class", aggregateNameRegex);
@ -108,16 +102,22 @@ extern (C++) class StyleChecker(AST) : BaseAnalyzerDmd
if (node.ident is null) if (node.ident is null)
return; return;
auto nodeSymbolName = node.ident.toString(); auto nodeSymbolName = cast(string) node.ident.toString();
if (nodeSymbolName.matchFirst(regex).length == 0) if (nodeSymbolName.matchFirst(regex).length == 0)
addError(node.loc, nodeName, nodeSymbolName);
}
}
private extern (D) void addError(Loc loc, string nodeType, string nodeName)
{ {
auto msg = MSG.format(nodeName, nodeSymbolName); auto fileOffset = cast(ulong) loc.fileOffset;
auto lineNum = cast(ulong) node.loc.linnum; auto lineNum = cast(ulong) loc.linnum;
auto charNum = cast(ulong) node.loc.charnum; auto charNum = cast(ulong) loc.charnum;
addErrorMessage(lineNum, charNum, KEY, msg); 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));
} }
} }

View File

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

View File

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

View File

@ -41,7 +41,12 @@ cd "$DSCANNER_DIR/tests"
# IDE APIs # IDE APIs
# -------- # --------
# checking that reporting format stays consistent or only gets extended # checking that reporting format stays consistent or only gets extended
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) 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) diff <(../bin/dscanner --resolveMessage b16 it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.autofix.json)
# CLI tests # CLI tests

View File

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

View File

@ -18,37 +18,37 @@
"type": "warn", "type": "warn",
"autofixes": [ "autofixes": [
{ {
"name": "Mark function `const`", "name": "Insert `const`",
"replacements": [ "replacements": [
{ {
"newText": "const ", "newText": "const ",
"range": [ "range": [
24, 25,
24 25
] ]
} }
] ]
}, },
{ {
"name": "Mark function `inout`", "name": "Insert `inout`",
"replacements": [ "replacements": [
{ {
"newText": "inout ", "newText": "inout ",
"range": [ "range": [
24, 25,
24 25
] ]
} }
] ]
}, },
{ {
"name": "Mark function `immutable`", "name": "Insert `immutable`",
"replacements": [ "replacements": [
{ {
"newText": "immutable ", "newText": "immutable ",
"range": [ "range": [
24, 25,
24 25
] ]
} }
] ]
@ -71,10 +71,53 @@
}, },
{ {
"name": "Wrap '{}' block around 'if'", "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, "endColumn": 10,
"endIndex": 71, "endIndex": 71,
"endLine": 8, "endLine": 8,
@ -88,8 +131,8 @@
"type": "warn" "type": "warn"
} }
], ],
"lineOfCodeCount": 3, "lineOfCodeCount": 0,
"statementCount": 4, "statementCount": 2,
"structCount": 1, "structCount": 1,
"templateCount": 0, "templateCount": 0,
"undocumentedPublicSymbols": 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
}