@sap/eslint-plugin-cds 3.0.5 → 3.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +1 -1
  3. package/lib/api/index.js +4 -4
  4. package/lib/conf/all.js +17 -17
  5. package/lib/conf/experimental.js +12 -0
  6. package/lib/conf/index.js +12 -3
  7. package/lib/conf/recommended.js +13 -14
  8. package/lib/constants.js +2 -0
  9. package/lib/index.js +2 -1
  10. package/lib/parser.js +10 -1
  11. package/lib/rules/assoc2many-ambiguous-key.js +40 -11
  12. package/lib/rules/auth-no-empty-restrictions.js +38 -10
  13. package/lib/rules/auth-restrict-grant-service.js +29 -29
  14. package/lib/rules/auth-use-requires.js +27 -15
  15. package/lib/rules/auth-valid-restrict-grant.js +138 -82
  16. package/lib/rules/auth-valid-restrict-keys.js +34 -18
  17. package/lib/rules/auth-valid-restrict-to.js +57 -106
  18. package/lib/rules/auth-valid-restrict-where.js +44 -43
  19. package/lib/rules/extension-restrictions.js +11 -3
  20. package/lib/rules/index.js +5 -1
  21. package/lib/rules/latest-cds-version.js +5 -4
  22. package/lib/rules/no-db-keywords.js +14 -5
  23. package/lib/rules/no-dollar-prefixed-names.js +9 -2
  24. package/lib/rules/no-java-keywords.js +181 -0
  25. package/lib/rules/no-join-on-draft.js +9 -3
  26. package/lib/rules/sql-cast-suggestion.js +19 -15
  27. package/lib/rules/sql-null-comparison.js +60 -0
  28. package/lib/rules/start-elements-lowercase.js +6 -2
  29. package/lib/rules/start-entities-uppercase.js +12 -5
  30. package/lib/rules/valid-csv-header.js +33 -13
  31. package/lib/types.d.ts +4 -4
  32. package/lib/utils/Cache.js +4 -2
  33. package/lib/utils/Colors.js +2 -0
  34. package/lib/utils/LintError.js +17 -0
  35. package/lib/utils/createRule.js +160 -134
  36. package/lib/utils/csnTraversal.js +163 -0
  37. package/lib/utils/findFuzzy.js +21 -12
  38. package/lib/utils/getConfigPath.js +4 -2
  39. package/lib/utils/getConfiguredFileTypes.js +2 -0
  40. package/lib/utils/getFileExtensions.js +2 -0
  41. package/lib/utils/getProjectRootPath.js +53 -15
  42. package/lib/utils/isConfiguredFileType.js +8 -3
  43. package/lib/utils/rules.js +15 -9
  44. package/lib/utils/runRuleTester.js +69 -36
  45. package/package.json +1 -1
  46. package/lib/utils/genDocs.js +0 -346
@@ -1,13 +1,16 @@
1
+ 'use strict'
2
+
1
3
  const cds = requireMtx()
2
4
 
3
- const { dirname } = require('path')
5
+ const { dirname } = require('node:path')
4
6
 
