Skip to content

feat(file): add file command with metrics, issues, clones and coverage - #68

Merged
alerizzo merged 2 commits into
mainfrom
feat/file-command
Nov 11, 2026
Merged

alerizzo merged 2 commits into
mainfrom
feat/file-command

Conversation

@alerizzo

Copy link
Copy Markdown
Collaborator

Summary

  • New codacy file [provider] [org] [repo] <path> command (alias fl), 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-source hides it. Without source, the issues are listed at the top as the same cards codacy issues prints. With --lines, issues outside the range are listed at the top, so no issue is hidden.
  • --output json returns { file, issues, clones, coverage } with per-line coverage.lines ({ line, hits }) and no source code. The content endpoint isn't called in JSON mode.
  • Account token only: none of the file endpoints are on the repository-token whitelist, so a repository token is refused before any request (added to 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 --> J
Loading
  • fileId resolution lists the parent folder and matches the path exactly. search matches substrings, so app.ts would also hit myapp.ts.
  • Source is pinned to the analysed commitSha, so issue and coverage line numbers match the code shown, even when the branch HEAD has moved on.
  • Optional data degrades gracefully: no coverage means no gutter. A failed content fetch (e.g. a file over 1MB) prints Source not available (…), falls back to listing the issues at the top, and renders the rest.
  • All repository-derived values (paths, file content, issue messages, clone paths) go through sanitizeText (CWE-150).

Example (--lines 36-42):

39   │ function printFindingDetail(
 ↳   • Medium | Complexity  #131497158287
       Method printFindingDetail has 81 lines of code (limit is 60)
 ──────────────────────────────────────────────────────────────────────────────
40   │   item: SrmItem,

Test plan

  • npm test (849 passing; 24 new: 23 in file.test.ts + 1 refusal case)
  • npm run build
  • Live: codacy 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

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>
@codacy-production

codacy-production Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 143 complexity · 3 duplication

Metric Results
Complexity 143
Duplication 3

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

@codacy-production codacy-production Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull Request Overview

⚠️ Resolve issues before merging

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

Comment thread src/commands/file.ts Outdated
provider,
organization,
repository,
encodeURIComponent(filePath),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Comment thread src/commands/file.ts Outdated
coverage: Map<number, number> | null,
issues: CommitIssue[] = [],
): string[] {
const width = String(Math.max(0, ...lines.map((l) => l.number))).length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Comment thread src/commands/file.ts
Comment on lines +164 to +166
RepositoryService.getFileCoverage(provider, organization, repository, fileId).catch(
() => null,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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>
Comment thread src/commands/file.ts
);
source = content.data;
} catch (err) {
sourceError = err instanceof Error ? err.message : String(err);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Comment thread src/commands/file.ts
Comment on lines +83 to +88
export function resolveFilePath(input: string, autoDetected: boolean): string {
const joined = autoDetected
? path.posix.join(getCwdRepoRelativePath(), input.trim())
: input;
return normalizeRepoPath(joined);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

@alerizzo
alerizzo merged commit 00b25d1 into main Nov 11, 2026
4 checks passed
@alerizzo
alerizzo deleted the feat/file-command branch November 11, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant