@sap/eslint-plugin-cds 4.1.1 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
8
8
|
|
|
9
|
+
## [4.1.2] - 2026-02-13
|
|
10
|
+
### Fixed
|
|
11
|
+
- No longer crash on .java files larger than 32 kB.
|
|
12
|
+
- Weaken `java/cql-class-targets` to only warn when a string literal is passed as parameter.
|
|
13
|
+
- `assoc2many-ambiguous-key` no longer falsely warns about 1-n joins when an infix filter reduces the joined relation to a single row.
|
|
14
|
+
|
|
9
15
|
## [4.1.1] - 2025-12-04
|
|
10
16
|
### Added
|
|
11
17
|
- Top level type definitions for package export
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { TextEncoder } = require('node:util')
|
|
3
4
|
const TreeSitterParser = require('tree-sitter')
|
|
4
5
|
const Java = require('tree-sitter-java')
|
|
5
6
|
const { JavaSourceCode } = require('./java-source-code')
|
|
@@ -18,15 +19,17 @@ jp.setLanguage(Java)
|
|
|
18
19
|
*/
|
|
19
20
|
function parseJavaCode (file /*, context*/) {
|
|
20
21
|
const code = file.body
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return { ast: treeSitterAst, ok:
|
|
22
|
+
try {
|
|
23
|
+
// https://stackoverflow.com/a/79771252
|
|
24
|
+
const codeByteSize = new TextEncoder().encode(code).length
|
|
25
|
+
const treeSitterAst = jp.parse(code, undefined, { bufferSize: codeByteSize + 32 }) // adding some extra buffer space
|
|
26
|
+
if (!treeSitterAst) return { ast: null, ok: false, errors: [] }
|
|
27
|
+
if (!treeSitterAst.rootNode) return { ast: treeSitterAst, ok: false, errors: [] }
|
|
28
|
+
return { ast: treeSitterAst, ok: true, errors: [] }
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// most likely a parsing error. This could mean we have to update the parser for newer Java versions
|
|
31
|
+
return { ast: null, ok: false, errors: [e] }
|
|
28
32
|
}
|
|
29
|
-
return { ast: treeSitterAst, ok: true }
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/**
|
|
@@ -4,12 +4,47 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
/** @type {import('../types').Rule} */
|
|
6
6
|
|
|
7
|
+
const isAssociationOrComposition = element => ['cds.Association', 'cds.Composition'].includes(element?.type)
|
|
8
|
+
const isEntity = element => element?.kind === 'entity'
|
|
9
|
+
const isStarCardinality = element => element?.cardinality?.max === '*'
|
|
10
|
+
/**
|
|
11
|
+
* Using infix filters, one-to-many associations can be reduced to one-to-one.
|
|
12
|
+
* @example
|
|
13
|
+
* ```cds
|
|
14
|
+
* SELECT name, books[1: favorite=true].title from Authors
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
const isScalarInfixFilter = element => element?.ref?.[0]?.cardinality?.max === 1
|
|
18
|
+
|
|
19
|
+
function resolveSourceAlias (csn, definition) {
|
|
20
|
+
const from = definition.query.SELECT.from
|
|
21
|
+
let sourceEntity
|
|
22
|
+
const sourceAlias = []
|
|
23
|
+
if (from?.ref) {
|
|
24
|
+
// From
|
|
25
|
+
sourceEntity = csn.definitions[from.ref.join('_')]
|
|
26
|
+
sourceAlias.push({
|
|
27
|
+
from: sourceEntity.name,
|
|
28
|
+
as: from.as ?? from.ref.at(-1).split('.').pop()
|
|
29
|
+
})
|
|
30
|
+
} else if (from?.args?.[0].ref) {
|
|
31
|
+
// Join
|
|
32
|
+
sourceEntity = csn.definitions[from.args[0].ref.join('_')]
|
|
33
|
+
for (const arg of from.args) {
|
|
34
|
+
sourceAlias.push({
|
|
35
|
+
from: arg.ref.join('_'),
|
|
36
|
+
as: arg.as ?? arg.ref.at(-1).split('.').pop()
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { sourceEntity, sourceAlias }
|
|
41
|
+
}
|
|
42
|
+
|
|
7
43
|
module.exports = {
|
|
8
44
|
meta: {
|
|
9
45
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
10
46
|
docs: {
|
|
11
|
-
description:
|
|
12
|
-
'Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.',
|
|
47
|
+
description: 'Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.',
|
|
13
48
|
category: 'Model Validation',
|
|
14
49
|
recommended: true,
|
|
15
50
|
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/assoc2many-ambiguous-key',
|
|
@@ -26,8 +61,7 @@ module.exports = {
|
|
|
26
61
|
function checkAssocs () {
|
|
27
62
|
let csnOdata
|
|
28
63
|
const m = context.getModel()
|
|
29
|
-
if (
|
|
30
|
-
if (m && m.definitions) {
|
|
64
|
+
if (m?.definitions) {
|
|
31
65
|
try {
|
|
32
66
|
csnOdata = cds.compile.for.odata(m)
|
|
33
67
|
const csnOdataLinked = cds.linked(csnOdata)
|
|
@@ -42,142 +76,85 @@ module.exports = {
|
|
|
42
76
|
}
|
|
43
77
|
|
|
44
78
|
/**
|
|
45
|
-
* @param {
|
|
46
|
-
* @param {CDSRuleContext} context
|
|
79
|
+
* @param {import('../types').CSN} csn
|
|
80
|
+
* @param {import('../types').CDSRuleContext} context
|
|
47
81
|
*/
|
|
48
82
|
function associationCardinalityFlaw (csn, context) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
definition,
|
|
55
|
-
sourceEntity,
|
|
56
|
-
sourceAlias,
|
|
57
|
-
() => {
|
|
58
|
-
refCardinalityMult = false
|
|
59
|
-
refPlainElement = false
|
|
60
|
-
},
|
|
61
|
-
(refEntity, refElement) => {
|
|
62
|
-
if (refElement.type === 'cds.Association' || refElement.type === 'cds.Composition') {
|
|
63
|
-
if (refElement.cardinality && refElement.cardinality.max === '*') {
|
|
64
|
-
refCardinalityMult = true
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
refPlainElement = true
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
column => {
|
|
71
|
-
if (
|
|
72
|
-
definition.keys &&
|
|
73
|
-
Object.keys(definition.keys).length === 1 &&
|
|
74
|
-
Object.keys(definition.keys)[0] === 'ID' &&
|
|
75
|
-
refCardinalityMult &&
|
|
76
|
-
refPlainElement
|
|
77
|
-
) {
|
|
78
|
-
const keyName = Object.keys(definition.keys)[0]
|
|
79
|
-
const key = definition.keys[keyName]
|
|
80
|
-
const keyLoc = context.getLocation(keyName, key, csn)
|
|
81
|
-
const colName = column.as ? column.as : column.name
|
|
82
|
-
context.report({
|
|
83
|
-
messageId: 'ambiguous',
|
|
84
|
-
data: { name: definition.name, 'column-name': colName, 'key-name': keyName },
|
|
85
|
-
loc: keyLoc,
|
|
86
|
-
file: key.$location.file
|
|
87
|
-
})
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
)
|
|
91
|
-
})
|
|
83
|
+
for (const [name, definition] of Object.entries(csn.definitions)) {
|
|
84
|
+
if (!name.startsWith('localized.')) {
|
|
85
|
+
processEntity({ definition, csn, context})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
92
88
|
}
|
|
93
89
|
|
|
94
90
|
/**
|
|
95
|
-
* @param {
|
|
96
|
-
* @param {Function} eachCallback
|
|
91
|
+
* @param {import('../types').CSN} csn
|
|
97
92
|
*/
|
|
98
|
-
function processEntity (csn,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
definition.query.SELECT &&
|
|
108
|
-
definition.query.SELECT.columns
|
|
109
|
-
) {
|
|
110
|
-
let sourceEntity
|
|
111
|
-
const sourceAlias = []
|
|
112
|
-
if (definition.query.SELECT.from.ref) {
|
|
113
|
-
// From
|
|
114
|
-
sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join('_')]
|
|
115
|
-
sourceAlias.push({
|
|
116
|
-
from: sourceEntity.name,
|
|
117
|
-
as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split('.').pop()
|
|
118
|
-
})
|
|
119
|
-
} else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) {
|
|
120
|
-
// Join
|
|
121
|
-
sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join('_')]
|
|
122
|
-
definition.query.SELECT.from.args.forEach(arg => {
|
|
123
|
-
sourceAlias.push({
|
|
124
|
-
from: arg.ref.join('_'),
|
|
125
|
-
as: arg.as || arg.ref.slice(-1)[0].split('.').pop()
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
if (!sourceEntity) {
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
eachCallback(definition, sourceEntity, sourceAlias)
|
|
133
|
-
}
|
|
134
|
-
})
|
|
93
|
+
function processEntity ({ definition, csn, context}) {
|
|
94
|
+
if (!isEntity(definition) || !definition.query?.SELECT?.columns) return
|
|
95
|
+
|
|
96
|
+
const { sourceEntity, sourceAlias } = resolveSourceAlias(csn, definition)
|
|
97
|
+
if (!sourceEntity) return
|
|
98
|
+
|
|
99
|
+
for (const column of definition.query.SELECT.columns) {
|
|
100
|
+
processElement({ csn, definition, column, sourceEntity, sourceAlias, context })
|
|
101
|
+
}
|
|
135
102
|
}
|
|
136
103
|
|
|
137
104
|
/**
|
|
138
|
-
* @param {object}
|
|
139
|
-
* @param {
|
|
140
|
-
* @param {object}
|
|
141
|
-
* @param {
|
|
142
|
-
* @param {
|
|
143
|
-
* @param {
|
|
144
|
-
* @param {
|
|
105
|
+
* @param {object} p
|
|
106
|
+
* @param {import('../types').CSN} p.csn
|
|
107
|
+
* @param {object} p.column
|
|
108
|
+
* @param {object} p.definition
|
|
109
|
+
* @param {object} p.sourceEntity
|
|
110
|
+
* @param {string} p.sourceAlias
|
|
111
|
+
* @param {import('../types').CDSRuleContext} p.context
|
|
145
112
|
*/
|
|
146
|
-
function processElement (csn, definition, sourceEntity, sourceAlias,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
} else {
|
|
162
|
-
refElement = refEntity.elements[ref]
|
|
113
|
+
function processElement ({csn, definition, column, sourceEntity, sourceAlias, context}) {
|
|
114
|
+
if (!(column.ref && column.ref.length > 1)) return
|
|
115
|
+
|
|
116
|
+
let refPlainElement = false
|
|
117
|
+
let refCardinalityMult = false
|
|
118
|
+
let refEntity = sourceEntity
|
|
119
|
+
const refAlias = sourceAlias
|
|
120
|
+
for (const ref of column.ref.map(ref => ref.id ?? ref)) {
|
|
121
|
+
// Alias
|
|
122
|
+
const matchAlias = refAlias.find(alias => alias.as === ref)
|
|
123
|
+
let refElement
|
|
124
|
+
if (matchAlias) {
|
|
125
|
+
refEntity = csn.definitions[matchAlias.from]
|
|
126
|
+
} else {
|
|
127
|
+
refElement = refEntity.elements[ref]
|
|
163
128
|
// Mixin
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
refAlias = []
|
|
179
|
-
})
|
|
180
|
-
afterCallback(column)
|
|
129
|
+
?? definition.elements[ref]
|
|
130
|
+
?? definition.query.SELECT.mixin[ref]
|
|
131
|
+
?? definition.query.SELECT.mixin[column.ref[0]]?._target?.elements[ref]
|
|
132
|
+
|
|
133
|
+
if (isAssociationOrComposition(refElement)) {
|
|
134
|
+
refCardinalityMult = isStarCardinality(refElement)
|
|
135
|
+
} else {
|
|
136
|
+
refPlainElement = true
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isAssociationOrComposition(refElement)) {
|
|
141
|
+
refEntity = csn.definitions[refElement.target]
|
|
181
142
|
}
|
|
182
|
-
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const keyNames = Object.keys(definition.keys ?? {})
|
|
146
|
+
const [firstkey] = keyNames
|
|
147
|
+
if (keyNames.length === 1 && firstkey === 'ID' && refCardinalityMult && refPlainElement && !isScalarInfixFilter(column)) {
|
|
148
|
+
const key = definition.keys[firstkey]
|
|
149
|
+
context.report({
|
|
150
|
+
messageId: 'ambiguous',
|
|
151
|
+
data: {
|
|
152
|
+
name: definition.name,
|
|
153
|
+
'column-name': column.as ?? column.name,
|
|
154
|
+
'key-name': firstkey
|
|
155
|
+
},
|
|
156
|
+
loc: context.getLocation(firstkey, key, csn),
|
|
157
|
+
file: key.$location.file
|
|
158
|
+
})
|
|
159
|
+
}
|
|
183
160
|
}
|
|
@@ -33,6 +33,9 @@ module.exports = {
|
|
|
33
33
|
// https://unicode.org/reports/tr18/#General_Category_Property
|
|
34
34
|
|| /^\p{Lu}/u.test(node.text.split('.').at(-1))
|
|
35
35
|
|
|
36
|
+
const isString = node =>
|
|
37
|
+
node?.type === 'string_literal'
|
|
38
|
+
|
|
36
39
|
return {
|
|
37
40
|
'import_declaration > scoped_identifier': function(node) {
|
|
38
41
|
if (!node) return
|
|
@@ -46,7 +49,13 @@ module.exports = {
|
|
|
46
49
|
const args = node.parent.argumentsNode.children
|
|
47
50
|
.filter(c => c.constructor.name !== 'SyntaxNode') // filter out commas, parens, etc.
|
|
48
51
|
?? []
|
|
49
|
-
|
|
52
|
+
// we actually want to detect anything that is NOT a class literal,
|
|
53
|
+
// but there are some edge cases we need to care for in the future,
|
|
54
|
+
// such as parameters passing in a class:
|
|
55
|
+
// void myMethod(Class<MyClass> cls) { Select.from(cls); }
|
|
56
|
+
// for the time being, we will just warn on string literals
|
|
57
|
+
//if (!refersToClass(args[0])) {
|
|
58
|
+
if (isString(args[0])) {
|
|
50
59
|
context.report({
|
|
51
60
|
node: args[0],
|
|
52
61
|
messageId: 'selectOnNonClass',
|
package/lib/types.d.ts
CHANGED
|
@@ -44,3 +44,9 @@ export interface CDSRuleTestOpts {
|
|
|
44
44
|
/** list of errors from ESLint's [RuleTester](https://eslint.org/docs/developer-guide/nodejs-api#ruletester) */
|
|
45
45
|
errors: CDSTestCaseError[]
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
// FIXME: this is just a dummy and needs to go
|
|
49
|
+
// once we consolidate the various CSN types across packages
|
|
50
|
+
export type CSN = {
|
|
51
|
+
definitions: {}
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/eslint-plugin-cds",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.2",
|
|
4
4
|
"description": "ESLint plugin including recommended SAP Cloud Application Programming model and environment rules",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"keywords": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@eslint/plugin-kit": "^0.
|
|
23
|
+
"@eslint/plugin-kit": "^0.6.0",
|
|
24
24
|
"semver": "^7.7.1",
|
|
25
25
|
"tree-sitter": "^0.21.1",
|
|
26
26
|
"tree-sitter-java": "^0.23.5"
|