D-Scanner/src/dscanner/analysis/helpers.d

353 lines
11 KiB
D

// Copyright (c) 2014, Matthew Brennan Jones <matthew.brennan.jones@gmail.com>
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
module dscanner.analysis.helpers;
import core.exception : AssertError;
import std.stdio;
import std.string;
import std.traits;
import dparse.ast;
import dparse.lexer : tok, Token;
import dparse.rollback_allocator;
import dscanner.analysis.base;
import dscanner.analysis.config;
import dscanner.analysis.run;
import dsymbol.modulecache : ModuleCache;
import std.experimental.allocator;
import std.experimental.allocator.mallocator;
S between(S)(S value, S before, S after) if (isSomeString!S)
{
return value.after(before).before(after);
}
S before(S)(S value, S separator) if (isSomeString!S)
{
immutable i = indexOf(value, separator);
if (i == -1)
return value;
return value[0 .. i];
}
S after(S)(S value, S separator) if (isSomeString!S)
{
immutable i = indexOf(value, separator);
if (i == -1)
return "";
return value[i + separator.length .. $];
}
string getLineIndentation(scope const(Token)[] tokens, size_t line, const AutoFixFormatting formatting)
{
import std.algorithm : countUntil;
import std.array : array;
import std.range : repeat;
import std.string : lastIndexOfAny;
auto idx = tokens.countUntil!(a => a.line == line);
if (idx == -1 || tokens[idx].column <= 1 || !formatting.indentation.length)
return "";
auto indent = tokens[idx].column - 1;
if (formatting.indentation[0] == '\t')
return (cast(immutable)'\t').repeat(indent).array;
else
return (cast(immutable)' ').repeat(indent).array;
}
/**
* This assert function will analyze the passed in code, get the warnings,
* and make sure they match the warnings in the comments. Warnings are
* marked like so if range doesn't matter: // [warn]: Failed to do somethings.
*
* To test for start and end column, mark warnings as multi-line comments like
* this: /+
* ^^^^^ [warn]: Failed to do somethings. +/
*/
void assertAnalyzerWarnings(string code, const StaticAnalysisConfig config,
string file = __FILE__, size_t line = __LINE__)
{
import dscanner.analysis.run : parseModule;
import dparse.lexer : StringCache, Token;
StringCache cache = StringCache(StringCache.defaultBucketCount);
RollbackAllocator r;
const(Token)[] tokens;
const(Module) m = parseModule(file, cast(ubyte[]) code, &r, defaultErrorFormat, cache, false, tokens);
ModuleCache moduleCache;
// Run the code and get any warnings
MessageSet rawWarnings = analyze("test", m, config, moduleCache, tokens);
string[] codeLines = code.splitLines();
struct FoundWarning
{
string msg;
size_t startColumn, endColumn;
}
// Get the warnings ordered by line
FoundWarning[size_t] warnings;
foreach (rawWarning; rawWarnings[])
{
// Skip the warning if it is on line zero
immutable size_t rawLine = rawWarning.endLine;
if (rawLine == 0)
{
stderr.writefln("!!! Skipping warning because it is on line zero:\n%s",
rawWarning.message);
continue;
}
size_t warnLine = line - 1 + rawLine;
warnings[warnLine] = FoundWarning(
format("[warn]: %s", rawWarning.message),
rawWarning.startLine != rawWarning.endLine ? 1 : rawWarning.startColumn,
rawWarning.endColumn,
);
}
// Get all the messages from the comments in the code
FoundWarning[size_t] messages;
bool lastLineStartedComment = false;
foreach (i, codeLine; codeLines)
{
scope (exit)
lastLineStartedComment = codeLine.stripRight.endsWith("/+", "/*") > 0;
// Get the line of this code line
size_t lineNo = i + line;
if (codeLine.stripLeft.startsWith("^") && lastLineStartedComment)
{
auto start = codeLine.indexOf("^") + 1;
assert(start != 0);
auto end = codeLine.indexOfNeither("^", start) + 1;
assert(end != 0);
auto warn = codeLine.indexOf("[warn]:");
assert(warn != -1, "malformed line, expected `[warn]: text` after `^^^^^` part");
auto message = codeLine[warn .. $].stripRight;
if (message.endsWith("+/", "*/"))
message = message[0 .. $ - 2].stripRight;
messages[lineNo - 1] = FoundWarning(message, start, end);
}
// Skip if no [warn] comment
else if (codeLine.indexOf("// [warn]:") != -1)
{
// Skip if there is no comment or code
immutable string codePart = codeLine.before("// ");
immutable string commentPart = codeLine.after("// ");
if (!codePart.length || !commentPart.length)
continue;
// Get the message
messages[lineNo] = FoundWarning(commentPart);
}
}
// Throw an assert error if any messages are not listed in the warnings
foreach (lineNo, message; messages)
{
// No warning
if (lineNo !in warnings)
{
immutable string errors = "Expected warning:\n%s\nFrom source code at (%s:?):\n%s".format(messages[lineNo],
lineNo, codeLines[lineNo - line]);
throw new AssertError(errors, file, lineNo);
}
// Different warning
else if (warnings[lineNo].msg != messages[lineNo].msg)
{
immutable string errors = "Expected warning:\n%s\nBut was:\n%s\nFrom source code at (%s:?):\n%s".format(
messages[lineNo], warnings[lineNo], lineNo, codeLines[lineNo - line]);
throw new AssertError(errors, file, lineNo);
}
// specified column range
if ((message.startColumn || message.endColumn)
&& warnings[lineNo] != message)
{
import std.algorithm : max;
import std.array : array;
import std.range : repeat;
import std.string : replace;
const(char)[] expectedRange = ' '.repeat(max(0, cast(int)message.startColumn - 1)).array
~ '^'.repeat(max(0, cast(int)(message.endColumn - message.startColumn))).array;
const(char)[] actualRange;
if (!warnings[lineNo].startColumn || warnings[lineNo].startColumn == warnings[lineNo].endColumn)
actualRange = "no column range defined!";
else
actualRange = ' '.repeat(max(0, cast(int)warnings[lineNo].startColumn - 1)).array
~ '^'.repeat(max(0, cast(int)(warnings[lineNo].endColumn - warnings[lineNo].startColumn))).array;
size_t paddingWidth = max(expectedRange.length, actualRange.length);
immutable string errors = "Wrong warning range: expected %s, but was %s\nFrom source code at (%s:?):\n%s\n%-*s <-- expected\n%-*s <-- actual".format(
[message.startColumn, message.endColumn],
[warnings[lineNo].startColumn, warnings[lineNo].endColumn],
lineNo, codeLines[lineNo - line].replace("\t", " "),
paddingWidth, expectedRange,
paddingWidth, actualRange);
throw new AssertError(errors, file, lineNo);
}
}
// Throw an assert error if there were any warnings that were not expected
string[] unexpectedWarnings;
foreach (lineNo, warning; warnings)
{
// Unexpected warning
if (lineNo !in messages)
{
unexpectedWarnings ~= "%s\nFrom source code at (%s:?):\n%s".format(warning,
lineNo, codeLines[lineNo - line]);
}
}
if (unexpectedWarnings.length)
{
immutable string message = "Unexpected warnings:\n" ~ unexpectedWarnings.join("\n");
throw new AssertError(message, file, line);
}
}
/// EOL inside this project, for tests
private static immutable fileEol = q{
};
/**
* This assert function will analyze the passed in code, get the warnings, and
* apply all specified autofixes all at once.
*
* Indicate which autofix to apply by adding a line comment at the end of the
* line with the following content: `// fix:0`, where 0 is the index which
* autofix to apply. There may only be one diagnostic on a line with this fix
* comment. Alternatively you can also just write `// fix` to apply the only
* available suggestion.
*/
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__)
{
import dparse.lexer : StringCache, Token;
import dscanner.analysis.run : parseModule;
import std.algorithm : canFind, findSplit, map, sort;
import std.conv : to;
import std.sumtype : match;
import std.typecons : tuple, Tuple;
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;
// Run the code and get any warnings
MessageSet rawWarnings = analyze("test", m, config, moduleCache, tokens, true, true, formattingConfig);
string[] codeLines = before.splitLines();
Tuple!(Message, int)[] toApply;
int[] applyLines;
scope (failure)
{
if (toApply.length)
stderr.writefln("Would have applied these fixes:%(\n- %s%)",
toApply.map!"a[0].autofixes[a[1]].name");
else
stderr.writeln("Did not find any fixes at all up to this point.");
stderr.writeln("Found warnings on lines: ", rawWarnings[].map!(a
=> a.endLine == 0 ? 0 : a.endLine - 1 + line));
}
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;
}
auto fixComment = codeLines[rawLine - 1].findSplit("// fix");
if (fixComment[1].length)
{
applyLines ~= cast(int)rawLine - 1;
if (fixComment[2].startsWith(":"))
{
auto i = fixComment[2][1 .. $].to!int;
assert(i >= 0, "can't use negative autofix indices");
if (i >= rawWarning.autofixes.length)
throw new AssertError("autofix index out of range, diagnostic only has %s autofixes (%s)."
.format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"),
file, rawLine + line);
toApply ~= tuple(rawWarning, i);
}
else
{
if (rawWarning.autofixes.length != 1)
throw new AssertError("diagnostic has %s autofixes (%s), but expected exactly one."
.format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"),
file, rawLine + line);
toApply ~= tuple(rawWarning, 0);
}
}
}
foreach (i, codeLine; codeLines)
{
if (!applyLines.canFind(i) && codeLine.canFind("// fix"))
throw new AssertError("Missing expected warning for autofix on line %s"
.format(i + line), file, i + line);
}
AutoFix.CodeReplacement[] replacements;
foreach_reverse (pair; toApply)
{
Message message = pair[0];
AutoFix fix = message.autofixes[pair[1]];
replacements ~= fix.expectReplacements;
}
replacements.sort!"a.range[0] < b.range[0]";
improveAutoFixWhitespace(before, replacements);
string newCode = before;
foreach_reverse (replacement; replacements)
{
newCode = newCode[0 .. replacement.range[0]] ~ replacement.newText
~ newCode[replacement.range[1] .. $];
}
if (newCode != after)
{
bool onlyWhitespaceDiffers = newCode.replace("\t", "").replace(" ", "")
== after.replace("\t", "").replace(" ", "").replace("\r", "");
string formatDisplay(string code)
{
string ret = code.lineSplitter!(KeepTerminator.yes).map!(a => "\t" ~ a).join;
if (onlyWhitespaceDiffers)
ret = ret
.replace("\r", "\x1B[2m\\r\x1B[m")
.replace("\t", "\x1B[2m→ \x1B[m")
.replace(" ", "\x1B[2m⸱\x1B[m");
return ret;
}
throw new AssertError("Applying autofix didn't yield expected results. Expected:\n"
~ formatDisplay(after)
~ "\n\nActual:\n"
~ formatDisplay(newCode),
file, line);
}
}