UNPKG

solhint

Version:
228 lines (195 loc) 7.63 kB
const BaseChecker = require('../base-checker') const { severityDescription } = require('../../doc/utils') const DEFAULT_SEVERITY = 'warn' const ruleId = 'imports-order' const meta = { type: 'naming', docs: { description: `Order the imports of the contract to follow a certain hierarchy (read "Notes section")`, category: 'Style Guide Rules', options: [ { description: severityDescription, default: DEFAULT_SEVERITY, }, ], notes: [ { note: 'Paths starting with "@" like "@openzeppelin/" and urls ("http" and "https") will go first', }, { note: 'Order by hierarchy of directories first, e.g. ./../../ comes before ./../, which comes before ./, which comes before ./foo', }, { note: 'Direct imports come before relative imports', }, { note: 'Order alphabetically for each path at the same level, e.g. ./contract/Zbar.sol comes before ./interface/Ifoo.sol', }, { note: 'Rule does NOT support this kind of import "import * as Alias from "./filename.sol"', }, { note: 'When "--fix", rule will re-write this notation "../folder/file.sol" or this one "../file.sol" to "./../folder/file.sol" or this one "./../file.sol"', }, ], }, isDefault: false, recommended: false, defaultSetup: 'warn', fixable: true, schema: null, } class ImportsOrderChecker extends BaseChecker { constructor(reporter) { super(reporter, ruleId, meta) this.orderedImports = [] // This will hold the sorted imports } SourceUnit(node) { // console.log('node.children :>> ', node.children) // get all the imports into one object this.fromContractImports = node.children .filter((child) => child.type === 'ImportDirective') .map((child) => { const normalizedPath = this.normalizePath(child.path) const result = { range: child.range, path: normalizedPath, fullSentence: child.symbolAliases ? `${this.getFullSentence(child.symbolAliases)}'${normalizedPath}';` : `import '${normalizedPath}';`, } return result }) // Create a deep copy of fromContractImports for sorting this.orderedImports = JSON.parse(JSON.stringify(this.fromContractImports)) // order the object to get the ordered and the contract order this.orderedImports = this.sortImports(this.orderedImports) // console.log('this.orderedImports :>> ', this.fromContractImports) // console.log('NO Order: \n') // this.fromContractImports.forEach((importItem) => console.log(importItem.fullSentence)) // console.log('\n\nOrdered: \n') // this.orderedImports.forEach((importItem) => console.log(importItem.fullSentence)) } 'SourceUnit:exit'(node) { // when finish analyzing check if ordered import array is equal to the import contract order const areEqual = areArraysEqual(this.fromContractImports, this.orderedImports) // if are equal do nothing, if not enter the if if (!areEqual) { // Find the lowest starting range to start the replacement let currentStart = Math.min(...this.fromContractImports.map((imp) => imp.range[0])) // Prepare replacements changing the range const replacements = this.orderedImports.map((orderedImport) => { // replace single quotes by double quotes const newText = orderedImport.fullSentence.replace(/'/g, '"') const rangeEnd = currentStart + newText.length const replacement = { range: [currentStart, rangeEnd], newText, } currentStart = rangeEnd return replacement }) // get the name of the contract only to report the error let name = '' const loopQty = replacements.length - 1 // loop through imports to report and correct if requested for (let i = loopQty; i >= 0; i--) { name = this.fromContractImports[i].path.split('/') name = name[name.length - 1] this.error( node, `Wrong Import Order for {${name}}`, // replace from bottom to top all the imports in the right order // flag the first one, which will be the last import this.fixStatement(replacements[i], i === loopQty) ) } } } fixStatement(replacement, isLast) { // the last import should replace all the chars up to the end // this is for imports path longer than the one it was before if (isLast) { const lastRangeEnd = this.fromContractImports[this.fromContractImports.length - 1].range[1] return (fixer) => fixer.replaceTextRange([replacement.range[0], lastRangeEnd], replacement.newText) } return (fixer) => fixer.replaceTextRange(replacement.range, replacement.newText + '\n') } sortImports(unorderedImports) { // Helper function to determine the hierarchical level of a path function getHierarchyLevel(path) { // put very large numbers so these comes first in precedence const protocolOrder = { '@': -40000, 'http://': -30000, 'https://': -20000, // eslint-disable-next-line prettier/prettier folderPath: -10000, } // Check for protocol-specific paths and assign them their respective order levels for (const protocol in protocolOrder) { if (protocol !== 'folderPath' && path.startsWith(protocol)) { return protocolOrder[protocol] } } // Handling for paths that are likely folder names without a leading './' if (!path.startsWith('./') && /^[a-zA-Z0-9]/.test(path)) { return protocolOrder.folderPath } // Relative path handling if (path.startsWith('./')) { // Count the number of '../' sequences to determine depth const depth = path.split('/').filter((part) => part === '..').length // Return a negative value to ensure that lesser depth has higher precedence (closer to 0) return -depth } // Default catch-all for unhandled cases return Infinity } // Sort imports based on the hierarchy level and then alphabetically const orderedImports = unorderedImports.sort((a, b) => { const levelA = getHierarchyLevel(a.path) const levelB = getHierarchyLevel(b.path) if (levelA !== levelB) { return levelA - levelB } // For same level, sort alphabetically by the entire path, case-insensitive return a.path.localeCompare(b.path, undefined, { sensitivity: 'base' }) }) return orderedImports } // Map through each subarray to convert it to the desired format getFullSentence(elements) { const importParts = elements.map(([name, alias]) => { // Check if there is an alias; if so, format it as 'name as alias', otherwise just return the name return alias ? `${name} as ${alias}` : name }) // Join all the parts with a comma and space, and format it into the final import string return `import { ${importParts.join(', ')} } from ` } normalizePath(path) { if (path.startsWith('../')) { return `./${path}` } return path } } // function to compare is both arrays are equal function areArraysEqual(arr1, arr2) { if (arr1.length !== arr2.length) { return false } for (let i = 0; i < arr1.length; i++) { if ( arr1[i].path !== arr2[i].path || arr1[i].range[0] !== arr2[i].range[0] || arr1[i].range[1] !== arr2[i].range[1] ) { return false } } return true } module.exports = ImportsOrderChecker