5 changes: 2 additions & 3 deletions src/dscanner/analysis/del.d
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 5 additions & 6 deletions src/dscanner/analysis/explicitly_annotated_unittests.d
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -78,7 +77,7 @@ unittest
}
}c, sac);

//// nested
// nested
assertAutoFix(q{
unittest {} // fix:0
pure nothrow @nogc unittest {} // fix:0
Expand Down
155 changes: 0 additions & 155 deletions src/dscanner/analysis/helpers.d
Original file line number Diff line number Diff line change
Expand Up @@ -64,161 +64,6 @@ string getLineIndentation(scope const(Token)[] tokens, size_t line, const AutoFi
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{
};
Expand Down
13 changes: 7 additions & 6 deletions src/dscanner/analysis/run.d
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ 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;
Expand Down Expand Up @@ -330,7 +331,8 @@ void generateReport(string[] fileNames, const StaticAnalysisConfig config,
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);
MessageSet messageSet = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
reporter.addMessageSet(messageSet);
}

Expand Down Expand Up @@ -367,7 +369,8 @@ void generateSonarQubeGenericIssueDataReport(string[] fileNames, const StaticAna
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);
}

Expand All @@ -392,9 +395,6 @@ bool analyze(string[] fileNames, const StaticAnalysisConfig config, string error
ref StringCache cache, ref ModuleCache moduleCache, bool staticAnalyze = true)
{
import std.string : toStringz;
import dscanner.analysis.rundmd : parseDmdModule;

import dscanner.analysis.rundmd : analyzeDmd;

bool hasErrors;
foreach (fileName; fileNames)
Expand Down Expand Up @@ -459,7 +459,8 @@ bool autofix(string[] fileNames, const StaticAnalysisConfig config, string error
assert(m);
if (errorCount > 0)
hasErrors = true;
MessageSet results = analyze(fileName, m, config, moduleCache, tokens, true, true, overrideFormattingConfig);
auto dmdModule = parseDmdModule(fileName, cast(string) code);
MessageSet results = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
if (results is null)
continue;

Expand Down
3 changes: 2 additions & 1 deletion src/dscanner/analysis/static_if_else.d
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
12 changes: 12 additions & 0 deletions tests/first-code.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
struct S
{
int myProp() @property
{
static if (a)
{
}
else if (b)
{
}
}
}
139 changes: 139 additions & 0 deletions tests/first-res.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
{
"classCount": 0,
"functionCount": 1,
"interfaceCount": 0,
"issues": [
{
"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
]
}
]
}
],
"column": 6,
"endColumn": 6,
"endIndex": 0,
"endLine": 3,
"fileName": "it\/autofix_ide\/source_autofix.d",
"index": 0,
"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 `static`",
"replacements": [
{
"newText": "static ",
"range": [
69,
69
]
}
]
},
{
"name": "Wrap '{}' block around 'if'",
"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": 8,
"endColumn": 8,
"endIndex": 0,
"endLine": 8,
"fileName": "it\/autofix_ide\/source_autofix.d",
"index": 0,
"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": 3,
"statementCount": 4,
"structCount": 1,
"templateCount": 0,
"undocumentedPublicSymbols": 0
}
17 changes: 9 additions & 8 deletions tests/it.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,42 +41,43 @@ cd "$DSCANNER_DIR/tests"
# IDE APIs
# --------
# checking that reporting format stays consistent or only gets extended
../bin/dscanner --report it/autofix_ide/source_autofix.d > first-res.json
diff <(../bin/dscanner --report it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.report.json)
diff <(../bin/dscanner --resolveMessage b16 it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.autofix.json)
diff -y <(../bin/dscanner --resolveMessage b16 it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.autofix.json)

# CLI tests
# ---------
# check that `dscanner fix` works as expected
section '1. test no changes if EOFing'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
printf "" | ../bin/dscanner fix it/autofix_cli/test.d
diff it/autofix_cli/test.d it/autofix_cli/source.d
diff -y it/autofix_cli/test.d it/autofix_cli/source.d
section '2. test no changes for simple enter pressing'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
printf "\n" | ../bin/dscanner fix it/autofix_cli/test.d
diff it/autofix_cli/test.d it/autofix_cli/source.d
diff -y it/autofix_cli/test.d it/autofix_cli/source.d
section '2.1. test no changes entering 0'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
printf "0\n" | ../bin/dscanner fix it/autofix_cli/test.d
diff it/autofix_cli/test.d it/autofix_cli/source.d
diff -y it/autofix_cli/test.d it/autofix_cli/source.d
section '3. test change applies automatically with --applySingle'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
../bin/dscanner fix --applySingle it/autofix_cli/test.d | grep -F 'Writing changes to it/autofix_cli/test.d'
diff it/autofix_cli/test.d it/autofix_cli/fixed.d
diff -y it/autofix_cli/test.d it/autofix_cli/fixed.d
section '4. test change apply when entering "1"'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
printf "1\n" | ../bin/dscanner fix it/autofix_cli/test.d | grep -F 'Writing changes to it/autofix_cli/test.d'
diff it/autofix_cli/test.d it/autofix_cli/fixed.d
diff -y it/autofix_cli/test.d it/autofix_cli/fixed.d
section '5. test invalid selection reasks what to apply'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
printf "2\n-1\n1000\na\n1\n" | ../bin/dscanner fix it/autofix_cli/test.d | grep -F 'Writing changes to it/autofix_cli/test.d'
diff it/autofix_cli/test.d it/autofix_cli/fixed.d
diff -y it/autofix_cli/test.d it/autofix_cli/fixed.d

# check that `dscanner @myargs.rst` reads arguments from file
section "Test @myargs.rst"
echo "-f" > "myargs.rst"
echo "github" >> "myargs.rst"
echo "lint" >> "myargs.rst"
echo "it/singleissue.d" >> "myargs.rst"
diff it/singleissue_github.txt <(../bin/dscanner "@myargs.rst")
diff -y it/singleissue_github.txt <(../bin/dscanner "@myargs.rst")
rm "myargs.rst"
24 changes: 12 additions & 12 deletions tests/it/autofix_ide/source_autofix.autofix.json
Original file line number Diff line number Diff line change
@@ -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
]
}
]
Expand Down
77 changes: 60 additions & 17 deletions tests/it/autofix_ide/source_autofix.report.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
"issues": [
{
"column": 6,
"endColumn": 12,
"endIndex": 22,
"endColumn": 6,
"endIndex": 0,
"endLine": 3,
"fileName": "it/autofix_ide/source_autofix.d",
"index": 16,
"index": 0,
"key": "dscanner.confusing.function_attributes",
"line": 3,
"message": "Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'.",
Expand All @@ -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
]
}
]
Expand All @@ -71,15 +71,58 @@
},
{
"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,
"endColumn": 10,
"endIndex": 71,
"endLine": 8,
"fileName": "it/autofix_ide/source_autofix.d",
"index": 64,
"index": 0,
"key": "dscanner.suspicious.static_if_else",
"line": 8,
"message": "Mismatched static if. Use 'else static if' here.",
Expand Down