@sap/eslint-plugin-cds 4.1.0 → 4.1.1

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,8 +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.0] - 2025-08-18
9
+ ## [4.1.1] - 2025-12-04
10
+ ### Added
11
+ - Top level type definitions for package export
10
12
 
13
+
14
+ ## [4.1.0] - 2025-08-18
11
15
  ### Added
12
16
  - Add new rule `case-sensitive-well-known-events` to detect when a well known event is not cased correctly.
13
17
 
package/lib/conf/index.js CHANGED
@@ -4,6 +4,18 @@ const path = require('node:path')
4
4
  const { FILES, GLOBALS } = require('../constants')
5
5
  const { parserPath } = require('../api')
6
6
 
7
+ function _createJavaConfig (plugin, configName) {
8
+ return {
9
+ name: '@sap/cds/java',
10
+ plugins: {
11
+ '@sap/cds': plugin,
12
+ },
13
+ files: ['**/*.java'],
14
+ language: '@sap/cds/java',
15
+ rules: require(path.join(__dirname, 'java', configName)),
16
+ }
17
+ }
18
+
7
19
  function _createJsConfig (plugin, configName) {
8
20
  return {
9
21
  name: '@sap/cds/js',
@@ -43,6 +55,11 @@ module.exports = function (plugin) {
43
55
  js: {
44
56
  all: _createJsConfig(plugin, 'all'),
45
57
  recommended: _createJsConfig(plugin, 'recommended')
58
+ },
59
+ java: {
60
+ all: _createJavaConfig(plugin, 'all'),
61
+ recommended: _createJavaConfig(plugin, 'recommended'),
62
+ experimental: _createJavaConfig(plugin, 'experimental')
46
63
  }
47
64
  }
48
65
  }
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ '@sap/cds/cql-class-targets': 'warn',
5
+ }
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ '@sap/cds/cql-class-targets': 'warn',
5
+ }
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ module.exports = {}
@@ -2,7 +2,7 @@
2
2
 
