Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ inputs:
description: "Additional CLI flags to pass to flaglint"
required: false
default: ""
version:
description: "FlagLint version to use (e.g. latest, 0.8.0)"
required: false
default: "latest"
node-version:
description: "Node.js version to use"
required: false
Expand All @@ -38,9 +42,10 @@ runs:
FLAGLINT_COMMAND: ${{ inputs.command }}
FLAGLINT_DIRECTORY: ${{ inputs.directory }}
FLAGLINT_EXTRA_ARGS: ${{ inputs.extra-args }}
FLAGLINT_VERSION: ${{ inputs.version }}
run: |
if [ "$FLAGLINT_COMMAND" = "validate" ]; then
npx flaglint@latest validate "$FLAGLINT_DIRECTORY" --no-direct-launchdarkly $FLAGLINT_EXTRA_ARGS
npx flaglint@"$FLAGLINT_VERSION" validate "$FLAGLINT_DIRECTORY" --no-direct-launchdarkly $FLAGLINT_EXTRA_ARGS
else
npx flaglint@latest "$FLAGLINT_COMMAND" "$FLAGLINT_DIRECTORY" $FLAGLINT_EXTRA_ARGS
npx flaglint@"$FLAGLINT_VERSION" "$FLAGLINT_COMMAND" "$FLAGLINT_DIRECTORY" $FLAGLINT_EXTRA_ARGS
fi
21 changes: 15 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ export const FlagLintConfigSchema = z.object({
"**/*.d.ts",
]),
// Governs which vendor SDK the scanner targets.
// Currently only "launchdarkly" is wired in the scanner.
// Other values are accepted for forward compatibility (v0.7+).
provider: z
.enum(["launchdarkly", "unleash", "growthbook", "custom"])
.default("launchdarkly"),
// Only "launchdarkly" is wired; other values are rejected with a clear error.
provider: z.string().default("launchdarkly"),
// TODO v0.3: replace minFileCount with real date-based staleness via git log
minFileCount: z.number().int().min(0).default(0),
wrappers: z
Expand Down Expand Up @@ -55,6 +52,15 @@ export type ScanConfig = Pick<FlagLintConfig, "include" | "exclude" | "minFileCo

const SEARCH_PATHS = [".flaglintrc", ".flaglintrc.json", "flaglint.config.json"];

function assertSupportedProvider(provider: string, configPath: string): void {
if (provider !== "launchdarkly") {
throw new Error(
`Error in ${configPath}: provider "${provider}" is not supported in this version. ` +
`Only "launchdarkly" is currently wired. Remove the provider field or set it to "launchdarkly".`
);
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
export async function loadConfig(configPath?: string): Promise<FlagLintConfig> {
const candidates = configPath ? [configPath] : SEARCH_PATHS;

Expand All @@ -73,15 +79,18 @@ export async function loadConfig(configPath?: string): Promise<FlagLintConfig> {
throw new Error(`Error reading ${candidate}: ${String(err)}`);
}

let parsed: FlagLintConfig;
try {
return FlagLintConfigSchema.parse(raw);
parsed = FlagLintConfigSchema.parse(raw);
} catch (err) {
if (err instanceof ZodError) {
const detail = err.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
throw new Error(`Error in ${candidate}: ${detail}`);
}
throw err;
}
assertSupportedProvider(parsed.provider, candidate);
return parsed;
}

return FlagLintConfigSchema.parse({});
Expand Down
8 changes: 3 additions & 5 deletions src/config/tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,9 @@ describe("loadConfig — partial config merges with defaults", () => {
expect(config.minFileCount).toBe(0);
});

it("overrides provider while keeping other defaults", async () => {
it("throws when provider is set to an unsupported value (e.g. unleash)", async () => {
const path = tracked(`flaglint-test-${Date.now()}.json`, { provider: "unleash" });
const config = await loadConfig(path);
expect(config.provider).toBe("unleash");
expect(config.outputDir).toBe(".");
await expect(loadConfig(path)).rejects.toThrow(/provider "unleash" is not supported/);
});

it("overrides include array", async () => {
Expand All @@ -87,7 +85,7 @@ describe("loadConfig — invalid config throws clear error", () => {

it("throws when provider is an unknown value", async () => {
const path = tracked(`flaglint-bad-${Date.now()}.json`, { provider: "unknown-provider" });
await expect(loadConfig(path)).rejects.toThrow(/Error in/);
await expect(loadConfig(path)).rejects.toThrow(/provider "unknown-provider" is not supported/);
});

it("throws when include is not an array", async () => {
Expand Down
39 changes: 3 additions & 36 deletions src/validator/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { resolve } from "path";
import { pathToFileURL } from "url";
import micromatch from "micromatch";
import type { ScanResult, FlagUsage, CallType } from "../types.js";

// ── Types ─────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -42,44 +43,10 @@ export interface ValidateOptions {

// ── Bootstrap-file glob matching ──────────────────────────────────────────────

/**
* Returns true when `file` matches any of the `patterns`.
*
* Supports:
* exact path "src/provider/setup.ts"
* * wildcard "src/provider/*.ts" — matches within one directory level
* ** wildcard "src/bootstrap/**" — matches across directory levels
*
* No external dependency — avoids importing micromatch/minimatch directly.
*/
export function matchesBootstrapPattern(file: string, patterns: string[]): boolean {
// Normalise leading "./"
if (patterns.length === 0) return false;
const clean = (s: string): string => s.replace(/^\.\//, "");
const cleanFile = clean(file);

return patterns.some((pattern) => {
const cleanPattern = clean(pattern);
if (cleanFile === cleanPattern) return true;

// Convert glob to a regular expression.
const regexStr = cleanPattern
// Escape regex-special chars except * and ?
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
// Temporarily hide ** before processing single *
.replace(/\*\*/g, "\x00")
// Single * → match any sequence that does NOT cross a /
.replace(/\*/g, "[^/]*")
// ** → match anything including /
.replace(/\x00/g, ".*")
// ? → single non-separator char
.replace(/\?/g, "[^/]");

try {
return new RegExp(`^${regexStr}$`).test(cleanFile);
} catch {
return false;
}
});
return micromatch.isMatch(clean(file), patterns.map(clean));
Comment on lines 46 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

micromatch isMatch negation pattern array semantics vs match filter

💡 Result:

In micromatch, the handling of negation patterns in arrays differs significantly between boolean checks and list filtering due to how they resolve inclusion and exclusion [1][2]. Micromatch follows these established conventions regarding negation: 1. When a pattern is a string, patterns are inclusive by default [1]. 2. When an array of patterns is passed, patterns are exclusive by default [1]. Boolean Matching (isMatch) The isMatch(str, patterns) function returns true if any of the given glob patterns match the specified string [3][4]. When using an array of patterns, the behavior of negations (e.g., ['!pattern']) can be counterintuitive because a positive match from any non-negated pattern will still result in a true value [5]. Effectively, isMatch checks if the string matches any of the "include" patterns; it does not inherently act as a filter that must pass every negation constraint unless specifically structured to do so [5]. Array Filtering (match and filter) Methods like match(list, patterns) or the function returned by filter(patterns) are designed to perform set-based filtering [1][2]. These methods implement exclusive-by-default logic: - They start with a set of candidates and apply the patterns to filter the list [1]. - When an array of patterns includes both positive and negative glob patterns, the negative patterns (e.g., '!*.js') correctly exclude matching items from the final result set [1]. Key Differences - isMatch is designed for testing a single string against a set of rules and returns a boolean [3][4]. Because it is "any-match" based, simply providing negative patterns in an array may not exclude the string if other patterns also match it [5]. - filter/match is designed for processing lists [1][2]. It respects the "exclusive by default" behavior, where negative patterns are effectively used to subtract items from the result set [1]. If you require a boolean check that respects all negation patterns as "must not match," you may need to use a custom wrapper or check the string against match to see if it remains in the result set [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File under review ==\n'
sed -n '1,140p' src/validator/index.ts | cat -n

printf '\n== Micromatch dependency context ==\n'
for f in package.json package-lock.json pnpm-lock.yaml yarn.lock; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n 'micromatch' "$f" -n -C 2 || true
  fi
done

printf '\n== Search for matching path normalization / negation handling ==\n'
rg -n 'replace\(/\\\\/g|isMatch\(|micromatch\(' src test . || true

Repository: flaglint/flaglint-js

Length of output: 8195


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Path normalization in scanner/usage flow ==\n'
rg -n 'replace\(/\\\\/g, "/"\)|usage\.file|filePath|scannedFiles|usages' src -C 2

printf '\n== Scanner fingerprint helper ==\n'
sed -n '1,80p' src/scanner/fingerprint.ts | cat -n

printf '\n== Validation call sites / usage shape ==\n'
sed -n '1,140p' src/types.ts | cat -n

Repository: flaglint/flaglint-js

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File source / path shape ==\n'
sed -n '1,220p' src/scanner/source.ts | cat -n 2>/dev/null || true
sed -n '1,220p' src/scanner/index.ts | awk 'NR>=640 && NR<=750 {print NR ":" $0}'
sed -n '1,220p' src/validator/tests/validate.test.ts | awk 'NR>=340 && NR<=390 {print NR ":" $0}'

printf '\n== Micromatch docs in repo ==\n'
rg -n 'bootstrapExclude|matchesBootstrapPattern|isMatch|micromatch' README.md src -C 2

Repository: flaglint/flaglint-js

Length of output: 12050


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File source / path shape ==\n'
sed -n '1,220p' src/scanner/source.ts | cat -n 2>/dev/null || true
sed -n '640,750p' src/scanner/index.ts | cat -n
sed -n '340,390p' src/validator/tests/validate.test.ts | cat -n

printf '\n== Micromatch/docs references ==\n'
rg -n 'bootstrapExclude|matchesBootstrapPattern|isMatch|micromatch' README.md src -C 2

Repository: flaglint/flaglint-js

Length of output: 18080


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== FileSource / path contracts ==\n'
sed -n '1,140p' src/types.ts | cat -n

printf '\n== Source implementations ==\n'
fd -a 'source.ts' src
fd -a 'source.*' src
fd -a '*source*' src

printf '\n== Fast-glob / path normalization usage ==\n'
rg -n 'fast-glob|fg\.|listFiles\(' src -C 2

Repository: flaglint/flaglint-js

Length of output: 4780


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== local-source implementation ==\n'
sed -n '1,220p' src/scanner/local-source.ts | cat -n

printf '\n== any Windows-path normalization/tests ==\n'
rg -n '\\\\|replace\(/\\\\/g, "/"\)|file: .*\\|scanRoot' src test -C 2

Repository: flaglint/flaglint-js

Length of output: 13381


Normalize usage.file before glob matching. LocalFileSource can feed backslash-separated relative paths on Windows, but clean() only strips ./, so bootstrapExclude can miss intended matches there. Add replace(/\\/g, "/") here to match the other path helpers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/validator/index.ts` around lines 46 - 49, Normalize the path before glob
matching in matchesBootstrapPattern so Windows backslash-separated relative
paths are handled consistently. Update the clean helper to also convert
backslashes to forward slashes, alongside stripping the leading ./, and keep
using it for both the file argument and the patterns passed to
micromatch.isMatch. This should align with the other path helpers and ensure
bootstrapExclude matches usage.file correctly.

}

// ── Core validation logic ─────────────────────────────────────────────────────
Expand Down
Loading