UNPKG

@slippy-lint/slippy

Version:

A simple but powerful linter for Solidity

440 lines (365 loc) 10.8 kB
import { CompilationUnit, File as SlangFile, } from "@nomicfoundation/slang/compilation"; import { Diagnostic, RuleContext, RuleWithConfig, RuleDefinitionWithConfig, } from "./types.js"; import { assertNonterminalNode, Cursor, NonterminalKind, TerminalKind, TextRange, } from "@nomicfoundation/slang/cst"; import { Definition } from "@nomicfoundation/slang/bindings"; import * as z from "zod"; import { ImportDirective, PragmaDirective, SourceUnitMember, } from "@nomicfoundation/slang/ast"; const Schema = z .object({ ignorePattern: z.string().optional(), }) .default({}); type Config = z.infer<typeof Schema>; export const NoUnusedVars: RuleDefinitionWithConfig<Config> = { name: "no-unused-vars", recommended: true, parseConfig: (config: unknown) => Schema.parse(config), create: function (config: Config) { return new NoUnusedVarsRule(this.name, config); }, }; class NoUnusedVarsRule implements RuleWithConfig<Config> { private ignorePattern?: RegExp; public constructor( public name: string, public config: Config, ) { this.ignorePattern = config.ignorePattern !== undefined ? new RegExp(config.ignorePattern) : undefined; } public run({ file, unit }: RuleContext): Diagnostic[] { const diagnostics: Diagnostic[] = []; const inheritDocComments = findInheritDocComments(file); const unusedVars = findUnusedVarsInFile(unit, file, inheritDocComments); for (const unusedVar of unusedVars) { if (unusedVar.definition.definiensLocation.isUserFileLocation()) { if ( this.ignorePattern !== undefined && this.ignorePattern.test(unusedVar.name) ) { continue; } diagnostics.push({ rule: this.name, sourceId: unusedVar.definition.definiensLocation.fileId, message: `'${unusedVar.name}' is defined but never used`, line: unusedVar.textRange.start.line, column: unusedVar.textRange.start.column, }); } } return diagnostics; } } interface UnusedVar { name: string; definition: Definition; textRange: TextRange; } function findUnusedVarsInFile( unit: CompilationUnit, file: SlangFile, inheritDocComments: string[], ): UnusedVar[] { const unusedDefinitions = []; const cursor = file.createTreeCursor(); const unusedPrivateVariables = findUnusedPrivateVariables( unit, cursor.spawn(), ); unusedDefinitions.push(...unusedPrivateVariables); const unusedPrivateFunctions = findUnusedPrivateFunctions( unit, cursor.spawn(), ); unusedDefinitions.push(...unusedPrivateFunctions); while ( cursor.goToNextNonterminalWithKind(NonterminalKind.FunctionDefinition) ) { const unusedDefinitionsInFunction = findUnusedDefinitionsInFunction( unit, cursor.spawn(), ); unusedDefinitions.push(...unusedDefinitionsInFunction); } const unusedImportedNames = findUnusedImportedNames( unit, cursor.spawn(), inheritDocComments, ); unusedDefinitions.push(...unusedImportedNames); return unusedDefinitions; } function findUnusedDefinitionsInFunction( unit: CompilationUnit, functionDefinitionCursor: Cursor, ): UnusedVar[] { const definitions: UnusedVar[] = []; assertNonterminalNode( functionDefinitionCursor.node, NonterminalKind.FunctionDefinition, ); const hasEmptyBlock = !functionDefinitionCursor .spawn() .goToNextNonterminalWithKind(NonterminalKind.Statement); if (!hasEmptyBlock) { let parametersCursor = functionDefinitionCursor.spawn(); parametersCursor.goToNextNonterminalWithKind( NonterminalKind.ParametersDeclaration, ); parametersCursor = parametersCursor.spawn(); while (parametersCursor.goToNextTerminalWithKind(TerminalKind.Identifier)) { const definition = unit.bindingGraph.definitionAt(parametersCursor); if (definition === undefined) { continue; } if (definition.references().length === 0) { definitions.push({ name: parametersCursor.node.unparse(), definition, textRange: parametersCursor.textRange, }); } } } const declarationsCursor = functionDefinitionCursor.spawn(); while ( declarationsCursor.goToNextNonterminalWithKind( NonterminalKind.VariableDeclarationStatement, ) ) { // ignore the type of the variable declarationsCursor.goToNextNonterminalWithKind( NonterminalKind.VariableDeclarationType, ); declarationsCursor.goToNextSibling(); if (!declarationsCursor.goToNextTerminalWithKind(TerminalKind.Identifier)) { continue; } const definition = unit.bindingGraph.definitionAt(declarationsCursor); if (definition === undefined) { continue; } if (definition.references().length === 0) { definitions.push({ name: declarationsCursor.node.unparse(), textRange: declarationsCursor.textRange, definition, }); } } return definitions; } function findUnusedPrivateVariables( unit: CompilationUnit, cursor: Cursor, ): UnusedVar[] { const definitions: UnusedVar[] = []; while ( cursor.goToNextNonterminalWithKind(NonterminalKind.StateVariableDefinition) ) { const variableDefinitionCursor = cursor.spawn(); const isPrivate = variableDefinitionCursor .spawn() .goToNextTerminalWithKind(TerminalKind.PrivateKeyword); if (!isPrivate) { continue; } // ignore the type of the variable variableDefinitionCursor.goToNextNonterminalWithKind( NonterminalKind.TypeName, ); variableDefinitionCursor.goToNextSibling(); if ( !variableDefinitionCursor.goToNextTerminalWithKind( TerminalKind.Identifier, ) ) { continue; } const definition = unit.bindingGraph.definitionAt(variableDefinitionCursor); if (definition === undefined) { continue; } if (definition.references().length === 0) { definitions.push({ name: variableDefinitionCursor.node.unparse(), textRange: variableDefinitionCursor.textRange, definition, }); } } return definitions; } function findUnusedPrivateFunctions( unit: CompilationUnit, cursor: Cursor, ): UnusedVar[] { const definitions: UnusedVar[] = []; while ( cursor.goToNextNonterminalWithKind(NonterminalKind.FunctionDefinition) ) { const isPrivate = checkIsPrivateFunction(cursor.spawn()); if (!isPrivate) { continue; } const definition = getFunctionNameDefinition(unit, cursor.spawn()); if (definition === undefined) { continue; } if (!definition.nameLocation.isUserFileLocation()) { continue; } const definitionCursor = definition.nameLocation.cursor; if (definition.references().length === 0) { definitions.push({ name: definitionCursor.node.unparse(), textRange: definitionCursor.textRange, definition, }); } } return definitions; } function findUnusedImportedNames( unit: CompilationUnit, cursor: Cursor, inheritDocComments: string[], ): UnusedVar[] { const definitions: UnusedVar[] = []; const fileHasOnlyImports = checkFileHasOnlyImports(cursor.spawn()); if (fileHasOnlyImports) { return []; } while ( cursor.goToNextNonterminalWithKinds([ NonterminalKind.PathImport, NonterminalKind.NamedImport, NonterminalKind.ImportDeconstructionSymbol, ]) ) { let variableDefinitionCursor; if ( cursor.node.kind === NonterminalKind.PathImport || cursor.node.kind === NonterminalKind.NamedImport ) { if (!cursor.goToNextNonterminalWithKind(NonterminalKind.ImportAlias)) { continue; } if (!cursor.goToNextTerminalWithKind(TerminalKind.Identifier)) { continue; } variableDefinitionCursor = cursor.spawn(); } else if ( cursor.node.kind === NonterminalKind.ImportDeconstructionSymbol ) { const importAliasCursor = cursor.spawn(); const hasAlias = importAliasCursor.goToNextNonterminalWithKind( NonterminalKind.ImportAlias, ); if (hasAlias) { if ( !importAliasCursor.goToNextTerminalWithKind(TerminalKind.Identifier) ) { continue; } variableDefinitionCursor = importAliasCursor; } else { if (!cursor.goToNextTerminalWithKind(TerminalKind.Identifier)) { continue; } variableDefinitionCursor = cursor; } } else { continue; } const definition = unit.bindingGraph.definitionAt(variableDefinitionCursor); if (definition === undefined) { continue; } if (definition.references().length === 0) { const name = variableDefinitionCursor.node.unparse(); if (!inheritDocComments.includes(name)) { definitions.push({ name, textRange: variableDefinitionCursor.textRange, definition, }); } } } return definitions; } function checkIsPrivateFunction(cursor: Cursor): boolean { if (!cursor.goToNextNonterminalWithKind(NonterminalKind.FunctionAttributes)) return false; cursor = cursor.spawn(); return cursor.goToNextTerminalWithKind(TerminalKind.PrivateKeyword); } function getFunctionNameDefinition( unit: CompilationUnit, functionDefinitionCursor: Cursor, ): Definition | undefined { if ( !functionDefinitionCursor.goToNextNonterminalWithKind( NonterminalKind.FunctionName, ) ) { return undefined; } if ( !functionDefinitionCursor.goToNextTerminalWithKind(TerminalKind.Identifier) ) { return undefined; } return unit.bindingGraph.definitionAt(functionDefinitionCursor); } function findInheritDocComments(file: SlangFile): string[] { const diagnostics: string[] = []; const cursor = file.createTreeCursor(); while ( cursor.goToNextTerminalWithKinds([ TerminalKind.SingleLineNatSpecComment, TerminalKind.MultiLineNatSpecComment, ]) ) { const commentText = cursor.node.unparse(); const inheritDocMatch = commentText.match(/@inheritdoc\s+([$\w]+)/); if (inheritDocMatch) { diagnostics.push(inheritDocMatch[1]); } } return diagnostics; } function checkFileHasOnlyImports(cursor: Cursor): boolean { while (cursor.goToNextNonterminalWithKind(NonterminalKind.SourceUnitMember)) { assertNonterminalNode(cursor.node); const sourceUnitMember = new SourceUnitMember(cursor.node); const isPragma = sourceUnitMember.variant instanceof PragmaDirective; const isImport = sourceUnitMember.variant instanceof ImportDirective; if (!isPragma && !isImport) { return false; } } return true; }