@slippy-lint/slippy
Version:
A simple but powerful linter for Solidity
609 lines • 24.6 kB
JavaScript
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