Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/lib/__tests__/githubYearInReview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,87 @@ describe("fetchYearInReviewData additional coverage", () => {
}
});




it("handles missing repository data in mergeTopRepository", async () => {
mockFetch.mockImplementation(() => {
return Promise.resolve(jsonResponse({
data: {
user: {
id: "123",
contributionsCollection: {
totalCommitContributions: 100,
totalPullRequestContributions: 50,
totalIssueContributions: 10,
totalPullRequestReviewContributions: 0,
contributionCalendar: { totalContributions: 160, weeks: [] },
}
}
}
}));
});

const data = await fetchYearInReviewData("testuser", 2023, "token");
expect(data.topRepository).toBeNull();
});

it("handles undefined repository data in mergeTopRepository", async () => {
mockFetch.mockImplementation(() => {
return Promise.resolve(jsonResponse({
data: {
user: {
id: "123",
contributionsCollection: {
totalCommitContributions: 100,
totalPullRequestContributions: 50,
totalIssueContributions: 10,
totalPullRequestReviewContributions: 0,
contributionCalendar: { totalContributions: 160, weeks: [] },
commitContributionsByRepository: undefined,
pullRequestContributionsByRepository: undefined,
issueContributionsByRepository: undefined,
}
}
}
}));
});

const data = await fetchYearInReviewData("testuser", 2023, "token");
expect(data.topRepository).toBeNull();
});

it("handles full repository data in mergeTopRepository", async () => {
mockFetch.mockImplementation(() => {
return Promise.resolve(jsonResponse({
data: {
user: {
id: "123",
contributionsCollection: {
totalCommitContributions: 100,
totalPullRequestContributions: 50,
totalIssueContributions: 10,
totalPullRequestReviewContributions: 0,
contributionCalendar: { totalContributions: 160, weeks: [] },
commitContributionsByRepository: [
{ repository: { owner: { login: "user1" }, name: "repo1" }, contributions: { totalCount: 50 } }
],
pullRequestContributionsByRepository: [
{ repository: { owner: { login: "user1" }, name: "repo1" }, contributions: { totalCount: 20 } }
],
issueContributionsByRepository: [
{ repository: { owner: { login: "user1" }, name: "repo1" }, contributions: { totalCount: 10 } }
],
}
}
}
}));
});

const data = await fetchYearInReviewData("testuser", 2023, "token");
expect(data.topRepository).toEqual({ name: "user1/repo1", contributions: 80 });
});

it("handles partial repository data in mergeTopRepository", async () => {
mockFetch.mockImplementation(() => {
return Promise.resolve(jsonResponse({
Expand Down
21 changes: 16 additions & 5 deletions src/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,13 +743,24 @@ export const fetchActivity = cache(async function fetchActivity(

/**
* 効率的に Map から上位 K 件を抽出するヘルパー関数
* 上位 K 件だけを返します。
* 配列の作成とソートを最小限に抑えることでパフォーマンスを向上させます
*/
function getTopK(map: Map<string, number>, k: number = 10): { name: string; count: number }[] {
const limit = Math.max(0, k);
return Array.from(map, ([name, count]) => ({ name, count }))
.sort((a, b) => b.count - a.count)
.slice(0, limit);
const top: { name: string; count: number }[] = [];
for (const [name, count] of map.entries()) {
if (top.length < k) {
top.push({ name, count });
top.sort((a, b) => b.count - a.count);
} else if (count > top[k - 1].count) {
let i = k - 2;
while (i >= 0 && top[i].count < count) {
top[i + 1] = top[i];
i--;
}
top[i + 1] = { name, count };
}
}
return top;
}

/**
Expand Down
16 changes: 10 additions & 6 deletions src/lib/githubYearInReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,19 @@ async function graphql<T>(query: string, token: string, variables: Record<string

function mergeTopRepository(data: NonNullable<YearInReviewResponse["user"]>["contributionsCollection"]): { name: string; contributions: number } | null {
const counter = new Map<string, number>();

const buckets = [
...(data.commitContributionsByRepository || []),
...(data.pullRequestContributionsByRepository || []),
...(data.issueContributionsByRepository || []),
data.commitContributionsByRepository,
data.pullRequestContributionsByRepository,
data.issueContributionsByRepository,
];

for (const item of buckets) {
const name = `${item.repository.owner.login}/${item.repository.name}`;
counter.set(name, (counter.get(name) ?? 0) + item.contributions.totalCount);
for (const bucket of buckets) {
if (!bucket) continue;
for (const item of bucket) {
const name = `${item.repository.owner.login}/${item.repository.name}`;
counter.set(name, (counter.get(name) ?? 0) + item.contributions.totalCount);
}
Comment on lines 122 to +133

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

型定義をオプショナルに更新し、if (!bucket) continue ガードの必要性を型レベルで保証すべきです。

実行時には commitContributionsByRepository 等が undefined になり得ます(テスト436-459行目で検証済み)が、型定義(75-77行目)では非オプショナル配列 ContributionsByRepoNode[] として宣言されています。TypeScriptは bucketsContributionsByRepoNode[][] と推論するため、if (!bucket) continue が不要に見えてしまい、将来の誤削除リスクがあります。

🔧 型定義の修正案(75-77行目)
-            commitContributionsByRepository: ContributionsByRepoNode[];
-            pullRequestContributionsByRepository: ContributionsByRepoNode[];
-            issueContributionsByRepository: ContributionsByRepoNode[];
+            commitContributionsByRepository?: ContributionsByRepoNode[];
+            pullRequestContributionsByRepository?: ContributionsByRepoNode[];
+            issueContributionsByRepository?: ContributionsByRepoNode[];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/githubYearInReview.ts` around lines 122 - 133, Update the type
definitions for commitContributionsByRepository,
pullRequestContributionsByRepository, and issueContributionsByRepository to
allow undefined values, matching their runtime behavior. Ensure the buckets
collection and loop in the contribution aggregation flow retain the if (!bucket)
continue guard as type-required handling.

}

let top: { name: string; contributions: number } | null = null;
Expand Down
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default defineConfig({
provider: "v8",
reporter: ["text", "lcov"],
include: [
"src/lib/githubYearInReview.ts",
"src/lib/**/*.ts",
"src/hooks/**/*.ts",
"src/components/ThemeController.tsx",
Expand Down
Loading