Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rulesets #86

Merged
merged 9 commits into from Jan 4, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions conf/rulesets/solhint-all.js
@@ -0,0 +1,12 @@
const { loadRules } = require('../../lib/load-rules')

const rulesConstants = loadRules()
const enabledRules = {}

rulesConstants.forEach(rule => {
if (!rule.meta.deprecated) {
enabledRules[rule.ruleId] = rule.meta.defaultSetup
}
})

module.exports = { rules: enabledRules }
12 changes: 12 additions & 0 deletions conf/rulesets/solhint-default.js
@@ -0,0 +1,12 @@
const { loadRules } = require('../../lib/load-rules')

const rulesConstants = loadRules()
const enabledRules = {}

rulesConstants.forEach(rule => {
if (!rule.meta.deprecated && rule.meta.isDefault) {
enabledRules[rule.ruleId] = rule.meta.defaultSetup
}
})

module.exports = { rules: enabledRules }
12 changes: 12 additions & 0 deletions conf/rulesets/solhint-recommended.js
@@ -0,0 +1,12 @@
const { loadRules } = require('../../lib/load-rules')

const rulesConstants = loadRules()
const enabledRules = {}

rulesConstants.forEach(rule => {
if (!rule.meta.deprecated && rule.meta.recommended) {
enabledRules[rule.ruleId] = rule.meta.defaultSetup
}
})

module.exports = { rules: enabledRules }
15 changes: 15 additions & 0 deletions lib/common/ajv.js
@@ -0,0 +1,15 @@
const Ajv = require('ajv')
const metaSchema = require('ajv/lib/refs/json-schema-draft-04.json')

const ajv = new Ajv({
meta: false,
validateSchema: false,
missingRefs: 'ignore',
verbose: true,
schemaId: 'auto'
})

ajv.addMetaSchema(metaSchema)
ajv._opts.defaultMeta = metaSchema.id

module.exports = ajv
11 changes: 11 additions & 0 deletions lib/common/errors.js
@@ -0,0 +1,11 @@
class ConfigMissingError extends Error {
constructor(data) {
const { configName } = data
const message = `Failed to load config "${configName}" to extend from.`
super(message)
}
}

