diff --git a/.gitmodules b/.gitmodules index 2f4bc6f..9b09120 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "lib/solidity-stringutils"] path = lib/solidity-stringutils url = https://github.com/Arachnid/solidity-stringutils diff --git a/foundry.toml b/foundry.toml index 838381c..f506602 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,6 @@ [profile.default] src = "src" out = "out" -libs = ["lib"] +libs = ["node_modules", "lib"] build_info = true extra_output = ["storageLayout"] diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts deleted file mode 160000 index 932fddf..0000000 --- a/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 932fddf69a699a9a80fd2396fd1a2ab91cdda123 diff --git a/package.json b/package.json index 72667ad..a4e06cc 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,11 @@ "lint:fix": "prettier --log-level warn --ignore-path .gitignore '{src,test}/**/*.sol' --write" }, "devDependencies": { + "@openzeppelin/contracts": "^5.0.0", "@openzeppelin/upgrades-core": "^1.31.0", "prettier": "^3.0.0", "prettier-plugin-solidity": "^1.1.0", "solhint": "^3.3.6", - "solhint-plugin-openzeppelin": "file:lib/openzeppelin-contracts/scripts/solhint-custom" + "solhint-plugin-openzeppelin": "file:scripts/solhint-custom" } } diff --git a/remappings.txt b/remappings.txt index 25d1e85..56af064 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1 @@ -@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ openzeppelin-foundry-upgrades/=src/ \ No newline at end of file diff --git a/scripts/solhint-custom/index.js b/scripts/solhint-custom/index.js new file mode 100644 index 0000000..9625027 --- /dev/null +++ b/scripts/solhint-custom/index.js @@ -0,0 +1,84 @@ +const path = require('path'); +const minimatch = require('minimatch'); + +// Files matching these patterns will be ignored unless a rule has `static global = true` +const ignore = ['contracts/mocks/**/*', 'test/**/*']; + +class Base { + constructor(reporter, config, source, fileName) { + this.reporter = reporter; + this.ignored = this.constructor.global || ignore.some(p => minimatch(path.normalize(fileName), p)); + this.ruleId = this.constructor.ruleId; + if (this.ruleId === undefined) { + throw Error('missing ruleId static property'); + } + } + + error(node, message) { + if (!this.ignored) { + this.reporter.error(node, this.ruleId, message); + } + } +} + +module.exports = [ + class extends Base { + static ruleId = 'interface-names'; + + ContractDefinition(node) { + if (node.kind === 'interface' && !/^I[A-Z]/.test(node.name)) { + this.error(node, 'Interface names should have a capital I prefix'); + } + } + }, + + class extends Base { + static ruleId = 'private-variables'; + + VariableDeclaration(node) { + const constantOrImmutable = node.isDeclaredConst || node.isImmutable; + if (node.isStateVar && !constantOrImmutable && node.visibility !== 'private') { + this.error(node, 'State variables must be private'); + } + } + }, + + class extends Base { + static ruleId = 'leading-underscore'; + + VariableDeclaration(node) { + if (node.isDeclaredConst) { + // TODO: expand visibility and fix + if (node.visibility === 'private' && /^_/.test(node.name)) { + this.error(node, 'Constant variables should not have leading underscore'); + } + } else if (node.visibility === 'private' && !/^_/.test(node.name)) { + this.error(node, 'Non-constant private variables must have leading underscore'); + } + } + + FunctionDefinition(node) { + if (node.visibility === 'private' || (node.visibility === 'internal' && node.parent.kind !== 'library')) { + if (!/^_/.test(node.name)) { + this.error(node, 'Private and internal functions must have leading underscore'); + } + } + if (node.visibility === 'internal' && node.parent.kind === 'library') { + if (/^_/.test(node.name)) { + this.error(node, 'Library internal functions should not have leading underscore'); + } + } + } + }, + + // TODO: re-enable and fix + // class extends Base { + // static ruleId = 'no-external-virtual'; + // + // FunctionDefinition(node) { + // if (node.visibility == 'external' && node.isVirtual) { + // this.error(node, 'Functions should not be external and virtual'); + // } + // } + // }, +]; diff --git a/scripts/solhint-custom/package.json b/scripts/solhint-custom/package.json new file mode 100644 index 0000000..075eb92 --- /dev/null +++ b/scripts/solhint-custom/package.json @@ -0,0 +1,5 @@ +{ + "name": "solhint-plugin-openzeppelin", + "version": "0.0.0", + "private": true +} diff --git a/solhint.config.js b/solhint.config.js index b8a8d0f..c09a7b1 100644 --- a/solhint.config.js +++ b/solhint.config.js @@ -1 +1,24 @@ -module.exports = require('./lib/openzeppelin-contracts/solhint.config.js'); \ No newline at end of file +const customRules = require('./scripts/solhint-custom'); + +console.log('Using custom rules:', JSON.stringify(customRules, null, 2)); + +const rules = [ + 'no-unused-vars', + 'const-name-snakecase', + 'contract-name-camelcase', + 'event-name-camelcase', + 'func-name-mixedcase', + 'func-param-name-mixedcase', + 'modifier-name-mixedcase', + 'var-name-mixedcase', + 'imports-on-top', + 'no-global-import', + ...customRules.map(r => `openzeppelin/${r.ruleId}`), +]; + +console.log('Using rules:', JSON.stringify(rules, null, 2)); + +module.exports = { + plugins: ['openzeppelin'], + rules: Object.fromEntries(rules.map(r => [r, 'error'])), +}; diff --git a/yarn.lock b/yarn.lock index 696de70..de1ff8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,6 +24,11 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@openzeppelin/contracts@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.0.tgz#ee0e4b4564f101a5c4ee398cd4d73c0bd92b289c" + integrity sha512-bv2sdS6LKqVVMLI5+zqnNrNU/CA+6z6CmwFXm/MzmOPBRSO5reEJN7z0Gbzvs0/bv/MZZXNklubpwy3v2+azsw== + "@openzeppelin/upgrades-core@^1.31.0": version "1.31.0" resolved "https://registry.yarnpkg.com/@openzeppelin/upgrades-core/-/upgrades-core-1.31.0.tgz#cc262e2618d90540a8ad4dfafbdf06afdb17b8fa" @@ -1194,7 +1199,7 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -"solhint-plugin-openzeppelin@file:lib/openzeppelin-contracts/scripts/solhint-custom": +"solhint-plugin-openzeppelin@file:scripts/solhint-custom": version "0.0.0" solhint@^3.3.6: