Repository: protofire/solhint
Pull request #390 (2023-01-25 21:30:44)

[new rule] No global imports
depends on #389 

this is my first time near a syntax tree, so I'm looking forward to your feedback!

I also took the liberty of making it a recommended rule, but if there isn't consensus we could just drop the last commit

as always, see technical details in git history


Patch:
diff --git a/docs/rules.md b/docs/rules.md
index b656971..e59f2a0 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -14,6 +14,7 @@ title:       "Rule Index of Solhint"
 | [max-states-count](./rules/best-practises/max-states-count.md)     | Contract has "some count" states declarations but allowed no more than maxstates.                                     | ✔️          |
 | [no-console](./rules/best-practises/no-console.md)                 | No console.log/logInt/logBytesX/logString/etc & No hardhat and forge-std console.sol import statements                | ✔️          |
 | [no-empty-blocks](./rules/best-practises/no-empty-blocks.md)       | Code contains empty block.                                                                                            | ✔️          |
+| [no-global-import](./rules/best-practises/no-global-import.md)     | Import statement includes an entire file instead of selected symbols                                                  | ✔️          |
 | [no-unused-vars](./rules/best-practises/no-unused-vars.md)         | Variable "name" is unused.                                                                                            | ✔️          |
 | [payable-fallback](./rules/best-practises/payable-fallback.md)     | When fallback is not payable you will not be able to receive ethers.                                                  | ✔️          |
 | [reason-string](./rules/best-practises/reason-string.md)           | Require or revert statement must have a reason string and check that each reason string is at most N characters long. | ✔️          |
