chore(root): adds better cross lang support#202
Conversation
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
| 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]
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
|
@greptile review |
|
@greptile review |
b87a1e2 to
7c16bd1
Compare
|
@greptile |
|
@greptile review |
|
@greptile review |
c13fb78 to
da78979
Compare
|
@greptile review |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptile review |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptile review |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptile review |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptile review |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptile review |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptile review |
|
@greptile review |
|
@greptile review |
|
@greptile review |
|
@greptile review |
|
@greptile review |
|
@greptile review |
|
@greptile review |
|
@greptile review |
|
@greptile review |
|
@greptile review |
|
@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, |
There was a problem hiding this 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.
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.| 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, |
There was a problem hiding this 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 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.| 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"); | ||
|
|
There was a problem hiding this 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.
| 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.
Proposed Changes