@sap/eslint-plugin-cds
Version:
ESLint plugin including recommended SAP Cloud Application Programming model and environment rules
184 lines (170 loc) • 4.6 kB
JavaScript
'use strict'
const { RULE_CATEGORIES } = require('../constants')
// Check that Java keywords are not used as identifiers unless they have
// a Java-specific annotation that renames/ignores them. This avoids issues
// later on in code-generation of CAP Java classes.
// Test Java code via godbolt.org: https://godbolt.org/z/1c5s49qjo
const { splitDefName } = require('../utils/rules')
// There is also `@cds.java.this.name`, which is not relevant for this check.
const ANNO_JAVA_NAME = '@cds.java.name'
const ANNO_JAVA_IGNORE = '@cds.java.ignore'
// CSN kinds that are relevant for code generation with possible keyword
// conflicts. For example, types are not relevant, because they use
// PascalCase, i.e. it can never be a keyword conflict, since all keywords
// are lowercase.
const relevantKinds = [
'element',
'param',
'action',
'function',
]
module.exports = {
meta: {
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
docs: {
category: RULE_CATEGORIES.model,
description: 'Reject reserved Java keywords as CDS identifiers.',
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/no-java-keywords',
},
type: 'problem',
model: 'inferred',
messages: {
keywordJava: `'{{name}}' is a reserved keyword in Java. Use '@cds.java.name' to override the name for Java code generation.`,
},
},
create (context) {
const rootPath = context.getRootPath()
if (!rootPath)
return
return function checkForJavaKeywords(){
const model = context.getModel()
if (!model)
return
for (const name in model.definitions)
checkDefinition(model.definitions[name])
}
function checkDefinition(def) {
checkNameIsNotReserved(def)
if (def.elements) {
for (const name in def.elements)
checkDefinition(def.elements[name])
}
if (def.actions) {
for (const name in def.actions)
checkDefinition(def.actions[name])
}
if (def.kind === 'action' || def.kind === 'function') {
for (const name in def.params)
checkDefinition(def.params[name])
}
}
function checkNameIsNotReserved(artifact) {
if (!artifact.$location?.file || !relevantKinds.includes(artifact.kind))
return
if (artifact[ANNO_JAVA_IGNORE])
return // ignored; no Java code generated
if (artifact[ANNO_JAVA_NAME])
return // explicitly renamed; assume the user uses a valid name
const name = artifact.is('element')
? artifact.name
: splitDefName(artifact).name
if (isValueReservedJavaKeyword(name)) {
context.report({
messageId: 'keywordJava',
data: { name },
node: context.getNode(artifact),
file: artifact.$location.file,
})
}
}
}
}
// List from https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html
// Also available at https://github.com/openjdk/jdk/blob/f92c60e1a9968620cbc92b52aa546b57c09da487/src/java.compiler/share/classes/javax/lang/model/SourceVersion.java#L651
// though that list includes fewer items.
const JAVA_RESERVED = [
'_',
'abstract',
'assert',
'boolean',
'break',
'byte',
'case',
'catch',
'char',
'class',
'const',
'continue',
'default',
'do',
'double',
'else',
'enum',
'extends',
'final',
'finally',
'float',
'for',
'goto',
'if',
'implements',
'import',
'instanceof',
'int',
'interface',
'long',
'native',
'new',
'package',
'private',
'protected',
'public',
'return',
'short',
'static',
'strictfp',
'super',
'switch',
'synchronized',
'this',
'throw',
'throws',
'transient',
'try',
'void',
'volatile',
'while',
// literals
'true',
'false',
'null',
]
/**
* Check if the given value is a reserved keyword.
*
* @param {any} name
* @returns {boolean}
*/
function isValueReservedJavaKeyword(name) {
if (!name || typeof name !== 'string')
return false
const normalized = identifierForJava(name)
return JAVA_RESERVED.includes(normalized)
}
/**
* Returns the check-relevant identifier for Java.
* CAP Java does not use lowercase for the full identifier, but instead
* uses lowerCamelCase, i.e. it is enough to change the first character
* of the identifier.
*
* @param {string} name
* @returns {string}
*/
function identifierForJava(name) {
if (!name)
return name
const firstChar = name.charAt(0)
if (firstChar === firstChar.toLowerCase())
return name
return `${firstChar.toLowerCase()}${name.slice(1)}`
}