Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/eslint-plugin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env node

// eslint plugin rule utility
// ============================
// node index.js generate-menu
// generates the _menu.md file to make sure all rules are included.
// node index.js generate-js-stub <rule-name>
// generates a stub markdown file for a new JS rule with name <rule-name>.

import * as fs from 'node:fs'
import * as path from 'node:path'

const RULES_BASE_PATH = path.join('tools', 'cds-lint', 'rules');
const EXAMPLES_BASE_PATH = path.join('tools', 'cds-lint', 'examples');
const MENU_FILE_NAME = '_menu.md';

/**
* Get a list of all rule description files.
* @returns {string[]} An array of rule description file names.
*/
const getRuleDescriptionFiles = () =>
fs.readdirSync(RULES_BASE_PATH)
.filter(file => file.endsWith('.md'))
.filter(file => !['index.md', MENU_FILE_NAME].includes(file))
.sort()

/**
* Generates the menu markdown file
* by completely overriding its current contents.
* The menu contains links to all rule description files
* in alphabetical order.
*/
function generateMenuMarkdown () {
const rules = getRuleDescriptionFiles();
const menu = rules.map(rule => {
const clean = rule.replace('.md', '');
return `# [${clean}](${clean})`
}).join('\n');
const menuFilePath = path.join(RULES_BASE_PATH, '_menu.md')
fs.writeFileSync(menuFilePath, menu);
console.info(`generated menu to ${menuFilePath}`)
}

/**
* Generates a stub markdown file for a new JS rule.
* The passed ruleName will be placed in the stub template
* where $RULE_NAME is defined.
* @param {string} ruleName - The name of the rule.
*/
function generateJsRuleStub (ruleName) {
if (!ruleName) {
console.error('Please provide a rule name, e.g. "no-shared-handler-variables" as second argument');
process.exit(1);
}
const stubFilePath = path.join(RULES_BASE_PATH, ruleName + '.md');
if (fs.existsSync(stubFilePath)) {
console.error(`file ${stubFilePath} already exists, will not overwrite`);
process.exit(2);
}
const stub = fs.readFileSync(path.join(import.meta.dirname, 'js-rule-stub.md'), 'utf-8').replaceAll('$RULE_NAME', ruleName);
fs.writeFileSync(stubFilePath, stub);
console.info(`generated stub to ${stubFilePath}`);
const correctPath = path.join(EXAMPLES_BASE_PATH, ruleName, 'correct', 'srv');
fs.mkdirSync(correctPath, { recursive: true });
const incorrectPath = path.join(EXAMPLES_BASE_PATH, ruleName, 'incorrect', 'srv');
fs.mkdirSync(incorrectPath, { recursive: true });
console.info(`generated example directories in ${path.join(EXAMPLES_BASE_PATH, ruleName)}`);
fs.writeFileSync(path.join(correctPath, 'admin-service.js'), '// correct example\n');
fs.writeFileSync(path.join(incorrectPath, 'admin-service.js'), '// incorrect example\n');
}

function main (argv) {
switch (argv[0]) {
case 'generate-menu':
generateMenuMarkdown();
break;
case 'generate-js-stub':
generateJsRuleStub(argv[1]);
generateMenuMarkdown();
break;
default:
console.log(`Unknown command: ${argv[0]}. Use one of: generate-menu, generate-stub`);
}
}

main(process.argv.slice(2));
44 changes: 44 additions & 0 deletions .github/eslint-plugin/js-rule-stub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
status: released
---

<script setup>
import PlaygroundBadge from '../components/PlaygroundBadge.vue'
</script>

# $RULE_NAME

## Rule Details

DETAILS

#### Version
This rule was introduced in `@sap/eslint-plugin-cds x.y.z`.

## Examples

### ✅ &nbsp; Correct example

DESCRIPTION OF CORRECT EXAMPLE

::: code-group
<<< ../examples/$RULE_NAME/correct/srv/admin-service.js#snippet{js:line-numbers} [srv/admin-service.js]
:::
<PlaygroundBadge
name="$RULE_NAME"
kind="correct"
:files="['srv/admin-service.js']"
/>

### ❌ &nbsp; Incorrect example

DESCRIPTION OF INCORRECT EXAMPLE

