@sap/eslint-plugin-cds 3.0.5 → 3.1.0

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 +32 -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 +14 -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 +36 -10
  13. package/lib/rules/auth-restrict-grant-service.js +19 -20
  14. package/lib/rules/auth-use-requires.js +25 -15
  15. package/lib/rules/auth-valid-restrict-grant.js +137 -81
  16. package/lib/rules/auth-valid-restrict-keys.js +34 -18
  17. package/lib/rules/auth-valid-restrict-to.js +67 -60
  18. package/lib/rules/auth-valid-restrict-where.js +31 -44
  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 +15 -7
  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 +13 -7
  44. package/lib/utils/runRuleTester.js +69 -36
  45. package/package.json +1 -1
  46. package/lib/utils/genDocs.js +0 -346
@@ -1,25 +1,63 @@
1
+ 'use strict'
2
+
3
+ const path = require('node:path')
4
+ const cds = require('@sap/cds')
5
+ const Cache = require('./Cache')
6
+ const fs = require('node:fs')
7
+
8
+ const commonCapProjectFiles = ['build.gradle', '.git', 'srv', 'db', 'app']
9
+
1
10
  /**
2
11
  * Searches for directory containing cds roots
12
+ *
13
+ * As of today, there is no unified way to find the root directory for a CDS project.
14
+ * ("The root is wherever the user typed `cds init`")
15
+ * We are therefore trying to resolve that path heuristically by ascending through the
16
+ * directory structure, looking for certain files.
17
+ *
18
+ * FIXME: Revisit and use a unified way once available, i.e. from @sap/cds
19
+ *
3
20
  * @param {string} currentDir start here and search until root dir
4
21
  * @returns {string} dir containing cds roots (empty if not exists)
5
22
  */
