Skip to content

Commit 3ef984b

Browse files
committed
Merge branch 'main' into collectioneur/transition-tracker-v2
2 parents 2e6571d + 9ba3c5d commit 3ef984b

1,072 files changed

Lines changed: 45357 additions & 11471 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/coding-standards/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Coding standards for the Expensify App. Each standard is a standalone file in `r
4444
- [CONSISTENCY-6](rules/consistency-6-proper-error-handling.md) — Proper error handling
4545

4646
### Clean React Patterns
47+
- [CLEAN-REACT-PATTERNS-0](rules/clean-react-0-compiler.md) — React Compiler compliance
4748
- [CLEAN-REACT-PATTERNS-1](rules/clean-react-1-composition-over-config.md) — Composition over configuration
4849
- [CLEAN-REACT-PATTERNS-2](rules/clean-react-2-own-behavior.md) — Components own their behavior
4950
- [CLEAN-REACT-PATTERNS-3](rules/clean-react-3-context-free-contracts.md) — Context-free component contracts
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
ruleId: CLEAN-REACT-PATTERNS-0
3+
title: React Compiler compliance
4+
---
5+
6+
## [CLEAN-REACT-PATTERNS-0] React Compiler compliance
7+
8+
### Reasoning
9+
10+
React Compiler is enabled in this codebase (`babel-plugin-react-compiler` runs first in both webpack and metro configs). It automatically memoizes components and hooks at the AST level — analyzing data flow, tracking dependencies, and inserting fine-grained caching that is more precise than any hand-written `useMemo`, `useCallback`, or `React.memo`.
11+
12+
Manual memoization is therefore:
13+
14+
1. **Redundant** — the compiler already handles it, so the manual wrapper adds zero value
15+
2. **Harmful** — it interferes with the compiler's optimization model, potentially preventing it from applying its own caching strategy or causing double-wrapping
16+
3. **Noisy** — it clutters the codebase with dependency arrays that must be maintained, reviewed, and debugged
17+
18+
The codebase enforces this via:
19+
- **Babel plugin**: `babel-plugin-react-compiler` in `babel.config.js`
20+
- **ESLint processor**: `eslint-plugin-react-compiler-compat` suppresses redundant lint rules when files compile successfully
21+
- **CI compliance check**: `scripts/react-compiler-compliance-check.ts` blocks PRs with manual memoization in new files
22+
23+
Reference: [React Compiler documentation](https://react.dev/learn/react-compiler)
24+
25+
### Incorrect
26+
27+
#### Incorrect (useCallback)
28+
29+
```tsx
30+
function ReportScreen({reportID}: {reportID: string}) {
31+
const handlePress = useCallback(() => {
32+
Navigation.navigate(ROUTES.REPORT_DETAILS.getRoute(reportID));
33+
}, [reportID]);
34+
35+
return <Button onPress={handlePress} />;
36+
}
37+
```
38+
39+
#### Incorrect (useMemo)
40+
41+
```tsx
42+
function PolicyList({policies}: {policies: Policy[]}) {
43+
const sortedPolicies = useMemo(
44+
() => policies.sort((a, b) => a.name.localeCompare(b.name)),
45+
[policies],
46+
);
47+
48+
return <FlatList data={sortedPolicies} renderItem={renderItem} />;
49+
}
50+
```
51+
52+
#### Incorrect (React.memo)
53+
54+
```tsx
55+
const Avatar = React.memo(function Avatar({source, size}: AvatarProps) {
56+
return <Image source={source} style={getAvatarStyle(size)} />;
57+
});
58+
```
59+
60+
### Correct
61+
62+
#### Correct (plain function — compiler memoizes automatically)
63+
64+
```tsx
65+
function ReportScreen({reportID}: {reportID: string}) {
66+
const handlePress = () => {
67+
Navigation.navigate(ROUTES.REPORT_DETAILS.getRoute(reportID));
68+
};
69+
70+
return <Button onPress={handlePress} />;
71+
}
72+
```
73+
74+
#### Correct (plain expression — compiler memoizes automatically)
75+
76+
```tsx
77+
function PolicyList({policies}: {policies: Policy[]}) {
78+
const sortedPolicies = policies.sort((a, b) => a.name.localeCompare(b.name));
79+
80+
return <FlatList data={sortedPolicies} renderItem={renderItem} />;
81+
}
82+
```
83+
84+
#### Correct (plain component — compiler memoizes automatically)
85+
86+
```tsx
87+
function Avatar({source, size}: AvatarProps) {
88+
return <Image source={source} style={getAvatarStyle(size)} />;
89+
}
90+
```
91+
92+
---
93+
94+
### Review Metadata
95+
96+
#### Verification
97+
98+
Before flagging, verify that the file actually compiles with React Compiler:
99+
100+
```bash
101+
npx react-compiler-healthcheck --src "<filepath>" --verbose
102+
```
103+
104+
If the output contains **"Failed to compile"** for the file under review, the rule **does not apply** — the author may have no alternative to manual memoization until the compilation issue is resolved.
105+
106+
#### Condition
107+
108+
The verification step above is a prerequisite. Only flag when the file compiles successfully AND any of these are true in new or modified code:
109+
110+
1. **`useCallback`** — A function is wrapped in `useCallback`. The compiler automatically memoizes closures based on their captured variables.
111+
2. **`useMemo`** — A value is wrapped in `useMemo`. The compiler automatically caches derived values.
112+
3. **`React.memo`** — A component is wrapped in `React.memo` (or `memo` imported from React). The compiler automatically skips re-rendering components whose props haven't changed.
113+
114+
**Response:** Challenge the author: "React Compiler is enabled — remove the manual memoization and restructure the code so the compiler can handle it."
115+
116+
The goal is to fix the root cause (make code compiler-friendly) rather than slap on manual memoization as a workaround.
117+
118+
**Search Patterns:**
119+
- `useCallback\s*\(` — manual callback memoization
120+
- `useMemo\s*\(` — manual value memoization
121+
- `React\.memo\s*\(` or `memo\s*\(` — manual component memoization
122+
- Import statements: `useCallback`, `useMemo` from `react`
123+
124+
**DO NOT flag if:**
125+
- The file does not compile with React Compiler (verified by the compliance check in the Verification section above)
126+
- The code is inside `node_modules/`, `patches/`, or test fixtures
127+
- The manual memoization exists in unchanged lines (pre-existing code not touched by the diff)

.github/ISSUE_TEMPLATE/OnboardOffboardExpertContributor.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ Which action do you wish to take for this team member (select one):
3434
### Ring0 Tasks
3535

3636
- [ ] If adding, add to the appropriate GitHub child team of [external-expert-contributors](https://github.com/orgs/Expensify/teams/external-expert-contributors/teams) (each agency must have its own child team)
37+
- [ ] If adding, create an [IdentityDot account](https://stackoverflowteams.com/c/expensify/questions/22914) for the contributor
3738
- [ ] If removing, remove from our organization [here](https://github.com/orgs/Expensify/people)
39+
- [ ] If removing, delete the contributor's [IdentityDot account](https://stackoverflowteams.com/c/expensify/questions/22914)

.github/actions/javascript/getPullRequestIncrementalChanges/getPullRequestIncrementalChanges.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as core from '@actions/core';
22
import {context} from '@actions/github';
3+
import {RequestError} from '@octokit/request-error';
34
import type {PullRequestEvent, PullRequestSynchronizeEvent} from '@octokit/webhooks-types';
45
import {getJSONInput} from '@github/libs/ActionUtils';
56
import CONST from '@github/libs/CONST';
@@ -89,7 +90,29 @@ async function run(): Promise<void> {
8990

9091
// Now we know there are local changes - get PR diff from the GitHub API to compare
9192
console.log(`🌐 Using GitHub API to validate ${localChangedFiles.size} files with local changes`);
92-
const prDiff = Git.parseDiff(await GitHubUtils.getPullRequestDiff(prNumber));
93+
94+
let prDiffString: string;
95+
try {
96+
prDiffString = await GitHubUtils.getPullRequestDiff(prNumber);
97+
} catch (error) {
98+
const isTooLarge =
99+
error instanceof RequestError &&
100+
typeof error.response?.data === 'object' &&
101+
error.response.data !== null &&
102+
'errors' in error.response.data &&
103+
((error.response.data as {errors?: Array<{code?: string}>}).errors ?? []).some((e) => e.code === 'too_large');
104+
105+
if (!isTooLarge) {
106+
throw error;
107+
}
108+
109+
core.warning(`PR #${prNumber} diff is too large for the GitHub API. Skipping incremental change detection.`);
110+
core.setOutput('CHANGED_FILES', JSON.stringify([]));
111+
core.setOutput('HAS_CHANGES', false);
112+
return;
113+
}
114+
115+
const prDiff = Git.parseDiff(prDiffString);
93116

94117
// Compare the local push diff with the PR diff and collect changed files, checking for overlapping content changes at the line level
95118
for (const prFileDiff of prDiff.files) {

.github/actions/javascript/getPullRequestIncrementalChanges/index.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11582,6 +11582,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
1158211582
Object.defineProperty(exports, "__esModule", ({ value: true }));
1158311583
const core = __importStar(__nccwpck_require__(2186));
1158411584
const github_1 = __nccwpck_require__(5438);
11585+
const request_error_1 = __nccwpck_require__(537);
1158511586
const ActionUtils_1 = __nccwpck_require__(6981);
1158611587
const CONST_1 = __importDefault(__nccwpck_require__(9873));
1158711588
const GithubUtils_1 = __importDefault(__nccwpck_require__(9296));
@@ -11655,7 +11656,25 @@ async function run() {
1165511656
}
1165611657
// Now we know there are local changes - get PR diff from the GitHub API to compare
1165711658
console.log(`🌐 Using GitHub API to validate ${localChangedFiles.size} files with local changes`);
11658-
const prDiff = Git_1.default.parseDiff(await GithubUtils_1.default.getPullRequestDiff(prNumber));
11659+
let prDiffString;
11660+
try {
11661+
prDiffString = await GithubUtils_1.default.getPullRequestDiff(prNumber);
11662+
}
11663+
catch (error) {
11664+
const isTooLarge = error instanceof request_error_1.RequestError &&
11665+
typeof error.response?.data === 'object' &&
11666+
error.response.data !== null &&
11667+
'errors' in error.response.data &&
11668+
(error.response.data.errors ?? []).some((e) => e.code === 'too_large');
11669+
if (!isTooLarge) {
11670+
throw error;
11671+
}
11672+
core.warning(`PR #${prNumber} diff is too large for the GitHub API. Skipping incremental change detection.`);
11673+
core.setOutput('CHANGED_FILES', JSON.stringify([]));
11674+
core.setOutput('HAS_CHANGES', false);
11675+
return;
11676+
}
11677+
const prDiff = Git_1.default.parseDiff(prDiffString);
1165911678
// Compare the local push diff with the PR diff and collect changed files, checking for overlapping content changes at the line level
1166011679
for (const prFileDiff of prDiff.files) {
1166111680
const filePath = prFileDiff.filePath;
@@ -12506,6 +12525,7 @@ class Git {
1250612525
newStart,
1250712526
newCount,
1250812527
lines: [],
12528+
contextLineCount: 0,
1250912529
};
1251012530
}
1251112531
continue;
@@ -12533,7 +12553,8 @@ class Git {
1253312553
});
1253412554
}
1253512555
else if (firstChar === ' ') {
12536-
// Context line - skip it (we only care about added/removed lines)
12556+
// Context line - count it so calculateLineNumber accounts for position advancement
12557+
currentHunk.contextLineCount++;
1253712558
continue;
1253812559
}
1253912560
else if (firstChar === '\\') {
@@ -12597,9 +12618,9 @@ class Git {
1259712618
const removedCount = hunk.lines.filter((l) => l.type === 'removed').length;
1259812619
switch (lineType) {
1259912620
case 'added':
12600-
return hunk.newStart + addedCount;
12621+
return hunk.newStart + hunk.contextLineCount + addedCount;
1260112622
case 'removed':
12602-
return hunk.oldStart + removedCount;
12623+
return hunk.oldStart + hunk.contextLineCount + removedCount;
1260312624
default:
1260412625
throw new Error(`Unknown line type: ${String(lineType)}`);
1260512626
}
@@ -12795,6 +12816,7 @@ class Git {
1279512816
newStart: 1,
1279612817
newCount: lines.length,
1279712818
lines: diffLines,
12819+
contextLineCount: 0,
1279812820
};
1279912821
const fileDiff = {
1280012822
filePath,

.github/actions/javascript/reviewerChecklist/index.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11622,15 +11622,13 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems) {
1162211622
})
1162311623
.then(() => {
1162411624
console.log(`Looking through all ${combinedComments.length} comments for the reviewer checklist...`);
11625+
const maxCompletedItems = numberOfChecklistItems + 2;
11626+
const minCompletedItems = numberOfChecklistItems - 2;
1162511627
let foundReviewerChecklist = false;
1162611628
let numberOfFinishedChecklistItems = 0;
1162711629
let numberOfUnfinishedChecklistItems = 0;
1162811630
// Once we've gathered all the data, loop through each comment and look to see if it contains the reviewer checklist
1162911631
for (let i = 0; i < combinedComments.length; i++) {
11630-
// Skip all other comments if we already found the reviewer checklist
11631-
if (foundReviewerChecklist) {
11632-
break;
11633-
}
1163411632
const whitespace = /([\n\r])/gm;
1163511633
const comment = combinedComments.at(i)?.replaceAll(whitespace, '');
1163611634
console.log(`Comment ${i} starts with: ${comment?.slice(0, 20)}...`);
@@ -11640,14 +11638,16 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems) {
1164011638
foundReviewerChecklist = true;
1164111639
numberOfFinishedChecklistItems = (comment?.match(/- \[x\]/gi) ?? []).length;
1164211640
numberOfUnfinishedChecklistItems = (comment?.match(/- \[ \]/g) ?? []).length;
11641+
if (numberOfFinishedChecklistItems >= minCompletedItems && numberOfFinishedChecklistItems <= maxCompletedItems && numberOfUnfinishedChecklistItems === 0) {
11642+
console.log('PR Reviewer checklist is complete 🎉');
11643+
return;
11644+
}
1164311645
}
1164411646
}
1164511647
if (!foundReviewerChecklist) {
1164611648
core.setFailed('No PR Reviewer Checklist was found');
1164711649
return;
1164811650
}
11649-
const maxCompletedItems = numberOfChecklistItems + 2;
11650-
const minCompletedItems = numberOfChecklistItems - 2;
1165111651
console.log(`You completed ${numberOfFinishedChecklistItems} out of ${numberOfChecklistItems} checklist items with ${numberOfUnfinishedChecklistItems} unfinished items`);
1165211652
if (numberOfFinishedChecklistItems >= minCompletedItems && numberOfFinishedChecklistItems <= maxCompletedItems && numberOfUnfinishedChecklistItems === 0) {
1165311653
console.log('PR Reviewer checklist is complete 🎉');

.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,14 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems: number) {
4343
})
4444
.then(() => {
4545
console.log(`Looking through all ${combinedComments.length} comments for the reviewer checklist...`);
46+
const maxCompletedItems = numberOfChecklistItems + 2;
47+
const minCompletedItems = numberOfChecklistItems - 2;
4648
let foundReviewerChecklist = false;
4749
let numberOfFinishedChecklistItems = 0;
4850
let numberOfUnfinishedChecklistItems = 0;
4951

5052
// Once we've gathered all the data, loop through each comment and look to see if it contains the reviewer checklist
5153
for (let i = 0; i < combinedComments.length; i++) {
52-
// Skip all other comments if we already found the reviewer checklist
53-
if (foundReviewerChecklist) {
54-
break;
55-
}
56-
5754
const whitespace = /([\n\r])/gm;
5855
const comment = combinedComments.at(i)?.replaceAll(whitespace, '');
5956

@@ -65,6 +62,11 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems: number) {
6562
foundReviewerChecklist = true;
6663
numberOfFinishedChecklistItems = (comment?.match(/- \[x\]/gi) ?? []).length;
6764
numberOfUnfinishedChecklistItems = (comment?.match(/- \[ \]/g) ?? []).length;
65+
66+
if (numberOfFinishedChecklistItems >= minCompletedItems && numberOfFinishedChecklistItems <= maxCompletedItems && numberOfUnfinishedChecklistItems === 0) {
67+
console.log('PR Reviewer checklist is complete 🎉');
68+
return;
69+
}
6870
}
6971
}
7072

@@ -73,9 +75,6 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems: number) {
7375
return;
7476
}
7577

76-
const maxCompletedItems = numberOfChecklistItems + 2;
77-
const minCompletedItems = numberOfChecklistItems - 2;
78-
7978
console.log(`You completed ${numberOfFinishedChecklistItems} out of ${numberOfChecklistItems} checklist items with ${numberOfUnfinishedChecklistItems} unfinished items`);
8079

8180
if (numberOfFinishedChecklistItems >= minCompletedItems && numberOfFinishedChecklistItems <= maxCompletedItems && numberOfUnfinishedChecklistItems === 0) {

0 commit comments

Comments
 (0)