@slippy-lint/slippy
Version:
A simple but powerful linter for Solidity
297 lines (254 loc) • 7.75 kB
text/typescript
import {
ConstructorAttributes,
ContractMembers,
InterfaceMembers,
LibraryMembers,
ModifierInvocation,
Statements,
YulStatements,
} from "@nomicfoundation/slang/ast";
import {
Diagnostic,
RuleContext,
RuleWithoutConfig,
RuleDefinitionWithoutConfig,
} from "./types.js";
import {
assertNonterminalNode,
Cursor,
NonterminalKind,
Query,
QueryMatch,
TerminalKind,
} from "@nomicfoundation/slang/cst";
import { File as SlangFile } from "@nomicfoundation/slang/compilation";
const name = "no-empty-blocks";
interface QueryHandler {
query: string;
handler: (file: SlangFile, match: QueryMatch) => Diagnostic[];
}
function checkEmptyBlock(
file: SlangFile,
root: Cursor,
isEmpty: boolean,
openBrace: Cursor,
closeBrace: Cursor,
): Diagnostic[] {
if (isEmpty) {
// check if it has comments
const commentCursor = root.spawn();
if (
commentCursor.goToNextTerminalWithKinds([
TerminalKind.SingleLineComment,
TerminalKind.MultiLineComment,
])
) {
const commentInsideBlock =
commentCursor.textRange.start.utf8 > openBrace.textRange.end.utf8 &&
commentCursor.textRange.end.utf8 < closeBrace.textRange.start.utf8;
if (commentInsideBlock) {
return [];
}
}
return [
{
rule: name,
sourceId: file.id,
line: openBrace.textRange.start.line,
column: openBrace.textRange.start.column,
message: `Empty blocks are not allowed`,
},
];
}
return [];
}
const handlers: QueryHandler[] = [
{
query: `
[ContractDefinition
open_brace: [OpenBrace]
members: [ContractMembers]
close_brace: [CloseBrace]
]
`,
handler: (file: SlangFile, match: QueryMatch): Diagnostic[] => {
const hasInheritanceSpecifiers = checkHasInheritanceSpecifier(
match.root.clone(),
);
if (hasInheritanceSpecifiers) {
return [];
}
const openBrace = match.captures.openBrace[0];
const members = match.captures.members[0];
const closeBrace = match.captures.closeBrace[0];
assertNonterminalNode(members.node);
const membersAst = new ContractMembers(members.node);
const isEmpty = membersAst.items.length === 0;
return checkEmptyBlock(file, match.root, isEmpty, openBrace, closeBrace);
},
},
{
query: `
[InterfaceDefinition
open_brace: [OpenBrace]
members: [InterfaceMembers]
close_brace: [CloseBrace]
]
`,
handler: (file: SlangFile, match: QueryMatch): Diagnostic[] => {
const hasInheritanceSpecifiers = checkHasInheritanceSpecifier(
match.root.clone(),
);
if (hasInheritanceSpecifiers) {
return [];
}
const openBrace = match.captures.openBrace[0];
const members = match.captures.members[0];
const closeBrace = match.captures.closeBrace[0];
assertNonterminalNode(members.node);
const membersAst = new InterfaceMembers(members.node);
const isEmpty = membersAst.items.length === 0;
return checkEmptyBlock(file, match.root, isEmpty, openBrace, closeBrace);
},
},
{
query: `
[LibraryDefinition
open_brace: [OpenBrace]
members: [LibraryMembers]
close_brace: [CloseBrace]
]
`,
handler: (file: SlangFile, match: QueryMatch): Diagnostic[] => {
const openBrace = match.captures.openBrace[0];
const members = match.captures.members[0];
const closeBrace = match.captures.closeBrace[0];
assertNonterminalNode(members.node);
const membersAst = new LibraryMembers(members.node);
const isEmpty = membersAst.items.length === 0;
return checkEmptyBlock(file, match.root, isEmpty, openBrace, closeBrace);
},
},
{
query: `
[Block
open_brace: [OpenBrace]
statements: [Statements]
close_brace: [CloseBrace]
]
`,
handler: (file: SlangFile, match: QueryMatch): Diagnostic[] => {
const isVirtual = checkIsVirtual(match.root.clone());
const isConstructorWithBase = checkIsValidConstructor(match.root.clone());
const isFallbackOrReceive = checkIsFallbackOrReceive(match.root.clone());
if (isVirtual || isConstructorWithBase || isFallbackOrReceive) {
return [];
}
const openBrace = match.captures.openBrace[0];
const statements = match.captures.statements[0];
const closeBrace = match.captures.closeBrace[0];
assertNonterminalNode(statements.node);
const membersAst = new Statements(statements.node);
const isEmpty = membersAst.items.length === 0;
return checkEmptyBlock(file, match.root, isEmpty, openBrace, closeBrace);
},
},
{
query: `
[YulBlock
open_brace: [OpenBrace]
statements: [YulStatements]
close_brace: [CloseBrace]
]
`,
handler: (file: SlangFile, match: QueryMatch): Diagnostic[] => {
const openBrace = match.captures.openBrace[0];
const statements = match.captures.statements[0];
const closeBrace = match.captures.closeBrace[0];
assertNonterminalNode(statements.node);
const membersAst = new YulStatements(statements.node);
const isEmpty = membersAst.items.length === 0;
return checkEmptyBlock(file, match.root, isEmpty, openBrace, closeBrace);
},
},
];
export const NoEmptyBlocks: RuleDefinitionWithoutConfig = {
name,
recommended: false,
create: function () {
return new NoEmptyBlocksRule(this.name);
},
};
class NoEmptyBlocksRule implements RuleWithoutConfig {
public constructor(public name: string) {}
public run({ file }: RuleContext): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
const cursor = file.createTreeCursor();
const queries = handlers.map((handler) => handler.query);
const matches = cursor.query(queries.map((query) => Query.create(query)));
for (const match of matches) {
const handler = handlers[match.queryIndex];
diagnostics.push(...handler.handler(file, match));
}
return diagnostics;
}
}
function checkIsVirtual(cursor: Cursor): boolean {
// function body
if (!cursor.goToParent()) {
return false;
}
// function definition
if (!cursor.goToParent()) {
return false;
}
if (!cursor.goToNextNonterminalWithKind(NonterminalKind.FunctionAttributes)) {
return false;
}
const functionAttributesCursor = cursor.spawn();
return functionAttributesCursor.goToNextTerminalWithKind(
TerminalKind.VirtualKeyword,
);
}
/**
* Checks if the constructor is valid, meaning:
* - It has a base constructor call
* - It has modifiers other than `public`
*/
function checkIsValidConstructor(cursor: Cursor): boolean {
// constructor definition
if (!cursor.goToParent()) {
return false;
}
if (
!cursor.goToNextNonterminalWithKind(NonterminalKind.ConstructorAttributes)
) {
return false;
}
assertNonterminalNode(cursor.node);
const attributes = new ConstructorAttributes(cursor.node);
return (
attributes.items.filter(
(x) =>
x.variant instanceof ModifierInvocation ||
x.variant.kind !== TerminalKind.PublicKeyword,
).length > 0
);
}
function checkIsFallbackOrReceive(cursor: Cursor): boolean {
if (!cursor.goToParent()) {
return false;
}
if (!cursor.goToParent()) {
return false;
}
return (
cursor.node.kind === NonterminalKind.FallbackFunctionDefinition ||
cursor.node.kind === NonterminalKind.ReceiveFunctionDefinition
);
}
function checkHasInheritanceSpecifier(cursor: Cursor): boolean {
return cursor.goToNextNonterminalWithKind(
NonterminalKind.InheritanceSpecifier,
);
}