Skip to content

chore(root): adds better cross lang support#202

Closed
yowainwright wants to merge 21 commits into
mainfrom
improve-lang-package-manager-support
Closed

chore(root): adds better cross lang support#202
yowainwright wants to merge 21 commits into
mainfrom
improve-lang-package-manager-support

Conversation

@yowainwright
Copy link
Copy Markdown
Owner

Proposed Changes

  • improves cross lang support

Read about referenced issues here. Replace words with this Pull Request's context.

@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 20, 2026

Codecov Report

❌ Patch coverage is 90.93783% with 86 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.18%. Comparing base (25d7a9f) to head (3c4dc35).

Files with missing lines Patch % Lines
src/utils/glob.ts 70.00% 36 Missing ⚠️
src/scripts/index.ts 88.88% 23 Missing ⚠️
src/config/validator.ts 84.87% 18 Missing ⚠️
src/program.ts 91.01% 8 Missing ⚠️
src/providers/detection.ts 97.36% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #202      +/-   ##
==========================================
- Coverage   99.19%   97.18%   -2.01%     
==========================================
  Files          36       36              
  Lines        3340     4048     +708     
==========================================
+ Hits         3313     3934     +621     
- Misses         27      114      +87     
Flag Coverage Δ
unittests 97.18% <90.93%> (-2.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR adds cross-language support for Python (pip/pipenv/Poetry/uv/PEP 621) and Go, rewrites the glob implementation and CLI arg parser, and hardens the GitHub Action. It addresses multiple issues from previous review rounds (replace-block corruption in go.mod, extras in TOML regexes, repeated array flags, action injection via eval, exit code disambiguation).

  • Python provider: reads and writes PEP 621 ([project] dependencies) alongside Poetry and requirements.txt; getAllVersions dispatches to the correct tool (conda/uv/pip); dot-notation package names and extras syntax are now handled by updated regex patterns.
  • Go provider: in-place line updating preserves comments and formatting; replace (...) blocks are skipped, but exclude (...) blocks have the same gap and can have their excluded version silently overwritten.
  • glob rewrite: custom directory walker replaces node:fs/promises.glob for Node 20 compatibility; symlinks, brace expansion, and directory pruning work correctly, but [...] character-class patterns are silently mis-handled as literals.

Confidence Score: 3/5

Safe to merge with targeted fixes; the go.mod exclude-block mutation is the only newly introduced correctness bug.

The go.mod exclude block issue is a concrete correctness bug: if a module appears in both a codependency list and an exclude block, the excluded version is silently rewritten. Issues from prior review rounds (non-Poetry pyproject.toml detection without uv.lock, PEP621_DEPS section boundary) remain open.

src/providers/go/index.ts (exclude block tracking), src/utils/glob.ts (character-class patterns), src/providers/python/constants.ts and src/providers/python/index.ts (PEP621_DEPS section boundary)

Important Files Changed

Filename Overview
src/providers/go/index.ts Adds in-place line updating for go.mod to preserve comments and formatting. replace () blocks are correctly skipped, but exclude () blocks are not, so modules appearing in an exclude block can have their excluded version silently rewritten.
src/utils/glob.ts Full rewrite of the glob implementation to avoid relying on node:fs/promises.glob. Symlink handling, brace expansion, and directory pruning are well-implemented. The [...] character-class syntax is not translated, silently producing wrong matches for patterns that use it.
src/providers/python/index.ts Adds PEP 621 ([project] dependencies) read/write paths alongside Poetry and requirements.txt. getAllVersions now correctly dispatches to conda/uv/pip. Multiple issues flagged in prior reviews (write fallback, extras, TOML corruption) are addressed here.
src/providers/python/constants.ts Adds PEP621_DEPS regex and updates all name patterns to allow . (fixing dot-notation PyPI names like zope.interface) and extras syntax [security].
src/cli/parser.ts Rewritten to accumulate repeated array flags correctly, validate values, parse JSON object codependencies, and throw on unknown flags instead of silently ignoring them.
src/program.ts Config loading now validates each source via validateConfigResult; validateUnknownFields removed (fixes silent breaking change for unknown config fields); errors now exit with code 2 instead of 1 for cleaner action integration.
src/scripts/index.ts Multi-language detection throws early when multiple languages are found without --language or --files; resolveMatchedManifestLanguage properly handles pyproject.toml for Poetry and uv; exclude block handling in go.mod is the remaining gap.
action.yml Significant improvements: inputs passed via env vars (no injection), append_words correctly handles multi-value inputs, exit code 1 vs 2 distinguished for outdated vs errors, new inputs (language, level, dryRun, etc.) exposed.
src/providers/detection.ts Adds isPoetryPyproject, detectPythonPackageManagerForManifest; detectLanguage now requires uv.lock or Poetry marker to include pyproject.toml; conda removed from detection path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[checkFiles called] --> B{hasExplicitMatchers or language?}
    B -- No --> C[detectLanguage]
    C --> D{Multiple languages?}
    D -- Yes --> E[throw: run with --language]
    D -- No --> F[resolveMatchers]
    B -- Yes --> F
    F --> G[glob walk files]
    G --> H[validateMatchedManifests]
    H --> I{resolveMatchedManifestLanguage per file}
    I -- package.json --> J[nodejs]
    I -- go.mod --> K[go]
    I -- requirements.txt / Pipfile --> L[python]
    I -- pyproject.toml --> M{Poetry or uv.lock present?}
    M -- Yes --> L
    M -- No --> N[throw: not supported]
    J & K & L --> O[loadManifests]
    O --> P[createProvider per language]
    P -- nodejs --> Q[NodeJSProvider]
    P -- go --> R[GoProvider updateExistingRequireLines]
    P -- python --> S[PythonProvider requirements / Poetry / PEP621]
    Q & R & S --> T[constructVersionMap]
    T --> U{update flag?}
    U -- Yes --> V[writeManifest]
    U -- No --> W[report diff / exit 1 if outdated]
Loading

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/providers/go/index.ts:63-95
**`exclude (...)` blocks silently version-bumped**

`updateExistingRequireLines` tracks `inReplaceBlock` to skip lines inside `replace (...)` blocks, but there is no equivalent guard for `exclude (...)` blocks. Lines inside an `exclude` block look identical to `require` block entries (e.g. `    github.com/some/mod v1.0.0`), so if the module name appears in `manifest.dependencies`, `updateRequireLine` will rewrite the *excluded* version. The result is a go.mod that excludes a different version than intended, which can confuse `go mod tidy`.

Note: single-line `exclude github.com/some/mod v1.0.0` is safe — the regex captures `exclude` as the "name" token, finds no matching dep, and leaves the line unchanged. Only the block form is affected.

```suggestion
  let updatedCount = 0;
  let inReplaceBlock = false;
  let inExcludeBlock = false;

  const updatedContent = content
    .split("\n")
    .map((line) => {
      if (/^\s*replace\s*\(/.test(line)) {
        inReplaceBlock = true;
        return line;
      }
      if (inReplaceBlock) {
        if (/^\s*\)/.test(line)) inReplaceBlock = false;
        return line;
      }
      if (/^\s*exclude\s*\(/.test(line)) {
        inExcludeBlock = true;
        return line;
      }
      if (inExcludeBlock) {
        if (/^\s*\)/.test(line)) inExcludeBlock = false;
        return line;
      }
```

### Issue 2 of 2
src/utils/glob.ts:88-93
The `[...]` character-class syntax from standard glob patterns is not handled: `escapeRegexChar` escapes `[` and `]` as literals, so a pattern like `**/file[0-9].ts` is converted to `^(?:.*/)?file\[0\-9\]\.ts$` and matches the literal string `file[0-9].ts` instead of `file0.ts``file9.ts`. This is a capability regression from `node:fs/promises.glob`. Most real-world patterns in this codebase don't use character classes, but a user who passes such a pattern via `--files` or the config would get silently wrong results.

```suggestion
    if (char === "?") {
      source += "[^/]";
      continue;
    }

    if (char === "[") {
      const closeIndex = normalizedPattern.indexOf("]", index + 1);
      if (closeIndex !== -1) {
        source += normalizedPattern.slice(index, closeIndex + 1);
        index = closeIndex;
        continue;
      }
    }

    source += escapeRegexChar(char);
```

Reviews (28): Last reviewed commit: "fix: dead code cleanup, exit 2 for error..." | Re-trigger Greptile

Comment thread src/providers/detection.ts Outdated
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread src/scripts/index.ts
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

@yowainwright yowainwright force-pushed the improve-lang-package-manager-support branch from b87a1e2 to 7c16bd1 Compare May 4, 2026 06:11
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile

@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread src/providers/python/constants.ts Outdated
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

@yowainwright yowainwright force-pushed the improve-lang-package-manager-support branch from c13fb78 to da78979 Compare May 4, 2026 09:41
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread src/program.ts Outdated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread src/providers/python/index.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread src/providers/python/constants.ts
Comment thread src/providers/python/index.ts Outdated
Comment thread src/utils/glob.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread src/providers/python/constants.ts Outdated
Comment thread action.yml
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread src/providers/python/constants.ts Outdated
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread src/providers/python/index.ts Outdated
Comment thread src/cli/parser.ts Outdated
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread src/providers/python/constants.ts Fixed
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread src/cli/parser.ts
@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

@yowainwright
Copy link
Copy Markdown
Owner Author

@greptile review

POETRY_DEPS: /\[tool\.poetry\.dependencies\]([\s\S]*?)(?=\[|$)/,
POETRY_LINE: /^([a-zA-Z0-9_-]+)\s*=\s*"([^"]+)"/,
POETRY_LINE: /^([a-zA-Z0-9_.-]+)\s*=\s*"([^"]+)"/,
PEP621_DEPS: /^(\[project\][\s\S]*?)^dependencies\s*=\s*\[([^\]"]*(?:"[^"]*"[^\]"]*)*)\]/m,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 PEP621_DEPS does not match single-line dependency arrays

The pattern requires a closing ] reachable only through the [^\]"]* portion. For a compact inline array like dependencies = ["flask>=2.0.0"] the match fails, so both readPyprojectToml (returns {}) and writePyprojectToml (skips the PEP 621 branch entirely) are broken for any pyproject.toml that uses an inline array.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/providers/python/constants.ts
Line: 6

Comment:
**`PEP621_DEPS` does not match single-line dependency arrays**

The pattern requires a closing `]` reachable only through the `[^\]"]*` portion. For a compact inline array like `dependencies = ["flask>=2.0.0"]` the match fails, so both `readPyprojectToml` (returns `{}`) and `writePyprojectToml` (skips the PEP 621 branch entirely) are broken for any `pyproject.toml` that uses an inline array.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines 278 to 315
filePath: string,
manifest: DependencyManifest,
): void {
const lines = Object.entries(manifest.dependencies)
const content = readFileSync(filePath, "utf8");
const lines = content.split("\n");
let updatedCount = 0;

const updatedLines = lines.map((line) => {
const parsed = parseRequirementLine(line);
if (!parsed) return line;

const [name] = parsed;
const version = manifest.dependencies[name];
if (!version) return line;

updatedCount++;
return line.replace(
PYTHON_PATTERNS.REQUIREMENT_LINE,
(_full, pkgName, extras) => `${pkgName}${extras ?? ""}${version}`,
);
});

if (updatedCount > 0) {
writeFileSync(
filePath,
preserveFinalNewline(content, updatedLines.join("\n")),
);
return;
}

const fallbackLines = Object.entries(manifest.dependencies)
.map(([name, version]) => `${name}${version}`)
.join("\n");
writeFileSync(filePath, lines + "\n");
writeFileSync(filePath, fallbackLines + "\n");
}

private writePyprojectToml(
filePath: string,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 writeRequirementsTxt in-place update drops the version operator

version is taken from manifest.dependencies[name], which constructUpdatedManifest sets to the bare registry version (e.g. "2.0.1") with no operator. The replacement (_full, pkgName, extras) => \${pkgName}${extras ?? ""}${version}`replaces the entireREQUIREMENT_LINEmatch including the original operator, sorequests>=1.5.0becomesrequests2.0.1— an invalid specifier that breakspip install -ron the next run. This in-place path is new in this PR and runs for every project that already has arequirements.txt`.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/providers/python/index.ts
Line: 278-315

Comment:
**`writeRequirementsTxt` in-place update drops the version operator**

`version` is taken from `manifest.dependencies[name]`, which `constructUpdatedManifest` sets to the bare registry version (e.g. `"2.0.1"`) with no operator. The replacement `(_full, pkgName, extras) => \`${pkgName}${extras ?? ""}${version}\`` replaces the entire `REQUIREMENT_LINE` match including the original operator, so `requests>=1.5.0` becomes `requests2.0.1` — an invalid specifier that breaks `pip install -r` on the next run. This in-place path is new in this PR and runs for every project that already has a `requirements.txt`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment thread src/providers/go/index.ts
Comment on lines +63 to +95
const nextVersion = dependencies[name];
if (!nextVersion) return { line, updated: false };

return {
line: `${prefix}${name}${spacing}${nextVersion}${suffix}`,
updated: true,
};
};

const updateExistingRequireLines = (
content: string,
dependencies: Record<string, string>,
): { content: string; updatedCount: number } => {
let updatedCount = 0;
let inReplaceBlock = false;

const updatedContent = content
.split("\n")
.map((line) => {
if (/^\s*replace\s*\(/.test(line)) {
inReplaceBlock = true;
return line;
}
if (inReplaceBlock) {
if (/^\s*\)/.test(line)) inReplaceBlock = false;
return line;
}
const result = updateRequireLine(line, dependencies);
if (result.updated) updatedCount++;
return result.line;
})
.join("\n");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 exclude (...) blocks silently version-bumped

updateExistingRequireLines tracks inReplaceBlock to skip lines inside replace (...) blocks, but there is no equivalent guard for exclude (...) blocks. Lines inside an exclude block look identical to require block entries (e.g. github.com/some/mod v1.0.0), so if the module name appears in manifest.dependencies, updateRequireLine will rewrite the excluded version. The result is a go.mod that excludes a different version than intended, which can confuse go mod tidy.

Note: single-line exclude github.com/some/mod v1.0.0 is safe — the regex captures exclude as the "name" token, finds no matching dep, and leaves the line unchanged. Only the block form is affected.

Suggested change
const nextVersion = dependencies[name];
if (!nextVersion) return { line, updated: false };
return {
line: `${prefix}${name}${spacing}${nextVersion}${suffix}`,
updated: true,
};
};
const updateExistingRequireLines = (
content: string,
dependencies: Record<string, string>,
): { content: string; updatedCount: number } => {
let updatedCount = 0;
let inReplaceBlock = false;
const updatedContent = content
.split("\n")
.map((line) => {
if (/^\s*replace\s*\(/.test(line)) {
inReplaceBlock = true;
return line;
}
if (inReplaceBlock) {
if (/^\s*\)/.test(line)) inReplaceBlock = false;
return line;
}
const result = updateRequireLine(line, dependencies);
if (result.updated) updatedCount++;
return result.line;
})
.join("\n");
let updatedCount = 0;
let inReplaceBlock = false;
let inExcludeBlock = false;
const updatedContent = content
.split("\n")
.map((line) => {
if (/^\s*replace\s*\(/.test(line)) {
inReplaceBlock = true;
return line;
}
if (inReplaceBlock) {
if (/^\s*\)/.test(line)) inReplaceBlock = false;
return line;
}
if (/^\s*exclude\s*\(/.test(line)) {
inExcludeBlock = true;
return line;
}
if (inExcludeBlock) {
if (/^\s*\)/.test(line)) inExcludeBlock = false;
return line;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/providers/go/index.ts
Line: 63-95

Comment:
**`exclude (...)` blocks silently version-bumped**

`updateExistingRequireLines` tracks `inReplaceBlock` to skip lines inside `replace (...)` blocks, but there is no equivalent guard for `exclude (...)` blocks. Lines inside an `exclude` block look identical to `require` block entries (e.g. `    github.com/some/mod v1.0.0`), so if the module name appears in `manifest.dependencies`, `updateRequireLine` will rewrite the *excluded* version. The result is a go.mod that excludes a different version than intended, which can confuse `go mod tidy`.

Note: single-line `exclude github.com/some/mod v1.0.0` is safe — the regex captures `exclude` as the "name" token, finds no matching dep, and leaves the line unchanged. Only the block form is affected.

```suggestion
  let updatedCount = 0;
  let inReplaceBlock = false;
  let inExcludeBlock = false;

  const updatedContent = content
    .split("\n")
    .map((line) => {
      if (/^\s*replace\s*\(/.test(line)) {
        inReplaceBlock = true;
        return line;
      }
      if (inReplaceBlock) {
        if (/^\s*\)/.test(line)) inReplaceBlock = false;
        return line;
      }
      if (/^\s*exclude\s*\(/.test(line)) {
        inExcludeBlock = true;
        return line;
      }
      if (inExcludeBlock) {
        if (/^\s*\)/.test(line)) inExcludeBlock = false;
        return line;
      }
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

@yowainwright yowainwright deleted the improve-lang-package-manager-support branch May 14, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants