UNPKG

@slippy-lint/slippy

Version:

A simple but powerful linter for Solidity

609 lines 24.6 kB
import { assertNonterminalNode, NonterminalKind, TerminalKind, } from "@nomicfoundation/slang/cst"; import * as z from "zod"; import { AssertionError } from "../errors.js"; var PredefinedFormats; (function (PredefinedFormats) { PredefinedFormats[PredefinedFormats["camelCase"] = 1] = "camelCase"; PredefinedFormats[PredefinedFormats["strictCamelCase"] = 2] = "strictCamelCase"; PredefinedFormats[PredefinedFormats["PascalCase"] = 3] = "PascalCase"; PredefinedFormats[PredefinedFormats["StrictPascalCase"] = 4] = "StrictPascalCase"; PredefinedFormats[PredefinedFormats["snake_case"] = 5] = "snake_case"; PredefinedFormats[PredefinedFormats["UPPER_CASE"] = 6] = "UPPER_CASE"; })(PredefinedFormats || (PredefinedFormats = {})); var UnderscoreOptions; (function (UnderscoreOptions) { UnderscoreOptions[UnderscoreOptions["forbid"] = 1] = "forbid"; UnderscoreOptions[UnderscoreOptions["allow"] = 2] = "allow"; UnderscoreOptions[UnderscoreOptions["require"] = 3] = "require"; UnderscoreOptions[UnderscoreOptions["requireDouble"] = 4] = "requireDouble"; UnderscoreOptions[UnderscoreOptions["allowDouble"] = 5] = "allowDouble"; UnderscoreOptions[UnderscoreOptions["allowSingleOrDouble"] = 6] = "allowSingleOrDouble"; })(UnderscoreOptions || (UnderscoreOptions = {})); export var Selectors; (function (Selectors) { Selectors[Selectors["contract"] = 1] = "contract"; Selectors[Selectors["interface"] = 2] = "interface"; Selectors[Selectors["library"] = 4] = "library"; Selectors[Selectors["stateVariable"] = 8] = "stateVariable"; Selectors[Selectors["function"] = 16] = "function"; Selectors[Selectors["variable"] = 32] = "variable"; Selectors[Selectors["struct"] = 64] = "struct"; Selectors[Selectors["structMember"] = 128] = "structMember"; Selectors[Selectors["enum"] = 256] = "enum"; Selectors[Selectors["enumMember"] = 512] = "enumMember"; Selectors[Selectors["parameter"] = 1024] = "parameter"; Selectors[Selectors["modifier"] = 2048] = "modifier"; Selectors[Selectors["event"] = 4096] = "event"; Selectors[Selectors["eventParameter"] = 8192] = "eventParameter"; Selectors[Selectors["userDefinedValueType"] = 16384] = "userDefinedValueType"; Selectors[Selectors["error"] = 32768] = "error"; Selectors[Selectors["errorParameter"] = 65536] = "errorParameter"; Selectors[Selectors["mappingParameter"] = 131072] = "mappingParameter"; })(Selectors || (Selectors = {})); export var MetaSelectors; (function (MetaSelectors) { MetaSelectors[MetaSelectors["default"] = -1] = "default"; MetaSelectors[MetaSelectors["typeLike"] = 53575] = "typeLike"; MetaSelectors[MetaSelectors["variableLike"] = 208056] = "variableLike"; })(MetaSelectors || (MetaSelectors = {})); var Modifiers; (function (Modifiers) { Modifiers[Modifiers["constant"] = 1] = "constant"; Modifiers[Modifiers["immutable"] = 2] = "immutable"; Modifiers[Modifiers["public"] = 4] = "public"; Modifiers[Modifiers["internal"] = 8] = "internal"; Modifiers[Modifiers["private"] = 16] = "private"; Modifiers[Modifiers["external"] = 32] = "external"; Modifiers[Modifiers["view"] = 64] = "view"; Modifiers[Modifiers["pure"] = 128] = "pure"; Modifiers[Modifiers["payable"] = 256] = "payable"; Modifiers[Modifiers["virtual"] = 512] = "virtual"; Modifiers[Modifiers["override"] = 1024] = "override"; Modifiers[Modifiers["abstract"] = 2048] = "abstract"; Modifiers[Modifiers["noParameters"] = 4096] = "noParameters"; Modifiers[Modifiers["hasParameters"] = 8192] = "hasParameters"; Modifiers[Modifiers["contract"] = 16384] = "contract"; Modifiers[Modifiers["interface"] = 32768] = "interface"; Modifiers[Modifiers["library"] = 65536] = "library"; })(Modifiers || (Modifiers = {})); const DEFAULT_CONFIG = [ { selector: "default", format: ["camelCase"], leadingUnderscore: "allow", trailingUnderscore: "allow", }, { selector: "typeLike", format: ["PascalCase"], }, { selector: "enumMember", format: ["PascalCase"], }, ]; const extractFirstIdentifier = (cursor) => { if (!cursor.goToNextTerminalWithKind(TerminalKind.Identifier)) { return []; } return [ { name: cursor.node.unparse(), textRange: cursor.textRange, }, ]; }; const extractIdentifierAfter = (cursor, nonterminalToIgnore) => { if (!cursor.goToNextNonterminalWithKind(nonterminalToIgnore)) { return []; } if (!cursor.goToNextSibling()) { return []; } return extractFirstIdentifier(cursor); }; const extractAllIdentifiers = (cursor) => { const identifiers = []; while (cursor.goToNextTerminalWithKind(TerminalKind.Identifier)) { identifiers.push({ name: cursor.node.unparse(), textRange: cursor.textRange, }); } return identifiers; }; const nonterminalKindToMetadata = { [NonterminalKind.ContractDefinition]: { selector: Selectors.contract, extractNames: extractFirstIdentifier, }, [NonterminalKind.InterfaceDefinition]: { selector: Selectors.interface, extractNames: extractFirstIdentifier, }, [NonterminalKind.LibraryDefinition]: { selector: Selectors.library, extractNames: extractFirstIdentifier, }, [NonterminalKind.StateVariableDefinition]: { selector: Selectors.stateVariable, extractNames: (cursor) => extractIdentifierAfter(cursor, NonterminalKind.TypeName), }, [NonterminalKind.FunctionDefinition]: { selector: Selectors.function, extractNames: extractFirstIdentifier, }, [NonterminalKind.VariableDeclarationStatement]: { selector: Selectors.variable, extractNames: (cursor) => extractIdentifierAfter(cursor, NonterminalKind.VariableDeclarationType), }, [NonterminalKind.TypedTupleMember]: { selector: Selectors.variable, extractNames: (cursor) => extractIdentifierAfter(cursor, NonterminalKind.TypeName), }, [NonterminalKind.StructDefinition]: { selector: Selectors.struct, extractNames: extractFirstIdentifier, }, [NonterminalKind.StructMember]: { selector: Selectors.structMember, extractNames: (cursor) => extractIdentifierAfter(cursor, NonterminalKind.TypeName), }, [NonterminalKind.EnumDefinition]: { selector: Selectors.enum, extractNames: extractFirstIdentifier, }, [NonterminalKind.EnumMembers]: { selector: Selectors.enumMember, extractNames: extractAllIdentifiers, }, [NonterminalKind.Parameter]: { selector: Selectors.parameter, extractNames: (cursor) => extractIdentifierAfter(cursor, NonterminalKind.TypeName), }, [NonterminalKind.ModifierDefinition]: { selector: Selectors.modifier, extractNames: extractFirstIdentifier, }, [NonterminalKind.EventDefinition]: { selector: Selectors.event, extractNames: extractFirstIdentifier, }, [NonterminalKind.EventParameter]: { selector: Selectors.eventParameter, extractNames: (cursor) => extractIdentifierAfter(cursor, NonterminalKind.TypeName), }, [NonterminalKind.UserDefinedValueTypeDefinition]: { selector: Selectors.userDefinedValueType, extractNames: extractFirstIdentifier, }, [NonterminalKind.ErrorDefinition]: { selector: Selectors.error, extractNames: extractFirstIdentifier, }, [NonterminalKind.ErrorParameter]: { selector: Selectors.errorParameter, extractNames: (cursor) => extractIdentifierAfter(cursor, NonterminalKind.TypeName), }, [NonterminalKind.MappingKey]: { selector: Selectors.mappingParameter, extractNames: (cursor) => extractIdentifierAfter(cursor, NonterminalKind.MappingKeyType), }, [NonterminalKind.MappingValue]: { selector: Selectors.mappingParameter, extractNames: (cursor) => extractIdentifierAfter(cursor, NonterminalKind.TypeName), }, }; export function normalizeConfig(userConfig) { return userConfig.flatMap(normalizeOption).sort((a, b) => { if (a.selector === b.selector) { // in the event of the same selector, order by modifier weight // sort descending - the type modifiers are "more important" return b.modifierWeight - a.modifierWeight; } const aIsMeta = isMetaSelector(a.selector); const bIsMeta = isMetaSelector(b.selector); // non-meta selectors should go ahead of meta selectors if (aIsMeta && !bIsMeta) { return 1; } if (!aIsMeta && bIsMeta) { return -1; } return b.selector - a.selector; }); } const MatchRegexSchema = z.object({ match: z.boolean(), regex: z.string(), }); const SelectorsStringSchema = z.enum(Object.keys(Selectors).filter((x) => isNaN(Number(x)))); const MetaSelectorsStringSchema = z.enum(Object.keys(MetaSelectors).filter((x) => isNaN(Number(x)))); const IndividualAndMetaSelectorsStringSchema = z.union([ SelectorsStringSchema, MetaSelectorsStringSchema, ]); const PredefinedFormatsStringSchema = z.enum(Object.keys(PredefinedFormats).filter((x) => isNaN(Number(x)))); const UnderscoreOptionsStringSchema = z.enum(Object.keys(UnderscoreOptions).filter((x) => isNaN(Number(x)))); const ModifiersStringSchema = z.enum(Object.keys(Modifiers).filter((x) => isNaN(Number(x)))); export const Schema = z .array(z.object({ custom: z.optional(MatchRegexSchema), filter: z.optional(z.union([z.string(), MatchRegexSchema])), format: z.nullable(z.array(PredefinedFormatsStringSchema)), leadingUnderscore: z.optional(UnderscoreOptionsStringSchema), modifiers: z.optional(z.array(ModifiersStringSchema)), selector: z.union([ IndividualAndMetaSelectorsStringSchema, z.array(IndividualAndMetaSelectorsStringSchema), ]), trailingUnderscore: z.optional(UnderscoreOptionsStringSchema), })) .default(DEFAULT_CONFIG); export const NamingConvention = { name: "naming-convention", recommended: false, parseConfig: (config) => { return Schema.parse(config); }, create: function (config) { return new NamingConventionRule(this.name, config); }, }; class NamingConventionRule { constructor(name, config) { this.name = name; this.config = config; this.normalizedConfig = normalizeConfig(config); } run({ file }) { const diagnostics = []; const cursor = file.createTreeCursor(); const definitionCursor = cursor.spawn(); while (definitionCursor.goToNextNonterminalWithKinds(Object.keys(nonterminalKindToMetadata))) { assertNonterminalNode(definitionCursor.node); const nonterminalMetadata = nonterminalKindToMetadata[definitionCursor.node.kind]; if (nonterminalMetadata === undefined) { throw new AssertionError(`Couldn't find selector for node kind: ${definitionCursor.node.kind}`); } const identifierCursor = definitionCursor.spawn(); const identifiersWithTextRange = nonterminalMetadata.extractNames(identifierCursor); for (const { name: originalName, textRange, } of identifiersWithTextRange) { for (const config of this.normalizedConfig) { if (!matchesSelector(config.selector, nonterminalMetadata.selector)) { continue; } if (config.filter?.regex.test(originalName) !== config.filter?.match) { // name does not match the filter continue; } if (config.modifiers?.some((modifier) => !hasModifier(definitionCursor.clone(), modifier)) === true) { // does not have the required modifiers continue; } let name = originalName; let diagnostic; diagnostic = this.validateUnderscore("leading", config, name, textRange, file, originalName); if (typeof diagnostic === "string") { name = diagnostic; } else { diagnostics.push(diagnostic); break; } diagnostic = this.validateUnderscore("trailing", config, name, textRange, file, originalName); if (typeof diagnostic === "string") { name = diagnostic; } else { diagnostics.push(diagnostic); break; } const customDiagnostic = this.validateCustom(config, name, textRange, file, originalName); if (customDiagnostic !== undefined) { diagnostics.push(customDiagnostic); break; } const predefinedFormatDiagnostic = this.validatePredefinedFormat(config, name, textRange, file, originalName); if (predefinedFormatDiagnostic !== undefined) { diagnostics.push(predefinedFormatDiagnostic); break; } // if we reach here, the selector matches and the name is valid break; } } } return diagnostics; } validateUnderscore(position, config, name, textRange, file, originalName) { const option = position === "leading" ? config.leadingUnderscore : config.trailingUnderscore; if (option === null) { return name; } const hasSingleUnderscore = position === "leading" ? () => name.startsWith("_") : () => name.endsWith("_"); const trimSingleUnderscore = position === "leading" ? () => name.slice(1) : () => name.slice(0, -1); const hasDoubleUnderscore = position === "leading" ? () => name.startsWith("__") : () => name.endsWith("__"); const trimDoubleUnderscore = position === "leading" ? () => name.slice(2) : () => name.slice(0, -2); switch (option) { // ALLOW - no conditions as the user doesn't care if it's there or not case UnderscoreOptions.allow: { if (hasSingleUnderscore()) { return trimSingleUnderscore(); } return name; } case UnderscoreOptions.allowDouble: { if (hasDoubleUnderscore()) { return trimDoubleUnderscore(); } return name; } case UnderscoreOptions.allowSingleOrDouble: { if (hasDoubleUnderscore()) { return trimDoubleUnderscore(); } if (hasSingleUnderscore()) { return trimSingleUnderscore(); } return name; } // FORBID case UnderscoreOptions.forbid: { if (hasSingleUnderscore()) { return { rule: this.name, sourceId: file.id, line: textRange.start.line, column: textRange.start.column, message: `'${originalName}' should not have a ${position} underscore`, }; } return name; } // REQUIRE case UnderscoreOptions.require: { if (!hasSingleUnderscore()) { return { rule: this.name, sourceId: file.id, line: textRange.start.line, column: textRange.start.column, message: `'${originalName}' should have a ${position} underscore`, }; } return trimSingleUnderscore(); } case UnderscoreOptions.requireDouble: { if (!hasDoubleUnderscore()) { return { rule: this.name, sourceId: file.id, line: textRange.start.line, column: textRange.start.column, message: `'${originalName}' should have a ${position} double underscore`, }; } return trimDoubleUnderscore(); } } } validateCustom(config, name, textRange, file, originalName) { const custom = config.custom; if (!custom) { return; } const matches = custom.regex.test(name); if (custom.match && matches) { return; } if (!custom.match && !matches) { return; } return { rule: this.name, sourceId: file.id, line: textRange.start.line, column: textRange.start.column, message: `'${originalName}' must ${custom.match ? "match" : "not match"} the RegExp ${custom.regex.toString()}`, }; } validatePredefinedFormat(config, name, textRange, file, originalName) { const formats = config.format; if (formats === null || formats.length === 0) { return; } for (const format of formats) { const checker = PredefinedFormatToCheckFunction[format]; if (checker(name)) { return; } } const formatsString = formats?.map((f) => PredefinedFormats[f]).join(", "); return { rule: this.name, sourceId: file.id, line: textRange.start.line, column: textRange.start.column, message: `'${originalName}' does not match the required format(s): ${formatsString}`, }; } } function matchesSelector(selector, kind) { return (selector & kind) !== 0 || selector === MetaSelectors.default; } function isMetaSelector(selector) { return selector in MetaSelectors; } function normalizeOption(option) { let weight = 0; option.modifiers?.forEach((mod) => { weight |= Modifiers[mod]; }); // give selectors with a filter the _highest_ priority if (option.filter !== undefined) { weight |= 1 << 30; } const normalizedOption = { // format options custom: option.custom ? { match: option.custom.match, regex: new RegExp(option.custom.regex, "u"), } : null, filter: option.filter != null ? typeof option.filter === "string" ? { match: true, regex: new RegExp(option.filter, "u"), } : { match: option.filter.match, regex: new RegExp(option.filter.regex, "u"), } : null, format: option.format ? option.format.map((f) => PredefinedFormats[f]) : null, leadingUnderscore: option.leadingUnderscore != null ? UnderscoreOptions[option.leadingUnderscore] : null, modifiers: option.modifiers?.map((m) => Modifiers[m]) ?? null, trailingUnderscore: option.trailingUnderscore != null ? UnderscoreOptions[option.trailingUnderscore] : null, // calculated ordering weight based on modifiers modifierWeight: weight, }; const selectors = Array.isArray(option.selector) ? option.selector : [option.selector]; return selectors.map((selector) => ({ selector: isMetaSelector(selector) ? MetaSelectors[selector] : Selectors[selector], ...normalizedOption, })); } function hasKeyword(cursor, keyword) { const spawned = cursor.spawn(); return spawned.goToNextTerminalWithKind(keyword); } const hasModifierMap = { [Modifiers.constant]: (cursor) => hasKeyword(cursor, TerminalKind.ConstantKeyword), [Modifiers.immutable]: (cursor) => hasKeyword(cursor, TerminalKind.ImmutableKeyword), [Modifiers.public]: (cursor) => hasKeyword(cursor, TerminalKind.PublicKeyword), [Modifiers.internal]: (cursor) => hasKeyword(cursor, TerminalKind.InternalKeyword), [Modifiers.private]: (cursor) => hasKeyword(cursor, TerminalKind.PrivateKeyword), [Modifiers.external]: (cursor) => hasKeyword(cursor, TerminalKind.ExternalKeyword), [Modifiers.view]: (cursor) => hasKeyword(cursor, TerminalKind.ViewKeyword), [Modifiers.pure]: (cursor) => hasKeyword(cursor, TerminalKind.PureKeyword), [Modifiers.payable]: (cursor) => hasKeyword(cursor, TerminalKind.PayableKeyword), [Modifiers.virtual]: (cursor) => hasKeyword(cursor, TerminalKind.VirtualKeyword), [Modifiers.override]: (cursor) => hasKeyword(cursor, TerminalKind.OverrideKeyword), [Modifiers.abstract]: (cursor) => hasKeyword(cursor, TerminalKind.AbstractKeyword), [Modifiers.noParameters]: (cursor) => !hasDescendant(cursor, NonterminalKind.Parameter), [Modifiers.hasParameters]: (cursor) => hasDescendant(cursor, NonterminalKind.Parameter), [Modifiers.contract]: (cursor) => hasAncestor(cursor, NonterminalKind.ContractDefinition), [Modifiers.interface]: (cursor) => hasAncestor(cursor, NonterminalKind.InterfaceDefinition), [Modifiers.library]: (cursor) => hasAncestor(cursor, NonterminalKind.LibraryDefinition), }; function hasAncestor(cursor, kind) { while (cursor.goToParent()) { if (cursor.node.kind === kind) { return true; } } return false; } function hasDescendant(cursor, kind) { const spawned = cursor.spawn(); return spawned.goToNextNonterminalWithKind(kind); } function hasModifier(cursor, modifier) { return hasModifierMap[modifier](cursor); } const PredefinedFormatToCheckFunction = { [PredefinedFormats.camelCase]: isCamelCase, [PredefinedFormats.PascalCase]: isPascalCase, [PredefinedFormats.snake_case]: isSnakeCase, [PredefinedFormats.strictCamelCase]: isStrictCamelCase, [PredefinedFormats.StrictPascalCase]: isStrictPascalCase, [PredefinedFormats.UPPER_CASE]: isUpperCase, }; function isPascalCase(name) { return (name.length === 0 || (name[0] === name[0].toUpperCase() && !name.includes("_"))); } function isStrictPascalCase(name) { return (name.length === 0 || (name[0] === name[0].toUpperCase() && hasStrictCamelHumps(name, true))); } function isCamelCase(name) { return (name.length === 0 || (name[0] === name[0].toLowerCase() && !name.includes("_"))); } function isStrictCamelCase(name) { return (name.length === 0 || (name[0] === name[0].toLowerCase() && hasStrictCamelHumps(name, false))); } function hasStrictCamelHumps(name, isUpper) { function isUppercaseChar(char) { return char === char.toUpperCase() && char !== char.toLowerCase(); } if (name.startsWith("_")) { return false; } for (let i = 1; i < name.length; ++i) { if (name[i] === "_") { return false; } if (isUpper === isUppercaseChar(name[i])) { if (isUpper) { return false; } } else { isUpper = !isUpper; } } return true; } function isSnakeCase(name) { return (name.length === 0 || (name === name.toLowerCase() && validateUnderscores(name))); } function isUpperCase(name) { return (name.length === 0 || (name === name.toUpperCase() && validateUnderscores(name))); } function validateUnderscores(name) { if (name.startsWith("_")) { return false; } let wasUnderscore = false; for (let i = 1; i < name.length; ++i) { if (name[i] === "_") { if (wasUnderscore) { return false; } wasUnderscore = true; } else { wasUnderscore = false; } } return !wasUnderscore; } //# sourceMappingURL=naming-convention.js.map