501 lines
14 KiB
D
501 lines
14 KiB
D
module dscanner.analysis.base;
|
|
|
|
import dparse.ast;
|
|
import dparse.lexer : IdType, str, Token;
|
|
import dsymbol.scope_ : Scope;
|
|
import std.array;
|
|
import std.container;
|
|
import std.string;
|
|
import std.sumtype;
|
|
|
|
///
|
|
struct AutoFix
|
|
{
|
|
///
|
|
struct CodeReplacement
|
|
{
|
|
/// Byte index `[start, end)` within the file what text to replace.
|
|
/// `start == end` if text is only getting inserted.
|
|
size_t[2] range;
|
|
/// The new text to put inside the range. (empty to delete text)
|
|
string newText;
|
|
}
|
|
|
|
/// Context that the analyzer resolve method can use to generate the
|
|
/// resolved `CodeReplacement` with.
|
|
struct ResolveContext
|
|
{
|
|
/// Arbitrary analyzer-defined parameters. May grow in the future with
|
|
/// more items.
|
|
ulong[3] params;
|
|
/// For dynamically sized data, may contain binary data.
|
|
string extraInfo;
|
|
}
|
|
|
|
/// Display name for the UI.
|
|
string name;
|
|
/// Either code replacements, sorted by range start, never overlapping, or a
|
|
/// context that can be passed to `BaseAnalyzer.resolveAutoFix` along with
|
|
/// the message key from the parent `Message` object.
|
|
///
|
|
/// `CodeReplacement[]` should be applied to the code in reverse, otherwise
|
|
/// an offset to the following start indices must be calculated and be kept
|
|
/// track of.
|
|
SumType!(CodeReplacement[], ResolveContext) replacements;
|
|
|
|
invariant
|
|
{
|
|
replacements.match!(
|
|
(const CodeReplacement[] replacement)
|
|
{
|
|
import std.algorithm : all, isSorted;
|
|
|
|
assert(replacement.all!"a.range[0] <= a.range[1]");
|
|
assert(replacement.isSorted!"a.range[0] < b.range[0]");
|
|
},
|
|
(_) {}
|
|
);
|
|
}
|
|
|
|
static AutoFix resolveLater(string name, ulong[3] params, string extraInfo = null)
|
|
{
|
|
AutoFix ret;
|
|
ret.name = name;
|
|
ret.replacements = ResolveContext(params, extraInfo);
|
|
return ret;
|
|
}
|
|
|
|
static AutoFix replacement(const Token token, string newText, string name = null)
|
|
{
|
|
if (!name.length)
|
|
{
|
|
auto text = token.text.length ? token.text : str(token.type);
|
|
if (newText.length)
|
|
name = "Replace `" ~ text ~ "` with `" ~ newText ~ "`";
|
|
else
|
|
name = "Remove `" ~ text ~ "`";
|
|
}
|
|
return replacement([token], newText, name);
|
|
}
|
|
|
|
static AutoFix replacement(const BaseNode node, string newText, string name)
|
|
{
|
|
return replacement(node.tokens, newText, name);
|
|
}
|
|
|
|
static AutoFix replacement(const Token[] tokens, string newText, string name)
|
|
in(tokens.length > 0, "must provide at least one token")
|
|
{
|
|
auto end = tokens[$ - 1].text.length ? tokens[$ - 1].text : str(tokens[$ - 1].type);
|
|
return replacement([tokens[0].index, tokens[$ - 1].index + end.length], newText, name);
|
|
}
|
|
|
|
static AutoFix replacement(size_t[2] range, string newText, string name)
|
|
{
|
|
AutoFix ret;
|
|
ret.name = name;
|
|
ret.replacements = [
|
|
AutoFix.CodeReplacement(range, newText)
|
|
];
|
|
return ret;
|
|
}
|
|
|
|
static AutoFix insertionBefore(const Token token, string content, string name = null)
|
|
{
|
|
return insertionAt(token.index, content, name);
|
|
}
|
|
|
|
static AutoFix insertionAfter(const Token token, string content, string name = null)
|
|
{
|
|
auto tokenText = token.text.length ? token.text : str(token.type);
|
|
return insertionAt(token.index + tokenText.length, content, name);
|
|
}
|
|
|
|
static AutoFix insertionAt(size_t index, string content, string name = null)
|
|
{
|
|
assert(content.length > 0, "generated auto fix inserting text without content");
|
|
AutoFix ret;
|
|
ret.name = name.length
|
|
? name
|
|
: content.strip.length
|
|
? "Insert `" ~ content.strip ~ "`"
|
|
: "Insert whitespace";
|
|
ret.replacements = [
|
|
AutoFix.CodeReplacement([index, index], content)
|
|
];
|
|
return ret;
|
|
}
|
|
|
|
static AutoFix indentLines(scope const(Token)[] tokens, const AutoFixFormatting formatting, string name = "Indent code")
|
|
{
|
|
CodeReplacement[] inserts;
|
|
size_t line = -1;
|
|
foreach (token; tokens)
|
|
{
|
|
if (line != token.line)
|
|
{
|
|
line = token.line;
|
|
inserts ~= CodeReplacement([token.index, token.index], formatting.indentation);
|
|
}
|
|
}
|
|
AutoFix ret;
|
|
ret.name = name;
|
|
ret.replacements = inserts;
|
|
return ret;
|
|
}
|
|
|
|
AutoFix concat(AutoFix other) const
|
|
{
|
|
import std.algorithm : sort;
|
|
|
|
static immutable string errorMsg = "Cannot concatenate code replacement with late-resolve";
|
|
|
|
AutoFix ret;
|
|
ret.name = name;
|
|
CodeReplacement[] concatenated = expectReplacements(errorMsg).dup
|
|
~ other.expectReplacements(errorMsg);
|
|
concatenated.sort!"a.range[0] < b.range[0]";
|
|
ret.replacements = concatenated;
|
|
return ret;
|
|
}
|
|
|
|
CodeReplacement[] expectReplacements(
|
|
string errorMsg = "Expected available code replacements, not something to resolve later"
|
|
) @safe pure nothrow @nogc
|
|
{
|
|
return replacements.match!(
|
|
(replacement)
|
|
{
|
|
if (false) return CodeReplacement[].init;
|
|
static if (is(immutable typeof(replacement) == immutable CodeReplacement[]))
|
|
return replacement;
|
|
else
|
|
assert(false, errorMsg);
|
|
}
|
|
);
|
|
}
|
|
|
|
const(CodeReplacement[]) expectReplacements(
|
|
string errorMsg = "Expected available code replacements, not something to resolve later"
|
|
) const @safe pure nothrow @nogc
|
|
{
|
|
return replacements.match!(
|
|
(const replacement)
|
|
{
|
|
if (false) return CodeReplacement[].init;
|
|
static if (is(immutable typeof(replacement) == immutable CodeReplacement[]))
|
|
return replacement;
|
|
else
|
|
assert(false, errorMsg);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Formatting style for autofix generation (only available for resolve autofix)
|
|
struct AutoFixFormatting
|
|
{
|
|
enum AutoFixFormatting invalid = AutoFixFormatting(BraceStyle.invalid, null, 0, null);
|
|
|
|
enum BraceStyle
|
|
{
|
|
/// invalid, shouldn't appear in usable configs
|
|
invalid,
|
|
/// $(LINK https://en.wikipedia.org/wiki/Indent_style#Allman_style)
|
|
allman,
|
|
/// $(LINK https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS)
|
|
otbs,
|
|
/// $(LINK https://en.wikipedia.org/wiki/Indent_style#Variant:_Stroustrup)
|
|
stroustrup,
|
|
/// $(LINK https://en.wikipedia.org/wiki/Indentation_style#K&R_style)
|
|
knr,
|
|
}
|
|
|
|
/// Brace style config
|
|
BraceStyle braceStyle = BraceStyle.allman;
|
|
/// String to insert on indentations
|
|
string indentation = "\t";
|
|
/// For calculating indentation size
|
|
uint indentationWidth = 4;
|
|
/// String to insert on line endings
|
|
string eol = "\n";
|
|
|
|
invariant
|
|
{
|
|
import std.algorithm : all;
|
|
|
|
assert(!indentation.length
|
|
|| indentation == "\t"
|
|
|| indentation.all!(c => c == ' '));
|
|
}
|
|
|
|
string getWhitespaceBeforeOpeningBrace(string lastLineIndent, bool isFuncDecl) pure nothrow @safe const
|
|
{
|
|
final switch (braceStyle)
|
|
{
|
|
case BraceStyle.invalid:
|
|
assert(false, "invalid formatter config");
|
|
case BraceStyle.knr:
|
|
if (isFuncDecl)
|
|
goto case BraceStyle.allman;
|
|
else
|
|
goto case BraceStyle.otbs;
|
|
case BraceStyle.otbs:
|
|
case BraceStyle.stroustrup:
|
|
return " ";
|
|
case BraceStyle.allman:
|
|
return eol ~ lastLineIndent;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A diagnostic message. Each message defines one issue in the file, which
|
|
/// consists of one or more squiggly line ranges within the code, as well as
|
|
/// human readable descriptions and optionally also one or more automatic code
|
|
/// fixes that can be applied.
|
|
struct Message
|
|
{
|
|
/// A squiggly line range within the code. May be the issue itself if it's
|
|
/// the `diagnostic` member or supplemental information that can aid the
|
|
/// user in resolving the issue.
|
|
struct Diagnostic
|
|
{
|
|
/// Name of the file where the warning was triggered.
|
|
string fileName;
|
|
/// Byte index from start of file the warning was triggered.
|
|
size_t startIndex, endIndex;
|
|
/// Line number where the warning was triggered, 1-based.
|
|
size_t startLine, endLine;
|
|
/// Column number where the warning was triggered. (in bytes)
|
|
size_t startColumn, endColumn;
|
|
/// Warning message, may be null for supplemental diagnostics.
|
|
string message;
|
|
|
|
deprecated("Use startLine instead") alias line = startLine;
|
|
deprecated("Use startColumn instead") alias column = startColumn;
|
|
|
|
static Diagnostic from(string fileName, const BaseNode node, string message)
|
|
{
|
|
return from(fileName, node !is null ? node.tokens : [], message);
|
|
}
|
|
|
|
static Diagnostic from(string fileName, const Token token, string message)
|
|
{
|
|
auto text = token.text.length ? token.text : str(token.type);
|
|
return from(fileName,
|
|
[token.index, token.index + text.length],
|
|
token.line,
|
|
[token.column, token.column + text.length],
|
|
message);
|
|
}
|
|
|
|
static Diagnostic from(string fileName, const Token[] tokens, string message)
|
|
{
|
|
auto start = tokens.length ? tokens[0] : Token.init;
|
|
auto end = tokens.length ? tokens[$ - 1] : Token.init;
|
|
auto endText = end.text.length ? end.text : str(end.type);
|
|
return from(fileName,
|
|
[start.index, end.index + endText.length],
|
|
[start.line, end.line],
|
|
[start.column, end.column + endText.length],
|
|
message);
|
|
}
|
|
|
|
static Diagnostic from(string fileName, size_t[2] index, size_t line, size_t[2] columns, string message)
|
|
{
|
|
return Message.Diagnostic(fileName, index[0], index[1], line, line, columns[0], columns[1], message);
|
|
}
|
|
|
|
static Diagnostic from(string fileName, size_t[2] index, size_t[2] lines, size_t[2] columns, string message)
|
|
{
|
|
return Message.Diagnostic(fileName, index[0], index[1], lines[0], lines[1], columns[0], columns[1], message);
|
|
}
|
|
}
|
|
|
|
/// Primary warning
|
|
Diagnostic diagnostic;
|
|
/// List of supplemental warnings / hint locations
|
|
Diagnostic[] supplemental;
|
|
/// Name of the warning
|
|
string key;
|
|
/// Check name
|
|
string checkName;
|
|
|
|
/// Either immediate code changes that can be applied or context to call
|
|
/// the `BaseAnalyzer.resolveAutoFix` method with.
|
|
AutoFix[] autofixes;
|
|
|
|
deprecated this(string fileName, size_t line, size_t column, string key = null, string message = null, string checkName = null)
|
|
{
|
|
diagnostic.fileName = fileName;
|
|
diagnostic.startLine = diagnostic.endLine = line;
|
|
diagnostic.startColumn = diagnostic.endColumn = column;
|
|
diagnostic.message = message;
|
|
this.key = key;
|
|
this.checkName = checkName;
|
|
}
|
|
|
|
this(Diagnostic diagnostic, string key = null, string checkName = null, AutoFix[] autofixes = null)
|
|
{
|
|
this.diagnostic = diagnostic;
|
|
this.key = key;
|
|
this.checkName = checkName;
|
|
this.autofixes = autofixes;
|
|
}
|
|
|
|
this(Diagnostic diagnostic, Diagnostic[] supplemental, string key = null, string checkName = null, AutoFix[] autofixes = null)
|
|
{
|
|
this.diagnostic = diagnostic;
|
|
this.supplemental = supplemental;
|
|
this.key = key;
|
|
this.checkName = checkName;
|
|
this.autofixes = autofixes;
|
|
}
|
|
|
|
alias diagnostic this;
|
|
}
|
|
|
|
enum comparitor = q{ a.startLine < b.startLine || (a.startLine == b.startLine && a.startColumn < b.startColumn) };
|
|
|
|
alias MessageSet = RedBlackTree!(Message, comparitor, true);
|
|
|
|
mixin template AnalyzerInfo(string checkName)
|
|
{
|
|
enum string name = checkName;
|
|
|
|
override protected string getName()
|
|
{
|
|
return name;
|
|
}
|
|
}
|
|
|
|
abstract class BaseAnalyzer : ASTVisitor
|
|
{
|
|
public:
|
|
this(string fileName, const Scope* sc, bool skipTests = false)
|
|
{
|
|
this.sc = sc;
|
|
this.fileName = fileName;
|
|
this.skipTests = skipTests;
|
|
_messages = new MessageSet;
|
|
}
|
|
|
|
string getName()
|
|
{
|
|
assert(0);
|
|
}
|
|
|
|
Message[] messages()
|
|
{
|
|
return _messages[].array;
|
|
}
|
|
|
|
alias visit = ASTVisitor.visit;
|
|
|
|
/**
|
|
* Visits a unittest.
|
|
*
|
|
* When overriden, the protected bool "skipTests" should be handled
|
|
* so that the content of the test is not analyzed.
|
|
*/
|
|
override void visit(const Unittest unittest_)
|
|
{
|
|
if (!skipTests)
|
|
unittest_.accept(this);
|
|
}
|
|
|
|
AutoFix.CodeReplacement[] resolveAutoFix(
|
|
const Module mod,
|
|
scope const(Token)[] tokens,
|
|
const AutoFix.ResolveContext context,
|
|
const AutoFixFormatting formatting,
|
|
)
|
|
{
|
|
cast(void) mod;
|
|
cast(void) tokens;
|
|
cast(void) context;
|
|
cast(void) formatting;
|
|
assert(0);
|
|
}
|
|
|
|
protected:
|
|
|
|
bool inAggregate;
|
|
bool skipTests;
|
|
|
|
template visitTemplate(T)
|
|
{
|
|
override void visit(const T structDec)
|
|
{
|
|
inAggregate = true;
|
|
structDec.accept(this);
|
|
inAggregate = false;
|
|
}
|
|
}
|
|
|
|
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
|
|
*/
|
|
string fileName;
|
|
|
|
const(Scope)* sc;
|
|
|
|
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;
|
|
}
|