From 1b72bbf140e88b67aa22f88879ddcfcc32eb71f8 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Tue, 1 Oct 2024 12:53:37 +0200 Subject: [PATCH] fix: run checks against any file provided by user and skip regex pattern ref: #23 --- README.md | 25 +++- src/util/files-finder.ts | 53 ++++---- ...r-compose.local.yml => compose.local.yaml} | 0 tests/util/files-finder.spec.ts | 116 ++++++++++++++++++ 4 files changed, 165 insertions(+), 29 deletions(-) rename tests/mocks/{docker-compose.local.yml => compose.local.yaml} (100%) create mode 100644 tests/util/files-finder.spec.ts diff --git a/README.md b/README.md index 49ee753..0979a5c 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ npx dclint . This command will lint your Docker Compose files in the current directory. -### Linting Specific Files +### Linting Specific Files and Directories To lint a specific Docker Compose file or a directory containing such files, specify the path relative to your project directory: @@ -59,6 +59,20 @@ project directory: npx dclint /path/to/docker-compose.yml ``` +To lint all Docker Compose files in a specific directory, use the path to the directory: + +```shell +npx dclint /path/to/directory +``` + +In this case, `dclint` will search the specified directory for files matching the following +pattern `/^(docker-)?compose.*\.ya?ml$/`. + +It will handle all matching files within the directory and, if [recursive search](./docs/cli.md#-r---recursive) is +enabled, also in any subdirectories. + +Files and directories like `node_modules`, `.git,` or others specified in the exclusion list will be ignored. + ### Display Help and Options To display help and see all available options: @@ -89,7 +103,7 @@ To lint your Docker Compose files, use the following command. This command mount docker run -t --rm -v ${PWD}:/app zavoloklom/dclint . ``` -### Linting Specific Files in Docker +### Linting Specific Files and Directories in Docker If you want to lint a specific Docker Compose file or a directory containing such files, specify the path relative to your project directory: @@ -98,6 +112,10 @@ to your project directory: docker run -t --rm -v ${PWD}:/app zavoloklom/dclint /app/path/to/docker-compose.yml ``` +```shell +docker run -t --rm -v ${PWD}:/app zavoloklom/dclint /app/path/to/directory +``` + ### Display Help in Docker To display help and see all available options: @@ -168,7 +186,8 @@ Here is an example of a configuration file using JSON format: ### Configure Rules In addition to enabling or disabling rules, some rules may support custom parameters to tailor them to your specific -needs. For example, the [require-quotes-in-ports](./docs/rules/require-quotes-in-ports-rule.md) rule allows you to configure +needs. For example, the [require-quotes-in-ports](./docs/rules/require-quotes-in-ports-rule.md) rule allows you to +configure whether single or double quotes should be used around port numbers. You can configure it like this: ```json diff --git a/src/util/files-finder.ts b/src/util/files-finder.ts index 64ca7bd..4549252 100644 --- a/src/util/files-finder.ts +++ b/src/util/files-finder.ts @@ -1,5 +1,5 @@ import fs from 'node:fs'; -import path from 'node:path'; +import { basename, join } from 'node:path'; import { Logger } from './logger.js'; import { FileNotFoundError } from '../errors/file-not-found-error.js'; @@ -29,36 +29,37 @@ export function findFilesForLinting(paths: string[], recursive: boolean, exclude throw new FileNotFoundError(fileOrDir); } - let allFiles: string[] = []; + let allPaths: string[] = []; - const stat = fs.statSync(fileOrDir); - if (stat.isDirectory()) { - allFiles = fs.readdirSync(fileOrDir).map((f) => path.join(fileOrDir, f)); - } else if (stat.isFile()) { - allFiles.push(fileOrDir); - } + const fileOrDirStats = fs.statSync(fileOrDir); + + if (fileOrDirStats.isDirectory()) { + allPaths = fs.readdirSync(fileOrDir).map((f) => join(fileOrDir, f)); - allFiles.forEach((file) => { - const fileStat = fs.statSync(file); + allPaths.forEach((path) => { + const pathStats = fs.statSync(path); - // Skip files and directories listed in the exclude array - if (exclude.some((ex) => file.includes(ex))) { - logger.debug('UTIL', `Excluding ${file}`); - return; - } + // Skip files and directories listed in the exclude array + if (exclude.some((ex) => path.includes(ex))) { + logger.debug('UTIL', `Excluding ${path}`); + return; + } - if (fileStat.isDirectory()) { - if (recursive) { - // If recursive search is enabled, search within the directory - logger.debug('UTIL', `Recursive search is enabled, search within the directory: ${file}`); - const nestedFiles = findFilesForLinting([file], recursive, exclude); - filesToCheck = filesToCheck.concat(nestedFiles); + if (pathStats.isDirectory()) { + if (recursive) { + // If recursive search is enabled, search within the directory + logger.debug('UTIL', `Recursive search is enabled, search within the directory: ${path}`); + const nestedFiles = findFilesForLinting([path], recursive, exclude); + filesToCheck = filesToCheck.concat(nestedFiles); + } + } else if (pathStats.isFile() && dockerComposePattern.test(basename(path))) { + // Add the file to the list if it matches the pattern + filesToCheck.push(path); } - } else if (fileStat.isFile() && dockerComposePattern.test(path.basename(file))) { - // Add the file to the list if it matches the pattern - filesToCheck.push(file); - } - }); + }); + } else if (fileOrDirStats.isFile()) { + filesToCheck.push(fileOrDir); + } }); logger.debug( diff --git a/tests/mocks/docker-compose.local.yml b/tests/mocks/compose.local.yaml similarity index 100% rename from tests/mocks/docker-compose.local.yml rename to tests/mocks/compose.local.yaml diff --git a/tests/util/files-finder.spec.ts b/tests/util/files-finder.spec.ts new file mode 100644 index 0000000..cd41739 --- /dev/null +++ b/tests/util/files-finder.spec.ts @@ -0,0 +1,116 @@ +/* eslint-disable sonarjs/no-duplicate-string, @stylistic/indent */ + +import test from 'ava'; +import esmock from 'esmock'; +import { Logger } from '../../src/util/logger.js'; +import { FileNotFoundError } from '../../src/errors/file-not-found-error.js'; + +const mockDirectory = '/path/to/directory'; +const mockNodeModulesDirectory = '/path/to/directory/node_modules'; +const mockFolderDirectory = '/path/to/directory/another_dir'; +const mockDockerComposeFile = '/path/to/directory/docker-compose.yml'; +const mockComposeFile = '/path/to/directory/compose.yaml'; +const mockAnotherFile = '/path/to/directory/another-file.yaml'; +const mockSubDirectoryFile = '/path/to/directory/another_dir/docker-compose.yml'; +const mockNonExistentPath = '/path/nonexistent'; + +// Mock files and directories for testing +const mockFilesInDirectory = ['docker-compose.yml', 'compose.yaml', 'another-file.yaml', 'example.txt']; +const mockDirectoriesInDirectory = ['another_dir', 'node_modules']; +const mockFilesInSubDirectory = ['docker-compose.yml', 'another-file.yaml', 'example.txt']; + +test.beforeEach(() => { + Logger.init(false); // Initialize logger +}); + +const mockReaddirSync = (dir: string): string[] => { + if (dir === mockDirectory) { + return [...mockFilesInDirectory, ...mockDirectoriesInDirectory]; + } + if (dir === mockNodeModulesDirectory || dir === mockFolderDirectory) { + return mockFilesInSubDirectory; + } + return []; +}; + +const mockStatSync = (filePath: string) => { + const isDirectory = + filePath === mockDirectory || filePath === mockNodeModulesDirectory || filePath === mockFolderDirectory; + return { + isDirectory: () => isDirectory, + isFile: () => !isDirectory, + }; +}; +const mockExistsSync = () => true; + +test('findFilesForLinting: should handle recursive search and find only compose files in directory and exclude node_modules', async (t) => { + // Use esmock to mock fs module + const { findFilesForLinting } = await esmock( + '../../src/util/files-finder.js', + { + 'node:fs': { existsSync: mockExistsSync, readdirSync: mockReaddirSync, statSync: mockStatSync }, + }, + ); + + const result = findFilesForLinting([mockDirectory], false, []); + + t.deepEqual(result, [mockDockerComposeFile, mockComposeFile], 'Should return only compose files on higher level'); + + const resultRecursive = findFilesForLinting([mockDirectory], true, []); + + t.deepEqual( + resultRecursive, + [mockDockerComposeFile, mockComposeFile, mockSubDirectoryFile], + 'Should should handle recursive search and return only compose files and exclude files in node_modules subdirectory', + ); +}); + +test('findFilesForLinting: should return file directly if file is passed', async (t) => { + // Use esmock to mock fs module + const { findFilesForLinting } = await esmock( + '../../src/util/files-finder.js', + { + 'node:fs': { existsSync: mockExistsSync, statSync: mockStatSync, readdirSync: mockReaddirSync }, + }, + ); + + const result = findFilesForLinting([mockAnotherFile], false, []); + + t.deepEqual(result, [mockAnotherFile], 'Should return the another file directly when passed'); +}); + +test('findFilesForLinting: should return file directly if file is passed and search only compose in directory', async (t) => { + // Use esmock to mock fs module + const { findFilesForLinting } = await esmock( + '../../src/util/files-finder.js', + { + 'node:fs': { existsSync: mockExistsSync, statSync: mockStatSync, readdirSync: mockReaddirSync }, + }, + ); + + const result = findFilesForLinting([mockAnotherFile, mockFolderDirectory], false, []); + + t.deepEqual(result, [mockAnotherFile, mockSubDirectoryFile], 'Should return the another file directly when passed'); +}); + +test('findFilesForLinting: should throw error if path does not exist', async (t) => { + const mockExistsSync = () => false; + + // Use esmock to mock fs module + const { findFilesForLinting } = await esmock( + '../../src/util/files-finder.js', + { + 'node:fs': { existsSync: mockExistsSync }, + }, + ); + + const error = t.throws(() => findFilesForLinting([mockNonExistentPath], false, []), { + instanceOf: FileNotFoundError, + }); + + t.is( + error.message, + `File or directory not found: ${mockNonExistentPath}`, + 'Should throw FileNotFoundError if path does not exist', + ); +});