UNPKG

eslint-plugin-license-header

Version:
287 lines (223 loc) 7.74 kB
'use strict'; const fs = require('fs'); const LINEBREAK_MATCHER = /\r\n|[\r\n\u2028\u2029]/u; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ module.exports = { meta: { type: 'layout', docs: { description: 'Validate the presence of a license header', category: 'Possible Errors', recommended: true }, fixable: true, schema: [ { type: [ 'string', 'array' ], items: { type: 'string' } } ] }, create: function(context) { const sourceCode = context.getSourceCode(); const newlineChar = getNewlineCharacter(sourceCode); const separator = `${newlineChar}${newlineChar}`; const { licenseHeader: rawHeader } = parseOptions(context); const licenseHeader = replaceNewlines(rawHeader, newlineChar); // ---------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------- function getNewlineCharacter(sourceCode) { const match = LINEBREAK_MATCHER.exec(sourceCode.getText()); return match && match[0] || '\n'; } function readLicenseFile(path) { if (!path) { throw new Error('missing license header path'); } try { return fs.readFileSync(path, 'utf-8'); } catch (e) { throw new Error(`could not read license header from <${path}>`); } } function replaceNewlines(text, newlineChar) { return text.replace(createGlobalLinebreakMatcher(), newlineChar); } function parseOptions(context) { const { options } = context; const [ pathOrLicense ] = options; let licenseHeader; if (Array.isArray(pathOrLicense)) { // If the path is an array, that means simply use each element as a line for the header licenseHeader = pathOrLicense.join('\n'); } else { licenseHeader = readLicenseFile(pathOrLicense).trim(); } return { licenseHeader }; } function isShebang(node) { return node.type === 'Shebang'; } function isLicenseHeader(node) { return /(Copyright|@license|SPDX-License-Identifier)\b/i.test(node.value); } function hasValidText(comment) { return sourceCode.getText(comment) === licenseHeader; } function getLeadingComments(node) { if (node.type === 'Program') { return sourceCode.getCommentsInside(node); } else { return sourceCode.getCommentsBefore(node); } } function findLicenseComment(comments) { return comments.reduce(function(found, comment) { if (found !== null) { return found; } if (isLicenseHeader(comment)) { return comment; } // continue searching, if shebang is found return isShebang(comment) ? null : false; }, null); } // ---------------------------------------------------------------------- // Public // ---------------------------------------------------------------------- return { Program: function(programNode) { // ignore Vue SFC if (programNode.templateBody) { return; } const firstStatement = programNode.body[0]; // ignore Svelte SFC if (/^Svelte*/.test(firstStatement && firstStatement.type)) { return; } const comments = getLeadingComments(firstStatement || programNode); const licenseNode = findLicenseComment(comments); if (licenseNode) { if (!hasValidText(licenseNode)) { return context.report({ node: licenseNode, message: 'Invalid license header', fix: function(fixer) { return fixer.replaceText(licenseNode, licenseHeader); } }); } const previousSibling = comments[comments.indexOf(licenseNode) - 1]; const nextSibling = comments[comments.indexOf(licenseNode) + 1] || firstStatement; if (nextSibling) { const lLocEnd = licenseNode.loc.end.line; const nLocStart = nextSibling.loc.start.line; const fix = function(fixer) { return fixer.replaceTextRange( [ licenseNode.range[1], nextSibling.range[0] ], separator ); }; if (nLocStart - lLocEnd > 2) { context.report({ node: licenseNode, loc: licenseNode.loc.end, message: 'Superfluous new lines after license header', fix }); } if (nLocStart - lLocEnd < 2) { context.report({ node: licenseNode, loc: licenseNode.loc.end, message: 'Missing new line after license header', fix }); } } if (previousSibling) { const lLocStart = licenseNode.loc.start.line; const nLocEnd = previousSibling.loc.end.line; const fix = function(fixer) { return fixer.replaceTextRange( [ previousSibling.range[1], licenseNode.range[0] ], separator ); }; if (lLocStart - nLocEnd > 2) { context.report({ node: licenseNode, loc: licenseNode.loc.start, message: 'Superfluous new lines before license header', fix }); } if (lLocStart - nLocEnd < 2) { context.report({ node: licenseNode, loc: licenseNode.loc.start, message: 'Missing new line before license header', fix }); } } else { const lLocStart = licenseNode.loc.start.line; if (lLocStart !== 1) { context.report({ node: licenseNode, loc: licenseNode.loc.start, message: 'Superfluous new lines before license header', fix: function(fixer) { return fixer.replaceTextRange([ 0, licenseNode.range[0] ], ''); } }); } } } else { const shebang = comments.find(isShebang); context.report({ node: shebang || comments[0] || firstStatement || programNode, message: 'Missing license header', fix: function(fixer) { if (shebang) { return fixer.insertTextAfter( shebang, `${separator}${licenseHeader}` ); } else { const beforeNode = comments[0] || firstStatement; const endRange = beforeNode ? beforeNode.range[0] : programNode.range[1]; return fixer.replaceTextRange( [ 0, endRange ], `${licenseHeader}${separator}` ); } } }); } } }; } }; // helpers ///////////////////// /** * Creates a version of the LINEBREAK_MATCHER regex with the global flag. * Global regexes are mutable, so this needs to be a function instead of a constant. * @returns {RegExp} A global regular expression that matches line terminators */ function createGlobalLinebreakMatcher() { return new RegExp(LINEBREAK_MATCHER.source, 'gu'); }