UNPKG

@sap/eslint-plugin-cds

Version:

ESLint plugin including recommended SAP Cloud Application Programming model and environment rules

220 lines (211 loc) 6.64 kB
'use strict' /** * 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 }