diff --git a/docs/rules/best-practises/no-global-import.md b/docs/rules/best-practises/no-global-import.md
new file mode 100644
index 0000000..e07bfc8
--- /dev/null
+++ b/docs/rules/best-practises/no-global-import.md
@@ -0,0 +1,59 @@
+---
+warning:     "This is a dynamically generated file. Do not edit manually."
+layout:      "default"
+title:       "no-global-import | Solhint"
+---
+
+# no-global-import
+![Recommended Badge](https://img.shields.io/badge/-Recommended-brightgreen)
+![Category Badge](https://img.shields.io/badge/-Best%20Practise%20Rules-informational)
+![Default Severity Badge warn](https://img.shields.io/badge/Default%20Severity-warn-yellow)
+> The {"extends": "solhint:recommended"} property in a configuration file enables this rule.
+
+
+## Description
+Import statement includes an entire file instead of selected symbols
+
+## Options
+This rule accepts a string option of rule severity. Must be one of "error", "warn", "off". Default to warn.
+
+### Example Config
+```json
+{
+  "rules": {
+    "no-global-import": "warn"
+  }
+}
+```
+
+
+## Examples
+### 👍 Examples of **correct** code for this rule
+
+#### import names explicitly
+
+```solidity
+import {A} from "./A.sol"
+```
+
+### 👎 Examples of **incorrect** code for this rule
+
+#### import all members from a file
+
+```solidity
+import * from "foo.sol"
+```
+
+#### import an entire file
+
+```solidity
+import "foo.sol"
+```
+
+## Version
+This rule is introduced in the latest version.
+
+## Resources
+- [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/best-practises/no-global-import.js)
+- [Document source](https://github.com/protofire/solhint/tree/master/docs/rules/best-practises/no-global-import.md)
+- [Test cases](https://github.com/protofire/solhint/tree/master/test/rules/best-practises/no-global-import.js)
diff --git a/lib/rules/best-practises/index.js b/lib/rules/best-practises/index.js
index 98ff7e2..47012e7 100644
--- a/lib/rules/best-practises/index.js
+++ b/lib/rules/best-practises/index.js
@@ -7,6 +7,7 @@ const NoUnusedVarsChecker = require('./no-unused-vars')
 const PayableFallbackChecker = require('./payable-fallback')
 const ReasonStringChecker = require('./reason-string')
 const NoConsoleLogChecker = require('./no-console')
+const NoGlobalImportsChecker = require('./no-global-import')
 
 module.exports = function checkers(reporter, config, inputSrc) {
   return [
@@ -19,5 +20,6 @@ module.exports = function checkers(reporter, config, inputSrc) {
     new PayableFallbackChecker(reporter),
     new ReasonStringChecker(reporter, config),
     new NoConsoleLogChecker(reporter),
+    new NoGlobalImportsChecker(reporter),
   ]
 }
diff --git a/lib/rules/best-practises/no-global-import.js b/lib/rules/best-practises/no-global-import.js
new file mode 100644
index 0000000..d9ae75f
--- /dev/null
+++ b/lib/rules/best-practises/no-global-import.js
@@ -0,0 +1,41 @@
+const BaseChecker = require('../base-checker')
+
+const ruleId = 'no-global-import'
+const meta = {
+  type: 'best-practises',
+
+  docs: {
+    description: 'Import statement includes an entire file instead of selected symbols',
+    category: 'Best Practise Rules',
+    examples: {
+      bad: [
+        { description: 'import all members from a file', code: 'import * from "foo.sol"' },
+        { description: 'import an entire file', code: 'import "foo.sol"' },
+      ],
+      good: [{ description: 'import names explicitly', code: 'import {A} from "./A.sol"' }],
+    },
+  },
+
+  isDefault: false,
+  recommended: true,
+  defaultSetup: 'warn',
+
+  schema: null,
+}
+
+class NoGlobalImportsChecker extends BaseChecker {
+  constructor(reporter) {
+    super(reporter, ruleId, meta)
+  }
+
+  ImportDirective(node) {
+    if (!node.symbolAliases) {
+      this.error(
+        node,
+        `global import of path ${node.path} is not allowed. Specify names to import individually`
+      )
+    }
+  }
+}
+
+module.exports = NoGlobalImportsChecker


Test patch:
diff --git a/test/rules/best-practises/no-global-import.js b/test/rules/best-practises/no-global-import.js
new file mode 100644
index 0000000..199f8c0
--- /dev/null
+++ b/test/rules/best-practises/no-global-import.js
@@ -0,0 +1,58 @@
+const linter = require('../../../lib/index')
+const {
+  assertNoWarnings,
+  assertLineNumber,
+  assertErrorMessage,
+  assertWarnsCount,
+} = require('../../common/asserts')
+
+describe('Linter - no-global-import', () => {
+  it('should raise on import * from "path"', () => {
+    const code = `import * from './A.sol';`
+
+    const report = linter.processStr(code, {
+      rules: { 'no-global-import': 'warn' },
+    })
+    assertWarnsCount(report, 1)
+    assertErrorMessage(report, 'global import')
+    assertErrorMessage(report, 'Specify names to import individually')
+  })
+  it('should raise on import "path"', () => {
+    const code = `import './A.sol';`
+
+    const report = linter.processStr(code, {
+      rules: { 'no-global-import': 'warn' },
+    })
+    assertWarnsCount(report, 1)
+    assertErrorMessage(report, 'global import')
+    assertErrorMessage(report, 'Specify names to import individually')
+  })
+  it('should raise warning when using solhint:recommended', () => {
+    const code = `pragma solidity ^0.5.8; import "./A.sol";`
+
+    const report = linter.processStr(code, {
+      extends: 'solhint:recommended',
+    })
+    assertWarnsCount(report, 1)
+    assertErrorMessage(report, 'global import')
+    assertErrorMessage(report, 'Specify names to import individually')
+  })
+  it('should report correct line', () => {
+    const code = `import {A} from './A.sol';
+    import './A.sol';`
+
+    const report = linter.processStr(code, {
+      rules: { 'no-global-import': 'warn' },
+    })
+    assertLineNumber(report.reports[0], 2)
+    assertWarnsCount(report, 1)
+  })
+  it('should not raise on import {identifier} from "path"', () => {
+    const code = `import {A} from './A.sol';`
+
+    const report = linter.processStr(code, {
+      rules: { 'no-global-import': 'warn' },
+    })
+    assertNoWarnings(report)
+  })
+})