const fs = require("fs"); const path = require("node:path"); const child_process = require("child_process"); /** @type {import('eslint').Rule.RuleModule} */ const copyrightHeaderRule = { meta: { schema: [{ type: 'object', properties: { author: {type: 'string'}, license: {type: 'string'}, fixedDate: {type: 'string'}, }, required: ['author', 'license'] }], type: 'problem', docs: { description: 'Files should start with a copyright header' }, messages: { missing: 'Missing a copyright header at the start of the file', invalid: 'Invalid copyright header', tryThisHeader: 'Use this copyright header:\n\n/*{{expected}}*/' }, fixable: "code", hasSuggestions: true, }, create: function(context) { const code = context.getSourceCode() const {author, license, fixedDate} = context.options[0] const year = fixedDate ? new Date(fixedDate) : getAuthorDate(context.getPhysicalFilename()) if (!year) return {} const expected = license .replace('{{year}}', year.getFullYear().toString()) .replace('{{author}}', author) const comment = code.getAllComments().find(it => it.type === 'Block') if (!comment) { context.report({ loc: { line: 0, column: 0, }, messageId: 'missing', suggest: [ { messageId: 'tryThisHeader', data: {expected}, fix(fixer) { return fixer.insertTextBeforeRange([0, 0], `/*${expected}*/\n`) } } ] }) return {} } if (comment.value !== expected && comment.loc && comment.range) { const range = comment.range context.report({ loc: comment.loc, messageId: 'invalid', suggest: [ { messageId: 'tryThisHeader', data: {expected}, fix(fixer) { return fixer.replaceTextRange(range, `/*${expected}*/`) } } ] }) } return {} } } /** * Retrieves the last edited date of a file * * Uses git history if available, last modified date otherwise. * * @param filePath {string} * @return {Date | undefined} */ function getAuthorDate(filePath) { // just to be on the safe side const sanitizedPath = path.resolve(filePath) try { const result = child_process.execSync(`git log -1 --pretty="format:%ad" "${sanitizedPath}"`, { cwd: path.dirname(sanitizedPath), stdio: 'pipe', }) const date = new Date(result.toString()) if (!Number.isNaN(date.getTime())) { return date } } catch {} try { const stats = fs.statSync(sanitizedPath) const date = new Date(stats.mtime) return Number.isNaN(date.getTime()) ? undefined : date } catch {} return undefined } module.exports = copyrightHeaderRule