@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
- const treeSitterAst = jp.parse(code)
22
-
23
- if (!treeSitterAst) {
24
- return { ast: null, ok: false }
25
- }
26
- if (!treeSitterAst.rootNode) {
27
- return { ast: treeSitterAst, ok: false }
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 (!m) return
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 {object} csn
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
- processEntity(csn, (definition, sourceEntity, sourceAlias) => {
50
- let refCardinalityMult = false
51
- let refPlainElement = false
52
- processElement(
53
- csn,
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 {object} csn
96
- * @param {Function} eachCallback
91
+ * @param {import('../types').CSN} csn
97
92
  */
98
- function processEntity (csn, eachCallback) {
99
- Object.keys(csn.definitions).forEach(name => {
100
- if (name.startsWith('localized.')) {
101
- return
102
- }
103
- const definition = csn.definitions[name]
104
- if (
105
- definition.kind === 'entity' &&
106
- definition.query &&
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} csn
139
- * @param {object} definition
140
- * @param {object} sourceEntity
141
- * @param {string} sourceAlias
142
- * @param {Function} beforeCallback
143
- * @param {Function} eachCallback
144
- * @param {Function} afterCallback
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, beforeCallback, eachCallback, afterCallback) {
147
- definition.query.SELECT.columns.forEach(column => {
148
- if (column.ref && column.ref.length > 1) {
149
- let refEntity = sourceEntity
150
- let refAlias = sourceAlias
151
- beforeCallback()
152
- column.ref.forEach(ref => {
153
- ref = ref.id || ref
154
- // Alias
155
- const matchAlias = refAlias.find(alias => {
156
- return alias.as === ref
157
- })
158
- let refElement
159
- if (matchAlias) {
160
- refEntity = csn.definitions[matchAlias.from]
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
- if (!refElement) {
165
- refElement = definition.elements[ref]
166
- if (!refElement && definition.query.SELECT.mixin) {
167
- refElement = definition.query.SELECT.mixin[ref]
168
- if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) {
169
- refElement = definition.query.SELECT.mixin[column.ref[0]]._target.elements[ref]
170
- }
171
- }
172
- }
173
- eachCallback(refEntity, refElement)
174
- if (refElement.type === 'cds.Association' || refElement.type === 'cds.Composition') {
175
- refEntity = csn.definitions[refElement.target]
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
- if (!refersToClass(args[0])) {
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.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.4.1",
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"