diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js
index 8e8570b249..aa17788170 100644
--- a/apps/remix-ide/src/app.js
+++ b/apps/remix-ide/src/app.js
@@ -448,7 +448,8 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
test,
filePanel.remixdHandle,
filePanel.gitHandle,
- filePanel.hardhatHandle
+ filePanel.hardhatHandle,
+ filePanel.slitherHandle
])
if (isElectron()) {
diff --git a/apps/remix-ide/src/app/editor/sourceHighlighter.js b/apps/remix-ide/src/app/editor/sourceHighlighter.js
index e25e2c948f..22f705001e 100644
--- a/apps/remix-ide/src/app/editor/sourceHighlighter.js
+++ b/apps/remix-ide/src/app/editor/sourceHighlighter.js
@@ -37,7 +37,7 @@ class SourceHighlighter {
this.statementMarker = null
this.fullLineMarker = null
this.source = null
- if (lineColumnPos) {
+ if (lineColumnPos && lineColumnPos.start && lineColumnPos.end) {
this.source = filePath
this.style = style || 'var(--info)'
// if (!this.source) this.source = this._deps.fileManager.currentFile()
diff --git a/apps/remix-ide/src/app/files/fileManager.js b/apps/remix-ide/src/app/files/fileManager.js
index 9b618a9fe9..f2d83d3915 100644
--- a/apps/remix-ide/src/app/files/fileManager.js
+++ b/apps/remix-ide/src/app/files/fileManager.js
@@ -155,9 +155,9 @@ class FileManager extends Plugin {
* @param {string} path path of the directory
* @returns {boolean} true if path is a directory.
*/
- isDirectory (path) {
+ async isDirectory (path) {
const provider = this.fileProviderOf(path)
- const result = provider.isDirectory(path)
+ const result = await provider.isDirectory(path)
return result
}
@@ -362,7 +362,6 @@ class FileManager extends Plugin {
path = this.limitPluginScope(path)
await this._handleExists(path, `Cannot remove file or directory ${path}`)
const provider = this.fileProviderOf(path)
-
return await provider.remove(path)
} catch (e) {
throw new Error(e)
diff --git a/apps/remix-ide/src/app/files/remixDProvider.js b/apps/remix-ide/src/app/files/remixDProvider.js
index bc59bfe90b..04b0217df1 100644
--- a/apps/remix-ide/src/app/files/remixDProvider.js
+++ b/apps/remix-ide/src/app/files/remixDProvider.js
@@ -38,7 +38,7 @@ module.exports = class RemixDProvider extends FileProvider {
})
this._appManager.on('remixd', 'fileRenamed', (oldPath, newPath) => {
- this.event.emit('fileRemoved', oldPath, newPath)
+ this.event.emit('fileRenamed', oldPath, newPath)
})
this._appManager.on('remixd', 'rootFolderChanged', () => {
@@ -141,7 +141,6 @@ module.exports = class RemixDProvider extends FileProvider {
this._appManager.call('remixd', 'remove', { path: unprefixedpath })
.then(result => {
const path = unprefixedpath
-
delete this.filesContent[path]
resolve(true)
this.init()
diff --git a/apps/remix-ide/src/app/files/remixd-handle.js b/apps/remix-ide/src/app/files/remixd-handle.js
index abea437b64..6f06d8773b 100644
--- a/apps/remix-ide/src/app/files/remixd-handle.js
+++ b/apps/remix-ide/src/app/files/remixd-handle.js
@@ -43,6 +43,7 @@ export class RemixdHandle extends WebsocketPlugin {
if (super.socket) super.deactivate()
// this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342
if (this.appManager.actives.includes('hardhat')) this.appManager.deactivatePlugin('hardhat')
+ if (this.appManager.actives.includes('slither')) this.appManager.deactivatePlugin('slither')
this.localhostProvider.close((error) => {
if (error) console.log(error)
})
@@ -88,6 +89,7 @@ export class RemixdHandle extends WebsocketPlugin {
this.call('filePanel', 'setWorkspace', { name: LOCALHOST, isLocalhost: true }, true)
})
this.call('manager', 'activatePlugin', 'hardhat')
+ this.call('manager', 'activatePlugin', 'slither')
}
}
if (this.localhostProvider.isConnected()) {
diff --git a/apps/remix-ide/src/app/files/slither-handle.js b/apps/remix-ide/src/app/files/slither-handle.js
new file mode 100644
index 0000000000..e8ff1e66c7
--- /dev/null
+++ b/apps/remix-ide/src/app/files/slither-handle.js
@@ -0,0 +1,18 @@
+import { WebsocketPlugin } from '@remixproject/engine-web'
+import * as packageJson from '../../../../../package.json'
+
+const profile = {
+ name: 'slither',
+ displayName: 'Slither',
+ url: 'ws://127.0.0.1:65523',
+ methods: ['analyse'],
+ description: 'Using Remixd daemon, run slither static analysis',
+ kind: 'other',
+ version: packageJson.version
+}
+
+export class SlitherHandle extends WebsocketPlugin {
+ constructor () {
+ super(profile)
+ }
+}
diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js
index 0d1523fa08..4ed0e6c8c9 100644
--- a/apps/remix-ide/src/app/panels/file-panel.js
+++ b/apps/remix-ide/src/app/panels/file-panel.js
@@ -9,6 +9,7 @@ import { checkSpecialChars, checkSlash } from '../../lib/helper'
const { RemixdHandle } = require('../files/remixd-handle.js')
const { GitHandle } = require('../files/git-handle.js')
const { HardhatHandle } = require('../files/hardhat-handle.js')
+const { SlitherHandle } = require('../files/slither-handle.js')
const globalRegistry = require('../../global/registry')
const examples = require('../editor/examples')
const GistHandler = require('../../lib/gist-handler')
@@ -59,6 +60,7 @@ module.exports = class Filepanel extends ViewPlugin {
this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager)
this.gitHandle = new GitHandle()
this.hardhatHandle = new HardhatHandle()
+ this.slitherHandle = new SlitherHandle()
this.registeredMenuItems = []
this.removedMenuItems = []
this.request = {}
diff --git a/apps/remix-ide/src/app/panels/tab-proxy.js b/apps/remix-ide/src/app/panels/tab-proxy.js
index 24edfa1da6..dba54aef61 100644
--- a/apps/remix-ide/src/app/panels/tab-proxy.js
+++ b/apps/remix-ide/src/app/panels/tab-proxy.js
@@ -44,7 +44,6 @@ export class TabProxy extends Plugin {
fileManager.events.on('fileRemoved', (name) => {
const workspace = this.fileManager.currentWorkspace()
-
workspace ? this.removeTab(workspace + '/' + name) : this.removeTab(this.fileManager.mode + '/' + name)
})
diff --git a/apps/remix-ide/src/app/tabs/compileTab/compileTab.js b/apps/remix-ide/src/app/tabs/compileTab/compileTab.js
index b357f8a8aa..a9c17192cc 100644
--- a/apps/remix-ide/src/app/tabs/compileTab/compileTab.js
+++ b/apps/remix-ide/src/app/tabs/compileTab/compileTab.js
@@ -7,6 +7,7 @@ const profile = {
name: 'solidity-logic',
displayName: 'Solidity compiler logic',
description: 'Compile solidity contracts - Logic',
+ methods: ['getCompilerState'],
version: packageJson.version
}
@@ -68,6 +69,10 @@ class CompileTab extends Plugin {
this.compiler.set('language', lang)
}
+ getCompilerState () {
+ return this.compiler.state
+ }
+
/**
* Compile a specific file of the file manager
* @param {string} target the path to the file to compile
diff --git a/apps/remix-ide/src/remixEngine.js b/apps/remix-ide/src/remixEngine.js
index abf20340f6..2836d974b2 100644
--- a/apps/remix-ide/src/remixEngine.js
+++ b/apps/remix-ide/src/remixEngine.js
@@ -10,6 +10,7 @@ export class RemixEngine extends Engine {
setPluginOption ({ name, kind }) {
if (kind === 'provider') return { queueTimeout: 60000 * 2 }
if (name === 'LearnEth') return { queueTimeout: 60000 }
+ if (name === 'slither') return { queueTimeout: 60000 * 4 } // Requires when a solc version is installed
return { queueTimeout: 10000 }
}
diff --git a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx
index 5535a05971..95913e533b 100644
--- a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx
+++ b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx
@@ -1,4 +1,4 @@
-import React from 'react' //eslint-disable-line
+import React, { CSSProperties } from 'react' //eslint-disable-line
import './remix-ui-checkbox.css'
/* eslint-disable-next-line */
@@ -12,6 +12,8 @@ export interface RemixUiCheckboxProps {
id?: string
itemName?: string
categoryId?: string
+ visibility?: string
+ display?: string
}
export const RemixUiCheckbox = ({
@@ -23,10 +25,12 @@ export const RemixUiCheckbox = ({
checked,
onChange,
itemName,
- categoryId
+ categoryId,
+ visibility,
+ display = 'flex'
}: RemixUiCheckboxProps) => {
return (
-
+
{
try {
await fileManager.remove(p)
} catch (e) {
- const isDir = state.fileManager.isDirectory(p)
+ const isDir = await state.fileManager.isDirectory(p)
toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`)
}
}
diff --git a/libs/remix-ui/solidity-compiler/src/lib/css/style.css b/libs/remix-ui/solidity-compiler/src/lib/css/style.css
index 3846ca0f0f..d2bbe9e606 100644
--- a/libs/remix-ui/solidity-compiler/src/lib/css/style.css
+++ b/libs/remix-ui/solidity-compiler/src/lib/css/style.css
@@ -103,7 +103,6 @@
}
.remixui_container {
margin: 0;
- margin-bottom: 2%;
}
.remixui_optimizeContainer {
display: flex;
diff --git a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts
index 50fc98410a..83b688f104 100644
--- a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts
+++ b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts
@@ -7,6 +7,7 @@ const profile = {
name: 'solidity-logic',
displayName: 'Solidity compiler logic',
description: 'Compile solidity contracts - Logic',
+ methods: ['getCompilerState'],
version: packageJson.version
}
export class CompileTab extends Plugin {
@@ -60,6 +61,10 @@ export class CompileTab extends Plugin {
this.compiler.set('evmVersion', this.evmVersion)
}
+ getCompilerState () {
+ return this.compiler.state
+ }
+
/**
* Set the compiler to using Solidity or Yul (default to Solidity)
* @params lang {'Solidity' | 'Yul'} ...
diff --git a/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx b/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx
index 8060310c46..a4419dc6f8 100644
--- a/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx
+++ b/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx
@@ -55,10 +55,12 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
return indexOfCategory
}
const [autoRun, setAutoRun] = useState(true)
+ const [slitherEnabled, setSlitherEnabled] = useState(false)
+ const [showSlither, setShowSlither] = useState('hidden')
const [categoryIndex, setCategoryIndex] = useState(groupedModuleIndex(groupedModules))
const warningContainer = React.useRef(null)
- const [warningState, setWarningState] = useState([])
+ const [warningState, setWarningState] = useState({})
const [state, dispatch] = useReducer(analysisReducer, initialState)
useEffect(() => {
@@ -66,7 +68,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
}, [])
useEffect(() => {
- setWarningState([])
+ setWarningState({})
if (autoRun) {
if (state.data !== null) {
run(state.data, state.source, state.file)
@@ -78,13 +80,15 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
}, [state])
useEffect(() => {
- props.analysisModule.on('filePanel', 'setWorkspace', () => {
+ props.analysisModule.on('filePanel', 'setWorkspace', (currentWorkspace) => {
// Reset warning state
setWarningState([])
// Reset badge
props.event.trigger('staticAnaysisWarning', [])
// Reset state
dispatch({ type: '', payload: {} })
+ // Show 'Enable Slither Analysis' checkbox
+ if (currentWorkspace && currentWorkspace.isLocalhost === true) setShowSlither('visible')
})
return () => { }
}, [props.analysisModule])
@@ -103,12 +107,35 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
)
}
+ const showWarnings = (warningMessage, groupByKey) => {
+ const resultArray = []
+ warningMessage.map(x => {
+ resultArray.push(x)
+ })
+ function groupBy (objectArray, property) {
+ return objectArray.reduce((acc, obj) => {
+ const key = obj[property]
+ if (!acc[key]) {
+ acc[key] = []
+ }
+ // Add object to list for given key's value
+ acc[key].push(obj)
+ return acc
+ }, {})
+ }
+
+ const groupedCategory = groupBy(resultArray, groupByKey)
+ setWarningState(groupedCategory)
+ }
+
const run = (lastCompilationResult, lastCompilationSource, currentFile) => {
if (state.data !== null) {
- if (lastCompilationResult && categoryIndex.length > 0) {
+ if (lastCompilationResult && (categoryIndex.length > 0 || slitherEnabled)) {
let warningCount = 0
const warningMessage = []
+ const warningErrors = []
+ // Remix Analysis
runner.run(lastCompilationResult, categoryIndex, results => {
results.map((result) => {
let moduleName
@@ -119,7 +146,6 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
}
})
})
- const warningErrors = []
result.report.map((item) => {
let location: any = {}
let locationString = 'not available'
@@ -163,28 +189,73 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
warningMessage.push({ msg, options, hasWarning: true, warningModuleName: moduleName })
})
})
- const resultArray = []
- warningMessage.map(x => {
- resultArray.push(x)
- })
- function groupBy (objectArray, property) {
- return objectArray.reduce((acc, obj) => {
- const key = obj[property]
- if (!acc[key]) {
- acc[key] = []
- }
- // Add object to list for given key's value
- acc[key].push(obj)
- return acc
- }, {})
- }
+ // Slither Analysis
+ if (slitherEnabled) {
+ props.analysisModule.call('solidity-logic', 'getCompilerState').then((compilerState) => {
+ const { currentVersion, optimize, evmVersion } = compilerState
+ props.analysisModule.call('terminal', 'log', { type: 'info', value: '[Slither Analysis]: Running...' })
+ props.analysisModule.call('slither', 'analyse', state.file, { currentVersion, optimize, evmVersion }).then((result) => {
+ if (result.status) {
+ props.analysisModule.call('terminal', 'log', { type: 'info', value: `[Slither Analysis]: Analysis Completed!! ${result.count} warnings found.` })
+ const report = result.data
+ report.map((item) => {
+ let location: any = {}
+ let locationString = 'not available'
+ let column = 0
+ let row = 0
+ let fileName = currentFile
- const groupedCategory = groupBy(resultArray, 'warningModuleName')
- setWarningState(groupedCategory)
+ if (item.sourceMap && item.sourceMap.length) {
+ const fileIndex = Object.keys(lastCompilationResult.sources).indexOf(item.sourceMap[0].source_mapping.filename_relative)
+ if (fileIndex >= 0) {
+ location = {
+ start: item.sourceMap[0].source_mapping.start,
+ length: item.sourceMap[0].source_mapping.length
+ }
+ location = props.analysisModule._deps.offsetToLineColumnConverter.offsetToLineColumn(
+ location,
+ fileIndex,
+ lastCompilationSource.sources,
+ lastCompilationResult.sources
+ )
+ row = location.start.line
+ column = location.start.column
+ locationString = row + 1 + ':' + column + ':'
+ fileName = Object.keys(lastCompilationResult.sources)[fileIndex]
+ }
+ }
+ warningCount++
+ const msg = message(item.title, item.description, item.more, fileName, locationString)
+ const options = {
+ type: 'warning',
+ useSpan: true,
+ errFile: fileName,
+ fileName,
+ errLine: row,
+ errCol: column,
+ item: { warning: item.description },
+ name: item.title,
+ locationString,
+ more: item.more,
+ location: location
+ }
+ warningErrors.push(options)
+ warningMessage.push({ msg, options, hasWarning: true, warningModuleName: 'Slither Analysis' })
+ })
+ showWarnings(warningMessage, 'warningModuleName')
+ props.event.trigger('staticAnaysisWarning', [warningCount])
+ }
+ }).catch((error) => {
+ console.log('Error found:', error) // This should be removed once testing done
+ props.analysisModule.call('terminal', 'log', { type: 'error', value: '[Slither Analysis]: Error occured! See remixd console for details.' })
+ showWarnings(warningMessage, 'warningModuleName')
+ })
+ })
+ } else {
+ showWarnings(warningMessage, 'warningModuleName')
+ props.event.trigger('staticAnaysisWarning', [warningCount])
+ }
})
- if (categoryIndex.length > 0) {
- props.event.trigger('staticAnaysisWarning', [warningCount])
- }
} else {
if (categoryIndex.length) {
warningContainer.current.innerText = 'No compiled AST available'
@@ -220,6 +291,14 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
}
}
+ const handleSlitherEnabled = () => {
+ if (slitherEnabled) {
+ setSlitherEnabled(false)
+ } else {
+ setSlitherEnabled(true)
+ }
+ }
+
const handleAutoRun = () => {
if (autoRun) {
setAutoRun(false)
@@ -317,7 +396,18 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
label="Autorun"
onChange={() => {}}
/>
-
+
+ {}}
+ visibility = {showSlither}
+ />
@@ -330,7 +420,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
}
- last results for:
+ Last results for:
{
{state.file}
+
{Object.entries(warningState).length > 0 &&
@@ -345,9 +436,9 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
(Object.entries(warningState).map((element, index) => (
{element[0]}
- {element[1].map((x, i) => (
- x.hasWarning ? (
-
+ {element[1]['map']((x, i) => ( // eslint-disable-line dot-notation
+ x.hasWarning ? ( // eslint-disable-next-line dot-notation
+
diff --git a/libs/remixd/src/bin/remixd.ts b/libs/remixd/src/bin/remixd.ts
index 2085735b8c..623fa5ef97 100644
--- a/libs/remixd/src/bin/remixd.ts
+++ b/libs/remixd/src/bin/remixd.ts
@@ -25,6 +25,7 @@ async function warnLatestVersion () {
const services = {
git: (readOnly: boolean) => new servicesList.GitClient(readOnly),
hardhat: (readOnly: boolean) => new servicesList.HardhatClient(readOnly),
+ slither: (readOnly: boolean) => new servicesList.SlitherClient(readOnly),
folder: (readOnly: boolean) => new servicesList.Sharedfolder(readOnly)
}
@@ -32,11 +33,12 @@ const services = {
const ports = {
git: 65521,
hardhat: 65522,
+ slither: 65523,
folder: 65520
}
const killCallBack: Array
= []
-function startService (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder, error?:Error) => void) {
+function startService (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder, error?:Error) => void) {
const socket = new WebSocket(ports[service], { remixIdeUrl: program.remixIde }, () => services[service](program.readOnly || false))
socket.start(callback)
killCallBack.push(socket.close.bind(socket))
@@ -94,6 +96,10 @@ function errorHandler (error: any, service: string) {
sharedFolderClient.setupNotifications(program.sharedFolder)
sharedFolderClient.sharedFolder(program.sharedFolder)
})
+ startService('slither', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => {
+ sharedFolderClient.setWebSocket(ws)
+ sharedFolderClient.sharedFolder(program.sharedFolder)
+ })
// Run hardhat service if a hardhat project is shared as folder
const hardhatConfigFilePath = absolutePath('./', program.sharedFolder) + '/hardhat.config.js'
const isHardhatProject = fs.existsSync(hardhatConfigFilePath)
diff --git a/libs/remixd/src/index.ts b/libs/remixd/src/index.ts
index 849f35b6fa..8b35cea6dd 100644
--- a/libs/remixd/src/index.ts
+++ b/libs/remixd/src/index.ts
@@ -2,6 +2,7 @@
import { RemixdClient as sharedFolder } from './services/remixdClient'
import { GitClient } from './services/gitClient'
import { HardhatClient } from './services/hardhatClient'
+import { SlitherClient } from './services/slitherClient'
import Websocket from './websocket'
import * as utils from './utils'
@@ -11,6 +12,7 @@ module.exports = {
services: {
sharedFolder,
GitClient,
- HardhatClient
+ HardhatClient,
+ SlitherClient
}
}
diff --git a/libs/remixd/src/serviceList.ts b/libs/remixd/src/serviceList.ts
index 19d613b7c2..ec8c1f374d 100644
--- a/libs/remixd/src/serviceList.ts
+++ b/libs/remixd/src/serviceList.ts
@@ -1,3 +1,4 @@
export { RemixdClient as Sharedfolder } from './services/remixdClient'
export { GitClient } from './services/gitClient'
export { HardhatClient } from './services/hardhatClient'
+export { SlitherClient } from './services/slitherClient'
diff --git a/libs/remixd/src/services/remixdClient.ts b/libs/remixd/src/services/remixdClient.ts
index 31588628ea..8645caf4ac 100644
--- a/libs/remixd/src/services/remixdClient.ts
+++ b/libs/remixd/src/services/remixdClient.ts
@@ -180,12 +180,34 @@ export class RemixdClient extends PluginClient {
if (!fs.existsSync(path)) return reject(new Error('File not found ' + path))
if (!isRealPath(path)) return
+ // Saving the content of the item{folder} before removing it
+ const ls = []
+ try {
+ const resolveList = (path) => {
+ if (!this._isFile(path)) {
+ const list = utils.resolveDirectory(path, this.currentSharedFolder)
+ Object.keys(list).forEach(itemPath => {
+ if (list[itemPath].isDirectory) {
+ resolveList(`${this.currentSharedFolder}/${itemPath}`)
+ }
+ ls.push(itemPath)
+ })
+ }
+ }
+ resolveList(path)
+ ls.push(args.path)
+ } catch (e) {
+ throw new Error(e)
+ }
return fs.remove(path, (error: Error) => {
if (error) {
console.log(error)
return reject(new Error('Failed to remove file/directory: ' + error))
}
- this.emit('fileRemoved', args.path)
+ for (const file in ls) {
+ this.emit('fileRemoved', ls[file])
+ }
+
resolve(true)
})
})
@@ -194,10 +216,17 @@ export class RemixdClient extends PluginClient {
}
}
+ _isFile (path: string): boolean {
+ try {
+ return fs.statSync(path).isFile()
+ } catch (error) {
+ throw new Error(error)
+ }
+ }
+
isDirectory (args: SharedFolderArgs): boolean {
try {
const path = utils.absolutePath(args.path, this.currentSharedFolder)
-
return fs.statSync(path).isDirectory()
} catch (error) {
throw new Error(error)
@@ -207,7 +236,6 @@ export class RemixdClient extends PluginClient {
isFile (args: SharedFolderArgs): boolean {
try {
const path = utils.absolutePath(args.path, this.currentSharedFolder)
-
return fs.statSync(path).isFile()
} catch (error) {
throw new Error(error)
diff --git a/libs/remixd/src/services/slitherClient.ts b/libs/remixd/src/services/slitherClient.ts
new file mode 100644
index 0000000000..e2d9a281bb
--- /dev/null
+++ b/libs/remixd/src/services/slitherClient.ts
@@ -0,0 +1,162 @@
+/* eslint dot-notation: "off" */
+
+import * as WS from 'ws' // eslint-disable-line
+import { PluginClient } from '@remixproject/plugin'
+import { existsSync, readFileSync, readdirSync } from 'fs'
+import { OutputStandard } from '../types' // eslint-disable-line
+const { spawn, execSync } = require('child_process')
+
+export class SlitherClient extends PluginClient {
+ methods: Array
+ websocket: WS
+ currentSharedFolder: string
+
+ constructor (private readOnly = false) {
+ super()
+ this.methods = ['analyse']
+ }
+
+ setWebSocket (websocket: WS): void {
+ this.websocket = websocket
+ }
+
+ sharedFolder (currentSharedFolder: string): void {
+ this.currentSharedFolder = currentSharedFolder
+ }
+
+ mapNpmDepsDir (list) {
+ const remixNpmDepsPath = `${this.currentSharedFolder}/.deps/npm`
+ const localNpmDepsPath = `${this.currentSharedFolder}/node_modules`
+ const npmDepsExists = existsSync(remixNpmDepsPath)
+ const nodeModulesExists = existsSync(localNpmDepsPath)
+ let isLocalDep = false
+ let isRemixDep = false
+ let allowPathString = ''
+ let remapString = ''
+
+ for (const e of list) {
+ const importPath = e.replace(/import ['"]/g, '').trim()
+ const packageName = importPath.split('/')[0]
+ if (nodeModulesExists && readdirSync(localNpmDepsPath).includes(packageName)) {
+ isLocalDep = true
+ remapString += `${packageName}=./node_modules/${packageName} `
+ } else if (npmDepsExists && readdirSync(remixNpmDepsPath).includes(packageName)) {
+ isRemixDep = true
+ remapString += `${packageName}=./.deps/npm/${packageName} `
+ }
+ }
+ if (isLocalDep) allowPathString += './node_modules,'
+ if (isRemixDep) allowPathString += './.deps/npm,'
+
+ return { remapString, allowPathString }
+ }
+
+ transform (detectors: Record[]): OutputStandard[] {
+ const standardReport: OutputStandard[] = []
+ for (const e of detectors) {
+ const obj = {} as OutputStandard
+ obj.description = e.description
+ obj.title = e.check
+ obj.confidence = e.confidence
+ obj.severity = e.impact
+ obj.sourceMap = e.elements.map((element) => {
+ delete element.source_mapping.filename_used
+ delete element.source_mapping.filename_absolute
+ return element
+ })
+ standardReport.push(obj)
+ }
+ return standardReport
+ }
+
+ analyse (filePath: string, compilerConfig: Record) {
+ return new Promise((resolve, reject) => {
+ if (this.readOnly) {
+ const errMsg: string = '[Slither Analysis]: Cannot analyse in read-only mode'
+ return reject(new Error(errMsg))
+ }
+ const options = { cwd: this.currentSharedFolder, shell: true }
+ const { currentVersion, optimize, evmVersion } = compilerConfig
+ if (currentVersion && currentVersion.includes('+commit')) {
+ // Get compiler version with commit id e.g: 0.8.2+commit.661d110
+ const versionString: string = currentVersion.substring(0, currentVersion.indexOf('+commit') + 16)
+ console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Compiler version is ${versionString}`)
+ let solcOutput: Buffer
+ // Check solc current installed version
+ try {
+ solcOutput = execSync('solc --version', options)
+ } catch (err) {
+ console.log(err)
+ reject(new Error('Error in running solc command'))
+ }
+ if (!solcOutput.toString().includes(versionString)) {
+ console.log('\x1b[32m%s\x1b[0m', '[Slither Analysis]: Compiler version is different from installed solc version')
+ // Get compiler version without commit id e.g: 0.8.2
+ const version: string = versionString.substring(0, versionString.indexOf('+commit'))
+ // List solc versions installed using solc-select
+ try {
+ const solcSelectInstalledVersions: Buffer = execSync('solc-select versions', options)
+ // Check if required version is already installed
+ if (!solcSelectInstalledVersions.toString().includes(version)) {
+ console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Installing ${version} using solc-select`)
+ // Install required version
+ execSync(`solc-select install ${version}`, options)
+ }
+ console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Setting ${version} as current solc version using solc-select`)
+ // Set solc current version as required version
+ execSync(`solc-select use ${version}`, options)
+ } catch (err) {
+ console.log(err)
+ reject(new Error('Error in running solc-select command'))
+ }
+ } else console.log('\x1b[32m%s\x1b[0m', '[Slither Analysis]: Compiler version is same as installed solc version')
+ }
+ // Allow paths and set solc remapping for import URLs
+ const fileContent = readFileSync(`${this.currentSharedFolder}/${filePath}`, 'utf8')
+ const importsArr = fileContent.match(/import ['"][^.|..](.+?)['"];/g)
+ let allowPaths = ''; let remaps = ''
+ if (importsArr?.length) {
+ const { remapString, allowPathString } = this.mapNpmDepsDir(importsArr)
+ allowPaths = allowPathString
+ remaps = remapString.trim()
+ }
+ const allowPathsOption: string = allowPaths ? `--allow-paths ${allowPaths}` : ''
+ const optimizeOption: string = optimize ? ' --optimize ' : ''
+ const evmOption: string = evmVersion ? ` --evm-version ${evmVersion}` : ''
+ const solcArgs: string = optimizeOption || evmOption || allowPathsOption ? `--solc-args '${allowPathsOption}${optimizeOption}${evmOption}'` : ''
+ const solcRemaps = remaps ? `--solc-remaps "${remaps}"` : ''
+
+ const outputFile: string = 'remix-slitherReport_' + Math.floor(Date.now() / 1000) + '.json'
+ const cmd: string = `slither ${filePath} ${solcArgs} ${solcRemaps} --json ${outputFile}`
+ console.log('\x1b[32m%s\x1b[0m', '[Slither Analysis]: Running Slither...')
+ // Added `stdio: 'ignore'` as for contract with NPM imports analysis which is exported in 'stderr'
+ // get too big and hangs the process. We process analysis from the report file only
+ const child = spawn(cmd, { cwd: this.currentSharedFolder, shell: true, stdio: 'ignore' })
+
+ const response = {}
+ child.on('close', () => {
+ const outputFileAbsPath: string = `${this.currentSharedFolder}/${outputFile}`
+ // Check if slither report file exists
+ if (existsSync(outputFileAbsPath)) {
+ let report = readFileSync(outputFileAbsPath, 'utf8')
+ report = JSON.parse(report)
+ if (report['success']) {
+ response['status'] = true
+ if (!report['results'] || !report['results'].detectors || !report['results'].detectors.length) {
+ response['count'] = 0
+ } else {
+ const { detectors } = report['results']
+ response['count'] = detectors.length
+ response['data'] = this.transform(detectors)
+ }
+ console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Analysis Completed!! ${response['count']} warnings found.`)
+ resolve(response)
+ } else {
+ console.log(report['error'])
+ reject(new Error('Error in running Slither Analysis.'))
+ }
+ } else reject(new Error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.'))
+ })
+ })
+ }
+}
diff --git a/libs/remixd/src/types/index.ts b/libs/remixd/src/types/index.ts
index cde06fcf07..66818dff48 100644
--- a/libs/remixd/src/types/index.ts
+++ b/libs/remixd/src/types/index.ts
@@ -1,6 +1,18 @@
import * as ServiceList from '../serviceList'
import * as Websocket from 'ws'
+export interface OutputStandard {
+ description: string
+ title: string
+ confidence: string
+ severity: string
+ sourceMap: any
+ category?: string
+ reference?: string
+ example?: any
+ [key: string]: any
+}
+
type ServiceListKeys = keyof typeof ServiceList;
export type Service = typeof ServiceList[ServiceListKeys]
diff --git a/libs/remixd/src/websocket.ts b/libs/remixd/src/websocket.ts
index 370dc48abb..324fa3332b 100644
--- a/libs/remixd/src/websocket.ts
+++ b/libs/remixd/src/websocket.ts
@@ -19,7 +19,8 @@ export default class WebSocket {
const listeners = {
65520: 'remixd',
65521: 'git',
- 65522: 'hardhat'
+ 65522: 'hardhat',
+ 65523: 'slither'
}
this.server.on('error', (error: Error) => {