diff --git a/apps/remix-ide/src/app/tabs/analysis-tab.js b/apps/remix-ide/src/app/tabs/analysis-tab.js index 4406ba875e..ad3c2fce54 100644 --- a/apps/remix-ide/src/app/tabs/analysis-tab.js +++ b/apps/remix-ide/src/app/tabs/analysis-tab.js @@ -1,9 +1,11 @@ +import React from 'react' // eslint-disable-line import { ViewPlugin } from '@remixproject/engine-web' +import ReactDOM from 'react-dom' import { EventEmitter } from 'events' +import {RemixUiStaticAnalyser} from '@remix-ui/static-analyser' // eslint-disable-line import * as packageJson from '../../../../../package.json' +var Renderer = require('../ui/renderer') -var yo = require('yo-yo') -var StaticAnalysis = require('./staticanalysis/staticAnalysisView') var EventManager = require('../../lib/events') const profile = { @@ -25,24 +27,39 @@ class AnalysisTab extends ViewPlugin { this.event = new EventManager() this.events = new EventEmitter() this.registry = registry + this.element = document.createElement('div') + this.element.setAttribute('id', 'staticAnalyserView') + this._components = { + renderer: new Renderer(this) + } + this._components.registry = this.registry + this._deps = { + offsetToLineColumnConverter: this.registry.get( + 'offsettolinecolumnconverter').api + } + } + + onActivation () { + this.renderComponent() } render () { - this.staticanalysis = new StaticAnalysis(this.registry, this) - this.staticanalysis.event.register('staticAnaysisWarning', (count) => { - if (count > 0) { - this.emit('statusChanged', { key: count, title: `${count} warning${count === 1 ? '' : 's'}`, type: 'warning' }) - } else if (count === 0) { - this.emit('statusChanged', { key: 'succeed', title: 'no warning', type: 'success' }) - } else { - // count ==-1 no compilation result - this.emit('statusChanged', { key: 'none' }) - } - }) - this.registry.put({ api: this.staticanalysis, name: 'staticanalysis' }) - - return yo`
${this.staticanalysis.render()}
` + return this.element } + + renderComponent () { + ReactDOM.render( + , + this.element + ) + } + } module.exports = AnalysisTab diff --git a/libs/remix-ui/static-analyser/.babelrc b/libs/remix-ui/static-analyser/.babelrc new file mode 100644 index 0000000000..09d67939cc --- /dev/null +++ b/libs/remix-ui/static-analyser/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@nrwl/react/babel"], + "plugins": [] +} diff --git a/libs/remix-ui/static-analyser/.eslintrc b/libs/remix-ui/static-analyser/.eslintrc new file mode 100644 index 0000000000..dae5c6feeb --- /dev/null +++ b/libs/remix-ui/static-analyser/.eslintrc @@ -0,0 +1,19 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": "../../../.eslintrc", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error" + } +} diff --git a/libs/remix-ui/static-analyser/README.md b/libs/remix-ui/static-analyser/README.md new file mode 100644 index 0000000000..7e4b95c0e5 --- /dev/null +++ b/libs/remix-ui/static-analyser/README.md @@ -0,0 +1,7 @@ +# remix-ui-static-analyser + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test remix-ui-static-analyser` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/remix-ui/static-analyser/src/index.ts b/libs/remix-ui/static-analyser/src/index.ts new file mode 100644 index 0000000000..86a00ccd14 --- /dev/null +++ b/libs/remix-ui/static-analyser/src/index.ts @@ -0,0 +1 @@ +export * from './lib/remix-ui-static-analyser' diff --git a/libs/remix-ui/static-analyser/src/lib/Button/StaticAnalyserButton.tsx b/libs/remix-ui/static-analyser/src/lib/Button/StaticAnalyserButton.tsx new file mode 100644 index 0000000000..5cd7e9392b --- /dev/null +++ b/libs/remix-ui/static-analyser/src/lib/Button/StaticAnalyserButton.tsx @@ -0,0 +1,23 @@ +import React from 'react' //eslint-disable-line + +interface StaticAnalyserButtonProps { + onClick: (event) => void + buttonText: string, + disabled?: boolean +} + +const StaticAnalyserButton = ({ + onClick, + buttonText, + disabled +}: StaticAnalyserButtonProps) => { + return ( +
+ +
+ ) +} + +export default StaticAnalyserButton diff --git a/libs/remix-ui/static-analyser/src/lib/Checkbox/StaticAnalyserCheckedBox.tsx b/libs/remix-ui/static-analyser/src/lib/Checkbox/StaticAnalyserCheckedBox.tsx new file mode 100644 index 0000000000..2326a39122 --- /dev/null +++ b/libs/remix-ui/static-analyser/src/lib/Checkbox/StaticAnalyserCheckedBox.tsx @@ -0,0 +1,45 @@ +import React from 'react' //eslint-disable-line + +interface StaticAnalyserCheckBoxProps { + onClick?: (event) => void + onChange?: (event) => void + label?: string + inputType?: string + name?: string + checked?: boolean + id?: string + itemName?: string + categoryId?: string +} + +const StaticAnalyserCheckedBox = ({ + id, + label, + onClick, + inputType, + name, + checked, + onChange, + itemName, + categoryId +}: StaticAnalyserCheckBoxProps) => { + return ( +
+ + +
+ ) +} + +export default StaticAnalyserCheckedBox diff --git a/libs/remix-ui/static-analyser/src/lib/ErrorRenderer.tsx b/libs/remix-ui/static-analyser/src/lib/ErrorRenderer.tsx new file mode 100644 index 0000000000..7b7a01f37d --- /dev/null +++ b/libs/remix-ui/static-analyser/src/lib/ErrorRenderer.tsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react' //eslint-disable-line + +interface ErrorRendererProps { + message: any; + opt: any, + warningErrors: any +} + +const ErrorRenderer = ({ message, opt }: ErrorRendererProps) => { + const [, setError] = useState( + { + row: null, + column: null, + text: null, + type: null, + errFile: null + } + ) + const getPositionDetails = (msg: any) => { + const result = { } as any + + // To handle some compiler warning without location like SPDX license warning etc + if (!msg.includes(':')) return { errLine: -1, errCol: -1, errFile: msg } + + // extract line / column + let position = msg.match(/^(.*?):([0-9]*?):([0-9]*?)?/) + result.errLine = position ? parseInt(position[2]) - 1 : -1 + result.errCol = position ? parseInt(position[3]) : -1 + + // extract file + position = msg.match(/^(https:.*?|http:.*?|.*?):/) + result.errFile = position ? position[1] : '' + return result + } + if (!message) return + let position = getPositionDetails(message) + if (!position.errFile || (opt.errorType && opt.errorType === position.errFile)) { + // Updated error reported includes '-->' before file details + const errorDetails = message.split('-->') + // errorDetails[1] will have file details + if (errorDetails.length > 1) position = getPositionDetails(errorDetails[1]) + } + opt.errLine = position.errLine + opt.errCol = position.errCol + opt.errFile = position.errFile.trim() + if (!opt.noAnnotations && opt.errFile) { + setError({ + errFile: opt.errFile, + row: opt.errLine, + column: opt.errCol, + text: message, + type: opt.type + }) + } + const classList = opt.type === 'error' ? 'alert alert-danger' : 'alert alert-warning' + return ( +
+
+
+ +
+ + {opt.item.name} + { opt.item.warning } + {opt.item.more + ? more + : + } + Pos: {opt.locationString} + +
+
+ ) +} + +export default ErrorRenderer 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 new file mode 100644 index 0000000000..1674d66d84 --- /dev/null +++ b/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx @@ -0,0 +1,381 @@ +import React, { useEffect, useState } from 'react' +import CheckBox from './Checkbox/StaticAnalyserCheckedBox' // eslint-disable-line +import Button from './Button/StaticAnalyserButton' // eslint-disable-line +import remixLib from '@remix-project/remix-lib' +import _ from 'lodash' +import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line +import ErrorRenderer from './ErrorRenderer' // eslint-disable-line +const StaticAnalysisRunner = require('@remix-project/remix-analyzer').CodeAnalysis +const utils = remixLib.util + +/* eslint-disable-next-line */ +export interface RemixUiStaticAnalyserProps { + renderStaticAnalysis: any + staticanalysis: any + analysisRunner: any, + lastCompilationResult: any, + lastCompilationSource: any, + registry: any, + event: any, + analysisModule: any + _deps: any +} + +export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { + const [runner] = useState(new StaticAnalysisRunner()) + + const preProcessModules = (arr: any) => { + return arr.map((Item, i) => { + const itemObj = new Item() + itemObj._index = i + itemObj.categoryDisplayName = itemObj.category.displayName + itemObj.categoryId = itemObj.category.id + return itemObj + }) + } + + const groupedModules = utils.groupBy( + preProcessModules(runner.modules()), + 'categoryId' + ) + + const getIndex = (modules, array) => { + Object.values(modules).map((value: {_index}) => { + if (Array.isArray(value)) { + value.forEach((x) => { + array.push(x._index.toString()) + }) + } else { + array.push(value._index.toString()) + } + }) + } + + const groupedModuleIndex = (modules) => { + const indexOfCategory = [] + if (!_.isEmpty(modules)) { + getIndex(modules, indexOfCategory) + } + return indexOfCategory + } + + const [categoryIndex, setCategoryIndex] = useState(groupedModuleIndex(groupedModules)) + + const warningContainer = React.useRef(null) + const [runButtonState, setRunButtonState] = useState(true) + const [autoRun, setAutoRun] = useState(true) + const [result, setResult] = useState({ + lastCompilationResult: null, + lastCompilationSource: null, + currentFile: 'No file compiled' + }) + const [, setModuleNameResult] = useState(null) + const [, setWarning] = useState({ + msg: '', + options: {}, + hasWarning: false, + warningErrors: [] + }) + const [warningState, setWarningState] = useState([]) + + useEffect(() => { + if (autoRun) { + const setCompilationResult = async (data, source, file) => { + await setResult({ lastCompilationResult: data, lastCompilationSource: source, currentFile: file }) + } + + if (props.analysisModule) { + console.log({ autoRun }) + props.analysisModule.on( + 'solidity', + 'compilationFinished', + (file, source, languageVersion, data) => { + if (languageVersion.indexOf('soljson') !== 0) return + setCompilationResult(data, source, file) + run(data, source, file) + } + ) + } + } + return () => { } + }, [autoRun]) + + const run = (lastCompilationResult, lastCompilationSource, currentFile) => { + // const highlightLocation = async (location, fileName) => { + // await props.analysisModule.call('editor', 'discardHighlight') + // await props.analysisModule.call('editor', 'highlight', location, fileName) + // } + setResult({ lastCompilationResult, lastCompilationSource, currentFile }) + if (lastCompilationResult && categoryIndex.length) { + setRunButtonState(false) + let warningCount = 0 + const warningMessage = [] + + runner.run(lastCompilationResult, categoryIndex, results => { + results.map((result) => { + let moduleName + Object.keys(groupedModules).map(key => { + groupedModules[key].forEach(el => { + if (el.name === result.name) { + moduleName = groupedModules[key][0].categoryDisplayName + } + }) + }) + setModuleNameResult(moduleName) + const warningErrors = [] + result.report.map((item) => { + let location: any = {} + let locationString = 'not available' + let column = 0 + let row = 0 + let fileName = currentFile + if (item.location) { + var split = item.location.split(':') + var file = split[2] + location = { + start: parseInt(split[0]), + length: parseInt(split[1]) + } + location = props.analysisModule._deps.offsetToLineColumnConverter.offsetToLineColumn( + location, + parseInt(file), + lastCompilationSource.sources, + lastCompilationResult.sources + ) + row = location.start.line + column = location.start.column + locationString = row + 1 + ':' + column + ':' + fileName = Object.keys(lastCompilationResult.contracts)[file] + } + warningCount++ + const msg = ` + + ${result.name} + ${item.warning} + ${item.more + ? `more` + : ' ' + } + Pos: ${locationString} + ` + const options = { + type: 'warning', + useSpan: true, + errFile: fileName, + errLine: row, + errCol: column, + item: item, + name: result.name, + locationString, + more: item.more + } + warningErrors.push(options) + setWarning({ msg, hasWarning: true, options, warningErrors: warningErrors }) + 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 + }, {}) + } + + const groupedCategory = groupBy(resultArray, 'warningModuleName') + setWarningState(groupedCategory) + }) + props.event.trigger('staticAnaysisWarning', [warningCount]) + } else { + setRunButtonState(true) + if (categoryIndex.length) { + warningContainer.current.innerText = 'No compiled AST available' + } + props.event.trigger('staticAnaysisWarning', [-1]) + } + } + + // const correctRunBtnDisabled = () => { + // if (props.lastCompilationResult && selectedCategoryIndex.length !== 0) { + // setRunButtonState(false) + // } else { + // setRunButtonState(true) + // } + // } + + const handleCheckAllModules = (groupedModules) => { + const index = groupedModuleIndex(groupedModules) + if (index.every(el => categoryIndex.includes(el))) { + setCategoryIndex( + categoryIndex.filter((el) => { + return !index.includes(el) + }) + ) + } else { + setCategoryIndex(_.uniq([...categoryIndex, ...index])) + } + } + + const handleCheckOrUncheckCategory = (category) => { + const index = groupedModuleIndex(category) + if (index.every(el => categoryIndex.includes(el))) { + setCategoryIndex( + categoryIndex.filter((el) => { + return !index.includes(el) + }) + ) + } else { + setCategoryIndex(_.uniq([...categoryIndex, ...index])) + } + } + + const handleAutoRun = () => { + if (autoRun) { + setAutoRun(false) + } else { + setAutoRun(true) + } + console.log(' auton run function') + } + + const handleCheckSingle = (event, _index) => { + _index = _index.toString() + if (categoryIndex.includes(_index)) { + setCategoryIndex(categoryIndex.filter(val => val !== _index)) + } else { + setCategoryIndex(_.uniq([...categoryIndex, _index])) + } + } + + const categoryItem = (categoryId, item, i) => { + return ( +
+ handleCheckSingle(event, item._index)} + checked={categoryIndex.includes(item._index.toString())} + /> +
+ ) + } + + const categorySection = (category, categoryId, i) => { + return ( +
+
+ + + {category[0].categoryDisplayName} +
+ +
+ + } + expand={true} + > +
+ handleCheckOrUncheckCategory(category)} id={categoryId} inputType="checkbox" label={`Select ${category[0].categoryDisplayName}`} name='checkCategoryEntry' checked={category.map(x => x._index.toString()).every(el => categoryIndex.includes(el))} /> +
+
+ {category.map((item, i) => { + return ( + categoryItem(categoryId, item, i) + ) + })} +
+
+
+
+
+ ) + } + + return ( +
+
+
+
+ { + return (value.map(x => { + return x._index.toString() + })) + }).flat().every(el => categoryIndex.includes(el))} + label="Select all" + onClick={() => handleCheckAllModules(groupedModules)} + /> +
+
+ +
+
+
+
+ {Object.keys(groupedModules).map((categoryId, i) => { + const category = groupedModules[categoryId] + return ( + categorySection(category, categoryId, i) + ) + }) + } +
+
+ last results for: + + {result.currentFile && result.currentFile} + +
+
+
+ { + (Object.entries(warningState).map((element) => ( + <> + {element[0]} + {element[1].map(x => ( + x.hasWarning ? () : null + ))} + + ))) + } +
+
+
+ ) +} + +export default RemixUiStaticAnalyser diff --git a/libs/remix-ui/static-analyser/tsconfig.json b/libs/remix-ui/static-analyser/tsconfig.json new file mode 100644 index 0000000000..6b65264565 --- /dev/null +++ b/libs/remix-ui/static-analyser/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "jsx": "react", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/remix-ui/static-analyser/tsconfig.lib.json b/libs/remix-ui/static-analyser/tsconfig.lib.json new file mode 100644 index 0000000000..b560bc4dec --- /dev/null +++ b/libs/remix-ui/static-analyser/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/nx.json b/nx.json index 5cf047d9cc..802b6fe374 100644 --- a/nx.json +++ b/nx.json @@ -95,6 +95,9 @@ }, "remix-ui-workspace": { "tags": [] + }, + "remix-ui-static-analyser": { + "tags": [] } } } diff --git a/package-lock.json b/package-lock.json index b2c38f3b0f..b5ab19b3ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25142,9 +25142,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash-es": { "version": "4.17.15", diff --git a/package.json b/package.json index bd2a0e8aa6..03cd967635 100644 --- a/package.json +++ b/package.json @@ -159,6 +159,7 @@ "isbinaryfile": "^3.0.2", "jquery": "^3.3.1", "jszip": "^3.6.0", + "lodash": "^4.17.21", "merge": "^1.2.0", "npm-install-version": "^6.0.2", "react": "16.13.1", diff --git a/tsconfig.json b/tsconfig.json index c2a383e9de..c90ed09031 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "target": "es2015", "module": "commonjs", "typeRoots": ["node_modules/@types"], - "lib": ["es2017", "dom"], + "lib": ["es2017", "es2019", "dom"], "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", @@ -38,7 +38,8 @@ "@remix-ui/modal-dialog": ["libs/remix-ui/modal-dialog/src/index.ts"], "@remix-ui/toaster": ["libs/remix-ui/toaster/src/index.ts"], "@remix-ui/file-explorer": ["libs/remix-ui/file-explorer/src/index.ts"], - "@remix-ui/workspace": ["libs/remix-ui/workspace/src/index.ts"] + "@remix-ui/workspace": ["libs/remix-ui/workspace/src/index.ts"], + "@remix-ui/static-analyser": ["libs/remix-ui/static-analyser/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 10793e13b3..9c734f96a7 100644 --- a/workspace.json +++ b/workspace.json @@ -725,6 +725,25 @@ } } } + }, + "remix-ui-static-analyser": { + "root": "libs/remix-ui/static-analyser", + "sourceRoot": "libs/remix-ui/static-analyser/src", + "projectType": "library", + "schematics": {}, + "architect": { + "lint": { + "builder": "@nrwl/linter:lint", + "options": { + "linter": "eslint", + "tsConfig": ["libs/remix-ui/static-analyser/tsconfig.lib.json"], + "exclude": [ + "**/node_modules/**", + "!libs/remix-ui/static-analyser/**/*" + ] + } + } + } } }, "cli": {