Skip to content

Conversation

@ericglau
Copy link
Member

Fixes #621

  • Adds a comment for the compatible git commit hash for Community Contracts imports
  • Updates the links for Community Contracts imports to the specific git commit's version of each imported Solidity file.

Note: This does not include hardhat/forge instructions on how to install a specific commit. It only adds a comment to the contract code, which is not the right context for additional instructions.

@ericglau ericglau requested review from a team as code owners August 12, 2025 19:28
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 12, 2025

Walkthrough

Adds commit-aware Community Contracts metadata to generated Solidity headers, utilities to detect Community Contracts imports and extract a pinned git commit from package.json, UI logic to hyperlink imports to that commit when present, pins the devDependency to a specific commit, adds tests and snapshots, and documents an OpenZeppelin declaration file.

Changes

Cohort / File(s) Summary
Type declarations docs
packages/core/solidity/openzeppelin-contracts.d.ts
Added JSDoc for OpenZeppelinContracts properties: version, sources, dependencies. No type or runtime changes.
Dependency pinning
packages/core/solidity/package.json
Updated devDependency @openzeppelin/community-contracts to a pinned git URL (git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#de17c8e).
Core: print header & helpers
packages/core/solidity/src/print.ts, packages/core/solidity/src/utils/community-contracts-git-commit.ts, packages/core/solidity/src/utils/imports-libraries.ts
Replaced static OZ compatibility header with printCompatibleLibraryVersions(contract) that conditionally appends Community Contracts commit. Added getCommunityContractsGitCommit() and extractGitCommitHash(...) to read/validate pinned commit from package.json, and importsCommunityContracts(contract) to detect Community Contracts imports.
UI: hyperlink injection updates
packages/ui/src/solidity/inject-hyperlinks.ts
Refactored regexes and implemented two-stage replacement: always hyperlink OpenZeppelin Contracts imports; if a Community Contracts commit is present, hyperlink Community Contracts imports to the repo tree at that commit and linkify the commit line. Public API unchanged.
Tests & snapshots
packages/core/solidity/src/utils/community-contracts-git-commit.test.ts, packages/core/solidity/src/*.test.ts.md
Added unit tests for commit extraction and updated snapshot blocks to include "and Community Contracts commit de17c8e" in generated headers.
Changeset & styles
.changeset/famous-parts-pump.md, packages/ui/src/common/styles/global.css
New changeset declaring a patch release; added .comment-link CSS class.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Generator as printContract
  participant Imports as importsCommunityContracts
  participant Commit as getCommunityContractsGitCommit

  User->>Generator: Request contract generation
  Generator->>Imports: Check for Community Contracts imports
  Imports-->>Generator: true/false
  alt imports Community Contracts
    Generator->>Commit: Read pinned commit from package.json
    Commit-->>Generator: <commit-hash>
    Generator-->>User: Emit header including Community Contracts commit
  else
    Generator-->>User: Emit header without community commit
  end
Loading
sequenceDiagram
  participant UI as injectHyperlinks
  participant Code as GeneratedCode

  UI->>Code: Replace OpenZeppelin Contracts import lines with links
  UI->>Code: Scan for "Community Contracts commit <hash>"
  alt Commit present
    UI->>Code: Replace Community Contracts imports with tree URL at commit
    UI->>Code: Linkify commit line to commit URL
  else
    UI-->>Code: Skip community-specific replacements
  end
  UI-->>Code: Return hyperlink-enhanced code
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Assessment against linked issues

Objective Addressed Explanation
Include Community Contracts git commit hash in generated contract comments when those imports are used (#621)
Provide instructions on installing the supported Community Contracts version (npm/forge command examples) (#621) No npm or forge install examples or documentation added.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Added JSDoc comments to OpenZeppelinContracts interface (packages/core/solidity/openzeppelin-contracts.d.ts) Documentation-only change not required by #621.
Added CSS class .comment-link (packages/ui/src/common/styles/global.css) UI styling tweak unrelated to adding commit hash or install instructions in #621.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@socket-security
Copy link

socket-security bot commented Aug 12, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License

View full report

@socket-security
Copy link

socket-security bot commented Aug 12, 2025

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
packages/core/solidity/src/utils/community-contracts-git-commit.ts (2)

3-9: Consider making failure modes non-fatal at call sites

Throwing here is fine to enforce configuration correctness. However, if this bubbles up during generation, it will break the Wizard for users in case of a packaging/config misalignment. I recommend wrapping calls in print.ts with a try/catch and skipping the community line if resolution fails.

To implement a non-fatal path, update printCompatibleLibraryVersions in print.ts to catch errors (see suggested diff in that file’s comment).


11-19: Validate that the segment after '#' is a 40-hex Git commit hash

Given the header explicitly says “commit”, we should enforce a full 40-hex hash. This prevents tags/branches from slipping in and ensures the UI can reliably hyperlink to a commit.

Apply:

 function extractGitCommitHash(dependencyName: string, dependencyVersion: string): string {
   const splitHash = dependencyVersion.split('#');
   if (!dependencyVersion.startsWith('git+') || splitHash.length !== 2) {
     throw new Error(
       `Expected package dependency for ${dependencyName} in format git+<url>#<commit-hash>, but got ${dependencyVersion}`,
     );
   }
-  return splitHash[1]!;
+  const hash = splitHash[1]!;
+  if (!/^[0-9a-fA-F]{40}$/.test(hash)) {
+    throw new Error(
+      `Expected a 40-hex commit hash for ${dependencyName}, but got: ${hash}`,
+    );
+  }
+  return hash;
 }
packages/core/solidity/src/utils/imports-libraries.ts (1)

3-5: Annotate return type and extract the package prefix constant

Minor clarity improvements: add an explicit boolean return type and extract the package scope to a constant to avoid string duplication across the codebase and future typos.

-import type { Contract } from '../contract';
+import type { Contract } from '../contract';
+
+const COMMUNITY_CONTRACTS_PREFIX = '@openzeppelin/community-contracts/';
 
-export function importsCommunityContracts(contract: Contract) {
-  return contract.imports.some(i => i.path.startsWith('@openzeppelin/community-contracts/'));
+export function importsCommunityContracts(contract: Contract): boolean {
+  return contract.imports.some(i => i.path.startsWith(COMMUNITY_CONTRACTS_PREFIX));
 }
packages/ui/src/solidity/inject-hyperlinks.ts (3)

10-11: Quiet false-positive lint and improve robustness of the commit regex.

  • The static-analysis warning about “RegExp from variable” is a false positive here because the source pattern is a static literal. However, prefer .source to avoid the warning and be explicit.
  • Consider accepting uppercase hex too (harmless and more robust).

Apply this diff:

-  const compatibleCommunityContractsRegexSingle = /OpenZeppelin Community Contracts commit ([a-f0-9]{40})/;
-  const compatibleCommunityContractsRegexGlobal = new RegExp(compatibleCommunityContractsRegexSingle, 'g');
+  const compatibleCommunityContractsRegexSingle = /OpenZeppelin Community Contracts commit ([A-Fa-f0-9]{40})/;
+  const compatibleCommunityContractsRegexGlobal = new RegExp(compatibleCommunityContractsRegexSingle.source, 'gi');

15-18: Potential version mismatch for upgradeable imports; suggest function replacer to select the correct tag.

You’re using contractsVersion for both contracts and contracts-upgradeable. If their tags ever diverge, upgradeable links will 404. Use a function replacer and, if available in this package, import @openzeppelin/contracts-upgradeable/package.json to select the right tag.

Example (requires adding the import; if not feasible in this package, please verify versions are always in lockstep):

// At top of file (if dependency is available)
import { version as contractsUpgradeableVersion } from '@openzeppelin/contracts-upgradeable/package.json';

// Then, replace usage:
result = code.replace(
  importContractsRegex,
  (_m, org: string, pkg: 'contracts' | 'contracts-upgradeable', rest: string) => {
    const tag = pkg === 'contracts-upgradeable' ? contractsUpgradeableVersion : contractsVersion;
    return `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-${pkg}/blob/v${tag}/contracts/${rest}" target="_blank" rel="noopener noreferrer">${org}${pkg}/${rest}</a>&quot;`;
  },
);

If you’d rather not add a new dependency, please confirm that the versions (and tags) of @openzeppelin/contracts and @openzeppelin/contracts-upgradeable are guaranteed to match across releases in the environment where this runs. If not, I can propose an alternative that builds the link against the installed package’s package.json at runtime.


20-30: Consider fallback hyperlinking when no commit line is present.

If the commit comment isn’t detected, community-contract imports remain plain text. Optional: fall back to linking to the default branch (e.g., HEAD) to preserve a consistent UX.

Possible fallback:

// else branch after the existing if (...)
result = result.replace(
  importCommunityContractsRegex,
  `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/HEAD/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f39adfd and 4fd2179.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (6)
  • packages/core/solidity/openzeppelin-contracts.d.ts (1 hunks)
  • packages/core/solidity/package.json (1 hunks)
  • packages/core/solidity/src/print.ts (3 hunks)
  • packages/core/solidity/src/utils/community-contracts-git-commit.ts (1 hunks)
  • packages/core/solidity/src/utils/imports-libraries.ts (1 hunks)
  • packages/ui/src/solidity/inject-hyperlinks.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/core/solidity/src/print.ts (2)
packages/core/solidity/src/utils/imports-libraries.ts (1)
  • importsCommunityContracts (3-5)
packages/core/solidity/src/utils/community-contracts-git-commit.ts (1)
  • getCommunityContractsGitCommit (3-9)
🪛 ast-grep (0.38.6)
packages/ui/src/solidity/inject-hyperlinks.ts

[warning] 10-10: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(compatibleCommunityContractsRegexSingle, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: build (solidity, default)
  • GitHub Check: check
  • GitHub Check: build (stellar, compile)
  • GitHub Check: build (stylus, default)
  • GitHub Check: build (stellar, default)
  • GitHub Check: mcp
  • GitHub Check: build (cairo, default)
  • GitHub Check: format-lint
  • GitHub Check: deno-check
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (5)
packages/core/solidity/package.json (1)

25-25: Pinned commit for community contracts: good for determinism

Pinning to a specific commit via a git+ URL aligns with the new commit-aware header logic and ensures reproducible output.

packages/core/solidity/src/utils/community-contracts-git-commit.ts (1)

1-1: ✔️ JSON import support and packaging verified

  • packages/core/solidity/tsconfig.json extends the root tsconfig.base.json, which has resolveJsonModule: true.
  • Although package.json isn’t listed under "files", the npm manifest (package.json) is always included in published packages.

No changes are needed.

packages/core/solidity/openzeppelin-contracts.d.ts (1)

2-13: JSDoc additions improve maintainability

Clear docs for version, sources, and dependencies. No type changes; looks good.

packages/core/solidity/src/print.ts (2)

20-21: New utils are integrated in the right place

Imports for importsCommunityContracts and getCommunityContractsGitCommit are accurate and scoped to this module.


34-36: Header composition via helper improves readability

Replacing the hard-coded header with printCompatibleLibraryVersions keeps printContract focused and easier to extend.

@ericglau
Copy link
Member Author

@SocketSecurity ignore npm/[email protected]
Unrelated to this PR. Appears to be a false positive.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
packages/ui/src/solidity/inject-hyperlinks.ts (2)

5-6: Fix broken contracts repo URLs: capture repo without trailing slash and add missing slash before "blob".

Current replacement yields URLs like https://github.com/OpenZeppelin/openzeppelin-contracts//blob/... due to capturing the trailing slash in group 2 and missing the separator before blob. Also fixes the displayed import path.

Apply this diff:

-  const importContractsRegex =
-    /&quot;(@openzeppelin\/)(contracts-upgradeable\/|contracts\/)((?:(?!\.\.)[^/]+\/)*?[^/]*?)&quot;/g;
+  const importContractsRegex =
+    /&quot;(@openzeppelin\/)(contracts-upgradeable|contracts)\/((?:(?!\.\.)[^/]+\/)*?[^/]*?)&quot;/g;

   let result = code.replace(
     importContractsRegex,
-    `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-$2blob/v${contractsVersion}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
+    `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-$2/blob/v${contractsVersion}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2/$3</a>&quot;`,
   );

Also applies to: 15-18


23-25: Use "blob" (not "tree") for Community Contracts file links.

The file link should use blob//path to render the source file.

-        `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/tree/${compatibleCommunityContractsGitCommit}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
+        `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/${compatibleCommunityContractsGitCommit}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
🧹 Nitpick comments (2)
packages/ui/src/solidity/inject-hyperlinks.ts (2)

27-29: Optional: Link the commit hash to the commit page (more informative) instead of the repo tree.

The current link goes to the repository tree at the commit. If you want users to see the actual commit details, point to /commit/ and adjust the title accordingly.

-        `OpenZeppelin Community Contracts commit <a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/tree/$1" target="_blank" rel="noopener noreferrer" title="View repository at commit $1">$1</a>`,
+        `OpenZeppelin Community Contracts commit <a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/commit/$1" target="_blank" rel="noopener noreferrer" title="View commit $1">$1</a>`,

10-12: Avoid constructing RegExp from another RegExp’s source; use literals and allow uppercase hex.

This silences the static-analysis warning, simplifies the code, and accepts uppercase A–F which GitHub may display.

-  const compatibleCommunityContractsRegexSingle = /OpenZeppelin Community Contracts commit ([a-f0-9]{40})/;
-  const compatibleCommunityContractsRegexGlobal = new RegExp(compatibleCommunityContractsRegexSingle.source, 'g');
+  const compatibleCommunityContractsRegexSingle = /OpenZeppelin Community Contracts commit ([A-Fa-f0-9]{40})/;
+  const compatibleCommunityContractsRegexGlobal = /OpenZeppelin Community Contracts commit ([A-Fa-f0-9]{40})/g;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4fd2179 and 83d74d9.

📒 Files selected for processing (1)
  • packages/ui/src/solidity/inject-hyperlinks.ts (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.38.6)
packages/ui/src/solidity/inject-hyperlinks.ts

[warning] 10-10: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(compatibleCommunityContractsRegexSingle.source, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build (solidity, default)
  • GitHub Check: semgrep-cloud-platform/scan

ericglau and others added 3 commits August 12, 2025 16:17
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/ui/src/solidity/inject-hyperlinks.ts (1)

10-12: Avoid constructing a RegExp from another RegExp’s source (silence false-positive and improve clarity)

This isn’t vulnerable (pattern is static), but using a literal is clearer and avoids static-analysis warnings.

Apply this diff:

-  const compatibleCommunityContractsRegexGlobal = new RegExp(compatibleCommunityContractsRegexSingle.source, 'g');
+  const compatibleCommunityContractsRegexGlobal = /OpenZeppelin Community Contracts commit ([a-f0-9]{40})/g;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83d74d9 and cc37716.

📒 Files selected for processing (2)
  • packages/core/solidity/src/print.ts (3 hunks)
  • packages/ui/src/solidity/inject-hyperlinks.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/solidity/src/print.ts
🧰 Additional context used
🪛 ast-grep (0.38.6)
packages/ui/src/solidity/inject-hyperlinks.ts

[warning] 10-10: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(compatibleCommunityContractsRegexSingle.source, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Redirect rules - openzeppelin-contracts-wizard
  • GitHub Check: Header rules - openzeppelin-contracts-wizard
  • GitHub Check: Pages changed - openzeppelin-contracts-wizard
  • GitHub Check: build (solidity, default)
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (7)
packages/ui/src/solidity/inject-hyperlinks.ts (7)

5-8: Regexes for OZ import detection look correct

The patterns correctly:

  • detect both contracts and contracts-upgradeable imports,
  • avoid paths with ..,
  • and capture a stable triplet for replacement.

No issues spotted here.


13-14: Commit extraction is straightforward and safe

Using optional chaining to extract the SHA works well and avoids exceptions when the line is absent.


15-18: Contracts/Upgradeable links are well-formed

Building:

  • openzeppelin-contracts/blob/v/contracts/ and
  • openzeppelin-contracts-upgradeable/blob/v/contracts/

is correct for both repos.


27-29: Correct choice of “tree” for the commit-level link

Using tree/$commit for the “View repository at commit” hyperlink is appropriate since it points to the repo root at that commit (blob would be for files).


32-32: Return value handling is clean

Returning the progressively built result string is clear and maintainable.


23-25: Fix broken Community Contracts file links: duplicated “contracts/” segment

$3 already includes “contracts/...”. Adding “/contracts/” in the href creates a path like “.../contracts/contracts/...”, which 404s.

Apply this diff:

-        `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/${compatibleCommunityContractsGitCommit}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
+        `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/${compatibleCommunityContractsGitCommit}/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,

Note: Using blob for file links here is correct (thanks for aligning with prior feedback).

Likely an incorrect or invalid review comment.


20-30: Commit line emission and link injection are properly gated on community-contracts imports

The core printer in packages/core/solidity/src/print.ts only calls

const commit = getCommunityContractsGitCommit();
lines.push(`// Compatible with OpenZeppelin Community Contracts commit ${commit}`);

inside

if (importsCommunityContracts(contract)) {  }

and getCommunityContractsGitCommit() always returns a string (or logs an error if misconfigured). The UI injector in packages/ui/src/solidity/inject-hyperlinks.ts then looks for that same commit marker before applying any hyperlinks.

Because both commit-line emission and hyperlink injection share the importsCommunityContracts check, any contract importing @openzeppelin/community-contracts/... will always get the commit line — and thus the links — and no-import cases fall back cleanly. No further changes needed.

@ericglau
Copy link
Member Author

/review

@qodo-code-review
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

621 - Partially compliant

Compliant requirements:

  • Include the git commit hash of OpenZeppelin Community Contracts in the generated contract's comments when those imports are used.
  • Ensure the commit hash corresponds to the version pinned/used by the Wizard (from lockfile/deps) that is tested to work.

Non-compliant requirements:

  • Consider providing information or examples on how to install that specific commit (npm/forge).

Requires further human verification:

  • Verify that the displayed commit hash always matches the version actually used in production builds (CI/packaging environment), not just local devDependencies.
  • Manually test UI hyperlink rendering for community contracts imports and commit link across browsers.
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Robustness

Reading the commit hash from devDependencies may fail or drift in environments where dependencies are pruned or different (e.g., production build, yarn/pnpm). Confirm this source is reliable at runtime and in all packaging contexts.

import { devDependencies } from '../../package.json';

export function getCommunityContractsGitCommit(): string {
  const communityContractsVersion = devDependencies['@openzeppelin/community-contracts'];
  if (!communityContractsVersion) {
    throw new Error('@openzeppelin/community-contracts not found in devDependencies');
  }
  return extractGitCommitHash('@openzeppelin/community-contracts', communityContractsVersion);
}

function extractGitCommitHash(dependencyName: string, dependencyVersion: string): string {
  const splitHash = dependencyVersion.split('#');
  if (!dependencyVersion.startsWith('git+') || splitHash.length !== 2) {
    throw new Error(
      `Expected package dependency for ${dependencyName} in format git+<url>#<commit-hash>, but got ${dependencyVersion}`,
    );
  }
  return splitHash[1]!;
Regex Fragility

The regex assumes a 40-hex commit and specific HTML-escaped patterns. Changes in formatting or comments could break link injection. Validate against multiline comments and ensure no false positives.

const compatibleCommunityContractsRegexSingle = /OpenZeppelin Community Contracts commit ([a-f0-9]{40})/;
const compatibleCommunityContractsRegexGlobal = new RegExp(compatibleCommunityContractsRegexSingle.source, 'g');

const compatibleCommunityContractsGitCommit = code.match(compatibleCommunityContractsRegexSingle)?.[1];

let result = code.replace(
  importContractsRegex,
  `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-$2blob/v${contractsVersion}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
);

if (compatibleCommunityContractsGitCommit !== undefined) {
  result = result
    .replace(
      importCommunityContractsRegex,
      `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/${compatibleCommunityContractsGitCommit}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
    )
    .replace(
      compatibleCommunityContractsRegexGlobal,
      `OpenZeppelin Community Contracts commit <a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/tree/$1" target="_blank" rel="noopener noreferrer" title="View repository at commit $1">$1</a>`,
    );
}

return result;
Silent Failure

Swallowing errors with console.error when commit extraction fails may hide issues and leave imports unlinked. Consider adding a fallback comment or explicit omission notice for transparency.

  const lines: string[] = [];
  lines.push(`// Compatible with OpenZeppelin Contracts ${compatibleContractsSemver}`);
  if (importsCommunityContracts(contract)) {
    try {
      const commit = getCommunityContractsGitCommit();
      lines.push(`// Compatible with OpenZeppelin Community Contracts commit ${commit}`);
    } catch (e) {
      console.error(e);
    }
  }
  return lines;
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/solidity/src/utils/community-contracts-git-commit.ts (1)

11-25: Add unit tests for parsing edge cases

Consider covering:

  • Valid: git+https://...git#<40-hex>
  • Valid: github:OpenZeppelin/community-contracts#<40-hex>
  • Invalid: missing “#”, non-hex, short/long hash

This will lock in behavior and prevent regressions if the dependency format changes.

I can draft a test file under packages/core/solidity/test for these scenarios. Want me to push a test scaffold?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5bca3cd and 22e6639.

📒 Files selected for processing (1)
  • packages/core/solidity/src/utils/community-contracts-git-commit.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/core/solidity/src/utils/community-contracts-git-commit.ts (1)
packages/core/stellar/src/contract.ts (1)
  • Error (19-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build (stellar, compile)
  • GitHub Check: build (solidity, default)
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (2)
packages/core/solidity/src/utils/community-contracts-git-commit.ts (2)

3-9: Exceptions from getCommunityContractsGitCommit Are Already Handled

The only usage of getCommunityContractsGitCommit() in print.ts (lines 62–68) is wrapped in a try/catch that logs errors, ensuring graceful degradation if the dependency is missing or malformed. No further changes are needed.


1-9: No action needed: named JSON import is supported

The packages/core/solidity/tsconfig.json extends the root tsconfig.base.json, which has
"resolveJsonModule": true
"esModuleInterop": true

and it compiles to CommonJS without a separate bundler. At runtime Node’s require('../../package.json') returns the JSON object, so

import { devDependencies } from '../../package.json';

desugars into

const { devDependencies } = require('../../package.json');

which works correctly. You can keep the named import as-is; no change is required here.

@ericglau ericglau requested a review from ernestognw August 12, 2025 21:45
@ericglau
Copy link
Member Author

/improve

@qodo-code-review
Copy link

qodo-code-review bot commented Aug 12, 2025

PR Code Suggestions ✨

CategorySuggestion                                                                                                                                    Impact
General
Tighten regex to target comment

Ensure regex is anchored to the comment line to avoid accidental matches
elsewhere and prevent partial replacement collisions. Use a non-capturing
boundary and word boundaries to precisely target the compatibility comment.

packages/ui/src/solidity/inject-hyperlinks.ts [10-30]

-const compatibleCommunityContractsRegexSingle = /Community Contracts commit ([a-fA-F0-9]{7,40})/;
+const compatibleCommunityContractsRegexSingle = /(?:^|[\n\r])\s*\/\/ Compatible with OpenZeppelin Contracts [^\n\r]*? and Community Contracts commit ([a-fA-F0-9]{7,40})/;
 const compatibleCommunityContractsRegexGlobal = new RegExp(compatibleCommunityContractsRegexSingle.source, 'g');
 
 const compatibleCommunityContractsGitCommit = code.match(compatibleCommunityContractsRegexSingle)?.[1];
 
 let result = code.replace(
   importContractsRegex,
   `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-$2blob/v${contractsVersion}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
 );
 
 if (compatibleCommunityContractsGitCommit !== undefined) {
   result = result
     .replace(
       importCommunityContractsRegex,
       `&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/${compatibleCommunityContractsGitCommit}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
     )
     .replace(
       compatibleCommunityContractsRegexGlobal,
-      `Community Contracts commit <a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/tree/$1" target="_blank" rel="noopener noreferrer" title="View repository at commit $1">$1</a>`,
+      (_m) =>
+        _m.replace(
+          /Community Contracts commit ([a-fA-F0-9]{7,40})/,
+          `Community Contracts commit <a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/tree/$1" target="_blank" rel="noopener noreferrer" title="View repository at commit $1">$1</a>`,
+        ),
     );
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion improves the robustness of the hyperlink injection by making the regex more specific, which prevents potential incorrect replacements if the pattern appears outside of the intended comment.

Medium
Remove stderr side effects

Avoid printing to stderr during code generation, as this can pollute generated
artifacts and CI logs. Instead, fall back silently to the base message or
include a minimal inline note without side effects. This prevents unexpected
runtime behavior when dependency format is not met.

packages/core/solidity/src/print.ts [59-70]

 function printCompatibleLibraryVersions(contract: Contract): string {
   let result = `// Compatible with OpenZeppelin Contracts ${compatibleContractsSemver}`;
   if (importsCommunityContracts(contract)) {
     try {
       const commit = getCommunityContractsGitCommit();
       result += ` and Community Contracts commit ${commit}`;
-    } catch (e) {
-      console.error(e);
+    } catch {
+      // Fallback silently if commit cannot be resolved
     }
   }
   return result;
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that logging to stderr is an undesirable side effect for a code generation function, and removing it improves robustness by preventing pollution of CI logs.

Low
Trim version string inputs

Guard against trailing whitespace or newline characters in the version string to
avoid false negatives. Trim the input before parsing and validating the hash.

packages/core/solidity/src/utils/community-contracts-git-commit.ts [24-38]

 export function extractGitCommitHash(dependencyName: string, dependencyVersion: string): string {
-  const split = dependencyVersion.split('#');
-  if (!dependencyVersion.startsWith('git+') || split.length !== 2) {
+  const cleaned = dependencyVersion.trim();
+  const split = cleaned.split('#');
+  if (!cleaned.startsWith('git+') || split.length !== 2) {
     throw new Error(
       `Expected package dependency for ${dependencyName} in format git+<url>#<commit-hash>, but got ${dependencyVersion}`,
     );
   }
-  const hash = split[1]!;
+  const hash = split[1]!.trim();
   if (!/^[a-fA-F0-9]{7,40}$/.test(hash)) {
     throw new Error(
       `Expected git commit hash for package dependency ${dependencyName} to have between 7 and 40 hex chars, but got ${hash}`,
     );
   }
   return hash.toLowerCase();
 }
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion improves the robustness of the hash extraction by trimming whitespace from the input, which makes the parsing more resilient to formatting variations in package.json.

Low
  • Update

Copy link
Member

@ernestognw ernestognw left a comment

Choose a reason for hiding this comment

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

Amazing, thanks @ericglau !

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
packages/core/solidity/src/utils/community-contracts-git-commit.ts (1)

15-23: Document the lowercase normalization in JSDoc

Since extractGitCommitHash returns a lowercased hash, make this explicit in the doc to avoid surprises for callers comparing hashes case-sensitively.

 /**
  * Extracts the git commit hash from a package dependency version string.
  * The expected format is `git+<url>#<commit-hash>`.
  *
  * @param dependencyName The name of the package dependency.
  * @param dependencyVersion The version string of the package dependency.
- * @returns The git commit hash.
+ * @returns The git commit hash, normalized to lowercase.
  * @throws Error if the version string or commit hash is not in the expected format.
  */
packages/core/solidity/src/utils/community-contracts-git-commit.test.ts (1)

1-116: Optional: add a test for 40-char uppercase normalization

You test 7-char uppercase normalization; consider adding a 40-char uppercase case to fully cover the normalization branch for both extremes.

packages/ui/src/solidity/inject-hyperlinks.ts (2)

10-12: Avoid dynamic RegExp construction (minor; readability and static analysis)

Static analysis flags this pattern, and it’s easy to avoid by declaring the global variant as a literal. It also reads simpler.

-  const compatibleCommunityContractsRegexSingle = /Community Contracts commit ([a-fA-F0-9]{7,40})/;
-  const compatibleCommunityContractsRegexGlobal = new RegExp(compatibleCommunityContractsRegexSingle.source, 'g');
+  const compatibleCommunityContractsRegexSingle = /Community Contracts commit ([a-fA-F0-9]{7,40})/;
+  const compatibleCommunityContractsRegexGlobal = /Community Contracts commit ([a-fA-F0-9]{7,40})/g;

Note: Functionality remains identical; this primarily silences tooling and improves clarity.


3-33: Minor perf/readability: hoist constant regexes out of the function

These regexes are pure constants. Hoisting them to module scope avoids re-allocating on every call and clarifies intent. Not critical, just a nicety.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22e6639 and 1afcd4f.

⛔ Files ignored due to path filters (3)
  • packages/core/solidity/src/account.test.ts.snap is excluded by !**/*.snap
  • packages/core/solidity/src/stablecoin.test.ts.snap is excluded by !**/*.snap
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (7)
  • packages/core/solidity/package.json (1 hunks)
  • packages/core/solidity/src/account.test.ts.md (77 hunks)
  • packages/core/solidity/src/print.ts (3 hunks)
  • packages/core/solidity/src/stablecoin.test.ts.md (4 hunks)
  • packages/core/solidity/src/utils/community-contracts-git-commit.test.ts (1 hunks)
  • packages/core/solidity/src/utils/community-contracts-git-commit.ts (1 hunks)
  • packages/ui/src/solidity/inject-hyperlinks.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • packages/core/solidity/src/stablecoin.test.ts.md
  • packages/core/solidity/src/account.test.ts.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/solidity/package.json
  • packages/core/solidity/src/print.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/core/solidity/src/utils/community-contracts-git-commit.test.ts (1)
packages/core/solidity/src/utils/community-contracts-git-commit.ts (1)
  • extractGitCommitHash (24-38)
🪛 ast-grep (0.38.6)
packages/ui/src/solidity/inject-hyperlinks.ts

[warning] 10-10: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(compatibleCommunityContractsRegexSingle.source, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: check
  • GitHub Check: build (stylus, default)
  • GitHub Check: build (stellar, default)
  • GitHub Check: build (cairo, default)
  • GitHub Check: build (stellar, compile)
  • GitHub Check: build (solidity, default)
  • GitHub Check: mcp
  • GitHub Check: format-lint
  • GitHub Check: deno-check
  • GitHub Check: Redirect rules - openzeppelin-contracts-wizard
  • GitHub Check: Header rules - openzeppelin-contracts-wizard
  • GitHub Check: Pages changed - openzeppelin-contracts-wizard
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (8)
packages/core/solidity/src/utils/community-contracts-git-commit.ts (2)

7-13: Dependency lookup and error handling look solid

Reading the pinned commit from devDependencies is straightforward, and the error message is clear for misconfiguration scenarios. No functional concerns here.


24-38: Validation is strict and consistent with tests; good normalization

The parser enforces git+ and a single #, validates 7–40 hex, and normalizes to lowercase. This matches the test suite and is appropriate given you control the package.json format.

packages/core/solidity/src/utils/community-contracts-git-commit.test.ts (4)

7-13: Covers the happy path well

Valid 40-char lowercase hash is handled correctly. Good.


15-21: Nice: case normalization behavior explicitly tested

Verifying uppercase short hash normalization helps prevent regressions.


25-68: Invalid format coverage is comprehensive

The three format guards (missing git+, missing #, multiple #) align with the implementation’s error messages.


72-115: Invalid hash content coverage is thorough

Length bounds and non-hex detection are well tested.

packages/ui/src/solidity/inject-hyperlinks.ts (2)

15-18: OZ Contracts links are constructed correctly

The capture for contracts{,-upgradeable}/ and the blob/v${contractsVersion}/contracts/$3 href yield valid file URLs without double slashes. Looks good.


20-30: Correct: use blob for Community Contracts file links and link the commit line

  • File imports to Community Contracts correctly use blob/${commit}.
  • The commit line linking to tree/$1 matches the intent (“View repository at commit …”).

Copy link
Contributor

@CoveMB CoveMB left a comment

Choose a reason for hiding this comment

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

Great work 🌾

@@ -0,0 +1,5 @@
import type { Contract } from '../contract';

export function importsCommunityContracts(contract: Contract) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nits: as it returns a boolean maybe could be named doesImportsCommunityContracts

Copy link
Member Author

Choose a reason for hiding this comment

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

The s in imports implies boolean

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/ui/src/common/styles/global.css (1)

149-151: Use design tokens and add hover/focus states for accessibility and consistency.

Hard-coding #7d828d bypasses your theme system and may have insufficient contrast in some themes. Prefer CSS vars and provide hover/focus styles so links look clickable.

Apply this diff:

-.comment-link {
-  color: #7d828d;
-}
+.comment-link {
+  color: var(--gray-4);
+  text-decoration: underline;
+  text-underline-offset: 2px;
+}
+.comment-link:hover,
+.comment-link:focus-visible {
+  color: var(--gray-3);
+}
packages/ui/src/solidity/inject-hyperlinks.ts (1)

10-12: Avoid constructing a RegExp from another at runtime; simplify flags instead.

This triggers static analysis warnings even though it’s safe here. Duplicating the pattern with desired flags improves clarity and avoids the false positive.

Apply this diff:

-  const compatibleCommunityContractsRegexSingle = /Community Contracts commit ([a-fA-F0-9]{7,40})/;
-  const compatibleCommunityContractsRegexGlobal = new RegExp(compatibleCommunityContractsRegexSingle.source, 'g');
+  const compatibleCommunityContractsRegexSingle = /Community Contracts commit ([a-f0-9]{7,40})/i;
+  const compatibleCommunityContractsRegexGlobal = /Community Contracts commit ([a-f0-9]{7,40})/gi;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11ec9af and 615ebbe.

📒 Files selected for processing (2)
  • packages/ui/src/common/styles/global.css (1 hunks)
  • packages/ui/src/solidity/inject-hyperlinks.ts (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.38.6)
packages/ui/src/solidity/inject-hyperlinks.ts

[warning] 10-10: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(compatibleCommunityContractsRegexSingle.source, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Redirect rules - openzeppelin-contracts-wizard
  • GitHub Check: Header rules - openzeppelin-contracts-wizard
  • GitHub Check: Pages changed - openzeppelin-contracts-wizard
  • GitHub Check: build (stellar, compile)
  • GitHub Check: build (solidity, default)
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
packages/ui/src/solidity/inject-hyperlinks.ts (4)

5-8: Regexes for OZ imports look good.

The patterns are scoped and exclude any path with “..”, reducing injection risk in hrefs. Grouping matches the replacement logic.


15-18: Contracts URL generation remains correct.

Using the trailing slash in group 2 with “openzeppelin-$2blob” continues to resolve to openzeppelin-contracts/blob and openzeppelin-contracts-upgradeable/blob as intended.


27-29: Correct choice of “tree” for the commit anchor.

Linking the commit hash to “tree/” is appropriate for showing the repo at that commit. Keeping file links on “blob/” is also correct.


20-29: No contracts/contracts/ duplication possible – current URL logic is correct.

Verified across the repo that every import from @openzeppelin/community-contracts/… omits a leading contracts/ segment (no instances of @openzeppelin/community-contracts/contracts/... in source files). The only occurrence of /contracts/ lives in the Solidity remappings file, not in code imports. Therefore $3 will never start with contracts/, and the existing unconditional contracts/$3 prefix will not produce duplicate paths.

You can safely ignore the proposed normalization change.

Likely an incorrect or invalid review comment.

@ericglau ericglau merged commit 2bb2a16 into OpenZeppelin:master Aug 13, 2025
18 checks passed
@ericglau ericglau deleted the communityhash branch August 13, 2025 16:09
@github-actions github-actions bot locked and limited conversation to collaborators Aug 13, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add git commit hash in contract comments for Community Contracts

3 participants