UNPKG

@sap/eslint-plugin-cds

Version:

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

123 lines (111 loc) 3.92 kB
'use strict' const cds = require('@sap/cds') const { basename, extname } = require('node:path') const findFuzzy = require('../utils/findFuzzy') const { RULE_CATEGORIES } = require('../constants') const SEP = '[,;\t]' const EOL = '\\r?\\n' module.exports = { meta: { schema: [{/* to avoid deprecation warning for ESLint 9 */}], docs: { description: 'CSV files for entities must refer to valid element names.', category: RULE_CATEGORIES.csv, recommended: true, url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/valid-csv-header', }, severity: 'warn', type: 'problem', hasSuggestions: true, messages: { InvalidColumn: "Invalid column '{{column}}'. Did you mean '{{candidates}}'?", ReplaceColumnWith: "Replace '{{column}}' with '{{candidates}}'" }, model: 'inferred' }, create: function (context) { return checkValidHeaders function checkValidHeaders () { const filePath = context.getFilename() const sourcecode = context.getSourceCode() const code = sourcecode.getText() let model = context.getModel() if (!filePath.endsWith('.csv')) return if (!model) return try { model = cds.compile.for.sql(model, { names: cds.env.sql.names, messages: [] }) } catch(e) { // ignore invalid models; the compiler emits errors already if (e.code !== 'ERR_CDS_COMPILATION_FAILURE') throw e } if (!model) return const filename = basename(filePath) const entityName = filename.replace(/-/g, '.').slice(0, -extname(filename).length) const entity = _entity4(entityName, model) if (!entity) return const elements = Object.values(entity.elements) .filter(e => !!e['@cds.persistence.name']) .map(e => e['@cds.persistence.name'].toUpperCase()) const [cols] = cds.parse.csv(code) const missing = cols.filter(col => !elements.includes(col.toUpperCase())) for (const miss of missing) { const index = _findInCode(miss, code) const loc = sourcecode.getLocFromIndex(index) const candidates = findFuzzy(miss, Object.keys(entity.elements).sort()) const suggest = candidates.map(cand => { return { messageId: 'ReplaceColumnWith', data: { column: miss, candidates: cand }, fix: fixer => fixer.replaceTextRange([index, index + miss.length], cand) } }) context.report({ messageId: 'InvalidColumn', data: { column: miss, candidates }, loc: { start: loc, end: { line: loc.line, column: loc.column + miss.length } }, file: filePath, suggest }) } } } } /** * @param {string} needle * @param {string} code * @returns {number} -1 if not found */ function _findInCode(needle, code) { // middle let match = new RegExp(SEP + needle + SEP).exec(code) if (match) return match.index + 1 // end of line match = new RegExp(SEP + needle + EOL).exec(code) if (match) return match.index + 1 // start of doc match = new RegExp('^' + needle + SEP).exec(code) if (match) return match.index // somewhere (fallback) return code.indexOf(needle) } /** * @param {string} name * @param {object} csn */ function _entity4 (name, csn) { const entity = csn.definitions[name] if (!entity) { if (/(.+)[._]texts_?/.test(name)) { // 'Books.texts', 'Books.texts_de' const base = csn.definitions[RegExp.$1] return base && _entity4(base.elements.texts.target, csn) } else return } // we also support simple views if they have no projection const p = (entity.query && entity.query.SELECT) || entity.projection if (p && !p.columns && p.from.ref && p.from.ref.length === 1) { if (csn.definitions[p.from.ref[0]]) return entity } return entity.name ? entity : { name, __proto__: entity } }