module.exports = {
ConfigMissingError
}
45 changes: 30 additions & 15 deletions lib/common/utils.js
@@ -1,18 +1,33 @@
module.exports = {
getLocFromIndex(text, index) {
let line = 1
let column = 0
let i = 0
while (i < index) {
if (text[i] === '\n') {
line++
column = 0
} else {
column++
}
i++
}
const fs = require('fs')
const path = require('path')

return { line, column }
const getLocFromIndex = (text, index) => {
let line = 1
let column = 0
let i = 0
while (i < index) {
if (text[i] === '\n') {
line++
column = 0
} else {
column++
}
i++
}

return { line, column }
}

const walkSync = (dir, filelist = []) => {
fs.readdirSync(dir).forEach(file => {
filelist = fs.statSync(path.join(dir, file)).isDirectory()
? walkSync(path.join(dir, file), filelist)
: filelist.concat(path.join(dir, file))
})
return filelist
}

module.exports = {
getLocFromIndex,
walkSync
}
85 changes: 85 additions & 0 deletions lib/config/config-file.js
@@ -0,0 +1,85 @@
const path = require('path')
const fs = require('fs')
const _ = require('lodash')
const cosmiconfig = require('cosmiconfig')
const { ConfigMissingError } = require('../common/errors')
const packageJson = require('../../package.json')

const getSolhintCoreConfigPath = name => {
if (name === 'solhint:recommended') {
return path.resolve(__dirname, '../../conf/rulesets/solhint-recommended.js')
}

if (name === 'solhint:all') {
return path.resolve(__dirname, '../../conf/rulesets/solhint-all.js')
}

if (name === 'solhint:default') {
return path.resolve(__dirname, '../../conf/rulesets/solhint-default.js')
}

throw new ConfigMissingError(name)
}

const createEmptyConfig = () => ({
excludedFiles: {},
extends: {},
globals: {},
env: {},
rules: {},
parserOptions: {}
})

const loadConfig = () => {
// Use cosmiconfig to get the config from different sources
const appDirectory = fs.realpathSync(process.cwd())
const moduleName = packageJson.name
const cosmiconfigOptions = {
searchPlaces: [
'package.json',
`.${moduleName}.json`,
`.${moduleName}rc`,
`.${moduleName}rc.json`,
`.${moduleName}rc.yaml`,
`.${moduleName}rc.yml`,
`.${moduleName}rc.js`,
`${moduleName}.config.js`
]
}

const explorer = cosmiconfig(moduleName, cosmiconfigOptions)
const searchedFor = explorer.searchSync(appDirectory)
return searchedFor.config || createEmptyConfig()
}

const applyExtends = config => {
let configExtends = config.extends

// normalize into an array for easier handling
if (!Array.isArray(config.extends)) {
configExtends = [config.extends]
}

return configExtends.reduceRight((previousValue, parentPath) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to reduce from the right? This means that the rules that appear first in the array will override the ones that appear next. I think it makes more sense to do it the other way around, and that's the way eslint does it if I'm not wrong.

try {
let extensionPath

if (parentPath.startsWith('solhint:')) {
extensionPath = getSolhintCoreConfigPath(parentPath)
} else {
// Load packages with rules
extensionPath = `solhint-config-${parentPath}`
}

const extensionConfig = require(extensionPath)
return _.merge(config, extensionConfig)
} catch (e) {
throw new ConfigMissingError(parentPath)
}
}, config)
}

module.exports = {
applyExtends,
loadConfig
}
16 changes: 16 additions & 0 deletions lib/config/config-schema.js
@@ -0,0 +1,16 @@
const baseConfigProperties = {
rules: { type: 'object' },
excludedFiles: { type: 'array' },
extends: { type: 'array' },
globals: { type: 'object' },
env: { type: 'object' },
parserOptions: { type: 'object' }
}

const configSchema = {
type: 'object',
properties: baseConfigProperties,
additionalProperties: false
}

module.exports = configSchema
119 changes: 119 additions & 0 deletions lib/config/config-validator.js
@@ -0,0 +1,119 @@
const _ = require('lodash')
const ajv = require('../common/ajv')
const configSchema = require('./config-schema')
const { loadRule } = require('../load-rules')

let validateSchema

const validSeverityMap = ['error', 'warn']

const invalidSeverityMap = ['off']

const defaultSchemaValueForRules = Object.freeze({
oneOf: [{ type: 'string', enum: [...validSeverityMap, ...invalidSeverityMap] }, { const: false }]
})

const validateRules = rulesConfig => {
if (!rulesConfig) {
return
}

const errorsSchema = []
const errorsRules = []
const rulesConfigKeys = Object.keys(rulesConfig)

for (const ruleId of rulesConfigKeys) {
const ruleInstance = loadRule(ruleId)
const ruleValue = rulesConfig[ruleId]

if (ruleInstance === undefined) {
errorsRules.push(ruleId)
continue
}

// Inject default schema
if (ruleInstance.meta.schema.length) {
let i
for (i = 0; i < ruleInstance.meta.schema.length; i++) {
const schema = ruleInstance.meta.schema[i]
if (schema.type === 'array') {
ruleInstance.meta.schema[i] = _.cloneDeep(defaultSchemaValueForRules)
ruleInstance.meta.schema[i].oneOf.push(schema)
ruleInstance.meta.schema[i].oneOf[2].items.unshift(defaultSchemaValueForRules)
}
}
} else {
ruleInstance.meta.schema.push(defaultSchemaValueForRules)
}

// Validate rule schema
validateSchema = ajv.compile(ruleInstance.meta.schema[0])

if (!validateSchema(ruleValue)) {
errorsSchema.push({ ruleId, defaultSetup: ruleInstance.meta.defaultSetup })
}
}

if (errorsRules.length) {
throw new Error(errorsRules.map(error => `\tRule ${error} doesn't exist.\n`).join(''))
}

if (errorsSchema.length) {
throw new Error(
errorsSchema
.map(
(ruleId, defaultSetup) =>
`\tRule ${ruleId} have an invalid schema.\n\tThe default setup is: ${JSON.stringify(
defaultSetup
)}`
)
.join('')
)
}
}

const formatErrors = errors =>
errors
.map(error => {
if (error.keyword === 'additionalProperties') {
const formattedPropertyPath = error.dataPath.length
? `${error.dataPath.slice(1)}.${error.params.additionalProperty}`
: error.params.additionalProperty

return `Unexpected top-level property "${formattedPropertyPath}"`
}
if (error.keyword === 'type') {
const formattedField = error.dataPath.slice(1)
const formattedExpectedType = Array.isArray(error.schema)
? error.schema.join('/')
: error.schema
const formattedValue = JSON.stringify(error.data)

return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`
}

const field = error.dataPath[0] === '.' ? error.dataPath.slice(1) : error.dataPath

return `"${field}" ${error.message}. Value: ${JSON.stringify(error.data)}`
})
.map(message => `\t- ${message}.\n`)
.join('')

const validateConfigSchema = config => {
validateSchema = validateSchema || ajv.compile(configSchema)

if (!validateSchema(config)) {
throw new Error(`Solhint configuration is invalid:\n${formatErrors(validateSchema.errors)}`)
}
}

const validate = config => {
validateConfigSchema(config)
validateRules(config.rules)
}

module.exports = {
validate,
validSeverityMap,
defaultSchemaValueForRules
}
6 changes: 0 additions & 6 deletions lib/constants.js

This file was deleted.