diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5f5b64..8766027 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,25 @@ Please note that this project is released with a [Contributor Code of Conduct](./CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its causes. +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Contribution Process](#contribution-process) + - [1. Fork and Clone](#1-fork-and-clone) + - [2. Create a Branch](#2-create-a-branch) + - [3. Development](#3-development) + - [4. Verify Everything](#4-verify-everything) + - [5. Push Changes](#5-push-changes) + - [6. Create a Pull Request](#6-create-a-pull-request) + - [7. Code Review](#7-code-review) + - [8. Merge](#8-merge) +3. [Commit Message Conventions](#commit-message-conventions) +4. [How to Add a New Rule](#how-to-add-a-new-rule) +5. [How to Build the Project](#how-to-build-the-project) +6. [How to Build SEA](#how-to-build-sea) +7. [How to Build Docker Locally](#how-to-build-docker-locally) +8. [How to Update Compose Schema](#how-to-update-compose-schema) + ## Prerequisites Before making contributions, ensure the following: @@ -13,15 +32,132 @@ Before making contributions, ensure the following: ## Contribution Process -1. **Fork and Clone**: Fork this project on GitHub and clone your fork locally. -2. **Create a Branch**: Create a new branch in your local repository. This keeps your changes organized and separate - from the main project. -3. **Development**: Make your changes in your branch. Here are a few things to keep in mind: - - **No Lint Errors**: Ensure your code changes adhere to the project's linting rules and do not introduce new lint - errors. - - **Testing**: All changes must be accompanied by passing tests. Add new tests if you are adding functionality or fix - existing tests if you are changing code. - - **Commit Convention**: Commit message must follow our [Commit Message Conventions](#commit-message-conventions). +### 1. Fork and Clone + +- **Fork**: Navigate to the repository on GitHub and click the "Fork" button to create a copy of the repository in your + own GitHub account. +- **Clone**: Clone your forked repository to your local machine using the following command: + + ```bash + git clone https://github.com//docker-compose-linter.git + ``` + +- Change into the project directory: + + ```bash + cd docker-compose-linter + ``` + +### 2. Create a Branch + +- Create a new branch to work on your changes. Use a descriptive name for the branch, such as `feature/add-new-rule` or + `fix/linting-error`: + + ```bash + git checkout -b + ``` + +### 3. Development + +Make your changes in the newly created branch. Follow these steps to ensure quality and consistency: + +#### No Lint Errors + +- Check your code against the project's linting rules to ensure consistency: + + ```bash + npm run lint + ``` + +- If linting errors are reported, fix them before proceeding. You can also use the following command to auto-fix issues: + + ```bash + # eslint + npm run eslint:fix + + # md files + npm run docs:fix + ``` + +#### Testing + +- All code changes must pass existing tests. If you are adding new functionality, ensure you write appropriate tests to + validate it. +- To run all tests: + + ```bash + npm run test + ``` + +- If a test fails, resolve the issue before proceeding. For new features or bug fixes, include corresponding test cases. + +#### Commit Convention + +- Commit messages must follow our [Commit Message Conventions](#commit-message-conventions). This ensures clear and + meaningful commit history. +- Examples: + - For a bug fix: `fix: resolve crash when parsing invalid YAML` + - For a new feature: `feat: add support for custom validation schemas` +- Use the following command to commit your changes: + + ```bash + git commit -m ": " + ``` + +#### Documentation Updates + +- If your changes impact the documentation (e.g., adding a new rule or updating an existing feature), ensure you update + the relevant files in the `docs` folder. +- Validate your changes with `npm run docs:check` to ensure there are no issues. + +### 4. Verify Everything + +Before submitting your changes: + +- **Run all tests**: Verify that your changes have not introduced any test failures. + + ```bash + npm run test + ``` + +- **Build the project**: Ensure that the project builds successfully to verify that no issues have been introduced in + the build process: + + ```bash + npm run build + ``` + + For more details on the build process and available configurations, refer to the + [How to Build the Project](#how-to-build-the-project) section. + +- **Confirm functionality**: Ensure that your changes (e.g., new rules, fixes) work as intended using the linter. + +### 5. Push Changes + +Push your branch to your forked repository: + +```bash +git push origin +``` + +### 6. Create a Pull Request + +- Navigate to the original repository on GitHub and click the "Compare & Pull Request" button for your branch. +- Provide a clear and detailed description of your changes in the pull request. + +### 7. Code Review + +- Once your pull request is submitted, it will undergo a review process. +- Be open to feedback and make adjustments as needed. Push updates to your branch, and they will automatically appear in + the pull request. + +### 8. Merge + +- After your pull request has been approved and all checks have passed, it will be merged into the main repository. + +Once your contribution is merged, it will become part of the project. I appreciate your hard work and contribution to +making this tool better. Also, I encourage you to continue participating in the project and joining in discussions and +future enhancements. ## Commit Message Conventions @@ -97,6 +233,9 @@ fixable, this method can return the content unchanged. 1. Go to the `docs/rules/` folder. 2. Create a markdown file describing your new rule (for example `new-check-rule.md`) based on [template](./docs/rules/__TEMPLATE__.md) +3. Run `npm run docs:check` to validate documentation. +4. Use the `npm run docs:update` script to automatically update the documentation for the rule from the source. +5. If there are formatting issues, run `npm run docs:fix` to automatically resolve them. ## How to Build the Project @@ -252,7 +391,7 @@ ldd /bin/dclint libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 ``` -## Build Docker File Locally +## How to Build Docker Locally ```shell docker build --file Dockerfile . --tag zavoloklom/dclint:dev \ @@ -302,24 +441,3 @@ schema update: ```shell npm run test ``` - -## Submitting Changes - -After you've made your changes: - -1. **Run Linters and Tests**: Before submitting your changes, run the linters and tests to ensure everything is in - order. -2. **Push to GitHub**: Push your changes to your fork on GitHub. -3. **Create a Merge Request**: Open a merge request from your fork/branch to the main repository on GitHub. Provide a - clear and detailed description of your changes and why they are necessary. -4. **Code Review**: Once your merge request is opened, it will be reviewed by other contributors. Be open to feedback - and willing to make further adjustments based on the discussions. -5. **Merge**: If your merge request passes the review, it will be merged into the main codebase. - -## After Your Contribution - -Once your contribution is merged, it will become part of the project. I appreciate your hard work and contribution to -making this tool better. Also, I encourage you to continue participating in the project and joining in discussions and -future enhancements. - -**Thank you for contributing!** diff --git a/docs/rules/__TEMPLATE__.md b/docs/rules/__TEMPLATE__.md index 7521ca2..95fddac 100644 --- a/docs/rules/__TEMPLATE__.md +++ b/docs/rules/__TEMPLATE__.md @@ -1,39 +1,57 @@ -# Rule name +# {{Capital Case ruleName}} Rule -Short rule description. +A concise and clear description of what this rule does and why it's important. -- **Rule Name:** -- **Type:** -- **Category:** -- **Severity:** -- **Fixable:** +- **Rule Name:** {{kebabCase ruleName}} +- **Type:** 'warning' | 'error' +- **Category:** 'style' | 'security' | 'best-practice' | 'performance' +- **Severity:** 'info' | 'minor' | 'major' | 'critical' +- **Fixable:** true | false ## Problematic Code Example ```yaml - +# Provide an example ``` ## Correct Code Example ```yaml - +# Provide an example ``` ## Rule Details and Rationale -Long rule description, why you should use it +Provide a detailed explanation of the rationale for this rule, including: + +- **Why this rule exists:** Explain the problems it aims to solve. +- **How it works:** Describe its scope and logic. +- **Rationale:** Clarify the benefit or importance of following the rule. + +## Options + +> OPTIONAL: If this section is not applicable, please remove it. -## Options /OPTIONAL/ +If this rule accepts any configuration options, explain them here. -## Known Limitations /OPTIONAL/ +## Known Limitations -## When Not To Use It /OPTIONAL/ +> OPTIONAL: If this section is not applicable, please remove it. + +Document any edge cases or situations where this rule might not work as expected. + +## When Not To Use It + +> OPTIONAL: If this section is not applicable, please remove it. + +Explain scenarios where disabling this rule might make sense or be necessary. ## Version -This rule was introduced in [v1.0.0](https://github.com/zavoloklom/docker-compose-linter/releases). +This rule was introduced in [v{{nextMajorVersion}}](https://github.com/zavoloklom/docker-compose-linter/releases). ## References +Provide all necessary references for this rule. + - [Reference link](https://example.com) diff --git a/package-lock.json b/package-lock.json index f3b5fb0..6c03899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,8 @@ }, "devDependencies": { "@babel/preset-env": "7.26.0", - "@commitlint/cli": "^19.6.0", - "@commitlint/config-conventional": "^19.6.0", + "@commitlint/cli": "19.6.0", + "@commitlint/config-conventional": "19.6.0", "@rollup/plugin-babel": "6.0.4", "@rollup/plugin-commonjs": "28.0.1", "@rollup/plugin-json": "6.1.0", @@ -43,6 +43,7 @@ "@typescript-eslint/parser": "7.14.1", "ava": "6.2.0", "c8": "10.1.2", + "change-case": "5.4.4", "conventional-changelog-conventionalcommits": "8.0.0", "eslint": "8.57.1", "eslint-config-airbnb-base": "15.0.0", @@ -55,7 +56,7 @@ "eslint-plugin-sonarjs": "1.0.3", "eslint-plugin-unicorn": "56.0.1", "esmock": "2.6.9", - "husky": "^9.1.7", + "husky": "9.1.7", "markdownlint-cli2": "0.16.0", "semantic-release": "24.2.0", "tap-xunit": "2.4.1", @@ -5021,6 +5022,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", diff --git a/package.json b/package.json index fbb56cc..544c597 100644 --- a/package.json +++ b/package.json @@ -36,25 +36,28 @@ "schemas" ], "scripts": { - "commitlint": "commitlint --from=origin/main", "build": "npm run build:lib & npm run build:cli & npm run build:pkg", "build:cli": "rimraf bin && rollup -c rollup.config.cli.js", "build:lib": "rimraf dist && rollup -c rollup.config.lib.js", "build:pkg": "rimraf pkg && rollup -c rollup.config.pkg.js", + "changelog:fix": "markdownlint-cli2 --fix \"CHANGELOG.md\" && prettier --write \"CHANGELOG.md\"", + "commitlint": "commitlint --from=origin/main", "debug": "tsc && node --import=tsimp/import --no-warnings --inspect ./src/cli/cli.ts ./tests/mocks/docker-compose.yml -c ./tests/mocks/.dclintrc", "debug:bin": "node ./bin/dclint.cjs ./tests/mocks/docker-compose.correct.yml --fix", + "docs:check": "node --import=tsimp/import ./scripts/check-documentation.ts && npm run markdownlint", + "docs:fix": "npm run prettier && npm run markdownlint:fix", + "docs:update": "node --import=tsimp/import ./scripts/update-documentation.ts", "eslint": "eslint .", "eslint:fix": "eslint . --fix", - "lint": "npm run eslint && npm run markdownlint && npm run commitlint", + "lint": "npm run eslint && npm run docs:check && npm run commitlint", "markdownlint": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#**/node_modules\"", "markdownlint:fix": "markdownlint-cli2 --fix \"**/*.md\" \"#node_modules\" \"#**/node_modules\"", - "markdownlint:fix-changelog": "markdownlint-cli2 --fix \"CHANGELOG.md\" && prettier --write \"CHANGELOG.md\"", + "prepare": "husky", "prettier": "prettier --write \"**/*.md\"", "test": "ava --verbose", "test:coverage": "rimraf coverage && mkdir -p coverage && c8 ava --tap | tap-xunit --package='dclint' > ./coverage/junit.xml", "tsc": "tsc", - "update-compose-schema": "node --import=tsimp/import ./scripts/download-compose-schema.ts", - "prepare": "husky" + "update-compose-schema": "node --import=tsimp/import ./scripts/download-compose-schema.ts" }, "dependencies": { "ajv": "^8.17.1", @@ -65,8 +68,8 @@ }, "devDependencies": { "@babel/preset-env": "7.26.0", - "@commitlint/cli": "^19.6.0", - "@commitlint/config-conventional": "^19.6.0", + "@commitlint/cli": "19.6.0", + "@commitlint/config-conventional": "19.6.0", "@rollup/plugin-babel": "6.0.4", "@rollup/plugin-commonjs": "28.0.1", "@rollup/plugin-json": "6.1.0", @@ -88,6 +91,7 @@ "@typescript-eslint/parser": "7.14.1", "ava": "6.2.0", "c8": "10.1.2", + "change-case": "5.4.4", "conventional-changelog-conventionalcommits": "8.0.0", "eslint": "8.57.1", "eslint-config-airbnb-base": "15.0.0", @@ -100,7 +104,7 @@ "eslint-plugin-sonarjs": "1.0.3", "eslint-plugin-unicorn": "56.0.1", "esmock": "2.6.9", - "husky": "^9.1.7", + "husky": "9.1.7", "markdownlint-cli2": "0.16.0", "semantic-release": "24.2.0", "tap-xunit": "2.4.1", diff --git a/release.config.js b/release.config.js index 71b1eff..28e127f 100644 --- a/release.config.js +++ b/release.config.js @@ -14,7 +14,7 @@ export default { [ '@semantic-release/exec', { - prepareCmd: 'npm run markdownlint:fix-changelog || true', + prepareCmd: 'npm run changelog:fix || true', }, ], [ diff --git a/scripts/check-documentation.ts b/scripts/check-documentation.ts new file mode 100644 index 0000000..5c6d00f --- /dev/null +++ b/scripts/check-documentation.ts @@ -0,0 +1,121 @@ +/* eslint-disable sonarjs/cognitive-complexity */ + +import fs from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { loadLintRules, getRuleDefinition } from '../src/util/rules-utils'; +import type { LintRuleDefinition } from '../src/linter/linter.types'; + +const documentationDirectory = join(dirname(fileURLToPath(import.meta.url)), '../docs'); + +let hasValidationErrors = false; + +async function validateDocumentation(ruleDefinition: LintRuleDefinition) { + const documentFilePath = join(documentationDirectory, `rules/${ruleDefinition.name}-rule.md`); + + try { + const content = await fs.readFile(documentFilePath, 'utf8'); + + const ruleNameMatch = content.match(/- \*\*Rule Name:\*\* (.+)/); + if (!ruleNameMatch || !ruleNameMatch[1].trim()) { + console.warn(`Rule Name is missing or empty in metadata block of ${documentFilePath}`); + hasValidationErrors = true; + } + + const typeMatch = content.match(/- \*\*Type:\*\* (.+)/); + if (!typeMatch || !typeMatch[1].trim()) { + console.warn(`Type is missing or empty in metadata block of ${documentFilePath}`); + hasValidationErrors = true; + } + + const categoryMatch = content.match(/- \*\*Category:\*\* (.+)/); + if (!categoryMatch || !categoryMatch[1].trim()) { + console.warn(`Category is missing or empty in metadata block of ${documentFilePath}`); + hasValidationErrors = true; + } + + const severityMatch = content.match(/- \*\*Severity:\*\* (.+)/); + if (!severityMatch || !severityMatch[1].trim()) { + console.warn(`Severity is missing or empty in metadata block of ${documentFilePath}`); + hasValidationErrors = true; + } + + const fixableMatch = content.match(/- \*\*Fixable:\*\* (.+)/); + if (!fixableMatch || !fixableMatch[1].trim()) { + console.warn(`Fixable is missing or empty in metadata block of ${documentFilePath}`); + hasValidationErrors = true; + } + + const problematicExampleMatch = content.match(/## Problematic Code Example[^\n]*\n+```yaml\n([\s\S]*?)```/i); + const correctExampleMatch = content.match(/## Correct Code Example[^\n]*\n+```yaml\n([\s\S]*?)```/i); + + const hasProblematicExample = problematicExampleMatch && problematicExampleMatch[1].trim().length >= 20; + const hasCorrectExample = correctExampleMatch && correctExampleMatch[1].trim().length >= 20; + + const detailsMatch = content.match(/## Rule Details and Rationale\n\n([^#]+)/); + const detailsContent = detailsMatch ? detailsMatch[1].trim() : ''; + const hasDetails = detailsContent.length >= 200; + + const hasVersion = /## Version\n\n.*?\[v\d+\.\d+\.\d+]\(.*?\)/.test(content); + const hasReferences = /## References\n\n- \[.*?]\(.*?\)/.test(content); + + if (ruleDefinition.hasOptions) { + const hasOptionsSection = /## Options\n\n/.test(content); + if (!hasOptionsSection) { + console.warn(`Missing Options section for rule with options in ${documentFilePath}`); + hasValidationErrors = true; + } + } + + if (!hasProblematicExample) { + console.warn(`Missing or incomplete problematic code example in ${documentFilePath}`); + hasValidationErrors = true; + } + if (!hasCorrectExample) { + console.warn(`Missing or incomplete correct code example in ${documentFilePath}`); + hasValidationErrors = true; + } + if (!hasDetails) { + console.warn(`Rule details and rationale section is too short in ${documentFilePath}`); + hasValidationErrors = true; + } + if (!hasVersion) { + console.warn(`Version section is missing or invalid in ${documentFilePath}`); + hasValidationErrors = true; + } + if (!hasReferences) { + console.warn(`References section is missing or invalid in ${documentFilePath}`); + hasValidationErrors = true; + } + console.log(`Validation completed: ${documentFilePath}`); + } catch (error) { + hasValidationErrors = true; + if (error instanceof Error) { + console.error(`Error validating ${documentFilePath}: ${error.message}`); + } else { + console.error(`Unexpected error: ${JSON.stringify(error)}`); + } + } +} + +async function main() { + const rules = await loadLintRules({ rules: {}, quiet: false, debug: false, exclude: [] }); + const promises = []; + + for (const rule of rules) { + const ruleDefinition = getRuleDefinition(rule); + promises.push(validateDocumentation(ruleDefinition)); + } + + await Promise.all(promises); + + if (hasValidationErrors) { + console.error('\nValidation failed with errors.'); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); + } else { + console.log('\nAll validations passed successfully.'); + } +} + +await main(); diff --git a/scripts/update-documentation.ts b/scripts/update-documentation.ts new file mode 100644 index 0000000..c5077bc --- /dev/null +++ b/scripts/update-documentation.ts @@ -0,0 +1,114 @@ +/* eslint-disable import/no-extraneous-dependencies */ + +import fs from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import * as changeCase from 'change-case'; +import { format } from 'prettier'; +import { loadLintRules, getRuleDefinition } from '../src/util/rules-utils'; +import type { LintRuleDefinition } from '../src/linter/linter.types'; + +const documentationDirectory = join(dirname(fileURLToPath(import.meta.url)), '../docs'); + +function generateRulesTable(ruleDefinitionList: LintRuleDefinition[]) { + const tableHeader = ` +| Name | Description | | +|------|-------------|---|`; + const tableRows = ruleDefinitionList.map((ruleDefinition) => { + const nameLink = `[${changeCase.capitalCase(ruleDefinition.name)}](./rules/${ruleDefinition.name}-rule.md)`; + const severityIcon = ruleDefinition.type === 'error' ? '🔴' : '🟡'; + const fixableIcon = ruleDefinition.fixable ? '🔧' : ''; + const optionsIcon = ruleDefinition.hasOptions ? '⚙️' : ''; + return `| ${nameLink} | ${ruleDefinition.meta.description} | ${severityIcon} ${fixableIcon} ${optionsIcon} |`; + }); + + return `${tableHeader}\n${tableRows.join('\n')}\n`; +} + +async function updateRulesReference(ruleDefinitionList: LintRuleDefinition[]) { + const rulesFilePath = join(documentationDirectory, './rules.md'); + + try { + console.log(`Reading file from: ${rulesFilePath}`); + const existingContent = await fs.readFile(rulesFilePath, 'utf8'); + + const categoryRegex = + /(## (Style|Security|Best Practice|Performance))([\s\S]*?)(\n\|.*?\|[\s\S]*?\n\|.*?\|[\s\S]*?)(?=\n##|$)/g; + + const updatedContent = existingContent.replaceAll( + categoryRegex, + (match, header: string, category: string, description: string) => { + console.log(`Updating category: ${category}`); + + const categoryRules = ruleDefinitionList.filter( + (ruleDefinition) => ruleDefinition.category === changeCase.kebabCase(category), + ); + console.log(`Found ${categoryRules.length} rules for category: ${category}`); + + if (categoryRules.length === 0) { + return match; + } + + const newTable = generateRulesTable(categoryRules); + const cleanDescription = description.trim(); + + return `${header}\n\n${cleanDescription}\n\n${newTable}`; + }, + ); + + console.log(`Writing updated content to: ${rulesFilePath}`); + const formattedContent = await format(updatedContent, { + filepath: rulesFilePath, + }); + await fs.writeFile(rulesFilePath, formattedContent, 'utf8'); + console.log(`Updated rules reference in ${rulesFilePath}`); + } catch (error) { + if (error instanceof Error) { + console.error(`Error updating Rules Reference: ${error.message}`); + } else { + console.error(`Unexpected error: ${JSON.stringify(error)}`); + } + } +} + +async function updateDocumentation(ruleDefinition: LintRuleDefinition) { + const documentFilePath = join(documentationDirectory, `./rules/${ruleDefinition.name}-rule.md`); + + try { + let updatedContent = await fs.readFile(documentFilePath, 'utf8'); + + const metaRegex = + /- \*\*Rule Name:\*\* .*?\n- \*\*Type:\*\* .*?\n- \*\*Category:\*\* .*?\n- \*\*Severity:\*\* .*?\n- \*\*Fixable:\*\* .*?\n/; + const metaSection = `- **Rule Name:** ${ruleDefinition.name}\n- **Type:** ${ruleDefinition.type}\n- **Category:** ${ruleDefinition.category}\n- **Severity:** ${ruleDefinition.severity}\n- **Fixable:** ${ruleDefinition.fixable}\n`; + updatedContent = updatedContent.replace(metaRegex, metaSection); + + await fs.writeFile(documentFilePath, updatedContent, 'utf8'); + console.log(`Updated rule documentation: ${documentFilePath}`); + } catch (error) { + if (error instanceof Error) { + console.error(`Error updating ${documentFilePath}: ${error.message}`); + } else { + console.error(`Unexpected error: ${JSON.stringify(error)}`); + } + } +} + +async function main() { + const rules = await loadLintRules({ rules: {}, quiet: false, debug: false, exclude: [] }); + const ruleDefinitionList = []; + const promises = []; + + for (const rule of rules) { + const ruleDefinition = getRuleDefinition(rule); + ruleDefinitionList.push(ruleDefinition); + promises.push(updateDocumentation(ruleDefinition)); + } + + // Update documentation + await Promise.all(promises); + + // Update rules reference + await updateRulesReference(ruleDefinitionList); +} + +await main(); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 008d1f9..430a44b 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "include": ["src/**/*.ts", "tests/**/*.ts", "./*.js"] + "include": ["src/**/*.ts", "tests/**/*.ts", "scripts/*.ts", "./*.js"] }