UNPKG

@sap/eslint-plugin-cds

Version:

ESLint plugin including recommended SAP Cloud Application Programming model and environment rules

149 lines (138 loc) 4.99 kB
'use strict' /** @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 }