::: code-group
<<< ../examples/$RULE_NAME/incorrect/srv/admin-service.js#snippet{js:line-numbers} [srv/admin-service.js]
:::
<PlaygroundBadge
name="$RULE_NAME"
kind="incorrect"
:files="['srv/admin-service.js']"
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const cds = require('@sap/cds')

module.exports = class AdminService extends cds.ApplicationService { async init() {
this.on('READ', 'Books', () => {}) // [!code highlight]
}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const cds = require('@sap/cds')

module.exports = class AdminService extends cds.ApplicationService { async init() {
this.on('Read', 'Books', () => {}) // [!code error]
}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const cds = require('@sap/cds')
const { Books } = require('#cds-models/sap/capire/bookshop/AdminService') // [!code highlight]

Check warning on line 2 in tools/cds-lint/examples/no-cross-service-import/correct/srv/AdminService.js

View workflow job for this annotation

GitHub Actions / build

'Books' is assigned a value but never used

module.exports = class AdminService extends cds.ApplicationService {
// …
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const cds = require('@sap/cds')
const { Books } = require('#cds-models/sap/capire/bookshop/CatalogService') // [!code error]

Check warning on line 2 in tools/cds-lint/examples/no-cross-service-import/incorrect/srv/AdminService.js

View workflow job for this annotation

GitHub Actions / build

'Books' is assigned a value but never used

module.exports = class AdminService extends cds.ApplicationService {
// …
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const cds = require('@sap/cds') // [!code highlight]

module.exports = class AdminService extends cds.ApplicationService {
// …
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const cdsService = require('@sap/cds/service') // [!code error]

module.exports = class AdminService extends cdsService.ApplicationService {
// …
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const cds = require('@sap/cds')

module.exports = class AdminService extends cds.ApplicationService { async init() {
this.after('READ', 'Books', async () => {
// local variable only, no state shared between handlers
const books = await cds.run(SELECT.from('Books')) // [!code highlight]
return books
})

this.on('CREATE', 'Books', newBookHandler)
await super.init()
}
}

/** @type {import('@sap/cds').CRUDEventHandler.On} */
async function newBookHandler (req) {
const { name } = req.data
// local variable only, no state shared between handlers
const newBook = await cds.run(INSERT.into('Books').entries({ name })) // [!code highlight]
return newBook
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const cds = require('@sap/cds')

let lastCreatedBook
let lastReadBooks

module.exports = class AdminService extends cds.ApplicationService { async init() {
this.after('READ', 'Books', async () => {
// variable from surrounding scope, state is shared between handler calls
lastReadBooks = await cds.run(SELECT.from('Books')) // [!code error]
return lastReadBooks
})

this.on('CREATE', 'Books', newBookHandler)
await super.init()
}
}

/** @type {import('@sap/cds').CRUDEventHandler.On} */
async function newBookHandler (req) {
const { name } = req.data
// variable from surrounding scope, state is shared between handler calls
lastCreatedBook = await cds.run(INSERT.into('Books').entries({ name })) // [!code error]
return lastCreatedBook
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const cds = require('@sap/cds')
module.exports = class AdminService extends cds.ApplicationService { init() {
const { Authors } = cds.entities('AdminService')

this.before (['CREATE', 'UPDATE'], Authors, async (req) => {
await SELECT`ID`.from `Authors`.where `name = ${req.data.name}` // [!code highlight]
})

return super.init()
}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const cds = require('@sap/cds')
module.exports = class AdminService extends cds.ApplicationService { init() {
const { Authors } = cds.entities('AdminService')

this.before (['CREATE', 'UPDATE'], Authors, async (req) => {
await SELECT`ID`.from `Authors`.where (`name = ${req.data.name}`) // [!code error]
})

return super.init()
}}
7 changes: 6 additions & 1 deletion tools/cds-lint/rules/_menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@
# [auth-valid-restrict-keys](auth-valid-restrict-keys)
# [auth-valid-restrict-to](auth-valid-restrict-to)
# [auth-valid-restrict-where](auth-valid-restrict-where)
# [case-sensitive-well-known-events](case-sensitive-well-known-events)
# [extension-restrictions](extension-restrictions)
# [latest-cds-version](latest-cds-version)
# [no-cross-service-import](no-cross-service-import)
# [no-db-keywords](no-db-keywords)
# [no-deep-sap-cds-import](no-deep-sap-cds-import)
# [no-dollar-prefixed-names](no-dollar-prefixed-names)
# [no-java-keywords](no-java-keywords)
# [no-join-on-draft](no-join-on-draft)
# [no-shared-handler-variable](no-shared-handler-variable)
# [sql-cast-suggestion](sql-cast-suggestion)
# [sql-null-comparison](sql-null-comparison)
# [start-elements-lowercase](start-elements-lowercase)
# [start-entities-uppercase](start-entities-uppercase)
# [valid-csv-header](valid-csv-header)
# [use-cql-select-template-strings](use-cql-select-template-strings)
# [valid-csv-header](valid-csv-header)
43 changes: 43 additions & 0 deletions tools/cds-lint/rules/case-sensitive-well-known-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
status: released
---

<script setup>
import PlaygroundBadge from '../components/PlaygroundBadge.vue'
</script>

# case-sensitive-well-known-events

## Rule Details

This rule identifies registrations to events that are likely well-known event names that must be written in all caps.

#### Version
This rule was introduced in `@sap/eslint-plugin-cds 4.0.2`.

## Examples

### ✅ &nbsp; Correct example

The following example shows the correctly capitalized event name `READ`:

::: code-group
<<< ../examples/case-sensitive-well-known-events/correct/srv/admin-service.js#snippet{js:line-numbers} [srv/admin-service.js]
:::
<PlaygroundBadge
name="case-sensitive-well-known-events"
kind="correct"
:files="['srv/admin-service.js']"
/>

### ❌ &nbsp; Incorrect example

This example shows a registration to an event `Read`, which should likely be `READ`. This can lead to unexpected behavior because event names in CAP are case sensitive:
::: code-group
<<< ../examples/case-sensitive-well-known-events/incorrect/srv/admin-service.js#snippet{js:line-numbers} [srv/admin-service.js]
:::
<PlaygroundBadge
name="case-sensitive-well-known-events"
kind="incorrect"
:files="['srv/admin-service.js']"
/>
2 changes: 1 addition & 1 deletion tools/cds-lint/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ status: released

Below you can find all rules of the `@sap/eslint-plugin-cds` ESLint plugin.

They are grouped by categories [Model Validation](#model-validation) and [Environment](#environment) to help you understand their purpose.
They are grouped by categories [Model Validation](#model-validation), [JavaScript](#javascript), and [Environment](#environment) to help you understand their purpose.

Your standard CDS project configuration turns on a subset of these rules by default, namely the *recommended*
(&nbsp;✅&nbsp;) rules to ensure basic standards are met.
Expand Down
43 changes: 43 additions & 0 deletions tools/cds-lint/rules/no-cross-service-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
status: released
---

<script setup>
import PlaygroundBadge from '../components/PlaygroundBadge.vue'
</script>

# no-cross-service-import

## Rule Details

This rule prevents importing artifacts generated for one service into another service's implementation when using cds-typer. Clear service boundaries make your codebase easier to maintain and understand.

#### Version
This rule was introduced in `@sap/eslint-plugin-cds 4.0.2`.

## Examples

### ✅ &nbsp; Correct example

The imported entity belongs to `AdminService` and is used within the implementation of `AdminService` itself. This is the recommended approach:
::: code-group
<<< ../examples/no-cross-service-import/correct/srv/AdminService.js#snippet{js:line-numbers} [srv/AdminService.js]
:::
<PlaygroundBadge
name="no-cross-service-import"
kind="correct"
:files="['srv/AdminService.js']"
/>

### ❌ &nbsp; Incorrect example

An entity from `CatalogService` is imported into the implementation of `AdminService`. This cross-service import is discouraged because it can lead to confusion and maintenance issues:

::: code-group
<<< ../examples/no-cross-service-import/incorrect/srv/AdminService.js#snippet{js:line-numbers} [srv/AdminService.js]
:::
<PlaygroundBadge
name="no-cross-service-import"
kind="incorrect"
:files="['srv/AdminService.js']"
/>
Loading
Loading