5
7
  const rule = module.exports = {
6
8
  meta: {
7
9
  docs: {
8
10
  description: 'Extensions must not violate restrictions set by the extended SaaS app.',
9
11
  category: 'Model Validation',
10
- recommended: true
12
+ recommended: true,
13
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/extension-restrictions',
11
14
  },
12
15
  hasSuggestions: false,
13
16
  type: 'problem',
@@ -41,6 +44,9 @@ const rule = module.exports = {
41
44
  }
42
45
  }
43
46
 
47
+ /**
48
+ * @param {CDSRuleContext} context
49
+ */
44
50
  function baseModel (context) {
45
51
  let dir = context.getFilename()
46
52
  do {
@@ -61,9 +67,11 @@ function baseModel (context) {
61
67
  function requireMtx () {
62
68
  const cds = require('@sap/cds')
63
69
  try {
70
+ // eslint-disable-next-line
64
71
  const pkg = require.resolve('@sap/cds-mtxs', { paths: [cds.root, __dirname] })
65
72
  return require(pkg)
66
73
  } catch (e) {
67
- if (e.code !== 'MODULE_NOT_FOUND') throw e
74
+ if (e.code !== 'MODULE_NOT_FOUND')
75
+ throw e
68
76
  }
69
77
  }
@@ -1,5 +1,7 @@
1
+ 'use strict'
2
+
1
3
  const Cache = require('../utils/Cache')
2
- const { createRule } = require('../api')
4
+ const createRule = require('../utils/createRule')
3
5
 
4
6
  const rules = {
5
7
  'assoc2many-ambiguous-key': () => createRule(require('./assoc2many-ambiguous-key')),
@@ -12,8 +14,10 @@ const rules = {
12
14
  'auth-valid-restrict-where': () => createRule(require('./auth-valid-restrict-where')),
13
15
  'latest-cds-version': () => createRule(require('./latest-cds-version')),
14
16
  'no-db-keywords': () => createRule(require('./no-db-keywords')),
17
+ 'no-java-keywords': () => createRule(require('./no-java-keywords')),
15
18
  'no-dollar-prefixed-names': () => createRule(require('./no-dollar-prefixed-names')),
16
19
  'no-join-on-draft': () => createRule(require('./no-join-on-draft')),
20
+ 'sql-null-comparison': () => createRule(require('./sql-null-comparison')),
17
21
  'sql-cast-suggestion': () => createRule(require('./sql-cast-suggestion')),
18
22
  'start-elements-lowercase': () => createRule(require('./start-elements-lowercase')),
19
23
  'start-entities-uppercase': () => createRule(require('./start-entities-uppercase')),
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const cp = require('child_process')
2
4
  const semver = require('semver')
3
5
 
@@ -5,12 +7,11 @@ module.exports = {
5
7
  meta: {
6
8
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
7
9
  docs: {
8
- description: 'Checks whether the latest `@sap/cds` version is being used.'
10
+ description: 'Checks whether the latest `@sap/cds` version is being used.',
9
11
  },
10
12
  type: 'suggestion',
11
- hasSuggestions: true,
12
13
  messages: {
13
- latestCDSVersion: 'A newer CDS version is available!'
14
+ latestCdsVersion: 'A newer CDS version is available!'
14
15
  },
15
16
  severity: 'off',
16
17
  model: 'none'
@@ -36,7 +37,7 @@ module.exports = {
36
37
  // If current cds version is not the latest
37
38
  if (Object.keys(cdsVersions).length !== 0 && !semver.satisfies(cdsVersions.latest, cdsVersions.current)) {
38
39
  context.report({
39
- messageId: 'latestCDSVersion',
40
+ messageId: 'latestCdsVersion',
40
41
  node: context.getNode()
41
42
  })
42
43
  }
@@ -1,11 +1,22 @@
1
+ 'use strict'
2
+
1
3
  const cds = require('@sap/cds')
2
4
 
5
+ // REVISIT: Replace by compiler-provided check
6
+ const RESERVED = cds.compile.to.sql.sqlite
7
+ ? cds.compile.to.sql.sqlite.keywords
8
+ : [ 'ORDER', 'GROUP', 'LIMIT' ]
9
+
3
10
  module.exports = {
4
11
  meta: {
5
12
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
6
13
  docs: {
7
14
  description: 'Avoid using reserved SQL keywords.',
8
- recommended: true
15
+ recommended: true,
16
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-db-keywords',
17
+ },
18
+ messages: {
19
+ reservedKeyword: `'{{name}}' is a reserved keyword in SQLite`,
9
20
  },
10
21
  type: 'problem',
11
22
  model: 'inferred'
@@ -29,7 +40,8 @@ module.exports = {
29
40
  if (srv && srv['@cds.external']) return
30
41
  if (d.kind === 'entity' && d['@cds.persistence.skip'] === true) return
31
42
  context.report({
32
- message: `'${d.name}' is a reserved keyword in SQLite`,
43
+ messageId: 'reservedKeyword',
44
+ data: { name: d.name },
33
45
  node: context.getNode(d),
34
46
  file: d.$location.file
35
47
  })
@@ -37,6 +49,3 @@ module.exports = {
37
49
  }
38
50
  }
39
51
  }
40
-
41
- // REVISIT: Replace by compiler-provided check
42
- const RESERVED = cds.compile.to.sql.sqlite ? cds.compile.to.sql.sqlite.keywords : ['ORDER', 'GROUP', 'LIMIT']
@@ -1,9 +1,15 @@
1
+ 'use strict'
2
+
1
3
  module.exports = {
2
4
  meta: {
3
5
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
4
6
  docs: {
5
7
  description: 'Names must not start with $ to avoid possible shadowing of reserved variables.',
6
- recommended: true
8
+ recommended: true,
9
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-dollar-prefixed-names',
10
+ },
11
+ messages: {
12
+ dollarPrefix: `'{{name}}' is prefixed with a dollar sign ($)`,
7
13
  },
8
14
  type: 'problem'
9
15
  },
@@ -15,7 +21,8 @@ module.exports = {
15
21
  if (srv && srv['@cds.external']) return
16
22
  if (d.name.startsWith('$')) {
17
23
  context.report({
18
- message: `'${d.name}' is prefixed with a dollar sign ($)`,
24
+ messageId: 'dollarPrefix',
25
+ data: { name: d.name },
19
26
  node: context.getNode(d)
20
27
  })
21
28
  }
@@ -0,0 +1,181 @@
1
+ 'use strict'
2
+
3
+ // Check that Java keywords are not used as identifiers unless they have
4
+ // a Java-specific annotation that renames/ignores them. This avoids issues
5
+ // later on in code-generation of CAP Java classes.
6
+ // Test Java code via godbolt.org: https://godbolt.org/z/1c5s49qjo
7
+
8
+ const { splitDefName } = require('../utils/rules')
9
+
10
+ // There is also `@cds.java.this.name`, which is not relevant for this check.
11
+ const ANNO_JAVA_NAME = '@cds.java.name'
12
+ const ANNO_JAVA_IGNORE = '@cds.java.ignore'
13
+
14
+ // CSN kinds that are relevant for code generation with possible keyword
15
+ // conflicts. For example, types are not relevant, because they use
16
+ // PascalCase, i.e. it can never be a keyword conflict, since all keywords
17
+ // are lowercase.
18
+ const relevantKinds = [
19
+ 'element',
20
+ 'param',
21
+ 'action',
22
+ 'function',
23
+ ]
24
+
25
+ module.exports = {
26
+ meta: {
27
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
28
+ docs: {
29
+ description: 'Reject reserved Java keywords as CDS identifiers.',
30
+ recommended: true
31
+ },
32
+ type: 'problem',
33
+ model: 'inferred',
34
+ messages: {
35
+ keywordJava: `'{{name}}' is a reserved keyword in Java. Use '@cds.java.name' to override the name for Java code generation.`,
36
+ },
37
+ },
38
+ create (context) {
39
+ const rootPath = context.getRootPath()
40
+ if (!rootPath)
41
+ return
42
+
43
+ return function checkForJavaKeywords(){
44
+ const model = context.getModel()
45
+ if (!model)
46
+ return
47
+ for (const name in model.definitions)
48
+ checkDefinition(model.definitions[name])
49
+ }
50
+
51
+ function checkDefinition(def) {
52
+ checkNameIsNotReserved(def)
53
+ if (def.elements) {
54
+ for (const name in def.elements)
55
+ checkDefinition(def.elements[name])
56
+ }
57
+ if (def.actions) {
58
+ for (const name in def.actions)
59
+ checkDefinition(def.actions[name])
60
+ }
61
+ if (def.kind === 'action' || def.kind === 'function') {
62
+ for (const name in def.params)
63
+ checkDefinition(def.params[name])
64
+ }
65
+ }
66
+
67
+ function checkNameIsNotReserved(artifact) {
68
+ if (!artifact.$location?.file || !relevantKinds.includes(artifact.kind))
69
+ return
70
+ if (artifact[ANNO_JAVA_IGNORE])
71
+ return // ignored; no Java code generated
72
+ if (artifact[ANNO_JAVA_NAME])
73
+ return // explicitly renamed; assume the user uses a valid name
74
+
75
+ const name = artifact.is('element')
76
+ ? artifact.name
77
+ : splitDefName(artifact).name
78
+
79
+ if (isValueReservedJavaKeyword(name)) {
80
+ context.report({
81
+ messageId: 'keywordJava',
82
+ data: { name },
83
+ node: context.getNode(artifact),
84
+ file: artifact.$location.file,
85
+ })
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ // List from https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html
92
+ // Also available at https://github.com/openjdk/jdk/blob/f92c60e1a9968620cbc92b52aa546b57c09da487/src/java.compiler/share/classes/javax/lang/model/SourceVersion.java#L651
93
+ // though that list includes fewer items.
94
+ const JAVA_RESERVED = [
95
+ '_',
96
+ 'abstract',
97
+ 'assert',
98
+ 'boolean',
99
+ 'break',
100
+ 'byte',
101
+ 'case',
102
+ 'catch',
103
+ 'char',
104
+ 'class',
105
+ 'const',
106
+ 'continue',
107
+ 'default',
108
+ 'do',
109
+ 'double',
110
+ 'else',
111
+ 'enum',
112
+ 'extends',
113
+ 'final',
114
+ 'finally',
115
+ 'float',
116
+ 'for',
117
+ 'goto',
118
+ 'if',
119
+ 'implements',
120
+ 'import',
121
+ 'instanceof',
122
+ 'int',
123
+ 'interface',
124
+ 'long',
125
+ 'native',
126
+ 'new',
127
+ 'package',
128
+ 'private',
129
+ 'protected',
130
+ 'public',
131
+ 'return',
132
+ 'short',
133
+ 'static',
134
+ 'strictfp',
135
+ 'super',
136
+ 'switch',
137
+ 'synchronized',
138
+ 'this',
139
+ 'throw',
140
+ 'throws',
141
+ 'transient',
142
+ 'try',
143
+ 'void',
144
+ 'volatile',
145
+ 'while',
146
+ // literals
147
+ 'true',
148
+ 'false',
149
+ 'null',
150
+ ]
151
+
152
+ /**
153
+ * Check if the given value is a reserved keyword.
154
+ *
155
+ * @param {any} name
156
+ * @returns {boolean}
157
+ */
158
+ function isValueReservedJavaKeyword(name) {
159
+ if (!name || typeof name !== 'string')
160
+ return false
161
+ const normalized = identifierForJava(name)
162
+ return JAVA_RESERVED.includes(normalized)
163
+ }
164
+
165
+ /**
166
+ * Returns the check-relevant identifier for Java.
167
+ * CAP Java does not use lowercase for the full identifier, but instead
168
+ * uses lowerCamelCase, i.e. it is enough to change the first character
169
+ * of the identifier.
170
+ *
171
+ * @param {string} name
172
+ * @returns {string}
173
+ */
174
+ function identifierForJava(name) {
175
+ if (!name)
176
+ return name
177
+ const firstChar = name.charAt(0)
178
+ if (firstChar === firstChar.toLowerCase())
179
+ return name
180
+ return `${firstChar.toLowerCase()}${name.slice(1)}`
181
+ }
@@ -1,9 +1,15 @@
1
+ 'use strict'
2
+
1
3
  module.exports = {
2
4
  meta: {
3
5
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
4
6
  docs: {
5
- description: "Draft-enabled entities shall not be used in views that make use of `JOIN`.",
6
- recommended: true
7
+ description: 'Draft-enabled entities shall not be used in views that make use of `JOIN`.',
8
+ recommended: true,
9
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/no-join-on-draft',
10
+ },
11
+ messages: {
12
+ draftJoin: 'Do not use draft-enabled entities in views that make use of `JOIN`.',
7
13
  },
8
14
  type: 'suggestion',
9
15
  model: 'inferred'
@@ -15,7 +21,7 @@ module.exports = {
15
21
  if (e['@odata.draft.enabled']) {
16
22
  if (e?.query?.SELECT?.from?.join) {
17
23
  context.report({
18
- message: 'Do not use draft-enabled entities in views that make use of `JOIN`.',
24
+ messageId: 'draftJoin',
19
25
  node: context.getNode(e),
20
26
  file: e.$location.file
21
27
  })
@@ -1,13 +1,14 @@
1
+ 'use strict'
1
2
 
2
3
  module.exports = {
3
4
  meta: {
4
5
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
5
6
  docs: {
6
7
  description: 'Should make suggestions for possible missing SQL casts.',
7
- recommended: true
8
+ recommended: true,
9
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/sql-cast-suggestion',
8
10
  },
9
11
  type: 'suggestion',
10
- hasSuggestions: true,
11
12
  messages: {
12
13
  missingSQLCast: 'Potential issue - Missing SQL cast for column expression?'
13
14
  }
@@ -16,19 +17,22 @@ module.exports = {
16
17
  return { view: checkSqlCast }
17
18
 
18
19
  function checkSqlCast (v) {
19
- if (v.query && v.query.SET) {
20
- for (const { SELECT } of v.query.SET.args) {
21
- // Only in UNION cases?
22
- for (const each of SELECT.columns || []) {
23
- const { xpr, cast } = each
24
- if (cast && xpr) {
25
- if (xpr[0].xpr && xpr[0].cast) {
26
- continue
27
- } else {
28
- context.report({
29
- messageId: 'missingSQLCast',
30
- node: context.getNode(v)
31
- })
20
+ // TODO: restructure and make more robust (#507)
21
+ if (v?.query?.SET?.args) {
22
+ for (const arg of v.query.SET.args) {
23
+ if (arg?.SELECT) {
24
+ // Only in UNION cases?
25
+ for (const each of arg.SELECT.columns || []) {
26
+ if (each) {
27
+ const { xpr, cast } = each
28
+ if (cast && xpr) {
29
+ if (!(xpr[0]?.xpr && xpr[0]?.cast)) {
30
+ context.report({
31
+ messageId: 'missingSQLCast',
32
+ node: context.getNode(v)
33
+ })
34
+ }
35
+ }
32
36
  }
33
37
  }
34
38
  }
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ const { forEachXprInDefinition } = require('../utils/csnTraversal')
4
+
5
+ const invalidComparisonOperators = [ '=', '!=', '<>' ]
6
+
7
+ module.exports = {
8
+ meta: {
9
+ schema: [{/* to avoid deprecation warning for ESLint 9 */}],
10
+ docs: {
11
+ description: 'Ensure SQL comparisons with \'null\' are valid',
12
+ category: 'Model Validation',
13
+ recommended: false,
14
+ // TODO: Add documentation
15
+ // url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/sql-null-comparison',
16
+ },
17
+ type: 'problem',
18
+ model: 'parsed',
19
+ messages: {
20
+ nullComparison: `Comparisons against 'null' are always null. Did you mean 'is not null'?`,
21
+ }
22
+ },
23
+ create(context) {
24
+ return {
25
+ view(v) {
26
+ forEachXprInDefinition(v, checkExpression)
27
+
28
+ function checkExpression(xpr, ctx) {
29
+ if (!xpr || !Array.isArray(xpr))
30
+ return
31
+
32
+ for (let i = 0; i < xpr.length; i++) {
33
+ if (typeof xpr[i] !== 'object')
34
+ continue // scalar value, etc.
35
+
36
+ if (xpr[i]?.val === null) {
37
+ const prev = i > 0 && typeof xpr[i-1] === 'string' ? xpr[i-1] : null
38
+ if (prev && invalidComparisonOperators.includes(prev)) {
39
+ reportComparison(xpr, ctx)
40
+ continue
41
+ }
42
+ const next = i+1 < xpr.length && typeof xpr[i+1] === 'string' ? xpr[i+1] : null
43
+ if (next && invalidComparisonOperators.includes(next))
44
+ reportComparison(xpr, ctx)
45
+ }
46
+ }
47
+ }
48
+
49
+ function reportComparison(xpr, ctx) {
50
+ context.report({
51
+ messageId: 'nullComparison',
52
+ loc: context.getLocation(null, ctx),
53
+ file: ctx.$location?.file,
54
+ })
55
+ }
56
+
57
+ }
58
+ }
59
+ }
60
+ }
@@ -1,8 +1,11 @@
1
+ 'use strict'
2
+
1
3
  module.exports = {
2
4
  meta: {
3
5
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
4
6
  docs: {
5
- description: 'Regular element names should start with lowercase letters.'
7
+ description: 'Regular element names should start with lowercase letters.',
8
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/start-elements-lowercase',
6
9
  },
7
10
  type: 'suggestion',
8
11
  hasSuggestions: true,
@@ -10,7 +13,8 @@ module.exports = {
10
13
  startLowercase: "Element name '{{entityName}}.{{elementName}}' should start with a lowercase letter.",
11
14
  fixLowercase: 'Start element name with a lowercase letter.'
12
15
  },
13
- fixable: 'code'
16
+ fixable: 'code',
17
+ model: 'parsed',
14
18
  },
15
19
  create: function (context) {
16
20
  const sourcecode = context.getSourceCode()
@@ -1,10 +1,13 @@
1
+ 'use strict'
2
+
1
3
  const { splitDefName } = require('../utils/rules')
2
4
 
3
5
  module.exports = {
4
6
  meta: {
5
7
  schema: [{/* to avoid deprecation warning for ESLint 9 */}],
6
8
  docs: {
7
- description: 'Regular entity names should start with uppercase letters.'
9
+ description: 'Regular entity names should start with uppercase letters.',
10
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/start-entities-uppercase',
8
11
  },
9
12
  type: 'suggestion',
10
13
  hasSuggestions: true,
@@ -12,20 +15,24 @@ module.exports = {
12
15
  startUppercase: "Entity name '{{entityName}}' should start with an uppercase letter.",
13
16
  fixUppercase: 'Start entity name with an uppercase letter.'
14
17
  },
15
- fixable: 'code'
18
+ fixable: 'code',
19
+ model: 'parsed',
16
20
  },
17
- create: function (context) {
21
+ create(context) {
18
22
  const sourcecode = context.getSourceCode()
19
23
 
20
24
  return { entity: checkStartsUppercase }
21
25
 
22
26
  function checkStartsUppercase (e) {
27
+ if (e.kind !== 'entity')
28
+ return // workaround for #424
29
+
23
30
  const entityName = splitDefName(e).name
24
31
  if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
25
- if (e.$location && e.$location.file) {
32
+ if (e.$location?.file) {
26
33
  const file = e.$location.file
27
34
  const loc = context.getLocation(entityName, e)
28
- const fix = (fixer) => {
35
+ const fix = fixer => {
29
36
  const entityNameSanitized = entityName.charAt(0).toUpperCase() + entityName.slice(1)
30
37
  const rangeEnd = sourcecode.getIndexFromLoc({
31
38
  line: loc.end.line,
@@ -1,5 +1,7 @@
1
+ 'use strict'
2
+
1
3
  const cds = require('@sap/cds')
2
- const { basename, extname } = require('path')
4
+ const { basename, extname } = require('node:path')
3
5
  const findFuzzy = require('../utils/findFuzzy')
4
6
  const SEP = '[,;\t]'
5
7
  const EOL = '\\r?\\n'
@@ -10,7 +12,8 @@ module.exports = {
10
12
  docs: {
11
13
  description: 'CSV files for entities must refer to valid element names.',
12
14
  category: 'Model Validation',
13
- recommended: true
15
+ recommended: true,
16
+ url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/valid-csv-header',
14
17
  },
15
18
  severity: 'warn',
16
19
  type: 'problem',
@@ -33,7 +36,15 @@ module.exports = {
33
36
  if (!filePath.endsWith('.csv')) return
34
37
  if (!model) return
35
38
 
36
- model = cds.compile.for.sql(model, { names: cds.env.sql.names, messages: [] })
39
+ try {
40
+ model = cds.compile.for.sql(model, { names: cds.env.sql.names, messages: [] })
41
+ } catch(e) {
42
+ // ignore invalid models; the compiler emits errors already
43
+ if (e.code !== 'ERR_CDS_COMPILATION_FAILURE')
44
+ throw e
45
+ }
46
+
47
+ if (!model) return
37
48
 
38
49
  const filename = basename(filePath)
39
50
  const entityName = filename.replace(/-/g, '.').slice(0, -extname(filename).length)
@@ -41,20 +52,20 @@ module.exports = {
41
52
  if (!entity) return
42
53
 
43
54
  const elements = Object.values(entity.elements)
44
- .filter((e) => !!e['@cds.persistence.name'])
45
- .map((e) => e['@cds.persistence.name'].toUpperCase())
55
+ .filter(e => !!e['@cds.persistence.name'])
56
+ .map(e => e['@cds.persistence.name'].toUpperCase())
46
57
 
47
58
  const [cols] = cds.parse.csv(code)
48
- const missing = cols.filter((col) => !elements.includes(col.toUpperCase()))
59
+ const missing = cols.filter(col => !elements.includes(col.toUpperCase()))
49
60
  for (const miss of missing) {
50
61
  const index = _findInCode(miss, code)
51
62
  const loc = sourcecode.getLocFromIndex(index)
52
63
  const candidates = findFuzzy(miss, Object.keys(entity.elements).sort())
53
- const suggest = candidates.map((cand) => {
64
+ const suggest = candidates.map(cand => {
54
65
  return {
55
66
  messageId: 'ReplaceColumnWith',
56
67
  data: { column: miss, candidates: cand },
57
- fix: (fixer) => fixer.replaceTextRange([index, index + miss.length], cand)
68
+ fix: fixer => fixer.replaceTextRange([index, index + miss.length], cand)
58
69
  }
59
70
  })
60
71
  context.report({
@@ -69,20 +80,29 @@ module.exports = {
69
80
  }
70
81
  }
71
82
 
72
- function _findInCode (miss, code) {
83
+ /**
84
+ * @param {string} needle
85
+ * @param {string} code
86
+ * @returns {number} -1 if not found
87
+ */
88
+ function _findInCode(needle, code) {
73
89
  // middle
74
- let match = new RegExp(SEP + miss + SEP).exec(code)
90
+ let match = new RegExp(SEP + needle + SEP).exec(code)
75
91
  if (match) return match.index + 1
76
92
  // end of line
77
- match = new RegExp(SEP + miss + EOL).exec(code)
93
+ match = new RegExp(SEP + needle + EOL).exec(code)
78
94
  if (match) return match.index + 1
79
95
  // start of doc
80
- match = new RegExp('^' + miss + SEP).exec(code)
96
+ match = new RegExp('^' + needle + SEP).exec(code)
81
97
  if (match) return match.index
82
98
  // somewhere (fallback)
83
- return code.indexOf(miss)
99
+ return code.indexOf(needle)
84
100
  }
85
101
 
102
+ /**
103
+ * @param {string} name
104
+ * @param {object} csn
105
+ */
86
106
  function _entity4 (name, csn) {
87
107
  const entity = csn.definitions[name]
88
108
  if (!entity) {
package/lib/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { Linter, Rule, RuleTester, SourceCode } from "eslint";
2
+ import { Rule, RuleTester, SourceCode } from "eslint";
3
3
 
4
4
  export interface CDSRuleContext extends Rule.RuleContext {
5
5
  cds: any;
@@ -9,13 +9,13 @@ export interface CDSRuleContext extends Rule.RuleContext {
9
9
  options: [];
10
10
  id: string;
11
11
  sourcecode: SourceCode;
12
- getModel: function;
13
- report: (CDSRuleReport) => void;
12
+ getModel: Function;
13
+ report: (descriptor: CDSRuleReport) => void;
14
14
  err: Error;
15
15
  }
16
16
 
17
17
  export interface Rule {
18
- meta: RuleMetaData,
18
+ meta: Rule.RuleMetaData,
19
19
  create: (context: CDSRuleContext) => void;
20
20
  }
21
21