Skip to content

Commit

Permalink
fix: run checks against any file provided by user and skip regex pattern
Browse files Browse the repository at this point in the history
ref: #23
  • Loading branch information
zavoloklom committed Oct 1, 2024
1 parent f3f996c commit 8a8e59e
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 29 deletions.
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
58 changes: 32 additions & 26 deletions src/util/files-finder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fs from 'node:fs';
import path from 'node:path';
import { basename, join, resolve } from 'node:path';
import { Logger } from './logger.js';
import { FileNotFoundError } from '../errors/file-not-found-error.js';

Expand Down Expand Up @@ -29,36 +29,42 @@ 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);
}

allFiles.forEach((file) => {
const fileStat = fs.statSync(file);
const fileOrDirStats = fs.statSync(fileOrDir);

// Skip files and directories listed in the exclude array
if (exclude.some((ex) => file.includes(ex))) {
logger.debug('UTIL', `Excluding ${file}`);
return;
if (fileOrDirStats.isDirectory()) {
try {
allPaths = fs.readdirSync(resolve(fileOrDir)).map((f) => join(fileOrDir, f));
} catch (error) {
logger.debug('UTIL', `Error reading directory: ${fileOrDir}`, error);
allPaths = [];
}

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);
allPaths.forEach((path) => {
// Skip files and directories listed in the exclude array
if (exclude.some((ex) => path.includes(ex))) {
logger.debug('UTIL', `Excluding ${path}`);
return;
}
} else if (fileStat.isFile() && dockerComposePattern.test(path.basename(file))) {
// Add the file to the list if it matches the pattern
filesToCheck.push(file);
}
});

const pathStats = fs.statSync(resolve(path));

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 (fileOrDirStats.isFile()) {
filesToCheck.push(fileOrDir);
}
});

logger.debug(
Expand Down
File renamed without changes.
108 changes: 108 additions & 0 deletions tests/util/files-finder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* 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<typeof import('../../src/util/files-finder.js')>(
'../../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 and search only compose in directory', async (t) => {
// Use esmock to mock fs module
const { findFilesForLinting } = await esmock<typeof import('../../src/util/files-finder.js')>(
'../../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');

const resultWithDirectory = findFilesForLinting([mockAnotherFile, mockFolderDirectory], false, []);

t.deepEqual(
resultWithDirectory,
[mockAnotherFile, mockSubDirectoryFile],
'Should return the another file directly when passed',
);
});

test('findFilesForLinting: should throw error if path does not exist', async (t) => {
// Use esmock to mock fs module
const { findFilesForLinting } = await esmock<typeof import('../../src/util/files-finder.js')>(
'../../src/util/files-finder.js',
{
'node:fs': { existsSync: () => false },
},
);

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',
);
});

0 comments on commit 8a8e59e

Please sign in to comment.