@sap/eslint-plugin-cds
Version:
ESLint plugin including recommended SAP Cloud Application Programming model and environment rules
184 lines (176 loc) • 5.69 kB
JavaScript
'use strict'
const cds = require('@sap/cds')
/** @type {import('../types').Rule} */
module.exports = {
meta: {
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
docs: {
description:
'Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.',
category: 'Model Validation',
recommended: true,
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/assoc2many-ambiguous-key',
},
messages: {
ambiguous: `Ambiguous key in '{{name}}'. Element '{{column-name}}' leads to multiple entries so that key '{{key-name}}' is not unique.`,
},
type: 'problem',
model: 'inferred'
},
create (context) {
return checkAssocs
function checkAssocs () {
let csnOdata
const m = context.getModel()
if (!m) return
if (m && m.definitions) {
try {
csnOdata = cds.compile.for.odata(m)
const csnOdataLinked = cds.linked(csnOdata)
associationCardinalityFlaw(csnOdataLinked, context)
} catch {
// FIXME: there are currently too many issues with this rule, e.g.
// assumptions that properties exist, etc.
}
}
}
}
}
/**
* @param {object} csn
* @param {CDSRuleContext} context
*/
function associationCardinalityFlaw (csn, context) {
processEntity(csn, (definition, sourceEntity, sourceAlias) => {
let refCardinalityMult = false
let refPlainElement = false
processElement(
csn,
definition,
sourceEntity,
sourceAlias,
() => {
refCardinalityMult = false
refPlainElement = false
},
(refEntity, refElement) => {
if (refElement.type === 'cds.Association' || refElement.type === 'cds.Composition') {
if (refElement.cardinality && refElement.cardinality.max === '*') {
refCardinalityMult = true
}
} else {
refPlainElement = true
}
},
column => {
if (
definition.keys &&
Object.keys(definition.keys).length === 1 &&
Object.keys(definition.keys)[0] === 'ID' &&
refCardinalityMult &&
refPlainElement
) {
const keyName = Object.keys(definition.keys)[0]
const key = definition.keys[keyName]
const keyLoc = context.getLocation(keyName, key, csn)
const colName = column.as ? column.as : column.name
context.report({
messageId: 'ambiguous',
data: { name: definition.name, 'column-name': colName, 'key-name': keyName },
loc: keyLoc,
file: key.$location.file
})
}
}
)
})
}
/**
* @param {object} csn
* @param {Function} eachCallback
*/
function processEntity (csn, eachCallback) {
Object.keys(csn.definitions).forEach(name => {
if (name.startsWith('localized.')) {
return
}
const definition = csn.definitions[name]
if (
definition.kind === 'entity' &&
definition.query &&
definition.query.SELECT &&
definition.query.SELECT.columns
) {
let sourceEntity
const sourceAlias = []
if (definition.query.SELECT.from.ref) {
// From
sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join('_')]
sourceAlias.push({
from: sourceEntity.name,
as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split('.').pop()
})
} else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) {
// Join
sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join('_')]
definition.query.SELECT.from.args.forEach(arg => {
sourceAlias.push({
from: arg.ref.join('_'),
as: arg.as || arg.ref.slice(-1)[0].split('.').pop()
})
})
}
if (!sourceEntity) {
return
}
eachCallback(definition, sourceEntity, sourceAlias)
}
})
}
/**
* @param {object} csn
* @param {object} definition
* @param {object} sourceEntity
* @param {string} sourceAlias
* @param {Function} beforeCallback
* @param {Function} eachCallback
* @param {Function} afterCallback
*/
function processElement (csn, definition, sourceEntity, sourceAlias, beforeCallback, eachCallback, afterCallback) {
definition.query.SELECT.columns.forEach(column => {
if (column.ref && column.ref.length > 1) {
let refEntity = sourceEntity
let refAlias = sourceAlias
beforeCallback()
column.ref.forEach(ref => {
ref = ref.id || ref
// Alias
const matchAlias = refAlias.find(alias => {
return alias.as === ref
})
let refElement
if (matchAlias) {
refEntity = csn.definitions[matchAlias.from]
} else {
refElement = refEntity.elements[ref]
// Mixin
if (!refElement) {
refElement = definition.elements[ref]
if (!refElement && definition.query.SELECT.mixin) {
refElement = definition.query.SELECT.mixin[ref]
if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) {
refElement = definition.query.SELECT.mixin[column.ref[0]]._target.elements[ref]
}
}
}
eachCallback(refEntity, refElement)
if (refElement.type === 'cds.Association' || refElement.type === 'cds.Composition') {
refEntity = csn.definitions[refElement.target]
}
}
refAlias = []
})
afterCallback(column)
}
})
}