Skip to content

Commit 5840e73

Browse files
author
dbale-altoros
committed
feature: duplicated import rule
1 parent 7055c5a commit 5840e73

File tree

8 files changed

+629
-1
lines changed

8 files changed

+629
-1
lines changed

conf/rulesets/solhint-all.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ module.exports = Object.freeze({
3636
'gas-strict-inequalities': 'warn',
3737
'gas-struct-packing': 'warn',
3838
'comprehensive-interface': 'warn',
39+
'duplicated-imports': 'warn',
3940
quotes: ['error', 'double'],
4041
'const-name-snakecase': 'warn',
4142
'contract-name-capwords': 'warn',

docs/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ title: "Rule Index of Solhint"
2828
| Rule Id | Error | Recommended | Deprecated |
2929
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | ------------ | ----------- |
3030
| [interface-starts-with-i](./rules/naming/interface-starts-with-i.md) | Solidity Interfaces names should start with an `I` | | |
31+
| [duplicated-imports](./rules/miscellaneous/duplicated-imports.md) | Check if an import is done twice in the same file and there is no alias | | |
3132
| [const-name-snakecase](./rules/naming/const-name-snakecase.md) | Constant name must be in capitalized SNAKE_CASE. (Does not check IMMUTABLES, use immutable-vars-naming) | $~~~~~~~~$✔️ | |
3233
| [contract-name-capwords](./rules/naming/contract-name-capwords.md) | Contract, Structs and Enums should be in CapWords. | $~~~~~~~~$✔️ | |
3334
| [event-name-capwords](./rules/naming/event-name-capwords.md) | Event name must be in CapWords. | $~~~~~~~~$✔️ | |
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
warning: "This is a dynamically generated file. Do not edit manually."
3+
layout: "default"
4+
title: "duplicated-imports | Solhint"
5+
---
6+
7+
# duplicated-imports
8+
![Category Badge](https://img.shields.io/badge/-Style%20Guide%20Rules-informational)
9+
![Default Severity Badge warn](https://img.shields.io/badge/Default%20Severity-warn-yellow)
10+
11+
## Description
12+
Check if an import is done twice in the same file and there is no alias
13+
14+
## Options
15+
This rule accepts a string option of rule severity. Must be one of "error", "warn", "off". Defaults to warn.
16+
17+
### Example Config
18+
```json
19+
{
20+
"rules": {
21+
"duplicated-imports": "warn"
22+
}
23+
}
24+
```
25+
26+
### Notes
27+
- Rule reports "(inline) duplicated" if the same object is imported more than once in the same import statement
28+
- Rule reports "(globalSamePath) duplicated" if the same object is imported on another import statement from same location
29+
- Rule reports "(globalDiffPath) duplicated" if the same object is imported on another import statement, from other location, but no alias
30+
- Rule does NOT support this kind of import "import * as Alias from "./filename.sol"
31+
32+
## Examples
33+
This rule does not have examples.
34+
35+
## Version
36+
This rule is introduced in the latest version.
37+
38+
## Resources
39+
- [Rule source](https://github.com/protofire/solhint/blob/master/lib/rules/miscellaneous/duplicated-imports.js)
40+
- [Document source](https://github.com/protofire/solhint/blob/master/docs/rules/miscellaneous/duplicated-imports.md)
41+
- [Test cases](https://github.com/protofire/solhint/blob/master/test/rules/miscellaneous/duplicated-imports.js)
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
const path = require('path')
2+
const BaseChecker = require('../base-checker')
3+
const { severityDescription } = require('../../doc/utils')
4+
5+
const DEFAULT_SEVERITY = 'warn'
6+
7+
const ruleId = 'duplicated-imports'
8+
const meta = {
9+
type: 'miscellaneous',
10+
11+
docs: {
12+
description: `Check if an import is done twice in the same file and there is no alias`,
13+
category: 'Style Guide Rules',
14+
options: [
15+
{
16+
description: severityDescription,
17+
default: DEFAULT_SEVERITY,
18+
},
19+
],
20+
notes: [
21+
{
22+
note: 'Rule reports "(inline) duplicated" if the same object is imported more than once in the same import statement',
23+
},
24+
{
25+
note: 'Rule reports "(globalSamePath) duplicated" if the same object is imported on another import statement from same location',
26+
},
27+
{
28+
note: 'Rule reports "(globalDiffPath) duplicated" if the same object is imported on another import statement, from other location, but no alias',
29+
},
30+
{
31+
note: 'Rule does NOT support this kind of import "import * as Alias from "./filename.sol"',
32+
},
33+
],
34+
},
35+
36+
isDefault: false,
37+
recommended: false,
38+
defaultSetup: 'warn',
39+
fixable: true,
40+
schema: null,
41+
}
42+
43+
class DuplicatedImportsChecker extends BaseChecker {
44+
constructor(reporter) {
45+
super(reporter, ruleId, meta)
46+
47+
this.imports = []
48+
}
49+
50+
ImportDirective(node) {
51+
const normalizedPath = this.normalizePath(node.path)
52+
53+
const importStatement = {
54+
path: '',
55+
objectNames: [],
56+
}
57+
58+
importStatement.path = normalizedPath
59+
importStatement.objectNames = node.symbolAliases
60+
? node.symbolAliases
61+
: this.getObjectName(normalizedPath)
62+
63+
this.imports.push(importStatement)
64+
}
65+
66+
'SourceUnit:exit'(node) {
67+
const duplicates = this.findDuplicates(this.imports)
68+
69+
for (let i = 0; i < duplicates.length; i++) {
70+
this.error(node, `Duplicated Import (${duplicates[i].type}) ${duplicates[i].name}`)
71+
}
72+
}
73+
74+
getObjectName(normalizedPath) {
75+
// get file name
76+
const fileNameWithExtension = path.basename(normalizedPath)
77+
// Remove extension
78+
const objectName = fileNameWithExtension.replace('.sol', '')
79+
return [[objectName, null]]
80+
}
81+
82+
normalizePath(path) {
83+
if (path.startsWith('../')) {
84+
return `./${path}`
85+
}
86+
return path
87+
}
88+
89+
findInlineDuplicates(data) {
90+
const inlineDuplicates = []
91+
92+
data.forEach((entry) => {
93+
const path = entry.path
94+
// To track object names
95+
const objectNamesSet = new Set()
96+
97+
entry.objectNames.forEach(([objectName]) => {
98+
// If object name already been found , it is a duplicated
99+
if (objectNamesSet.has(objectName)) {
100+
inlineDuplicates.push({
101+
name: objectName,
102+
type: 'inline',
103+
paths: [path],
104+
})
105+
} else {
106+
// If it is not found before, we add it
107+
objectNamesSet.add(objectName)
108+
}
109+
})
110+
})
111+
112+
return inlineDuplicates
113+
}
114+
115+
finGlobalDuplicatesSamePath(data) {
116+
const duplicates = []
117+
118+
// Loop through data
119+
data.forEach((entry) => {
120+
const path = entry.path
121+
122+
// Object to track object names on each path
123+
const objectNamesMap = {}
124+
125+
// Loop through each objectName of current object
126+
entry.objectNames.forEach(([objectName]) => {
127+
if (!objectNamesMap[objectName]) {
128+
objectNamesMap[objectName] = []
129+
}
130+
objectNamesMap[objectName].push(path)
131+
})
132+
133+
// Compare this object with the rest to detect duplicates
134+
data.forEach((otherEntry) => {
135+
if (otherEntry !== entry) {
136+
otherEntry.objectNames.forEach(([objectName]) => {
137+
if (
138+
objectNamesMap[objectName] &&
139+
objectNamesMap[objectName].includes(otherEntry.path)
140+
) {
141+
// Add path only if it is not present
142+
const existingDuplicate = duplicates.find(
143+
(duplicate) =>
144+
duplicate.name === objectName &&
145+
duplicate.type === 'global' &&
146+
duplicate.paths.includes(entry.path)
147+
)
148+
149+
if (!existingDuplicate) {
150+
duplicates.push({
151+
name: objectName,
152+
type: 'globalSamePath',
153+
paths: [entry.path], // Just add path once, it is always the same
154+
})
155+
}
156+
}
157+
})
158+
}
159+
})
160+
})
161+
162+
return duplicates
163+
}
164+
165+
finGlobalDuplicatesDiffPathNoAlias(data) {
166+
const duplicates = []
167+
168+
// Loop through data
169+
data.forEach((entry) => {
170+
// Object to track names on each path
171+
entry.objectNames.forEach(([objectName, alias]) => {
172+
// Only compare if there is no alias
173+
if (!alias) {
174+
// Go through rest of objects to search for duplicates
175+
data.forEach((otherEntry) => {
176+
if (otherEntry !== entry) {
177+
otherEntry.objectNames.forEach(([otherObjectName, otherAlias]) => {
178+
// If object name is the same, has no alias and different path
179+
if (
180+
objectName === otherObjectName &&
181+
!otherAlias &&
182+
entry.path !== otherEntry.path
183+
) {
184+
// Check if the name is already in the duplicated array
185+
const existingDuplicate = duplicates.find(
186+
(duplicate) =>
187+
duplicate.name === objectName &&
188+
duplicate.type === 'global' &&
189+
duplicate.paths.includes(entry.path)
190+
)
191+
192+
// Add new object if doesn't exist
193+
if (!existingDuplicate) {
194+
duplicates.push({
195+
name: objectName,
196+
type: 'globalDiffPath',
197+
paths: [entry.path, otherEntry.path],
198+
})
199+
}
200+
201+
// Add path if already exists
202+
if (existingDuplicate && !existingDuplicate.paths.includes(otherEntry.path)) {
203+
existingDuplicate.paths.push(otherEntry.path)
204+
}
205+
}
206+
})
207+
}
208+
})
209+
}
210+
})
211+
})
212+
213+
return duplicates
214+
}
215+
216+
removeDuplicatedObjects(data) {
217+
const uniqueData = data.filter((value, index, self) => {
218+
// Order path arrays to be compared later
219+
const sortedPaths = value.paths.slice().sort()
220+
221+
return (
222+
index ===
223+
self.findIndex(
224+
(t) =>
225+
t.name === value.name &&
226+
t.type === value.type &&
227+
// Compare ordered arrays of paths
228+
JSON.stringify(t.paths.slice().sort()) === JSON.stringify(sortedPaths)
229+
)
230+
)
231+
})
232+
233+
return uniqueData
234+
}
235+
236+
findDuplicates(data) {
237+
/// @TODO THIS LOGIC CAN BE IMPROVED - Not done due lack of time
238+
239+
const duplicates1 = this.findInlineDuplicates(data)
240+
241+
const duplicates2 = this.finGlobalDuplicatesSamePath(data)
242+
243+
const duplicates3 = this.finGlobalDuplicatesDiffPathNoAlias(data)
244+
245+
const duplicates = this.removeDuplicatedObjects(duplicates1.concat(duplicates2, duplicates3))
246+
247+
return duplicates
248+
}
249+
}
250+
251+
module.exports = DuplicatedImportsChecker

lib/rules/miscellaneous/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
const QuotesChecker = require('./quotes')
22
const ComprehensiveInterfaceChecker = require('./comprehensive-interface')
3+
const DuplicatedImportsChecker = require('./duplicated-imports')
34

45
module.exports = function checkers(reporter, config, tokens) {
56
return [
67
new QuotesChecker(reporter, config, tokens),
78
new ComprehensiveInterfaceChecker(reporter, config, tokens),
9+
new DuplicatedImportsChecker(reporter),
810
]
911
}

solhint.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const _ = require('lodash')
44
const fs = require('fs')
55
const process = require('process')
66
const readline = require('readline')
7+
const chalk = require('chalk')
78

89
const linter = require('./lib/index')
910
const { loadConfig } = require('./lib/config/config-file')
@@ -306,7 +307,28 @@ function printReports(reports, formatter) {
306307
}
307308

308309
const fullReport = formatter(reports) + (finalMessage || '')
309-
if (!program.opts().quiet) console.log(fullReport)
310+
311+
if (!program.opts().quiet) {
312+
console.log(fullReport)
313+
314+
console.log(
315+
chalk.italic.bgYellow.black.bold(
316+
' -------------------------------------------------------------------------- '
317+
)
318+
)
319+
320+
console.log(
321+
chalk.italic.bgYellow.black.bold(
322+
' ===> Join SOLHINT Community at: https://discord.com/invite/4TYGq3zpjs <=== '
323+
)
324+
)
325+
326+
console.log(
327+
chalk.italic.bgYellow.black.bold(
328+
' -------------------------------------------------------------------------- \n'
329+
)
330+
)
331+
}
310332

311333
if (program.opts().save) {
312334
writeStringToFile(fullReport)

0 commit comments

Comments
 (0)