D-Scanner/src/dscanner/analysis/base.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;
}