diff --git a/docs/md/rules.md b/docs/md/rules.md index c394a31f..9283501a 100644 --- a/docs/md/rules.md +++ b/docs/md/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..f232c7a3 --- /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 fileExistence (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 = fileExistence 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 eeb29cbb..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" } } } }, @@ -66,7 +67,8 @@ "default": {}, "allOf": [ { "if": { "properties": { "type": { "const": "file-modify" } } }, "then": { "properties": { "options": { "$ref": "../fixes/file-modify-config.json" } } } }, - { "if": { "properties": { "type": { "const": "file-create" } } }, "then": { "properties": { "options": { "$ref": "../fixes/file-create-config.json" } } } } + { "if": { "properties": { "type": { "const": "file-create" } } }, "then": { "properties": { "options": { "$ref": "../fixes/file-create-config.json" } } } }, + { "if": { "properties": { "type": { "const": "file-remove" } } }, "then": { "properties": { "options": { "$ref": "../fixes/file-remove-config.json" } } } } ] }, "policyInfo": { "type": "string" }, diff --git a/tests/fixes/file_remove_tests.js b/tests/fixes/file_remove_tests.js new file mode 100644 index 00000000..808a56b0 --- /dev/null +++ b/tests/fixes/file_remove_tests.js @@ -0,0 +1,113 @@ +// Copyright 2017 TODO Group. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const chai = require('chai') +const expect = chai.expect + +describe('fixes', () => { + describe('file-remove', () => { + const fileRemove = require('../../fixes/file-remove') + + it('removes a file', async () => { + const removePaths = [] + /** @type {any} */ + const mockFs = { + removeFile (path) { + removePaths.push(path) + } + } + + const res = await fileRemove(mockFs, {}, ['myfile'], false) + expect(res.passed).to.equal(true) + expect(res.targets).to.have.length(1) + expect(res.targets[0].passed).to.equal(true) + expect(res.targets[0].path).to.equal('myfile') + expect(removePaths).to.deep.equal(['myfile']) + }) + + it('does nothing if dryRun is true', async () => { + const removePaths = [] + /** @type {any} */ + const mockFs = { + removeFile (path) { + removePaths.push(path) + } + } + + const res = await fileRemove(mockFs, {}, ['myfile'], true) + expect(res.passed).to.equal(true) + expect(res.targets).to.have.length(1) + expect(res.targets[0].passed).to.equal(true) + expect(res.targets[0].path).to.equal('myfile') + expect(removePaths).to.deep.equal([]) + }) + + it('removes multiple files', async () => { + const removePaths = [] + /** @type {any} */ + const mockFs = { + removeFile (path) { + removePaths.push(path) + } + } + + const res = await fileRemove(mockFs, {}, ['myfile', 'otherfile'], false) + expect(res.passed).to.equal(true) + expect(res.targets).to.have.length(2) + expect(res.targets[0].passed).to.equal(true) + expect(res.targets[0].path).to.equal('myfile') + expect(res.targets[1].passed).to.equal(true) + expect(res.targets[1].path).to.equal('otherfile') + expect(removePaths).to.deep.equal(['myfile', 'otherfile']) + }) + + it('uses the glob option', async () => { + const removePaths = [] + /** @type {any} */ + const mockFs = { + removeFile (path) { + removePaths.push(path) + }, + findAllFiles () { + return ['myfile.txt'] + } + } + + const res = await fileRemove(mockFs, { globsAll: ['myfile'] }, [], false) + expect(res.passed).to.equal(true) + expect(res.targets).to.have.length(1) + expect(res.targets[0].passed).to.equal(true) + expect(res.targets[0].path).to.equal('myfile.txt') + expect(removePaths).to.deep.equal(['myfile.txt']) + }) + + it('overrides targets with the glob option', async () => { + const removePaths = [] + /** @type {any} */ + const mockFs = { + removeFile (path) { + removePaths.push(path) + }, + findAllFiles () { + return ['myfile.txt'] + } + } + + const res = await fileRemove(mockFs, { globsAll: ['myfile'] }, ['otherfile'], false) + expect(res.passed).to.equal(true) + expect(res.targets).to.have.length(1) + expect(res.targets[0].passed).to.equal(true) + expect(res.targets[0].path).to.equal('myfile.txt') + expect(removePaths).to.deep.equal(['myfile.txt']) + }) + + it('returns failure if no files are found', async () => { + /** @type {any} */ + const mockFs = {} + + const res = await fileRemove(mockFs, {}, [], false) + expect(res.passed).to.equal(false) + expect(res.targets).to.have.length(0) + }) + }) +}) 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']) + }) + }) +})