@sap/eslint-plugin-cds
Version:
ESLint plugin including recommended SAP Cloud Application Programming model and environment rules
149 lines (138 loc) • 4.99 kB
JavaScript
/** @typedef {import('eslint').Rule.RuleModule} RuleModule */
const fs = require('node:fs')
const path = require('node:path')
const { RuleTester } = require('eslint')
const { globalCache } = require('./Cache')
const isConfiguredFileType = require('./isConfiguredFileType')
const { compileModelFromDict } = require('../parser')
const rules = require('../rules')
/**
* A wrapper around the return value of `createRule()` that initializes the global
* cache only when the rule is actually executed. This allows tests to be run
* with test runners that don't set up a new environment for each test, such as
* mocha or the Node test runner.
*
* @param {RuleModule} rule
* @returns {RuleModule}
*/
function testRuleWrapper(rule) {
return { ...rule, create: prepareAndRunRule }
function prepareAndRunRule(context) {
return {
Program: node => {
const filePath = context.getFilename()
_initModelRuleTester(filePath, rule.meta.model)
const createValue = rule.create(context)
const result = createValue.Program(node)
globalCache.clear()
return result
}
}
}
}
/**
* ESLint RuleTester (used by custom rule creator api)
* Calls ESLint's RuleTester with custom cds parser and input for
* valid/invalid checks:
* Model checks require input 'code' entries
* Env checks require input 'options' with selected parameters
*
* @param { CDSRuleTestOpts } options RuleTester input options
*/
module.exports = function runRuleTester(options) {
const pluginRootPath = path.resolve(__dirname, '../..')
let parserPath
let rule = {}
const rulename = path.basename(options.root)
if (options.root.startsWith(pluginRootPath)) {
// For plugin's internal tests, resolve parser from here
parserPath = require.resolve('../parser')
rule = testRuleWrapper(rules[path.basename(options.root)]())
} else {
// Otherwise from project root
// eslint-disable-next-line
const resolvedPlugin = require.resolve('@sap/eslint-plugin-cds', {
paths: [options.root]
})
parserPath = path.join(path.dirname(resolvedPlugin), 'parser')
rule = testRuleWrapper(require(path.join(options.root, `../../rules/${path.basename(options.root)}`)))
}
let tester
if (parserPath) {
const options = { languageOptions: { parser: require(parserPath) } }
tester = new RuleTester(options)
} else {
tester = new RuleTester()
}
const testerCases = {};
['valid', 'invalid'].forEach(type => {
const filePath = path.join(options.root, `${type}/${options.filename}`)
testerCases[type] = [
{
filename: filePath,
}
]
testerCases[type][0].name = `${path.basename(options.root)}/${type}/${options.filename}`
if (!isConfiguredFileType(options.filename, 'FILES')) {
const fileContents = JSON.parse(fs.readFileSync(filePath, 'utf8'))
testerCases[type][0].code = ''
testerCases[type][0].filename = '<text>'
testerCases[type][0].options = [{ environment: fileContents }]
} else {
testerCases[type][0].code = fs.readFileSync(filePath, 'utf8')
if (options.options) {
testerCases[type][0].options = options.options
}
}
if (type === 'invalid') {
testerCases[type][0].errors = options.errors
const fileFixed = path.join(options.root, `fixed/${options.filename}`)
if (fs.existsSync(fileFixed) && rule.meta.type !== 'suggestion') {
testerCases[type][0].output = fs.readFileSync(fileFixed, 'utf8')
}
}
})
return tester.run(rulename, rule, testerCases)
}
/**
* Creates a model for ESLint unit tests
* @param {string} filePath
* @param {string} flavor
*/
function _initModelRuleTester(filePath, flavor) {
globalCache.set('rules', rules)
globalCache.set('test', true)
const rootPath = path.dirname(filePath)
globalCache.set('rootpath', rootPath)
if (flavor !== 'none') { // not for env rules
const files = fs.readdirSync(rootPath)
const modelfiles = files.map(f => path.join(rootPath, f)).filter(fp => isConfiguredFileType(fp, 'MODEL_FILES'))
globalCache.set(`modelfiles:${rootPath}`, modelfiles)
const dictFiles = _getDictFiles(rootPath, modelfiles)
globalCache.set(`dictfiles:${rootPath}`, dictFiles)
const reflectedModel = compileModelFromDict(dictFiles, { flavor })
globalCache.set(`model:${rootPath}`, reflectedModel)
}
}
/**
* Creates or updates a dictionary of files/file contents for a given
* project path.
*
* @param {string} input
* @param {string[]} filenames
* @returns {Record<string, string>} dictFiles
*/
function _getDictFiles(input, filenames) {
let dictFiles = {}
if (globalCache.has(`dictfiles:${input}`)) {
dictFiles = globalCache.get(`dictfiles:${input}`)
} else {
filenames.forEach(file => {
dictFiles[file] = globalCache.has(`file:${file}`)
? globalCache.get(`file:${file}`)
: fs.readFileSync(file, 'utf8')
})
}
return dictFiles
}