Repository navigation
feat(file): add file command with metrics, issues, clones and coverage - #68
Conversation
The CLI could list files with metrics (`ls`) but not drill into one. `codacy file` mirrors the Codacy UI's file view: a summary (grade, issues, complexity, duplication, coverage), the file's clones, and its source with a green/red coverage gutter and each issue inline under its line. Without source (`--no-source` or a failed fetch) the issues are listed at the top instead; with `--lines`, issues outside the range are listed at the top. `--output json` carries metadata, issues, clones and per-line coverage, but no source code. The command is account-token only: none of the file endpoints are on the repository-token whitelist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 143 |
| Duplication | 3 |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull Request Overview
The source request path is encoded twice, causing source retrieval to fail for nested files and paths requiring escaping. Coverage API failures are also silently converted into missing coverage, and large files with many short lines can crash rendering due to argument spreading.
Codacy also found:
- 82 complexity points in
src/commands/file.ts, with the action handler exceeding the configured limit. - Coverage for the new command could not be assessed.
- Three new findings, including one complexity warning and two HTML-template warnings classified as false positives.
Test suggestions
- Parse valid, open-ended, single-line, malformed, and inverted line ranges.
- Resolve auto-detected paths relative to the cwd and explicit paths relative to the repository root.
- Resolve a file ID using the parent directory and exact path matching, including root-level files and missing files.
- Render summary metrics, clones, source, coverage gutter, and inline issues.
- List issues outside a requested source range while rendering in-range issues inline.
- Render false-positive information and sanitize issue messages and source content.
- Pin source retrieval to the analyzed commit and pass the requested line range.
- Skip source retrieval for JSON output and --no-source.
- Gracefully handle missing coverage and failed source retrieval.
- Follow pagination for issues and clones.
- Produce the documented JSON projection, including null coverage when unavailable.
- Refuse repository-token authentication before issuing requests.
- Verify source retrieval works with file paths containing characters requiring URL path escaping.
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Verify source retrieval works with file paths containing characters requiring URL path escaping.
TIP How was this review? Give us feedback
| provider, | ||
| organization, | ||
| repository, | ||
| encodeURIComponent(filePath), |
There was a problem hiding this comment.
🔴 HIGH RISK
Source retrieval pre-encodes the path even though the generated client already applies the global path-segment encoder, so paths such as src/app.ts can become src%252Fapp.ts and fail with a 404. Pass the repository-relative path directly and add a test covering a path with characters that require encoding.
TIP React 👍 if this review is right, or reply with what's wrong and how it could be better to help improve the reviewer.
There was a problem hiding this comment.
Partly right, fixed in 8e63e96. The path was encoded twice. It didn't 404, though: I checked against the API, and both src%2Futils%2Fsanitize.ts and src%252Futils%252Fsanitize.ts return 200. Single encoding is still the correct form, so the raw path now goes to getFileContent and the global encodePathSegment escapes it once. I added a test with a path containing a space, # and %.
🤖 Generated by /pr-fixup command
| coverage: Map<number, number> | null, | ||
| issues: CommitIssue[] = [], | ||
| ): string[] { | ||
| const width = String(Math.max(0, ...lines.map((l) => l.number))).length; |
There was a problem hiding this comment.
🟡 MEDIUM RISK
A file with roughly 150,000 short lines can crash source rendering with a RangeError, even when its content is under 1 MB. Consider finding the maximum line number with a loop or reduction instead of spreading the array into Math.max.
TIP React 👍 if this review is right, or reply with what's wrong and how it could be better to help improve the reviewer.
There was a problem hiding this comment.
Good catch. Fixed in 8e63e96: the widest line number now comes from a reduce, and a test renders 200k lines.
🤖 Generated by /pr-fixup command
| RepositoryService.getFileCoverage(provider, organization, repository, fileId).catch( | ||
| () => null, | ||
| ), |
There was a problem hiding this comment.
🟡 MEDIUM RISK
A coverage API failure is reported as missing coverage, and JSON can contain an empty lines array despite existing coverage. Consider treating only the expected no-coverage response as absent and surfacing other errors.
TIP React 👍 if this review is right, or reply with what's wrong and how it could be better to help improve the reviewer.
There was a problem hiding this comment.
Agreed, fixed in 8e63e96. A file without coverage data answers 404 (No coverage data for file …), so only a 404 now means "no coverage". Any other error fails the command through handleError. Tests cover both cases.
🤖 Generated by /pr-fixup command
- Pass the raw file path to getFileContent; the client's global path encoder already escapes it once (it was being encoded twice). - Only a 404 from getFileCoverage means "no coverage"; other errors now surface instead of silently dropping the gutter and JSON line data. - Compute the widest line number with a reduce instead of spreading the lines into Math.max, which overflows on very long files. - Extract the fetch and render steps from the action handler (Lizard nloc) and drop `<...>` placeholders from the --lines errors (Semgrep html-in-template false positives). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| ); | ||
| source = content.data; | ||
| } catch (err) { | ||
| sourceError = err instanceof Error ? err.message : String(err); |
There was a problem hiding this comment.
SHOULD fix before merge — Source not available (…) shows the client's static status name, not the server's reason
When getFileContent fails with an ApiError, err.message is filled by the generated client's catchErrorCodes table. So a 404 or 400 prints Source not available (Not Found) / (Bad Request) and drops whatever the server said about why. The test at src/commands/file.test.ts:377 asserts Payload Too Large, which locks in the static-table value.
AGENTS.md (Error handling): a command that reports failures itself "uses errorReason(err) for the same extraction without the Error: prefix or the (HTTP n) suffix — never err.message, which is the client's static status table". errorReason is exported from src/utils/error.ts. The fix is sourceError = errorReason(err);, plus a test with a server message in the body.
🤖 Generated by /review-pr command
There was a problem hiding this comment.
Agreed, fixing this. sourceError now comes from errorReason(err), so the server's message is shown. The test now expects a server message ("File is too large to display") instead of the static Payload Too Large.
🤖 Generated by /pr-fixup command
| export function resolveFilePath(input: string, autoDetected: boolean): string { | ||
| const joined = autoDetected | ||
| ? path.posix.join(getCwdRepoRelativePath(), input.trim()) | ||
| : input; | ||
| return normalizeRepoPath(joined); | ||
| } |
There was a problem hiding this comment.
SHOULD fix before merge — With auto-detection, an absolute path or one that climbs above the repo is joined onto the cwd prefix
resolveFilePath always runs path.posix.join(getCwdRepoRelativePath(), input). From repo/src, codacy file /Users/me/repo/src/a.ts becomes src/Users/me/repo/src/a.ts and fails as "not found on Codacy". A path like ../../x.ts run from a top-level folder gives ../x.ts, which is then sent to listFiles as the parent folder.
Absolute paths are what shell tab-completion and editors often hand over. Resolve them against the git root (path.relative(gitRoot, input)), and refuse any result that starts with .. with a clear "path is outside the repository" error before making a request.
🤖 Generated by /review-pr command
There was a problem hiding this comment.
Good catch, fixing this. When the repository is auto-detected, an absolute path is now resolved against the git root. A path that lands outside the repository fails with Path '…' is outside the repository. before any request is made. There are tests for both, and I checked it live from src/ with $PWD/utils/sanitize.ts, ../../x.ts and /etc/passwd.
🤖 Generated by /pr-fixup command
Summary
codacy file [provider] [org] [repo] <path>command (aliasfl), the CLI counterpart of the Codacy UI's file view. It shows a summary (grade, issues, complexity, clones + duplicated lines, coverage), the file's clones, and its source code with a coverage gutter (green = covered, red = not covered) and each issue inline under its line.-l, --lines <from-to>narrows the source;-N, --no-sourcehides it. Without source, the issues are listed at the top as the same cardscodacy issuesprints. With--lines, issues outside the range are listed at the top, so no issue is hidden.--output jsonreturns{ file, issues, clones, coverage }with per-linecoverage.lines({ line, hits }) and no source code. The content endpoint isn't called in JSON mode.repository-token-refusals.test.ts).How it works
flowchart TD A["file path arg"] --> B["resolveFilePath() (cwd-relative when repo auto-detected)"] B --> C["listFiles(parent dir) + exact path match -> fileId"] C --> D["getFileWithAnalysis"] C --> E["getFileIssues (all pages)"] C --> F["getFileClones (all pages)"] C --> G["getFileCoverage (optional)"] D --> H{"source wanted? (not --no-source, not json)"} H -- yes --> I["getFileContent(path, lines, commitSha)"] H -- no --> J["render"] I --> J E --> J F --> J G --> JfileIdresolution lists the parent folder and matches the path exactly.searchmatches substrings, soapp.tswould also hitmyapp.ts.commitSha, so issue and coverage line numbers match the code shown, even when the branch HEAD has moved on.Source not available (…), falls back to listing the issues at the top, and renders the rest.sanitizeText(CWE-150).Example (
--lines 36-42):Test plan
npm test(849 passing; 24 new: 23 infile.test.ts+ 1 refusal case)npm run buildcodacy file gh codacy codacy-cloud-cli src/commands/finding.ts(issues + clones),codacy file gh codacy coverage-service <path> --lines 20-60(coverage gutter),-o json, a relative path from a subdirectory, and a missing file🤖 Generated with Claude Code