Skip to content

Commit ae8eb5b

Browse files
chgeodaogradygithub-actions[bot]ecklierenejeglinsky
authored
Details page for JS rules (#2052)
First attempt for a rules details page Missing: details on when the rule does not work, like code structures it does not detect. --------- Co-authored-by: Daniel O'Grady <daniel.o-grady@sap.com> Co-authored-by: Daniel O'Grady <103028279+daogrady@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ecklie <52252271+ecklie@users.noreply.github.com> Co-authored-by: Rene Jeglinsky <rene.jeglinsky@sap.com>
1 parent 17b9088 commit ae8eb5b

File tree

19 files changed

+497
-2
lines changed

19 files changed

+497
-2
lines changed

.github/eslint-plugin/index.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env node
2+
3+
// eslint plugin rule utility
4+
// ============================
5+
// node index.js generate-menu
6+
// generates the _menu.md file to make sure all rules are included.
7+
// node index.js generate-js-stub <rule-name>
8+
// generates a stub markdown file for a new JS rule with name <rule-name>.
9+
10+
import * as fs from 'node:fs'
11+
import * as path from 'node:path'
12+
13+
const RULES_BASE_PATH = path.join('tools', 'cds-lint', 'rules');
14+
const EXAMPLES_BASE_PATH = path.join('tools', 'cds-lint', 'examples');
15+
const MENU_FILE_NAME = '_menu.md';
16+
17+
/**
18+
* Get a list of all rule description files.
19+
* @returns {string[]} An array of rule description file names.
20+
*/
21+
const getRuleDescriptionFiles = () =>
22+
fs.readdirSync(RULES_BASE_PATH)
23+
.filter(file => file.endsWith('.md'))
24+
.filter(file => !['index.md', MENU_FILE_NAME].includes(file))
25+
.sort()
26+
27+
/**
28+
* Generates the menu markdown file
29+
* by completely overriding its current contents.
30+
* The menu contains links to all rule description files
31+
* in alphabetical order.
32+
*/
33+
function generateMenuMarkdown () {
34+
const rules = getRuleDescriptionFiles();
35+
const menu = rules.map(rule => {
36+
const clean = rule.replace('.md', '');
37+
return `# [${clean}](${clean})`
38+
}).join('\n');
39+
const menuFilePath = path.join(RULES_BASE_PATH, '_menu.md')
40+
fs.writeFileSync(menuFilePath, menu);
41+
console.info(`generated menu to ${menuFilePath}`)
42+
}
43+
44+
/**
45+
* Generates a stub markdown file for a new JS rule.
46+
* The passed ruleName will be placed in the stub template
47+
* where $RULE_NAME is defined.
48+
* @param {string} ruleName - The name of the rule.
49+
*/
50+
function generateJsRuleStub (ruleName) {
51+
if (!ruleName) {
52+
console.error('Please provide a rule name, e.g. "no-shared-handler-variables" as second argument');
53+
process.exit(1);
54+
}
55+
const stubFilePath = path.join(RULES_BASE_PATH, ruleName + '.md');
56+
if (fs.existsSync(stubFilePath)) {
57+
console.error(`file ${stubFilePath} already exists, will not overwrite`);
58+
process.exit(2);
59+
}
60+
const stub = fs.readFileSync(path.join(import.meta.dirname, 'js-rule-stub.md'), 'utf-8').replaceAll('$RULE_NAME', ruleName);
61+
fs.writeFileSync(stubFilePath, stub);
62+
console.info(`generated stub to ${stubFilePath}`);
63+
const correctPath = path.join(EXAMPLES_BASE_PATH, ruleName, 'correct', 'srv');
64+
fs.mkdirSync(correctPath, { recursive: true });
65+
const incorrectPath = path.join(EXAMPLES_BASE_PATH, ruleName, 'incorrect', 'srv');
66+
fs.mkdirSync(incorrectPath, { recursive: true });
67+
console.info(`generated example directories in ${path.join(EXAMPLES_BASE_PATH, ruleName)}`);
68+
fs.writeFileSync(path.join(correctPath, 'admin-service.js'), '// correct example\n');
69+
fs.writeFileSync(path.join(incorrectPath, 'admin-service.js'), '// incorrect example\n');
70+
}
71+
72+
function main (argv) {
73+
switch (argv[0]) {
74+
case 'generate-menu':
75+
generateMenuMarkdown();
76+
break;
77+
case 'generate-js-stub':
78+
generateJsRuleStub(argv[1]);
79+
generateMenuMarkdown();
80+
break;
81+
default:
82+
console.log(`Unknown command: ${argv[0]}. Use one of: generate-menu, generate-stub`);
83+
}
84+
}
85+
86+
main(process.argv.slice(2));
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
status: released
3+
---
4+
5+
<script setup>
6+
import PlaygroundBadge from '../components/PlaygroundBadge.vue'
7+
</script>
8+
9+
# $RULE_NAME
10+
11+
## Rule Details
12+
13+
DETAILS
14+
15+
#### Version
16+
This rule was introduced in `@sap/eslint-plugin-cds x.y.z`.
17+
18+
## Examples
19+
20+
### &nbsp; Correct example
21+
22+
DESCRIPTION OF CORRECT EXAMPLE
23+
24+
::: code-group
25+
<<< ../examples/$RULE_NAME/correct/srv/admin-service.js#snippet{js:line-numbers} [srv/admin-service.js]
26+
:::
27+
<PlaygroundBadge
28+
name="$RULE_NAME"
29+
kind="correct"
30+
:files="['srv/admin-service.js']"
31+
/>
32+
33+
### &nbsp; Incorrect example
34+
35+
DESCRIPTION OF INCORRECT EXAMPLE
36+
37+
::: code-group
38+
<<< ../examples/$RULE_NAME/incorrect/srv/admin-service.js#snippet{js:line-numbers} [srv/admin-service.js]
39+
:::
40+
<PlaygroundBadge
41+
name="$RULE_NAME"
42+
kind="incorrect"
43+
:files="['srv/admin-service.js']"
44+
/>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const cds = require('@sap/cds')
2+
3+
module.exports = class AdminService extends cds.ApplicationService { async init() {
4+
this.on('READ', 'Books', () => {}) // [!code highlight]
5+
}}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const cds = require('@sap/cds')
2+
3+
module.exports = class AdminService extends cds.ApplicationService { async init() {
4+
this.on('Read', 'Books', () => {}) // [!code error]
5+
}}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const cds = require('@sap/cds')
2+
const { Books } = require('#cds-models/sap/capire/bookshop/AdminService') // [!code highlight]
3+
4+
module.exports = class AdminService extends cds.ApplicationService {
5+
// …
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const cds = require('@sap/cds')
2+
const { Books } = require('#cds-models/sap/capire/bookshop/CatalogService') // [!code error]
3+
4+
module.exports = class AdminService extends cds.ApplicationService {
5+
// …
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const cds = require('@sap/cds') // [!code highlight]
2+
3+
module.exports = class AdminService extends cds.ApplicationService {
4+
// …
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const cdsService = require('@sap/cds/service') // [!code error]
2+
3+
module.exports = class AdminService extends cdsService.ApplicationService {
4+
// …
5+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const cds = require('@sap/cds')
2+
3+
module.exports = class AdminService extends cds.ApplicationService { async init() {
4+
this.after('READ', 'Books', async () => {
5+
// local variable only, no state shared between handlers
6+
const books = await cds.run(SELECT.from('Books')) // [!code highlight]
7+
return books
8+
})
9+
10+
this.on('CREATE', 'Books', newBookHandler)
11+
await super.init()
12+
}
13+
}
14+
15+
/** @type {import('@sap/cds').CRUDEventHandler.On} */
16+
async function newBookHandler (req) {
17+
const { name } = req.data
18+
// local variable only, no state shared between handlers
19+
const newBook = await cds.run(INSERT.into('Books').entries({ name })) // [!code highlight]
20+
return newBook
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const cds = require('@sap/cds')
2+
3+
let lastCreatedBook
4+
let lastReadBooks
5+
6+
module.exports = class AdminService extends cds.ApplicationService { async init() {
7+
this.after('READ', 'Books', async () => {
8+
// variable from surrounding scope, state is shared between handler calls
9+
lastReadBooks = await cds.run(SELECT.from('Books')) // [!code error]
10+
return lastReadBooks
11+
})
12+
13+
this.on('CREATE', 'Books', newBookHandler)
14+
await super.init()
15+
}
16+
}
17+
18+
/** @type {import('@sap/cds').CRUDEventHandler.On} */
19+
async function newBookHandler (req) {
20+
const { name } = req.data
21+
// variable from surrounding scope, state is shared between handler calls
22+
lastCreatedBook = await cds.run(INSERT.into('Books').entries({ name })) // [!code error]
23+
return lastCreatedBook
24+
}

0 commit comments

Comments
 (0)