@sap/eslint-plugin-cds
Version:
ESLint plugin including recommended SAP Cloud Application Programming model and environment rules
220 lines (211 loc) • 6.64 kB
JavaScript
/**
* Custom ESLint parser:
* https://eslint.org/docs/developer-guide/working-with-custom-parsers
* This file must:
* - Expose 'parseForESLint' method on the parser which should return the AST,
* optional properties services, a scopeManager, and visitorKeys
* - Expose default method 'parse' which should return the AST
* Both methods should take in the source code and an optional configuration
* (parserOptions).
*/
const cds = require('@sap/cds')
const { globalCache } = require('./utils/Cache')
const LOG = cds.debug('lint:plugin')
const colors = require('./utils/Colors')
const { splitDefName } = require('./utils/rules')
const packageJson = require('../package.json')
const newLineRegEx = /\r\n?|\n/g
module.exports = {
meta: {
name: packageJson.name,
version: packageJson.version
},
parse(code, parserOptions) {
return module.exports.parseForESLint(code, parserOptions).ast
},
// See https://eslint.org/docs/latest/extend/custom-parsers#parseforeslint-return-object
// eslint-disable-next-line no-unused-vars
parseForESLint(code, parserOptions) {
return {
ast: createProgramAST(code),
services: {
getParsedCsn: function () {
const compileOptions = {
messages: [],
}
let compiledModel
let reflectedModel
try {
compiledModel = cds.parse(code, compileOptions)
} catch {
// Do nothing
}
if (compiledModel) {
try {
reflectedModel = cds.linked(compiledModel)
if (compileOptions.messages) {
reflectedModel.messages = compileOptions.messages
}
} catch (err) {
LOG?.(colors.red + 'ERROR:' + colors.reset, err)
LOG?.('COMPILED', compiledModel)
LOG?.('REFLECTED', reflectedModel)
}
}
return reflectedModel
},
getInferredCsn: function () {
const rootPath = globalCache.get('rootpath')
if (globalCache.has('test')) {
return globalCache.get(`model:${rootPath}`)
}
let compiledModel
let reflectedModel
cds.resolve.cache = {}
if (!globalCache.has(`model:${rootPath}`) && rootPath) {
const roots = globalCache.get(`roots:${rootPath}`)
const messages = []
if (roots) {
try {
compiledModel = cds.load(roots, {
cwd: rootPath,
sync: true,
locations: true,
messages
})
globalCache.remove('errRootModel')
} catch (err) {
// TODO: Only catch Compile Errors?
globalCache.set('errRootModel', err)
}
if (compiledModel) {
reflectedModel = cds.linked(compiledModel)
globalCache.set(`model:${globalCache.get('rootpath')}`, reflectedModel)
if (messages) {
reflectedModel.messages = messages
}
}
}
} else {
reflectedModel = globalCache.get(`model:${rootPath}`)
}
return reflectedModel
},
updateInferredCsn: compileModelFromDict,
getEnvironment: function () {
const options = globalCache.get('options')
return options?.[0]?.environment
},
getLocation: function (name, obj, model) {
let loc
const defaultLoc = {
start: { line: 0, column: 0 },
end: { line: 1, column: 0 }
}
if (obj.$location) {
const objLoc = obj.$location
if (objLoc) {
// CSN entry with column 0 is equivalent to 'undefined'
// It means that the column in that line cannot be determined,
// so we assign a value 1 to get a column location of 0
if (objLoc.col === 0) {
objLoc.col = 1
}
loc = defaultLoc
loc.start.column = objLoc.col - 1
loc.start.line = objLoc.line
let colLength = name?.length // use length of `name` property
// TODO bug in reflect? : `annotate` elements have an unusable index-like `name`, e.g. "1"
if (obj.annotate) colLength = 0
loc.end.column = objLoc.col - 1 + colLength
loc.end.line = objLoc.line
} else if (obj.parent) {
this.getLocation(name, obj.parent, model)
}
}
// Empty locations default to line 0, column 0
if (!loc) {
loc = defaultLoc
}
return loc
},
getNode: function (obj) {
let loc
if (obj?.name) {
let name = obj.name
// TODO: 'action'/'function' not correct for bound action/function
if (['action', 'entity', 'function', 'service'].includes(obj.kind)) {
name = splitDefName(obj, name)?.name
}
loc = this.getLocation(name, obj)
}
return createProgramAST(code, loc)
}
},
scopeManager: null,
tokensAndComments: [],
visitorKeys: []
}
},
createProgramAST,
compileModelFromDict,
}
/**
* Generates dummy AST with just single Program node.
*
* @param code Parse file contents
* @param {object} [loc]
* @returns ESLint AST
*/
function createProgramAST (code, loc) {
if (!loc && code.length) {
const newLines = [...code.matchAll(newLineRegEx)]
const endColumn = newLines.length ? (code.length - newLines.at(-1).index) : code.length
loc = {
start: {
line: 1,
column: 1
},
end: {
line: newLines.length + 1,
column: endColumn
},
}
} else if (!loc) {
loc = {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 },
}
}
return {
type: 'Program',
body: [],
sourceType: 'module',
tokens: [],
comments: [],
range: [0, code.length],
loc
}
}
/**
* @param {object} dictFiles
* @param options
*/
function compileModelFromDict (dictFiles, options) {
let reflectedModel
const messages = []
const compiledModel = cds.compile(dictFiles, {
sync: true,
locations: true,
messages,
...options
})
if (compiledModel) {
reflectedModel = cds.linked(compiledModel)
if (messages) {
reflectedModel.messages = messages
}
}
return reflectedModel
}