├── .gitignore ├── tests ├── test-template.js ├── lib │ └── rules │ │ ├── fix-result-3.js │ │ ├── output.js │ │ ├── fix-result-1.js │ │ ├── fix-result-2.js │ │ ├── test-template-4.js │ │ ├── test-case-4.js │ │ └── notice.js └── test-utils.js ├── staging ├── test-template.txt ├── test-lf.js ├── test-crlf.js └── staging.spec.js ├── .npmignore ├── .eslintrc ├── .github └── workflows │ └── main.yml ├── package.json ├── LICENSE ├── index.js ├── utils.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | *.log 3 | /tmp/ -------------------------------------------------------------------------------- /tests/test-template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) <%= YEAR %>, Nick Deis 3 | */ 4 | 5 | -------------------------------------------------------------------------------- /staging/test-template.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) <%= YEAR %>, Nick Deis 3 | */ 4 | 5 | -------------------------------------------------------------------------------- /staging/test-lf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2024, Nick Deis 3 | */ 4 | 5 | function x(){ 6 | return 1; 7 | } -------------------------------------------------------------------------------- /staging/test-crlf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2024, Nick Deis 3 | */ 4 | 5 | function x(){ 6 | return 1; 7 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests/* 2 | /node_modules/ 3 | *.log 4 | /tmp/ 5 | staging/ 6 | .travis.yml 7 | .eslintrc 8 | .github/ 9 | .eslintrc -------------------------------------------------------------------------------- /tests/lib/rules/fix-result-3.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Copyright (c) 2024, Nick Deis 4 | */ 5 | 6 | 7 | function leastYouTried(){ 8 | return false; 9 | } 10 | -------------------------------------------------------------------------------- /tests/lib/rules/output.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2024, Nick Deis 3 | */ 4 | 5 | 6 | function noStyle(){ 7 | return "I didn't read the style guide :("; 8 | } 9 | -------------------------------------------------------------------------------- /tests/lib/rules/fix-result-1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2024, Nick Deis 3 | */ 4 | 5 | 6 | function noStyle(){ 7 | return "I didn't read the style guide :("; 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins":["notice"], 3 | "rule":{ 4 | "notice":["error",{"mustMatch":"[0-9]{0,4}, Nicholas Deis","templateFile":"../tests/test-template.js"}] 5 | } 6 | } -------------------------------------------------------------------------------- /tests/lib/rules/fix-result-2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2024, Nick Deis 3 | */ 4 | 5 | 6 | /** 7 | * Not exactly what I was looking for 8 | */ 9 | function leastYouTried(){ 10 | return false; 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | # Have to use 18.x and up because later versions of eslint require structuredClone 15 | node-version: [18.x, 20.x] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Run tests against node ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: "npm" 23 | - run: npm ci 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /tests/lib/rules/test-template-4.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright <%= YEAR %> Moshe Simantov 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/lib/rules/test-case-4.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Moshe Simantov 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | console.error("Don't run `mocha` directly. Use `make test`."); 17 | process.exit(0); -------------------------------------------------------------------------------- /tests/test-utils.js: -------------------------------------------------------------------------------- 1 | const { regexpizeTemplate } = require("../utils"); 2 | const assert = require("assert"); 3 | 4 | const template = ` 5 | /** 6 | * Copyright (c) <%= YEAR %>, <%= NAME %> 7 | */ 8 | `; 9 | 10 | const header1 = ` 11 | /** 12 | * Copyright (c) 2017, Nick Deis 13 | */ 14 | `; 15 | 16 | const header2 = ` 17 | /** 18 | * Copyright (c) 2016, Nicholas Deis 19 | */ 20 | `; 21 | 22 | const HEADERS = [header1, header2]; 23 | 24 | function testRegepizeTemplate() { 25 | const varRegexps = { NAME: /(Nick|Nicholas) Deis/ }; 26 | const mustMatch = regexpizeTemplate({ template, varRegexps }); 27 | HEADERS.forEach(header => { 28 | if (!header.match(mustMatch)) { 29 | throw new Error(`Expected ${header} to match ${mustMatch}`); 30 | } 31 | }); 32 | } 33 | 34 | testRegepizeTemplate(); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-notice", 3 | "version": "1.0.0", 4 | "description": "An eslint rule that checks the top of files and --fix them too!", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "test": "node tests/lib/rules/notice.js && node tests/test-utils.js && mocha staging" 11 | }, 12 | "keywords": [ 13 | "eslint", 14 | "plugin", 15 | "notice", 16 | "copyright", 17 | "header", 18 | "lint", 19 | "eslintplugin" 20 | ], 21 | "repository": "https://github.com/nickdeis/eslint-plugin-notice", 22 | "author": "nickdeis", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "eslint": "^8.57.0", 26 | "eslint-plugin-self": "^1.2.0", 27 | "eslint6": "npm:eslint@^6.8.0", 28 | "eslint7": "npm:eslint@^7.32.0", 29 | "eslint9": "npm:eslint@^9.3.0", 30 | "mocha": "^10.4.0" 31 | }, 32 | "dependencies": { 33 | "find-root": "^1.1.0", 34 | "lodash": "^4.17.21", 35 | "metric-lcs": "^0.1.2" 36 | }, 37 | "peerDependencies": { 38 | "eslint": ">=3.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nick Deis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /staging/staging.spec.js: -------------------------------------------------------------------------------- 1 | const ESLint8 = require("eslint").ESLint, 2 | ESLint9 = require("eslint9").ESLint, 3 | ESLint7 = require("eslint7").ESLint; 4 | const path = require("path"); 5 | const assert = require('assert'); 6 | 7 | 8 | const RULE_CONFIG = ["error",{ templateFile: path.join(__dirname, "./test-template.txt") }]; 9 | 10 | const LINE_ENDING_CONFIG_V8 = { 11 | plugins: ["self"], 12 | rules: { 13 | "self/notice": RULE_CONFIG 14 | } 15 | }; 16 | 17 | const LINE_ENDING_CONFIG_V9 = { 18 | "plugins":{ 19 | "notice": require("..") 20 | }, 21 | "rules":{ 22 | "notice/notice": RULE_CONFIG, 23 | } 24 | } 25 | 26 | 27 | const textLF = `/** 28 | * Copyright (c) 2020, Nick Deis 29 | */ 30 | 31 | function x(){ 32 | return 1; 33 | } 34 | `; 35 | 36 | //TODO: Test fix 37 | 38 | const textLFNoHeader = ` 39 | function x(){ 40 | return 1; 41 | } 42 | `; 43 | 44 | const textCRLF = textLF.replace(/\n/g,"\r\n"); 45 | const textCRLFNoHeader = textLFNoHeader.replace(/\n/g,"\r\n"); 46 | const ESLINTS = [ 47 | [7,ESLint7,{overrideConfig:LINE_ENDING_CONFIG_V8,useEslintrc:false}], 48 | [8,ESLint8,{overrideConfig:LINE_ENDING_CONFIG_V8,useEslintrc:false}], 49 | [9,ESLint9,{overrideConfig:LINE_ENDING_CONFIG_V9,overrideConfigFile:true}] 50 | ] 51 | 52 | for(const [version,ESLint,config] of ESLINTS){ 53 | describe(`Staging Version ${version}`, () => { 54 | describe("Line ending control character testing", () => { 55 | const eslint = new ESLint(config); 56 | it('Should work on files with CRLF and LF', async () => { 57 | 58 | const results = await eslint.lintFiles([path.join(__dirname, "./test-crlf.js"), path.join(__dirname, "./test-lf.js")]); 59 | const [crlfResults,lfResults] = results; 60 | assert.equal(lfResults.errorCount,0,"Should work on LF"); 61 | assert.equal(crlfResults.errorCount,0,"Should work on CRLF"); 62 | }); 63 | it("Should work on CRLF text", async () => { 64 | const report = await eslint.lintText(textCRLF); 65 | assert.equal(report[0].errorCount,0); 66 | }); 67 | it("Should work on LF text", async () => { 68 | const report = await eslint.lintText(textLF); 69 | assert.equal(report[0].errorCount,0); 70 | }); 71 | const eslintFix = new ESLint(Object.assign({fix:true},config)); 72 | it("Should correctly fix CRLF", async () => { 73 | const report = await eslintFix.lintText(textCRLFNoHeader); 74 | assert.equal(report[0].errorCount,0); 75 | }); 76 | it("Should correctly fix LF", async () => { 77 | const report = await eslintFix.lintText(textLFNoHeader); 78 | assert.equal(report[0].errorCount,0); 79 | }); 80 | }); 81 | }); 82 | } 83 | 84 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview An eslint rule that checks the top of files and --fix them too! 3 | * @author Nick Deis 4 | */ 5 | 6 | "use strict"; 7 | 8 | const fs = require("fs"), 9 | _ = require("lodash"), 10 | utils = require("./utils"), 11 | metriclcs = require("metric-lcs"); 12 | 13 | const { resolveOptions, createFixer } = utils; 14 | 15 | module.exports = { 16 | meta: { 17 | name: "eslint-plugin-notice", 18 | version: "1.0.0" 19 | }, 20 | rules: { 21 | notice: { 22 | meta: { 23 | docs: { 24 | description: "An eslint rule that checks the top of files and --fix them too!", 25 | category: "Stylistic Issues" 26 | }, 27 | fixable: "code", 28 | schema: false 29 | }, 30 | create(context) { 31 | const { 32 | resolvedTemplate, 33 | mustMatch, 34 | chars, 35 | onNonMatchingHeader, 36 | nonMatchingTolerance, 37 | messages 38 | } = resolveOptions(context.options[0], context.getFilename()); 39 | 40 | const sourceCode = context.getSourceCode(); 41 | const text = sourceCode.getText().substring(0, chars); 42 | const firstComment = sourceCode.getAllComments()[0]; 43 | return { 44 | Program(node) { 45 | let topNode; 46 | let hasHeaderComment = false; 47 | if (!firstComment) { 48 | topNode = node; 49 | } else if (firstComment.loc.start.line <= node.loc.start.line) { 50 | hasHeaderComment = true; 51 | topNode = firstComment; 52 | } else { 53 | topNode = node; 54 | } 55 | let headerMatches = false; 56 | if (!headerMatches && mustMatch && text) { 57 | headerMatches = !!(String(text).replace(/\r\n/g, "\n")).match(mustMatch); 58 | //If the header matches, return early 59 | if (headerMatches) return; 60 | } 61 | //If chars doesn't match, a header comment/template exists and nonMatchingTolerance is set, try calculating string distance 62 | if (!headerMatches && hasHeaderComment && resolvedTemplate && _.isNumber(nonMatchingTolerance)) { 63 | const dist = metriclcs(resolvedTemplate, firstComment.value); 64 | //Return early, mark as true for future work if needed 65 | if (nonMatchingTolerance <= dist) { 66 | headerMatches = true; 67 | return; 68 | } else { 69 | const fix = createFixer({ resolvedTemplate, hasHeaderComment, topNode, onNonMatchingHeader }); 70 | const report = { 71 | node, 72 | message: messages.whenOutsideTolerance, 73 | fix, 74 | data: { similarity: Math.round(dist * 1000) / 1000 } 75 | }; 76 | context.report(report); 77 | return; 78 | } 79 | } 80 | //report and skip 81 | if (hasHeaderComment && onNonMatchingHeader === "report" && !headerMatches) { 82 | const report = { 83 | node, 84 | message: messages.reportAndSkip 85 | }; 86 | context.report(report); 87 | return; 88 | } 89 | //Select fixer based off onNonMatchingHeader 90 | const fix = createFixer({ resolvedTemplate, hasHeaderComment, topNode, onNonMatchingHeader }); 91 | if (!headerMatches) { 92 | const report = { node, message: messages.whenFailedToMatch, fix }; 93 | context.report(report); 94 | return; 95 | } 96 | } 97 | }; 98 | } 99 | } 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared lib between rule and tests 3 | */ 4 | 5 | const _ = require("lodash"), 6 | fs = require("fs"), 7 | findRoot = require("find-root"), 8 | path = require("path"); 9 | 10 | const COULD_NOT_FIND = `Missing notice header`; 11 | const REPORT_AND_SKIP = `Found a header comment which did not have a notice header, skipping fix and reporting`; 12 | const OUTSIDE_TOLERANCE = `Found a header comment which was too different from the required notice header (similarity={{ similarity }})`; 13 | 14 | const DEFAULT_MESSAGE_CONFIG = { 15 | whenFailedToMatch: COULD_NOT_FIND, 16 | reportAndSkip: REPORT_AND_SKIP, 17 | whenOutsideTolerance: OUTSIDE_TOLERANCE 18 | }; 19 | 20 | const ESCAPE = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g; 21 | const YEAR_REGEXP = /20\d{2}/; 22 | const NON_MATCHING_HEADER_ACTIONS = ["prepend", "replace", "report"]; 23 | 24 | function escapeRegExp(str) { 25 | return String(str).replace(ESCAPE, "\\$&"); 26 | } 27 | 28 | function stringifyRegexp(regexp) { 29 | return regexp instanceof RegExp ? regexp.source : String(regexp); 30 | } 31 | 32 | function regexpizeTemplate({ template, varRegexps }) { 33 | const allRegexpVars = Object.assign({}, { YEAR: YEAR_REGEXP }, varRegexps); 34 | const allPatternVars = _.mapValues(allRegexpVars, stringifyRegexp); 35 | return new RegExp(_.template(escapeRegExp(template))(allPatternVars)); 36 | } 37 | 38 | function resolveTemplate({ templateFile, template, fileName }) { 39 | if (template) return template; 40 | //No template file, so move foward and disable --fix 41 | if (!templateFile) return null; 42 | //Naively look for the templateFile first 43 | if (fs.existsSync(templateFile)) { 44 | return fs.readFileSync(templateFile, "utf8"); 45 | } 46 | if (!fs.existsSync(fileName)) { 47 | throw new Error(`Could not find the file name ${fileName}. This is necessary to find the root`); 48 | } 49 | const root = findRoot(fileName); 50 | const rootTemplateFile = path.join(root, templateFile); 51 | if (fs.existsSync(rootTemplateFile)) { 52 | return fs.readFileSync(rootTemplateFile, "utf8"); 53 | } 54 | const absRootTemplateFile = path.resolve(rootTemplateFile); 55 | if (fs.existsSync(absRootTemplateFile)) { 56 | return fs.readFileSync(absRootTemplateFile, "utf8"); 57 | } 58 | throw new Error(`Can't find templateFile @ ${absRootTemplateFile}`); 59 | } 60 | 61 | function resolveOptions( 62 | { 63 | mustMatch, 64 | templateFile, 65 | template, 66 | templateVars, 67 | chars, 68 | onNonMatchingHeader, 69 | varRegexps, 70 | nonMatchingTolerance, 71 | messages 72 | }, 73 | fileName 74 | ) { 75 | onNonMatchingHeader = onNonMatchingHeader || "prepend"; 76 | templateVars = templateVars || {}; 77 | varRegexps = varRegexps || {}; 78 | chars = chars || 1000; 79 | nonMatchingTolerance = nonMatchingTolerance || null; 80 | messages = Object.assign({}, DEFAULT_MESSAGE_CONFIG, messages || {}); 81 | 82 | let mustMatchTemplate = false; 83 | if (!mustMatch) { 84 | mustMatchTemplate = true; 85 | } else if (!(mustMatch instanceof RegExp)) { 86 | mustMatch = new RegExp(mustMatch); 87 | } 88 | template = resolveTemplate({ templateFile, template, fileName }); 89 | if(typeof template === 'string'){ 90 | template = template.replace(/\r\n/g, "\n"); 91 | } 92 | const YEAR = new Date().getFullYear(); 93 | const allVars = Object.assign({}, { YEAR }, templateVars); 94 | if (mustMatchTemplate && template) { 95 | //create mustMatch from varRegexps and template 96 | mustMatch = regexpizeTemplate({ template, varRegexps }); 97 | } else if (!template && mustMatchTemplate) { 98 | throw new Error("Either mustMatch, template, or templateFile must be set"); 99 | } 100 | const resolvedTemplate = _.template(template)(allVars).replace(/\r\n/g, "\n"); 101 | 102 | return { resolvedTemplate, mustMatch, chars, onNonMatchingHeader, nonMatchingTolerance, messages }; 103 | } 104 | 105 | function createFixer({ resolvedTemplate, hasHeaderComment, topNode, onNonMatchingHeader }) { 106 | if (!resolvedTemplate) { 107 | return undefined; 108 | } 109 | if (!hasHeaderComment || (hasHeaderComment && onNonMatchingHeader === "prepend")) { 110 | return fixer => fixer.insertTextBeforeRange([0, 0], resolvedTemplate); 111 | } 112 | if (hasHeaderComment && onNonMatchingHeader === "replace") { 113 | return fixer => fixer.replaceText(topNode, resolvedTemplate); 114 | } 115 | } 116 | 117 | module.exports = { createFixer, regexpizeTemplate, COULD_NOT_FIND, REPORT_AND_SKIP, resolveOptions }; 118 | -------------------------------------------------------------------------------- /tests/lib/rules/notice.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for top rule 3 | * @author Nick Deis 4 | */ 5 | const RuleTester8 = require("eslint").RuleTester, 6 | RuleTester7 = require("eslint7").RuleTester, 7 | RuleTester6 = require("eslint6").RuleTester, 8 | RuleTester9 = require("eslint9").RuleTester, 9 | rule = require("../../..").rules.notice, 10 | fs = require("fs"), 11 | path = require("path"), 12 | utils = require("../../../utils"); 13 | 14 | const { COULD_NOT_FIND, REPORT_AND_SKIP, escapeRegExp } = utils; 15 | 16 | const templateFile = path.join(__dirname, "../../test-template.js"); 17 | 18 | const template = fs.readFileSync(templateFile, "utf8"); 19 | 20 | const mustMatch = /Copyright \(c\) [0-9]{0,4}, Nick Deis/; 21 | 22 | const RULE_TESTERS = [ 23 | [8,new RuleTester8()], 24 | [7, new RuleTester7()], 25 | [6, new RuleTester6()], 26 | [9, new RuleTester9()] 27 | ] 28 | 29 | 30 | 31 | 32 | const notExact = ` 33 | /** 34 | * Not exactly what I was looking for 35 | */ 36 | function leastYouTried(){ 37 | return false; 38 | } 39 | `; 40 | 41 | const noStyle = ` 42 | function noStyle(){ 43 | return "I didn't read the style guide :("; 44 | } 45 | `; 46 | 47 | const noStyle2 = ` 48 | function noStyle2(){ 49 | return "I didn't read the style guide :("; 50 | } 51 | `; 52 | 53 | const testCode4 = fs.readFileSync(__dirname + "/test-case-4.js", "utf8"); 54 | 55 | const testCase4 = { 56 | code: testCode4, 57 | options: [{ template: fs.readFileSync(__dirname + "/test-template-4.js", "utf8"), onNonMatchingHeader: "report" }], 58 | errors: [{ message: REPORT_AND_SKIP }], 59 | output: null 60 | }; 61 | 62 | function createToleranceTestCase(nonMatchingTolerance) { 63 | return { 64 | code: `/* Copyright (c) 2014-present, Foo bar Inc. */`, 65 | options: [{ template: "/* Copyright (c) 2014-present, FooBar, Inc. */", nonMatchingTolerance, onNonMatchingHeader:"report" }] 66 | }; 67 | } 68 | 69 | /** 70 | * Since we norm the output of templates, we need to norm the expected output of --fix 71 | */ 72 | function readAndNormalize(rpath){ 73 | return fs.readFileSync(__dirname + rpath, "utf8").replace(/\r\n/g, "\n"); 74 | } 75 | 76 | for(const [version,ruleTester] of RULE_TESTERS){ 77 | console.log(`Running rule tester version ${version}`); 78 | runRuleTester(ruleTester); 79 | } 80 | 81 | function runRuleTester(ruleTester){ 82 | ruleTester.run("notice", rule, { 83 | invalid: [ 84 | { 85 | code: noStyle, 86 | options: [{ mustMatch, template }], 87 | errors: [{ message: COULD_NOT_FIND }], 88 | output: readAndNormalize("/fix-result-1.js") 89 | }, 90 | { 91 | code: notExact, 92 | options: [{ mustMatch, template }], 93 | errors: [{ message: COULD_NOT_FIND }], 94 | output: readAndNormalize("/fix-result-2.js") 95 | }, 96 | { 97 | code: notExact, 98 | options: [{ mustMatch, template, onNonMatchingHeader: "replace" }], 99 | errors: [{ message: COULD_NOT_FIND }], 100 | output: readAndNormalize("/fix-result-3.js") 101 | }, 102 | { 103 | code: notExact, 104 | options: [{ mustMatch, template, onNonMatchingHeader: "report" }], 105 | errors: [{ message: REPORT_AND_SKIP }], 106 | output: null 107 | }, 108 | testCase4, 109 | //Similarity message test 110 | Object.assign({}, createToleranceTestCase(0.9), { 111 | errors: [ 112 | { message: "Found a header comment which was too different from the required notice header (similarity=0.87)" } 113 | ] 114 | }), 115 | //test configurable messages 116 | { 117 | code: noStyle, 118 | options: [{ mustMatch, template, messages: { whenFailedToMatch: "Custom message" } }], 119 | errors: [{ message: "Custom message" }], 120 | output: readAndNormalize("/fix-result-1.js") 121 | }, 122 | /** 123 | * Test the case where no template file is set, should COULD_NOT_FIND error with no autofixes suggested 124 | */ 125 | { code: noStyle2, options: [{ mustMatch }], errors: [{ message: COULD_NOT_FIND }], output: null } 126 | ], 127 | valid: [ 128 | { 129 | code: ` 130 | /** 131 | * Copyright (c) 2017, Nick Deis 132 | * All rights reserved. 133 | */ 134 | function stylin(){ 135 | return "I read the style guide, or eslint handled it for me"; 136 | } 137 | `, 138 | options: [{ mustMatch, template }] 139 | }, 140 | { 141 | code: ` 142 | /** 143 | * Copyright (c) 2017, Nick Deis 144 | * All rights reserved. 145 | */ 146 | function stylin(){ 147 | return "I'm a little off, but close enough"; 148 | }`, 149 | options: [{ template, nonMatchingTolerance: 0.7 }] 150 | }, 151 | createToleranceTestCase(0.7) 152 | ] 153 | }); 154 | } 155 | 156 | 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/nickdeis/eslint-plugin-notice/actions/workflows/main.yml/badge.svg)](https://github.com/nickdeis/eslint-plugin-notice/actions/workflows/main.yml/badge.svg) 2 | 3 | # eslint-plugin-notice 4 | 5 | An eslint rule that checks the top of files and `--fix` them too! 6 | 7 | ## Usage 8 | 9 | `npm i -D eslint-plugin-notice` 10 | 11 | ### Flag config 12 | 13 | _eslint.config.js_ 14 | 15 | ```js 16 | import notice from "eslint-plugin-notice"; 17 | 18 | export default [ 19 | { 20 | files: ["**/*.js"], 21 | plugins: { 22 | notice, 23 | }, 24 | rules: { 25 | "notice/notice": [ 26 | "error", 27 | { mustMatch: "Copyright \\(c\\) [0-9]{0,4}, Nick Deis" }, 28 | ], 29 | }, 30 | }, 31 | ]; 32 | ``` 33 | 34 | ### eslintrc 35 | 36 | Throw an error when a file doesn't have copyright notice 37 | 38 | ```json 39 | { 40 | "plugins": ["notice"], 41 | "rules": { 42 | "notice/notice": [ 43 | "error", 44 | { "mustMatch": "Copyright \\(c\\) [0-9]{0,4}, Nick Deis" } 45 | ] 46 | } 47 | } 48 | ``` 49 | 50 | Add a template to `--fix` it 51 | 52 | ```json 53 | { 54 | "notice/notice": [ 55 | "error", 56 | { 57 | "mustMatch": "Copyright \\(c\\) [0-9]{0,4}, Nick Deis", 58 | "template": "/** Copyright (c) <%= YEAR %>, Nick Deis **/" 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | or use a file 65 | 66 | _config/copyright.js_ 67 | 68 | ```js 69 | /** 70 | * Copyright (c) <%= YEAR %>, Nick Deis 71 | */ 72 | ``` 73 | 74 | ```json 75 | { 76 | "notice/notice": [ 77 | "error", 78 | { 79 | "mustMatch": "Copyright \\(c\\) [0-9]{0,4}, Nick Deis", 80 | "templateFile": "config/copyright.js" 81 | } 82 | ] 83 | } 84 | ``` 85 | 86 | or just use your template, eslint-plugin-notice will reverse into a pattern for `mustMatch` 87 | 88 | ```json 89 | { 90 | "notice/notice": [ 91 | "error", 92 | { 93 | "templateFile": "config/copyright.js" 94 | } 95 | ] 96 | } 97 | ``` 98 | 99 | Want a more expressive template? Add `templateVars` and `varRegexps` 100 | _config/copyright.js_ 101 | 102 | ```js 103 | /** 104 | * Copyright (c) <%= YEAR %>, <%= NAME %> 105 | */ 106 | ``` 107 | 108 | ```js 109 | { 110 | "notice/notice":["error", 111 | { 112 | templateFile:"config/copyright.js", 113 | //YEAR will still be added unless you add your own value 114 | templateVars:{NAME:"Nick Deis"}, 115 | //The regexp for YEAR is /20\d{2}/ and is automatically added 116 | varRegexps:{NAME:/(Nick|Nicholas) Deis/} 117 | } 118 | ] 119 | } 120 | ``` 121 | 122 | ## Options 123 | 124 | | Option | Description | Default/Required/Optional | Type | 125 | | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------- | 126 | | mustMatch | A pattern that must be present in the notice | **Required** unless `template` is set | RegExp/string | 127 | | template | A lodash template that will be used to fix files that do not match `mustMatch` or are less than `nonMatchingTolerance` | **Optional** unless `mustMatch` is not set | string | 128 | | templateFile | `template` will override this setting. A file which contains the `template` | **Optional** | string | 129 | | chars | The number of characters to check for the `mustMatch` pattern | `1000` | number | 130 | | templateVars | The variables to be used with the lodash template, always contains the variable YEAR | `{YEAR:new Date().getFullYear()}` | object | 131 | | [onNonMatchingHeader](#onnonmatchingheader) | Action that should be taken when there is a header comment, but it does not match `mustMatch` or is less than `nonMatchingTolerance` | `"prepend"` | string | 132 | | nonMatchingTolerance | Optional fallback for `mustMatch`. Compares a non-matching header comment (if it exists) to the resolved template using [Metric Longest Common Subsequence](http://heim.ifi.uio.no/~danielry/StringMetric.pdf). `1` means the strings must be exactly the same, where anything less is varying degrees of dissimiliar. `.70` seems like a good choice | **Optional** | number between 0 and 1 | 133 | | varRegexps | If `mustMatch` is not set and `template` is set, a regexp that will be replaced in the `template` to create a regexp for `mustMatch` | `{YEAR:/20\d{2}/}` | object | 134 | | messages | Allows you to change the error messages. See [messages](#messages) | **Optional** | object | 135 | 136 | ### onNonMatchingHeader 137 | 138 | - **prepend**: Prepends the fix template, if it exists, leaving the former header comment intact. 139 | - **replace**: Replaces the former header comment with the fix template if it exists 140 | - **report**: Does not apply fix, simply reports it based on the level assigned to the rule ("error" or "warn") 141 | 142 | ### messages 143 | 144 | The `messages` option allows you to change the default error messages. 145 | There are three messages you can change by passing in an object with the pairs you wish to change. 146 | For example, if you want to change the default message for when a header does not match `mustMatch`: 147 | 148 | ```js 149 | { 150 | "notice/notice":["error", 151 | { 152 | "mustMatch":"Apache License", 153 | "templateFile":"config/apache.js", 154 | "messages":{ 155 | "whenFailedToMatch":"Couldn't find 'Apache License', are you sure you added it?" 156 | } 157 | } 158 | ] 159 | } 160 | ``` 161 | 162 | The three configurable messages are: 163 | 164 | - **whenFailedToMatch**: When the header fails to match the `mustMatch` pattern. 165 | - **reportAndSkip**: When using `"onNonMatchingHeader":"report"` and a non-matching notice is found. 166 | - **whenOutsideTolerance**: When you using `nonMatchingTolerance` to check for notice similarity and it fails to be similar enough. Passes in `similarity` as a template variable (eg `"The similarity is {{ similarity }}"`) 167 | --------------------------------------------------------------------------------