From ebca0b1215aa8d57aad511da41263c7ac265887b Mon Sep 17 00:00:00 2001 From: Noah Koontz Date: Thu, 1 Oct 2020 10:20:05 -0700 Subject: [PATCH] feat: add file-not-exists rule (#183) * feat: add file-not-exists rule --- docs/rules.md | 13 +++ rules/directory-existence.js | 2 +- rules/file-existence-config.json | 6 +- rules/file-not-exists-config.json | 17 ++++ rules/file-not-exists.js | 29 ++++++ rules/rules.js | 1 + rulesets/schema.json | 1 + tests/axioms/contributor_count_test.js | 3 + tests/cli/cli_tests.js | 3 + tests/formatters/markdown_formatter_tests.js | 2 +- tests/rules/file_not_exists_tests.js | 94 ++++++++++++++++++++ 11 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 rules/file-not-exists-config.json create mode 100644 rules/file-not-exists.js create mode 100644 tests/rules/file_not_exists_tests.js diff --git a/docs/rules.md b/docs/rules.md index c394a31f..9283501a 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -12,6 +12,7 @@ Below is a complete list of rules that Repolinter can run, along with their conf - [`file-existence`](#file-existence) - [`file-hash`](#file-hash) - [`file-not-contents`](#file-not-contents) + - [`file-not-exists`](#file-not-exists) - [`file-starts-with`](#file-starts-with) - [`file-type-exclusion`](#file-type-exclusion) - [`git-grep-commits`](#git-grep-commits) @@ -64,6 +65,7 @@ Checks the existence of a given file. | -------------- | -------- | ---------- | ------- | -------------------------------------------------------------------------------------------------- | | `globsAny` | **Yes** | `string[]` | | A list of globs to search for. This rule passes if at least one glob returns a file. | | `nocase` | No | `boolean` | `false` | Set to `true` to perform an case insensitive search. | +| `dirs` | No | `boolean` | `false` | Set to `true` to include directories in the search (equivalent to `directory-exists`) | | `fail-message` | No | `string` | `""` | The string to print if the directory does not exist, used to create human-readable error messages. | ### `file-hash` @@ -91,6 +93,17 @@ Checks none of a given list of files match a given regular expression. | `human-readable-content` | No | `string` | The regular expression in `content` | The string to print instead of the regular expression when generating human-readable output. | | `fail-on-non-exist` | No | `boolean` | `false` | Set to `true` to disable passing if no files are found from `globsAll`. | +### `file-not-exists` + +Checks that a file doesn't exist. + +| Input | Required | Type | Default | Description | +| -------------- | -------- | ---------- | ------- | --------------------------------------------------------------------------------------------- | +| `globsAll` | **Yes** | `string[]` | | A list of globs to search for. This rule fails if at least one glob returns a file. | +| `nocase` | No | `boolean` | `false` | Set to `true` to perform an case insensitive search. | +| `dirs` | No | `boolean` | `false` | Set to `true` to include directories in the search. | +| `pass-message` | No | `string` | `""` | The string to print if the file does not exist, used to create human-readable error messages. | + ### `file-starts-with` Checks that the first lines of a file contain a set of regular expressions. diff --git a/rules/directory-existence.js b/rules/directory-existence.js index 3fb40fb4..38417ada 100644 --- a/rules/directory-existence.js +++ b/rules/directory-existence.js @@ -3,5 +3,5 @@ const fileExistence = require('./file-existence') module.exports = function (fileSystem, opts) { - return fileExistence(fileSystem, Object.assign(opts, { dirs: true })) + return fileExistence(fileSystem, Object.assign({}, opts, { dirs: true })) } diff --git a/rules/file-existence-config.json b/rules/file-existence-config.json index abc39e2b..d834748e 100644 --- a/rules/file-existence-config.json +++ b/rules/file-existence-config.json @@ -11,7 +11,11 @@ "type": "array", "items": { "type": "string" } }, - "fail-message": { "type": "string" } + "fail-message": { "type": "string" }, + "dirs": { + "type": "boolean", + "default": false + } }, "oneOf": [ { "required": [ "globsAny" ] }, diff --git a/rules/file-not-exists-config.json b/rules/file-not-exists-config.json new file mode 100644 index 00000000..287a40d2 --- /dev/null +++ b/rules/file-not-exists-config.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://raw.githubusercontent.com/todogroup/repolinter/master/rules/file-not-exists-config.json", + "type": "object", + "properties": { + "nocase": { + "type": "boolean", + "default": false + }, + "globsAll": { + "type": "array", + "items": { "type": "string" } + }, + "pass-message": { "type": "string" } + }, + "required": ["globsAll"] +} diff --git a/rules/file-not-exists.js b/rules/file-not-exists.js new file mode 100644 index 00000000..6673344a --- /dev/null +++ b/rules/file-not-exists.js @@ -0,0 +1,29 @@ +// Copyright 2017 TODO Group. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const Result = require('../lib/result') +// eslint-disable-next-line no-unused-vars +const FileSystem = require('../lib/file_system') + +/** + * Check if a file is not present in the repository. Fails on the first file + * matching the glob pattern, succeeds if no file matching any of the patterns + * is found. + * + * @param {FileSystem} fs A filesystem object configured with filter paths and target directories + * @param {object} options The rule configuration + * @returns {Promise} The lint rule result + */ +async function fileNotExistence (fs, options) { + const fileList = options.globsAll + const file = options.dirs ? await fs.findAll(fileList, options.nocase) : await fs.findAllFiles(fileList, options.nocase) + + return file.length !== 0 + ? new Result('Found files', file.map(f => { return { passed: false, path: f } }), false) + : new Result( + `${options['pass-message'] !== undefined ? options['pass-message'] + '. ' : ''}Did not find a file matching the specified patterns`, + fileList.map(f => { return { pattern: f, passed: true } }), + true) +} + +module.exports = fileNotExistence diff --git a/rules/rules.js b/rules/rules.js index ce9d5991..583b3b6d 100644 --- a/rules/rules.js +++ b/rules/rules.js @@ -8,6 +8,7 @@ module.exports = [ 'file-existence', 'file-hash', 'file-not-contents', + 'file-not-exists', 'file-starts-with', 'file-type-exclusion', 'git-grep-commits', diff --git a/rulesets/schema.json b/rulesets/schema.json index 38445bb0..9ebd7671 100644 --- a/rulesets/schema.json +++ b/rulesets/schema.json @@ -53,6 +53,7 @@ { "if": { "properties": { "type": { "const": "file-existence" } } }, "then": { "properties": { "options": { "$ref": "../rules/file-existence-config.json" } } } }, { "if": { "properties": { "type": { "const": "file-hash" } } }, "then": { "properties": { "options": { "$ref": "../rules/file-hash-config.json" } } } }, { "if": { "properties": { "type": { "const": "file-not-contents" } } }, "then": { "properties": { "options": { "$ref": "../rules/file-not-contents-config.json" } } } }, + { "if": { "properties": { "type": { "const": "file-not-exists" } } }, "then": { "properties": { "options": { "$ref": "../rules/file-not-exists-config.json" } } } }, { "if": { "properties": { "type": { "const": "file-starts-with" } } }, "then": { "properties": { "options": { "$ref": "../rules/file-starts-with-config.json" } } } }, { "if": { "properties": { "type": { "const": "file-type-exclusion" } } }, "then": { "properties": { "options": { "$ref": "../rules/file-type-exclusion-config.json" } } } }, { "if": { "properties": { "type": { "const": "git-grep-commits" } } }, "then": { "properties": { "options": { "$ref": "../rules/git-grep-commits-config.json" } } } }, diff --git a/tests/axioms/contributor_count_test.js b/tests/axioms/contributor_count_test.js index 0b5bcd49..c1842f1a 100644 --- a/tests/axioms/contributor_count_test.js +++ b/tests/axioms/contributor_count_test.js @@ -1,3 +1,6 @@ +// Copyright 2017 TODO Group. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + const chai = require('chai') const expect = chai.expect const path = require('path') diff --git a/tests/cli/cli_tests.js b/tests/cli/cli_tests.js index 52465030..f9055ad2 100644 --- a/tests/cli/cli_tests.js +++ b/tests/cli/cli_tests.js @@ -1,3 +1,6 @@ +// Copyright 2017 TODO Group. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + const path = require('path') const chai = require('chai') const cp = require('child_process') diff --git a/tests/formatters/markdown_formatter_tests.js b/tests/formatters/markdown_formatter_tests.js index 3f019e72..dfe6d111 100644 --- a/tests/formatters/markdown_formatter_tests.js +++ b/tests/formatters/markdown_formatter_tests.js @@ -94,7 +94,7 @@ describe('formatters', () => { // console.debug(actual) // console.debug(JSON.stringify(res)) - expect(res.test).to.have.length(0) + expect(res.test).to.deep.equal([]) }) it('does not contain the string "undefined"', async function () { diff --git a/tests/rules/file_not_exists_tests.js b/tests/rules/file_not_exists_tests.js new file mode 100644 index 00000000..809f1160 --- /dev/null +++ b/tests/rules/file_not_exists_tests.js @@ -0,0 +1,94 @@ +// Copyright 2017 TODO Group. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const chai = require('chai') +const expect = chai.expect + +describe('rule', () => { + describe('files_not_exists', () => { + const fileNotExists = require('../../rules/file-not-exists') + + it('returns a passed result if no files exist', async () => { + /** @type {any} */ + const mockfs = { + findAllFiles () { + return [] + }, + targetDir: '.' + } + + const ruleopts = { + globsAll: ['LICENSE*'] + } + + const actual = await fileNotExists(mockfs, ruleopts) + + expect(actual.passed).to.equal(true) + expect(actual.targets).to.have.length(1) + expect(actual.targets[0].pattern).to.equal(ruleopts.globsAll[0]) + }) + + it('returns a passed result if no directories or files exist', async () => { + /** @type {any} */ + const mockfs = { + findAll () { + return [] + }, + targetDir: '.' + } + + const ruleopts = { + globsAll: ['LICENSE*'], + dirs: true + } + + const actual = await fileNotExists(mockfs, ruleopts) + + expect(actual.passed).to.equal(true) + expect(actual.targets).to.have.length(1) + expect(actual.targets[0].pattern).to.equal(ruleopts.globsAll[0]) + }) + + it('returns a failure result if requested file exists', async () => { + /** @type {any} */ + const mockfs = { + findAllFiles () { + return ['somefile'] + }, + targetDir: '.' + } + + const ruleopts = { + globsAll: ['LICENSE*'] + } + + const actual = await fileNotExists(mockfs, ruleopts) + + expect(actual.passed).to.equal(false) + expect(actual.targets).to.have.length(1) + expect(actual.targets[0]).to.deep.include({ passed: false, path: 'somefile' }) + }) + + it('returns a pass result if requested file doesn\'t exist with a pass message', async () => { + /** @type {any} */ + const mockfs = { + findAllFiles () { + return [] + }, + targetDir: '.' + } + + const ruleopts = { + globsAll: ['LICENSE*'], + 'pass-message': 'The license file should exist.' + } + + const actual = await fileNotExists(mockfs, ruleopts) + + expect(actual.passed).to.equal(true) + expect(actual.targets).to.have.length(1) + expect(actual.targets[0].pattern).to.equal(ruleopts.globsAll[0]) + expect(actual.message).to.contain(ruleopts['pass-message']) + }) + }) +})