diff --git a/.github/workflows/empty-string-warning.yml b/.github/workflows/empty-string-warning.yml new file mode 100644 index 0000000..0092038 --- /dev/null +++ b/.github/workflows/empty-string-warning.yml @@ -0,0 +1,67 @@ +name: Warn for Empty Strings + +on: + pull_request: + paths: + - "**/*.ts" + - "**/*.tsx" + +permissions: + issues: write + pull-requests: write + +jobs: + check-empty-strings: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Find empty strings in TypeScript files + id: find_empty_strings + run: | + # Find empty strings and mark them explicitly + output=$(grep -Rn --include=\*.ts "''\|\"\"" --exclude-dir={node_modules,dist,out,tests} . || true) + + if [ -z "$output" ]; then + echo "::set-output name=results::No empty strings found." + exit 0 + else + output=$(echo "$output" | sed 's/""/"[EMPTY STRING]"/g' | sed "s/''/'[EMPTY STRING]'/g") + echo "::set-output name=results::${output//$'\n'/%0A}" + fi + + - name: findings + if: steps.find_empty_strings.outputs.results != 'No empty strings found.' + run: | + if [ "${{ steps.find_empty_strings.outputs.results }}" == "No empty strings found." ]; then + echo "No empty strings found. No action required." + else + echo "::warning::Empty strings found in the following files:" + echo "${{ steps.find_empty_strings.outputs.results }}" + fi + + - name: Post review comments for findings + if: steps.find_empty_strings.outputs.results != 'No empty strings found.' + uses: actions/github-script@v7 + with: + script: | + const findings = `${{ steps.find_empty_strings.outputs.results }}`.split('\n'); + for (const finding of findings) { + const [path, line] = finding.split(':'); + const body = "Empty string detected!"; + const prNumber = context.payload.pull_request.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + + await github.rest.pulls.createReviewComment({ + owner, + repo, + pull_number: prNumber, + body, + commit_id: context.payload.pull_request.head.sha, + path, + line: parseInt(line, 10), + }); + } diff --git a/tests/workflows/empty-strings/empty-strings.test.ts b/tests/workflows/empty-strings/empty-strings.test.ts new file mode 100644 index 0000000..30c31c5 --- /dev/null +++ b/tests/workflows/empty-strings/empty-strings.test.ts @@ -0,0 +1,60 @@ +import { expect, describe, beforeEach, it } from "@jest/globals"; +import { CheckEmptyStringsOptions, checkForEmptyStringsInFiles } from "./empty-strings"; + +const mockReadDir = jest.fn(); +const mockReadFile = jest.fn(); + +const options: CheckEmptyStringsOptions = { + readDir: mockReadDir, + readFile: mockReadFile, + ignorePatterns: [], +}; + +describe("checkForEmptyStringsInFiles", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("identifies files with empty strings", async () => { + mockReadDir.mockResolvedValue(["file1.ts", "file2.ts"]); + mockReadFile.mockImplementation((path) => { + if (path.includes("file1.ts")) { + return Promise.resolve('const a = "";'); + } + return Promise.resolve('const b = "notEmpty";'); + }); + + const result = await checkForEmptyStringsInFiles(".", options); + expect(result).toEqual(["file1.ts"]); + }); + + it("ignores files based on ignorePatterns and .gitignore", async () => { + options.ignorePatterns = ["file2.ts"]; + mockReadDir.mockResolvedValue(["file1.ts", "file2.ts", "file3.ts"]); + mockReadFile.mockImplementation((path) => { + if (path === ".gitignore") { + return Promise.resolve("file3.ts"); + } else if (path.includes("file1.ts")) { + return Promise.resolve('const a = "";'); + } + return Promise.resolve('const b = "notEmpty";'); + }); + + const result = await checkForEmptyStringsInFiles(".", options); + expect(result).toEqual(["file1.ts"]); + }); + + it("returns an empty array when no empty strings are found", async () => { + mockReadDir.mockResolvedValue(["file1.ts"]); + mockReadFile.mockResolvedValue('const a = "notEmpty";'); + + const result = await checkForEmptyStringsInFiles(".", options); + expect(result).toHaveLength(0); + }); + + it("handles errors gracefully", async () => { + mockReadDir.mockRejectedValue(new Error("Error reading directory")); + + await expect(checkForEmptyStringsInFiles(".", options)).resolves.toEqual([]); + }); +}); diff --git a/tests/workflows/empty-strings/empty-strings.ts b/tests/workflows/empty-strings/empty-strings.ts new file mode 100644 index 0000000..f3f1939 --- /dev/null +++ b/tests/workflows/empty-strings/empty-strings.ts @@ -0,0 +1,45 @@ +export interface CheckEmptyStringsOptions { + readDir: (path: string) => Promise; + readFile: (path: string, encoding: string) => Promise; + ignorePatterns?: string[]; +} + +export async function checkForEmptyStringsInFiles(dir: string, options: CheckEmptyStringsOptions): Promise { + const { readDir, readFile, ignorePatterns = [] } = options; + const filesWithEmptyStrings: string[] = []; + const ignoreList: string[] = ["^\\.\\/\\.git", "^\\.\\/\\..*", "^\\.\\/node_modules", "^\\.\\/dist", "^\\.\\/out", ".*\\.test\\.ts$", ...ignorePatterns]; + + try { + const gitignoreContent = await readFile(".gitignore", "utf8"); + gitignoreContent.split("\n").forEach((line) => { + if (line.trim() !== "") { + ignoreList.push(`^\\.\\/${line.replace(/\./g, "\\.").replace(/\//g, "\\/")}`); + } + }); + } catch (error) { + console.error("Error reading .gitignore file:", error); + } + + try { + const files = await readDir(dir); + for (const fileName of files) { + let shouldIgnore = false; + for (const pattern of ignoreList) { + if (new RegExp(pattern).test(fileName)) { + shouldIgnore = true; + break; + } + } + if (shouldIgnore || !fileName.endsWith(".ts")) continue; + + const fileContent = await readFile(`${dir}/${fileName}`, "utf8"); + if (fileContent.includes('""') || fileContent.includes("''")) { + filesWithEmptyStrings.push(fileName); + } + } + } catch (error) { + console.error("Error reading directory or file contents:", error); + } + + return filesWithEmptyStrings; +} diff --git a/tests/workflows/empty-strings/test-function.ts b/tests/workflows/empty-strings/test-function.ts new file mode 100644 index 0000000..f89d4f9 --- /dev/null +++ b/tests/workflows/empty-strings/test-function.ts @@ -0,0 +1,3 @@ +export function testFunction() { + return ""; +} diff --git a/tests/workflows/empty-strings/test.config.ts b/tests/workflows/empty-strings/test.config.ts new file mode 100644 index 0000000..86686fc --- /dev/null +++ b/tests/workflows/empty-strings/test.config.ts @@ -0,0 +1,6 @@ +module.exports = { + test: { + test: "", + }, + tester: "", +};