6
-
7
- const path = require('path')
8
- const cds = require('@sap/cds')
9
- const Cache = require('./Cache')
10
-
11
- module.exports = (currentDir = '.') => {
23
+ module.exports = function getProjectRootPath(currentDir = '.') {
12
24
  let dir = path.resolve(currentDir)
13
- while (dir !== path.resolve(dir, '..')) {
14
- // @ts-ignore
15
- cds.resolve.cache = {}
16
- // @ts-ignore
17
- const roots = cds.resolve('*', { root: dir })
18
- if (roots && roots.length > 0) {
19
- Cache.set(`roots:${dir}`, roots)
20
- return dir
21
- }
25
+
26
+ while (!couldBeProjectRoot(dir)) {
27
+ if (dir === path.resolve(dir, '..'))
28
+ return '' // we reached the file system root -> abort
22
29
  dir = path.join(dir, '..')
23
30
  }
31
+
32
+ cds.resolve.cache = {}
33
+ const roots = cds.resolve('*', { root: dir })
34
+ if (roots?.length > 0) {
35
+ Cache.set(`roots:${dir}`, roots)
36
+ return dir
37
+ }
24
38
  return ''
25
39
  }
40
+
41
+ /**
42
+ * Checks whether the given directory could be a CDS project root.
43
+ *
44
+ * @param {string} dir
45
+ * @returns {boolean}
46
+ */
47
+ function couldBeProjectRoot(dir) {
48
+ return isRootPackageJson(path.join(dir, 'package.json')) ||
49
+ commonCapProjectFiles.some(entry => fs.existsSync(path.join(dir, entry)))
50
+ }
51
+
52
+ function isRootPackageJson(filepath) {
53
+ const filename = path.basename(filepath)
54
+ if (filename !== 'package.json')
55
+ return false
56
+
57
+ try {
58
+ const config = JSON.parse(fs.readFileSync(filepath, 'utf8'))
59
+ return Object.keys(config?.dependencies ?? {}).some(dep => dep === '@sap/cds')
60
+ } catch {
61
+ return false
62
+ }
63
+ }
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { FILES, MODEL_FILES } = require('../constants')
2
4
 
3
5
  const regexByFileType = {
@@ -8,13 +10,16 @@ const regexByFileType = {
8
10
  /**
9
11
  * Checks whether the given filePath matches a regex `files`
10
12
  * @param {string} filePath
13
+ * @param {string} fileType
11
14
  * @returns boolean
12
15
  */
13
16
  module.exports = (filePath, fileType) => {
14
- const isValid = regexByFileType[fileType].test(filePath)
15
- return isValid
17
+ return regexByFileType[fileType].test(filePath)
16
18
  }
17
19
 
20
+ /**
21
+ * @param arr
22
+ */
18
23
  function globArrayToRegex (arr) {
19
- return new RegExp(`${arr.map((e) => e.replace('*', '')).join('$|')}$`)
24
+ return new RegExp(`${arr.map(e => e.replace('*', '')).join('$|')}$`)
20
25
  }
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const SEP = '[,;\t]'
2
4
  const EOL = '\\r?\\n'
3
5
 
@@ -9,7 +11,7 @@ module.exports = {
9
11
  *
10
12
  * @param {*} e
11
13
  */
12
- splitDefName: function (e) {
14
+ splitDefName(e) {
13
15
  // Entity names from CSN are of the form:
14
16
  // <namespace>.<service>.<def>.<'texts'|'localized'>|<composition value>
15
17
  let prefix = ''
@@ -22,7 +24,7 @@ module.exports = {
22
24
  // Managed composition get compiler tag `_up`
23
25
  let isManagedComposition = false
24
26
  if (e.elements) {
25
- isManagedComposition = Object.keys(e.elements).some((k) => k === 'up_')
27
+ isManagedComposition = Object.keys(e.elements).some(k => k === 'up_')
26
28
  }
27
29
  // Check for compiler tags
28
30
  const compilerTagsToExclude = ['texts', 'localized']
@@ -51,11 +53,15 @@ module.exports = {
51
53
  return code.indexOf(miss)
52
54
  },
53
55
 
56
+ /**
57
+ * Returns true if the given string is non-empty. The string is trimmed, i.e. leading/trailing
58
+ * whitespace is removed prior to checking.
59
+ *
60
+ * @param {string} value
61
+ * @returns {boolean}
62
+ */
54
63
  isEmptyString: function (value) {
55
- if (typeof value !== 'string' || (typeof value === 'string' && value && value.length > 0)) {
56
- return false
57
- }
58
- return true
64
+ return value?.trim() === ''
59
65
  },
60
66
 
61
67
  isEmptyObject: function (value) {
@@ -96,7 +102,7 @@ module.exports = {
96
102
  const suggest = {
97
103
  messageId: 'ReplaceItemWith',
98
104
  data: { invalid, candidates },
99
- fix: (fixer) => fixer.replaceTextRange([startIndex, startIndex + invalid.length] + 1, candidates)
105
+ fix: fixer => fixer.replaceTextRange([startIndex, startIndex + invalid.length] + 1, candidates)
100
106
  }
101
107
  return ({
102
108
  messageId: 'InvalidItem',
@@ -1,11 +1,40 @@
1
- const fs = require('fs')
2
- const path = require('path')
1
+ 'use strict'
2
+
3
+ /** @typedef {import('eslint').Rule.RuleModule} RuleModule */
4
+
5
+ const fs = require('node:fs')
6
+ const path = require('node:path')
3
7
 
4
8
  const { Linter, RuleTester } = require('eslint')
5
9
  const Cache = require('./Cache')
6
- const createRule = require('./createRule')
7
10
  const isConfiguredFileType = require('./isConfiguredFileType')
8
11
  const { compileModelFromDict } = require('../parser')
12
+ const rules = require('../rules')
13
+
14
+ /**
15
+ * A wrapper around the return value of `createRule()` that initializes the global
16
+ * cache only when the rule is actually executed. This allows tests to be run
17
+ * with test runners that don't set up a new environment for each test, such as
18
+ * mocha or the Node test runner.
19
+ *
20
+ * @param {RuleModule} rule
21
+ * @returns {RuleModule}
22
+ */
23
+ function testRuleWrapper(rule) {
24
+ return { ...rule, create: prepareAndRunRule }
25
+ function prepareAndRunRule(context) {
26
+ return {
27
+ Program: node => {
28
+ const filePath = context.getFilename()
29
+ _initModelRuleTester(filePath, rule.meta.model)
30
+ const createValue = rule.create(context)
31
+ const result = createValue.Program(node)
32
+ Cache.clear()
33
+ return result
34
+ }
35
+ }
36
+ }
37
+ }
9
38
 
10
39
  /**
11
40
  * ESLint RuleTester (used by custom rule creator api)
@@ -13,51 +42,50 @@ const { compileModelFromDict } = require('../parser')
13
42
  * valid/invalid checks:
14
43
  * Model checks require input 'code' entries
15
44
  * Env checks require input 'options' with selected parameters
45
+ *
16
46
  * @param { CDSRuleTestOpts } options RuleTester input options
17
- * @returns RuleTester results
18
47
  */
19
- module.exports = (options) => {
48
+ module.exports = function runRuleTester(options) {
49
+ const pluginRootPath = path.resolve(__dirname, '../..')
20
50
  let parserPath
21
51
  let rule = {}
22
- Cache.set('rules', require(path.join(__dirname, '../rules')))
23
52
  const rulename = path.basename(options.root)
24
- if (options.root.startsWith(path.resolve(__dirname, '../..'))) {
53
+ if (options.root.startsWith(pluginRootPath)) {
25
54
  // For plugin's internal tests, resolve parser from here
26
55
  parserPath = require.resolve('../parser')
27
- const pluginPath = path.join(path.dirname(options.root), '../..')
28
- rule = createRule(require(`../rules/${path.basename(options.root)}`))
29
- Cache.set('pluginpath', pluginPath)
56
+ rule = testRuleWrapper(rules[path.basename(options.root)]())
30
57
  } else {
31
58
  // Otherwise from project root
59
+ // eslint-disable-next-line
32
60
  const resolvedPlugin = require.resolve('@sap/eslint-plugin-cds', {
33
61
  paths: [options.root]
34
62
  })
35
63
  parserPath = path.join(path.dirname(resolvedPlugin), 'parser')
36
- rule = require(path.join(options.root, `../../rules/${path.basename(options.root)}`))
37
- const pluginPath = path.join(path.dirname(options.root), '../../..')
38
- Cache.set('pluginpath', pluginPath)
64
+ rule = testRuleWrapper(require(path.join(options.root, `../../rules/${path.basename(options.root)}`)))
39
65
  }
40
- let tester = new RuleTester({})
66
+
67
+ let tester
41
68
  if (parserPath) {
42
- let options
43
- if (Number(Linter.version.split('.')[0]) >= 9) {
44
- options = { languageOptions: { parser: require(parserPath) } }
45
- } else {
46
- options = { parser: parserPath }
47
- }
69
+ const options = _isEslint9OrLater()
70
+ ? { languageOptions: { parser: require(parserPath) } }
71
+ : { parser: parserPath }
48
72
  tester = new RuleTester(options)
73
+ } else {
74
+ tester = new RuleTester()
49
75
  }
50
76
 
51
77
  const testerCases = {};
52
- ['valid', 'invalid'].forEach((type) => {
78
+ ['valid', 'invalid'].forEach(type => {
53
79
  const filePath = path.join(options.root, `${type}/${options.filename}`)
54
- Cache.set('rootpath', path.dirname(filePath))
55
- _initModelRuleTester(filePath, rule.meta?.model)
56
80
  testerCases[type] = [
57
81
  {
58
- filename: filePath
82
+ filename: filePath,
59
83
  }
60
84
  ]
85
+ if (_isEslint9OrLater()) {
86
+ // property not supported for ESLint 8
87
+ testerCases[type][0].name = `${path.basename(options.root)}/${type}/${options.filename}`
88
+ }
61
89
  if (!isConfiguredFileType(options.filename, 'FILES')) {
62
90
  const fileContents = JSON.parse(fs.readFileSync(filePath, 'utf8'))
63
91
  testerCases[type][0].code = ''
@@ -77,22 +105,22 @@ module.exports = (options) => {
77
105
  }
78
106
  }
79
107
  })
80
- if (Cache.get('testerCases')) {
81
- Cache.set(`testerCases:${rulename}`, testerCases)
82
- }
83
108
  return tester.run(rulename, rule, testerCases)
84
109
  }
85
110
 
86
111
  /**
87
112
  * Creates a model for ESLint unit tests
113
+ * @param {string} filePath
114
+ * @param {string} flavor
88
115
  */
89
116
  function _initModelRuleTester(filePath, flavor) {
117
+ Cache.set('rules', rules)
90
118
  Cache.set('test', true)
91
119
  const rootPath = path.dirname(filePath)
92
120
  Cache.set('rootpath', rootPath)
93
121
  if (flavor !== 'none') { // not for env rules
94
122
  const files = fs.readdirSync(rootPath)
95
- const modelfiles = files.map((f) => path.join(rootPath, f)).filter((fp) => isConfiguredFileType(fp, 'MODEL_FILES'))
123
+ const modelfiles = files.map(f => path.join(rootPath, f)).filter(fp => isConfiguredFileType(fp, 'MODEL_FILES'))
96
124
  Cache.set(`modelfiles:${rootPath}`, modelfiles)
97
125
  const dictFiles = _getDictFiles(rootPath, modelfiles)
98
126
  Cache.set(`dictfiles:${rootPath}`, dictFiles)
@@ -102,18 +130,19 @@ function _initModelRuleTester(filePath, flavor) {
102
130
  }
103
131
 
104
132
  /**
105
- * Creates or updates a dictionary of files/file contents for a given
106
- * project path.
107
- * @param input
108
- * @param files
109
- * @returns dictFiles
110
- */
111
- function _getDictFiles(input, files) {
133
+ * Creates or updates a dictionary of files/file contents for a given
134
+ * project path.
135
+ *
136
+ * @param {string} input
137
+ * @param {string[]} filenames
138
+ * @returns {Record<string, string>} dictFiles
139
+ */
140
+ function _getDictFiles(input, filenames) {
112
141
  let dictFiles = {}
113
142
  if (Cache.has(`dictfiles:${input}`)) {
114
143
  dictFiles = Cache.get(`dictfiles:${input}`)
115
144
  } else {
116
- files.forEach((file) => {
145
+ filenames.forEach(file => {
117
146
  dictFiles[file] = Cache.has(`file:${file}`)
118
147
  ? Cache.get(`file:${file}`)
119
148
  : fs.readFileSync(file, 'utf8')
@@ -121,3 +150,7 @@ function _getDictFiles(input, files) {
121
150
  }
122
151
  return dictFiles
123
152
  }
153
+
154
+ function _isEslint9OrLater() {
155
+ return Number(Linter.version.split('.')[0]) >= 9
156
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/eslint-plugin-cds",
3
- "version": "3.0.5",
3
+ "version": "3.1.0",
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": [
@@ -1,346 +0,0 @@
1
- const fs = require('fs')
2
- const os = require('os')
3
- const path = require('path')
4
- const semver = require('semver')
5
- const cp = require('child_process')
6
-
7
- const cds = require('@sap/cds')
8
- const { mkdirp } = cds.utils
9
-
10
- const Cache = require('./Cache')
11
- const IS_WIN = os.platform() === 'win32'
12
-
13
- const { exit } = require('process')
14
-
15
- const constants = require('../constants')
16
- const LOG = process.env.SILENT ? undefined : constants.log
17
-
18
- /**
19
- * Generates custom rules documentation (markdown files)
20
- * for user according to contents of:
21
- * - Rule files
22
- * - Test files (with valid/invalid/fixed examples)
23
- */
24
- module.exports = async (projectPath, customRulesDir, registry, prepareRelease = false) => {
25
- let docsPath, rulePath, testPath, release
26
-
27
- Cache.set('testerCases', true)
28
-
29
- if (!projectPath) {
30
- docsPath = path.join(__dirname, '../../docs')
31
- rulePath = path.join(__dirname, '../rules')
32
- testPath = path.join(__dirname, '../../tests/lib/rules')
33
- release = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
34
- } else {
35
- docsPath = path.join(projectPath, `${customRulesDir}/docs`)
36
- rulePath = path.join(projectPath, `${customRulesDir}/rules`)
37
- testPath = path.join(projectPath, `${customRulesDir}/tests`)
38
- await Promise.all(
39
- [docsPath, rulePath, testPath].filter((path) => !fs.existsSync(path)).map((path) => mkdirp(path))
40
- )
41
- }
42
-
43
- if (registry) {
44
- // Get rules (internal on artifactory)
45
- const versionInternal = prepareRelease
46
- ? JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
47
- : getPackageVersion(registry)
48
- if (versionInternal) {
49
- LOG?.(`Updating internal rules from v>=${versionInternal}:\n${registry}\n`)
50
- const rulesInternal = getRules(docsPath, rulePath, testPath, versionInternal)
51
- genDocFiles(rulesInternal, docsPath)
52
- }
53
- // Get rules released (external on npm)
54
- const npmRegistry = 'https://registry.npmjs.org'
55
- const versionExternal = prepareRelease
56
- ? JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
57
- : getPackageVersion(npmRegistry)
58
- if (versionExternal) {
59
- LOG?.(`Updating external rules from v>=${versionExternal}:\n${npmRegistry}\n`)
60
- const rulesExternal = getRules(docsPath, rulePath, testPath, versionExternal, release)
61
- genDocFiles(rulesExternal, docsPath, release)
62
- }
63
- } else {
64
- // Get "custom" rules
65
- const rules = getRules(docsPath, rulePath, testPath)
66
- genDocFiles(rules, docsPath)
67
- }
68
- LOG?.('Done!')
69
- }
70
-
71
- /**
72
- * Generates markdown table of all rules with their respective properties based on input rules
73
- * @param rules array of rules with mandatory name, details properties
74
- * @param release
75
- * @returns markdown table of all rules
76
- */
77
- function genMdRules (rules, release = false) {
78
- let mdRulesTable = ''
79
- if (rules.length > 0) {
80
- const emojiRecommended = '✔️'
81
- const emojiFixable = '🔧'
82
- const emojiSuggestions = '💡'
83
- const emojiConstruction = '🚧'
84
- let mdRulesHeader = 'Rules in ESLint are grouped by type to help you understand their purpose. Each rule has emojis denoting:\n\n'
85
- mdRulesHeader += `${emojiRecommended} if the plugin's "recommended" configuration enables the rule\n\n`
86
- mdRulesHeader += `${emojiFixable} if problems reported by the rule are automatically fixable (\`--fix\`)\n\n`
87
- mdRulesHeader += `${emojiSuggestions} if problems reported by the rule are manually fixable (editor)\n\n`
88
- if (!release) {
89
- mdRulesHeader += `${emojiConstruction} if rule exists in plugin (main branch) but is not yet released (artifactory)\n\n`
90
- mdRulesHeader += '| | | | | | | |\n'
91
- mdRulesHeader += '|:-:|:-:|:-:|:-:|-:|:-|:-|\n'
92
- } else {
93
- mdRulesHeader += '| | | | | | | |\n'
94
- mdRulesHeader += '|:-:|:-:|:-:|:-:|-:|:-|:-|\n'
95
- }
96
- let mdRules = ''
97
- rules.forEach((rule) => {
98
- if (rule.name && rule.details) {
99
- const isRecommended = rule.recommended ? emojiRecommended : ''
100
- const isFixable = rule.fixable ? emojiFixable : ''
101
- const hasSuggestions = rule.hasSuggestions ? emojiSuggestions : ''
102
- const underConstruction = rule.construction ? emojiConstruction : ''
103
- mdRules += release
104
- ? `| ${isRecommended} | ${isFixable} | ${hasSuggestions} | | &nbsp; | [${rule.name}](Rules-released.md#${rule.name}) | ${rule.details}|\n`
105
- : `| ${isRecommended} | ${isFixable} | ${hasSuggestions} | ${underConstruction} | &nbsp; | [${rule.name}](Rules.md#${rule.name}) | ${rule.details}|\n`
106
- }
107
- })
108
- mdRulesTable = mdRules ? `${mdRulesHeader}${mdRules}\n` : ''
109
- }
110
- return mdRulesTable
111
- }
112
-
113
- /**
114
- * Generates markdown documentation files for:
115
- * - Overview of all rules in form of markdown table (RuleList)
116
- * - List of all rules details in form of markdown page (Rules)
117
- * If used internally within the @sap/eslint-plugin-cds, this
118
- * also generates 'released' files, which only contain information
119
- * on rules published until the currently released version.
120
- * @param rules
121
- * @param docsPath
122
- * @param release
123
- */
124
- function genDocFiles (rules, docsPath, release = false) {
125
- let suffix = ''
126
- if (release) {
127
- suffix = '-released'
128
- }
129
- const ruleDocsPath = path.join(docsPath, `Rules${suffix}.md`)
130
- const ruleListDocsPath = path.join(docsPath, `RuleList${suffix}.md`)
131
-
132
- if (!fs.existsSync(ruleDocsPath)) {
133
- fs.writeFileSync(ruleDocsPath, '', 'utf8')
134
- }
135
- if (!fs.existsSync(ruleListDocsPath)) {
136
- fs.writeFileSync(ruleListDocsPath, '', 'utf8')
137
- }
138
- const mdRulesCur = fs.readFileSync(ruleDocsPath, 'utf8')
139
- const mdRuleListCur = fs.readFileSync(ruleListDocsPath, 'utf8')
140
-
141
- // Get rules table
142
-
143
- const header = '# @sap/eslint-plugin-cds [latest]\n\n'
144
- const mdRuleList = genMdRules(rules, release)
145
-
146
- // Get rule details
147
- let mdRules = ''
148
-
149
- rules.forEach(rule => {
150
- mdRules += `${rule.contents}\n\n${rule.sources}\n\n---\n\n`
151
- })
152
-
153
- if (mdRuleListCur !== mdRuleList || mdRulesCur !== mdRules) {
154
- fs.writeFileSync(ruleDocsPath, header + mdRules, 'utf8')
155
- fs.writeFileSync(ruleListDocsPath, header + mdRuleList, 'utf8')
156
- }
157
- }
158
-
159
- function getPackageVersion (registry) {
160
- let result
161
- try {
162
- result = cp
163
- .execSync(`npm show @sap/eslint-plugin-cds --@sap:registry=${registry} --json`, {
164
- cwd: process.cwd(),
165
- shell: IS_WIN,
166
- stdio: 'pipe'
167
- })
168
- .toString()
169
- } catch (_err) {
170
- LOG?.(`Failed to connect to ${registry} - check your connection and try again.`)
171
- exit(0)
172
- }
173
- const version = JSON.parse(result).version
174
- if (!version) {
175
- LOG?.(`Failed to get latest plugin version from ${registry} - check your connection and try again.`)
176
- exit(0)
177
- }
178
- return version
179
- }
180
-
181
- function getRules (docsPath, rulePath, testPath, versionRequired = '0.0.0', release = false) {
182
- let mdRule, mdRuleSources, mdRuleContents
183
- const rules = []
184
- const ruleVersionsPath = path.join(docsPath, '_data', 'rule_versions.json')
185
- const ruleVersions = require(ruleVersionsPath)
186
- let fileNumber = 0
187
- fs.readdirSync(rulePath).filter((file) => {
188
- if (path.extname(file).toLowerCase() === '.js' && file !== 'index.js') {
189
- const rule = path.basename(file).replace(path.extname(file), '')
190
- const ruleTestPath = path.join(testPath, rule, 'rule.test.js')
191
- fileNumber++
192
-
193
- // Get rule meta information
194
- const ruleMeta = require(path.join(rulePath, file)).meta
195
- let version = ruleVersions.added[rule]
196
- if (!version) {
197
- version = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
198
- ruleVersions.added[rule] = version
199
- fs.writeFileSync(ruleVersionsPath, JSON.stringify(ruleVersions, null, 4), 'utf8')
200
- }
201
- if ((release && semver.satisfies(version, `<=${versionRequired}`)) || !release) {
202
- LOG?.(`${fileNumber}> preparing docs for ${ruleTestPath}`)
203
-
204
- const details = ruleMeta.docs.description
205
- const flavor = ruleMeta.model ? ruleMeta.model : constants.DEFAULT_RULE_FLAVOR
206
- const category = (flavor === 'none') ? 'Environment' : 'Model Validation'
207
- const fixable = ruleMeta.fixable
208
- const messages = ruleMeta.messages ? ruleMeta.messages : []
209
- const recommended = ruleMeta.docs.recommended
210
- const suggestions = ruleMeta.hasSuggestions
211
-
212
- let underConstruction = ''
213
- if (!release && (version === 'TBD' || semver.satisfies(version, `>${versionRequired}`))) {
214
- underConstruction = '🚧'
215
- LOG?.(` > 🚧 Rule '${rule}' still under construction.\n`)
216
- }
217
-
218
- const isFixable = ['code', 'whitespace'].includes(fixable) ? '🔧' : ''
219
- const isRecommended = recommended === true ? '✔️' : ''
220
- const hasSuggestions = suggestions === true ? '💡' : ''
221
-
222
- const rulesEntry = {
223
- name: rule,
224
- details,
225
- recommended: isRecommended,
226
- fixable: isFixable,
227
- hasSuggestions,
228
- construction: underConstruction,
229
- messages,
230
- version
231
- }
232
- try {
233
- mdRule = getRuleExamples(rule, ruleTestPath, testPath, rulesEntry)
234
- } catch (_err) {
235
- // Just continue
236
- }
237
- mdRuleContents = ''
238
-
239
- mdRuleContents +=
240
- !release && underConstruction
241
- ? `## ${rule}\n<span class='shifted'>${underConstruction}&nbsp;&nbsp;<span class='label'>${category}</span></span>\n\n`
242
- : `## ${rule}\n<span class='shifted label'>${category}</span>\n\n`
243
-
244
- mdRuleContents += `### Rule Details\n${details}\n\n`
245
- if (mdRule) {
246
- mdRuleContents += `### Examples\n${mdRule}\n\n`
247
- }
248
- mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`
249
- mdRuleSources = `### Resources\n[Rule & Documentation source](${path
250
- .relative(docsPath, path.join(rulePath, `${rule}.js`))
251
- .replace(/\\/g, '/')})\n\n`
252
-
253
- rulesEntry.contents = mdRuleContents
254
- rulesEntry.sources = mdRuleSources
255
- rules.push(rulesEntry)
256
- }
257
- }
258
- return undefined
259
- })
260
- return rules
261
- }
262
-
263
- function getRuleExamples (rule, ruleTestPath, testPath, ruleDictEntry) {
264
- // Get rule valid/invalid tests
265
- let mdRule = ''
266
- if (fs.existsSync(ruleTestPath)) {
267
- require(`${ruleTestPath}`)
268
- const testerCases = Cache.get(`testerCases:${rule}`)
269
- const isEnvRule = testerCases.valid[0].filename === '<text>'
270
- const valid = !isEnvRule ? fs.readFileSync(testerCases.valid[0].filename, 'utf8') : JSON.stringify(testerCases.valid[0].options[0].environment, null, 4)
271
- const invalid = !isEnvRule ? fs.readFileSync(testerCases.invalid[0].filename, 'utf8') : JSON.stringify(testerCases.invalid[0].options[0].environment, null, 4)
272
- let validString = ''
273
- let invalidString = ''
274
- if (!isEnvRule) {
275
- const errors = testerCases.invalid[0].errors
276
- let errorsSorted = []
277
- errors.forEach((err) => {
278
- if (errorsSorted.length === 0) {
279
- errorsSorted = [err]
280
- } else {
281
- const errLast = errorsSorted[errorsSorted.length - 1]
282
- if (err.line > errLast.line) {
283
- errorsSorted.push(err)
284
- } else if (err.line < errLast.line) {
285
- errorsSorted.unshift(err)
286
- } else {
287
- if (err.column > errLast.column) {
288
- errorsSorted.push(err)
289
- } else if (err.line < errLast.line) {
290
- errorsSorted.unshift(err)
291
- } else {
292
- if (err.messageId) {
293
- errorsSorted[errorsSorted.length - 1].messageId += '\n' + err.messageId
294
- }
295
- if (err.message) {
296
- errorsSorted[errorsSorted.length - 1].message += '\n' + err.message
297
- }
298
- }
299
- }
300
- }
301
- })
302
- const code = invalid.split('\n')
303
- errorsSorted.forEach((err, i) => {
304
- if (err.messageId && ruleDictEntry.messages) {
305
- let msg = ruleDictEntry.messages[err.messageId]
306
- let data
307
- if (errorsSorted[i].suggestions && errorsSorted[i].suggestions[0]) {
308
- data = errorsSorted[i].suggestions[0].data
309
- }
310
- if (data && msg) {
311
- Object.keys(data).forEach((d) => {
312
- msg = msg.replace(`{{${d}}}`, data[d])
313
- })
314
- }
315
- err.message = msg
316
- }
317
- const msg = err.message && err.message.includes && err.message.includes('"') ? err.message.replace(/"/gm, '`') : err.message
318
- if (err.line) {
319
- const stringStart = errorsSorted[i - 1] ? errorsSorted[i - 1].line : 0
320
- invalidString += code.slice(stringStart, err.line - 1).join('\n')
321
- const errorString = err.line === err.endLine ? code[err.line - 1] : code.slice(err.line - 1, err.endLine - 1).join('\n')
322
- const replacedErrorLine = errorString.substring(0, err.column - 1) +
323
- `<span style="display:inline-block; position:relative; color:red; border-bottom:2pt dotted red" title="${msg}"><b><i>` +
324
- errorString.substring(err.column - 1, err.endColumn - 1) +
325
- '</i></b></span>' + errorString.substring(err.endColumn - 1)
326
- invalidString += '\n' + replacedErrorLine
327
- const stringEnd = errorsSorted[i + 1] ? code.slice(err.line, errorsSorted[i + 1].line - 1).join('\n') : code.slice(err.line).join('\n')
328
- invalidString += errorsSorted[i + 1] ? stringEnd : '\n' + stringEnd
329
- }
330
- })
331
- } else {
332
- invalidString = invalid
333
- }
334
- validString = valid
335
-
336
- mdRule +=
337
- '<span>✔️&nbsp;&nbsp; Example of ' +
338
- '<span style="color:green">correct</span> ' +
339
- `code for this rule:</span>\n\n<pre><code>${validString.trim()}</code></pre>\n\n`
340
- mdRule +=
341
- '<span>❌&nbsp;&nbsp; Example of ' +
342
- '<span style="color:red">incorrect</span> ' +
343
- `code for this rule:</span>\n\n<pre><code>${invalidString.trim()}</code></pre>`
344
- }
345
- return mdRule
346
- }