3
3
  module.exports = {
4
4
  '@sap/cds/no-shared-handler-variable': 'error',
5
- '@sap/cds/use-cql-select-template-strings': 'error',
5
+ '@sap/cds/cql-template-strings': 'error',
6
6
  '@sap/cds/no-cross-service-import': 'warn',
7
7
  '@sap/cds/no-deep-sap-cds-import': 'warn',
8
8
  '@sap/cds/case-sensitive-well-known-events': 'warn',
@@ -2,7 +2,7 @@
2
2
 
3
3
  module.exports = {
4
4
  '@sap/cds/no-shared-handler-variable': 'error',
5
- '@sap/cds/use-cql-select-template-strings': 'error',
5
+ '@sap/cds/cql-template-strings': 'error',
6
6
  '@sap/cds/no-cross-service-import': 'warn',
7
7
  '@sap/cds/no-deep-sap-cds-import': 'warn',
8
8
  '@sap/cds/case-sensitive-well-known-events': 'warn',
package/lib/constants.js CHANGED
@@ -21,6 +21,7 @@ const RULE_CATEGORIES = {
21
21
  javascript: 'JavaScript Validation',
22
22
  environment: 'Environment Validation',
23
23
  csv: 'CSV Validation',
24
+ java: 'Java Validation'
24
25
  }
25
26
  const DEFAULT_RULE_CATEGORY = 'Model Validation'
26
27
  const DEFAULT_RULE_FLAVOR = RULE_FLAVORS[0]
package/lib/index.js CHANGED
@@ -18,18 +18,20 @@
18
18
 
19
19
  const api = require('./api')
20
20
  const getConfigs = require('./conf')
21
- const rules = Object.assign(
22
- {},
23
- ...Object.entries(require('./rules')).map(([k, v]) => ({ [k]: v() }))
24
- )
25
-
21
+ const { allRules, initialiseRules } = require('./rules')
22
+ const { javaLanguage } = require('./languages/java/java-language')
26
23
  const packageJson = require('../package.json')
27
24
 
25
+ const rules = initialiseRules(allRules)
26
+
28
27
  const plugin = {
29
28
  meta: {
30
29
  name: packageJson.name,
31
30
  version: packageJson.version
32
31
  },
32
+ languages: {
33
+ java: javaLanguage
34
+ },
33
35
  configs: {},
34
36
  rules,
35
37
  ...api
@@ -0,0 +1,69 @@
1
+ 'use strict'
2
+
3
+ const TreeSitterParser = require('tree-sitter')
4
+ const Java = require('tree-sitter-java')
5
+ const { JavaSourceCode } = require('./java-source-code')
6
+
7
+ /**
8
+ * @typedef {object} JavaLanguageOptions
9
+ * @typedef {{languageOptions: JavaLanguageOptions}} JavaLanguageContext
10
+ */
11
+
12
+ const jp = new TreeSitterParser()
13
+ jp.setLanguage(Java)
14
+
15
+ /**
16
+ * @param {import('@eslint/core').File} file
17
+ * @param {JavaLanguageContext} context
18
+ */
19
+ function parseJavaCode (file /*, context*/) {
20
+ 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 }
28
+ }
29
+ return { ast: treeSitterAst, ok: true }
30
+ }
31
+
32
+ /**
33
+ * @param {import('@eslint/core').File} file
34
+ * @param {ReturnType<typeof parseJavaCode>} parseResult
35
+ * @param {JavaLanguageContext} context
36
+ */
37
+ function createJavaSourceCode(file, parseResult /*, context*/) {
38
+ if (!parseResult.ok || !parseResult.ast) {
39
+ throw new Error('createJavaSourceCode: parseResult is not ok or has no AST', {
40
+ ok: parseResult.ok,
41
+ hasAst: !!parseResult.ast
42
+ })
43
+ }
44
+ return new JavaSourceCode({ text: file.body, ast: parseResult.ast })
45
+ }
46
+
47
+ /**
48
+ * @param {JavaLanguageOptions} languageOptions
49
+ */
50
+ function validateJavaLanguageOptions(/*languageOptions*/) {
51
+ // No specific options to validate for Java currently
52
+ }
53
+
54
+ /**
55
+ * @link https://eslint.org/docs/latest/extend/languages#the-language-object
56
+ */
57
+ const javaLanguage = /** @type {const}*/({
58
+ fileType: 'text',
59
+ lineStart: 0,
60
+ columnStart: 0,
61
+ nodeTypeKey: 'type',
62
+ validateLanguageOptions: validateJavaLanguageOptions,
63
+ parse: parseJavaCode,
64
+ createSourceCode: createJavaSourceCode
65
+ })
66
+
67
+ module.exports = {
68
+ javaLanguage,
69
+ }
@@ -0,0 +1,179 @@
1
+ 'use strict'
2
+
3
+ // not 100% accurate, as it matches _any_ comment containing 'eslint-disable' etc.
4
+ // but in Java sources, users will hopefully only mention eslint-* when they
5
+ // actually mean to configure ESLint.
6
+ const INLINE_CONFIG = /eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u
7
+
8
+ /**
9
+ * @link see https://github.com/eslint/json/blob/main/src/languages/json-source-code.js
10
+ */
11
+ const {
12
+ VisitNodeStep,
13
+ TextSourceCodeBase,
14
+ Directive
15
+ } = require('@eslint/plugin-kit')
16
+
17
+ /** @typedef {'disable' | 'enable' | 'disable-next-line' | 'disable-line'} DirectiveType */
18
+
19
+
20
+ /**
21
+ * @param {import('tree-sitter').SyntaxNode} comment
22
+ * @returns {{
23
+ * label: 'eslint-disable' | 'eslint-enable' | 'eslint-disable-next-line' | 'eslint-disable-line',
24
+ * value: string,
25
+ * justification: string | undefined
26
+ * }}
27
+ */
28
+ function parseDirective(comment) {
29
+ // label, value, justification
30
+ // * but before the "--" that indicates the justification.
31
+ const [, label, value, justification] = comment.text.match(/^\W*(eslint[\w-]*) (\S+)(?: -- (.+))?/)
32
+ return { label, value, justification}
33
+ }
34
+
35
+ class JavaTraversalStep extends VisitNodeStep {
36
+ /** @param {{ target: unknown, phase: 1|2, args: Array<any> }} arg0 */
37
+ constructor({ target, phase, args }) {
38
+ super({ target, phase, args })
39
+ }
40
+ }
41
+
42
+ /**
43
+ * @param {import('tree-sitter').SyntaxNode} node
44
+ * @param {(node: import('tree-sitter').SyntaxNode) => void} enterAction
45
+ * @param {undefined | (node: import('tree-sitter').SyntaxNode) => void} exitAction
46
+ */
47
+ function traverse(node, enterAction, exitAction) {
48
+ enterAction?.(node)
49
+ //steps.push(new JavaTraversalStep({ target: node, phase: 1, args: [] }))
50
+ for (const child of node.children) {
51
+ traverse(child, enterAction, exitAction)
52
+ }
53
+ exitAction?.(node)
54
+ }
55
+
56
+
57
+ /** @param {import('tree-sitter').SyntaxNode} node */
58
+ const getAncestors = node => !node
59
+ ? []
60
+ : [...getAncestors(node.parent), node]
61
+
62
+ /**
63
+ * @link https://eslint.org/docs/latest/extend/languages#the-sourcecode-object
64
+ */
65
+ class JavaSourceCode extends TextSourceCodeBase {
66
+ /** @type {undefined | import('tree-sitter').SyntaxNode[]} */
67
+ #comments
68
+ /** @type {undefined | import('tree-sitter').SyntaxNode[]} */
69
+ #inlineConfigComments
70
+
71
+ get comments() {
72
+ if (!this.#comments) {
73
+ this.#comments = []
74
+ traverse(this.ast.rootNode, node => {
75
+ if (['line_comment', 'block_comment'].includes(node.type)) {
76
+ this.#comments.push(node)
77
+ }
78
+ })
79
+ }
80
+ return this.#comments
81
+ }
82
+
83
+ get inlineConfigNodes() {
84
+ if (!this.#inlineConfigComments) {
85
+ this.#inlineConfigComments = this
86
+ .comments
87
+ .filter(comment =>
88
+ INLINE_CONFIG.test(comment.text),
89
+ ) ?? []
90
+ }
91
+
92
+ return this.#inlineConfigComments
93
+ }
94
+
95
+ get lines () { return this.text.split(/\r\n|\r|\n/) }
96
+
97
+ /** @param {import('tree-sitter').SyntaxNode} node */
98
+ getLoc(node) {
99
+ return {
100
+ start: { line: node.startPosition.row, column: node.startPosition.column },
101
+ end: { line: node.endPosition.row, column: node.endPosition.column }
102
+ }
103
+ }
104
+
105
+ /** @param {import('tree-sitter').SyntaxNode} node */
106
+ getRange(node) {
107
+ return [node.startIndex, node.endIndex]
108
+ }
109
+
110
+ /** @param {import('tree-sitter').SyntaxNode} node */
111
+ getParent(node) {
112
+ return node.parent ?? undefined // .parent is null if root, we need undefined
113
+ }
114
+
115
+ /** @param {import('tree-sitter').SyntaxNode} node */
116
+ getAncestors(node) {
117
+ // ESLint wants ancestors excluding the node itself
118
+ return getAncestors(node.parent)
119
+ }
120
+
121
+ /** @param {import('tree-sitter').SyntaxNode} node */
122
+ getText(node) {
123
+ return node.text
124
+ }
125
+
126
+ getDisableDirectives() {
127
+ /** @type {FileProblem[]} */
128
+ const problems = []
129
+ /** @type {Directive[]} */
130
+ const directives = []
131
+
132
+ for (const comment of this.inlineConfigNodes) {
133
+ const { label, value, justification } = parseDirective(comment)
134
+
135
+ // `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
136
+ if (label === 'eslint-disable-line' && comment.loc.start.line !== comment.loc.end.line) {
137
+ problems.push({
138
+ ruleId: null,
139
+ message: `${label} comment should not span multiple lines.`,
140
+ loc: comment.loc,
141
+ })
142
+ } else if (['eslint-disable', 'eslint-enable', 'eslint-disable-next-line', 'eslint-disable-line'].includes(label)) {
143
+ directives.push(
144
+ new Directive({
145
+ type: /** @type {DirectiveType} */ (label.slice('eslint-'.length)),
146
+ node: comment,
147
+ value,
148
+ justification,
149
+ }),
150
+ )
151
+ }
152
+ }
153
+ return { problems, directives }
154
+ }
155
+
156
+ traverse() {
157
+ const steps = []
158
+ if (!this.ast || !this.ast.rootNode) {
159
+ return steps
160
+ }
161
+ traverse(this.ast.rootNode, node => {
162
+ steps.push(new JavaTraversalStep({ target: node, phase: 1 }))
163
+ }, node => {
164
+ steps.push(new JavaTraversalStep({ target: node, phase: 2 }))
165
+ })
166
+ return steps
167
+ }
168
+
169
+ /** @param {{text: string, ast: import('tree-sitter').Tree}} param0 */
170
+ constructor({text, ast}) {
171
+ super({text, ast})
172
+ this.text = text
173
+ this.ast = ast
174
+ }
175
+ }
176
+
177
+ module.exports = {
178
+ JavaSourceCode
179
+ }
@@ -17,10 +17,28 @@ const readRulesFromDir = (dir, post) => Object.fromEntries(fs.readdirSync(path.j
17
17
  .filter(([,module]) => Object.hasOwn(module, 'create')) // create() is required to exist on top level by eslint -> good check to find actual rules
18
18
  .map(([file, module]) => [file, () => post(module)]))
19
19
 
20
+ /**
21
+ * Calls the initialisation function for each rule in the given rule set.
22
+ * @param {Record<string, () => unknown>} ruleSet
23
+ * @returns {Record<string, unknown>}
24
+ */
25
+ const initialiseRules = ruleSet => Object.fromEntries(
26
+ Object.entries(ruleSet)
27
+ .map(([k, v]) => [[k], v()]))
28
+
20
29
  const cdsRules = readRulesFromDir('.', createRule)
21
30
  const jsRules = readRulesFromDir('js', module => module)
22
- const rules = {...cdsRules, ...jsRules}
31
+ // backwards compat, remove in next minor
32
+ jsRules['use-cql-select-template-strings'] = jsRules['cql-template-strings']
33
+ const javaRules = readRulesFromDir('java', module => module)
34
+ const allRules = {...cdsRules, ...jsRules, ...javaRules}
23
35
 
24
- globalCache.set('rules', rules)
36
+ globalCache.set('rules', allRules)
25
37
 
26
- module.exports = rules
38
+ module.exports = {
39
+ allRules,
40
+ jsRules,
41
+ javaRules,
42
+ cdsRules,
43
+ initialiseRules
44
+ }
@@ -0,0 +1,59 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * In Select.from(x), x should be a class literal.
5
+ */
6
+
7
+ const { RULE_CATEGORIES } = require('../../constants')
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ recommended: true,
14
+ category: RULE_CATEGORIES.java,
15
+ description: 'java.'
16
+ },
17
+ fixable: 'code',
18
+ schema: [],
19
+ messages: {
20
+ selectOnNonClass: `'{{target}}' is not a class. Prefer to pass class literals to Select.from!`,
21
+ },
22
+ hasSuggestions: true
23
+ },
24
+ create: context => {
25
+ const knownClasses = new Set()
26
+
27
+ /** @param {Node} node */
28
+ const refersToClass = node =>
29
+ node.type === 'class_literal'
30
+ // or part of explicit named imports
31
+ || knownClasses.has(node.text)
32
+ // or first letter of last part of text is uppercase: a.b.C -> true
33
+ // https://unicode.org/reports/tr18/#General_Category_Property
34
+ || /^\p{Lu}/u.test(node.text.split('.').at(-1))
35
+
36
+ return {
37
+ 'import_declaration > scoped_identifier': function(node) {
38
+ if (!node) return
39
+ if (node.nameNode) {
40
+ knownClasses.add(node.nameNode.text)
41
+ }
42
+ },
43
+
44
+ "method_invocation > identifier[text='from']": function(node) {
45
+ if (!node?.parent?.argumentsNode) return
46
+ const args = node.parent.argumentsNode.children
47
+ .filter(c => c.constructor.name !== 'SyntaxNode') // filter out commas, parens, etc.
48
+ ?? []
49
+ if (!refersToClass(args[0])) {
50
+ context.report({
51
+ node: args[0],
52
+ messageId: 'selectOnNonClass',
53
+ data: { target: args[0].text }
54
+ })
55
+ }
56
+ },
57
+ }
58
+ }
59
+ }
@@ -9,7 +9,7 @@ const { RuleTester } = require('eslint')
9
9
  const { globalCache } = require('./Cache')
10
10
  const isConfiguredFileType = require('./isConfiguredFileType')
11
11
  const { compileModelFromDict } = require('../parser')
12
- const rules = require('../rules')
12
+ const { allRules: rules } = require('../rules')
13
13
 
14
14
  /**
15
15
  * A wrapper around the return value of `createRule()` that initializes the global
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/eslint-plugin-cds",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
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,10 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "semver": "^7.7.1"
23
+ "@eslint/plugin-kit": "^0.4.1",
24
+ "semver": "^7.7.1",
25
+ "tree-sitter": "^0.21.1",
26
+ "tree-sitter-java": "^0.23.5"
24
27
  },
25
28
  "peerDependencies": {
26
29
  "@sap/cds": ">=9",