Skip to content

Commit 26f1fda

Browse files
committed
Merge remote-tracking branch 'origin' into fix/83781
# Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.
2 parents 9ae271a + 5d9c146 commit 26f1fda

226 files changed

Lines changed: 4398 additions & 1810 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/commands/review-code-pr.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
allowed-tools: Bash(gh pr diff:*),Bash(gh pr view:*),Bash(check-compiler.sh:*)
3-
description: Review a code contribution pull request
3+
description: Run the coding-standards rule linter on a PR diff. Use when user wants to review their changes against our custom rules.
44
---
55

66
Perform a comprehensive PR review using a specialized subagent:

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ yarn-error.log
5858
# Bundled code
5959
dist/
6060

61-
# Generated eslint-report HTML (npm run eslint-report)
62-
/reports/
61+
# Generated eslint-report HTML (npm run eslint-report; written under .eslint-reports/)
62+
/.eslint-reports/
6363

6464
# BUCK
6565
buck-out/

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ index.js
2222
web/snippets/**
2323

2424
# Automatically generated files
25+
.eslint-reports/
2526
src/libs/SearchParser/searchParser.js
2627
src/libs/SearchParser/autocompleteParser.js
2728

Mobile-Expensify

android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ android {
111111
minSdkVersion rootProject.ext.minSdkVersion
112112
targetSdkVersion rootProject.ext.targetSdkVersion
113113
multiDexEnabled rootProject.ext.multiDexEnabled
114-
versionCode 1009036423
115-
versionName "9.3.64-23"
114+
versionCode 1009036502
115+
versionName "9.3.65-2"
116116
// Supported language variants must be declared here to avoid from being removed during the compilation.
117117
// This also helps us to not include unnecessary language variants in the APK.
118118
resConfigs "en", "es"

assets/images/table-pencil.svg

Lines changed: 1 addition & 0 deletions
Loading

config/eslint/eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ const config = defineConfig([
738738
'**/*.config.mjs',
739739
'**/node_modules/**/*',
740740
'**/dist/**/*',
741+
'.eslint-reports/**/*',
741742
'android/**/build/**/*',
742743
'docs/vendor/**/*',
743744
'docs/assets/**/*',

config/eslint/eslint.seatbelt.tsv

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
"../../.github/actions/javascript/getAndroidRolloutPercentage/getAndroidRolloutPercentage.ts" "rulesdir/no-default-id-values" 1
55
"../../.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts" "no-restricted-imports" 1
6-
"../../__mocks__/@react-native-camera-roll/camera-roll.ts" "@typescript-eslint/no-deprecated" 1
6+
"../../__mocks__/@react-native-camera-roll/camera-roll.ts" "@typescript-eslint/no-deprecated/CameraRoll.save" 1
77
"../../__mocks__/expo-video.tsx" "react-hooks/refs" 2
88
"../../__mocks__/react-native-safe-area-context.tsx" "react-hooks/refs" 4
99
"../../modules/ExpensifyNitroUtils/src/index.ts" "no-restricted-syntax" 1
@@ -66,7 +66,7 @@
6666
"../../src/components/Form/FormProvider.tsx" "react-hooks/immutability" 2
6767
"../../src/components/Form/FormProvider.tsx" "react-hooks/refs" 2
6868
"../../src/components/Form/InputWrapper.tsx" "react-hooks/refs" 1
69-
"../../src/components/FullscreenLoadingIndicator.tsx" "@typescript-eslint/no-deprecated" 1
69+
"../../src/components/FullscreenLoadingIndicator.tsx" "@typescript-eslint/no-deprecated/StyleSheet.absoluteFillObject" 1
7070
"../../src/components/GrowlNotification/index.tsx" "no-restricted-syntax" 1
7171
"../../src/components/HTMLEngineProvider/index.tsx" "no-restricted-syntax" 1
7272
"../../src/components/Hoverable/ActiveHoverable.tsx" "react-hooks/refs" 3
@@ -76,8 +76,7 @@
7676
"../../src/components/ImportSpreadsheet.tsx" "react-hooks/refs" 1
7777
"../../src/components/InteractiveStepSubPageHeader.tsx" "no-restricted-syntax" 1
7878
"../../src/components/KYCWall/BaseKYCWall.tsx" "react-hooks/refs" 2
79-
"../../src/components/LHNOptionsList/OptionRowLHN.tsx" "react-hooks/preserve-manual-memoization" 1
80-
"../../src/components/LoadingIndicator.tsx" "@typescript-eslint/no-deprecated" 1
79+
"../../src/components/LoadingIndicator.tsx" "@typescript-eslint/no-deprecated/StyleSheet.absoluteFillObject" 1
8180
"../../src/components/LocaleContextProvider.tsx" "react-hooks/set-state-in-effect" 1
8281
"../../src/components/Lottie/index.tsx" "react-hooks/set-state-in-effect" 1
8382
"../../src/components/MagicCodeInput.tsx" "react-hooks/refs" 1
@@ -123,7 +122,7 @@
123122
"../../src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx" "react-hooks/refs" 1
124123
"../../src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx" "react-hooks/set-state-in-effect" 1
125124
"../../src/components/RoomHeaderAvatars.tsx" "no-restricted-syntax" 2
126-
"../../src/components/SAMLLoadingIndicator.tsx" "@typescript-eslint/no-deprecated" 1
125+
"../../src/components/SAMLLoadingIndicator.tsx" "@typescript-eslint/no-deprecated/StyleSheet.absoluteFillObject" 1
127126
"../../src/components/ScreenWrapper/ScreenWrapperContainer.tsx" "react-hooks/refs" 2
128127
"../../src/components/Search/FilterComponents/DateFilterBase.tsx" "react-hooks/set-state-in-effect" 1
129128
"../../src/components/Search/FilterDropdowns/CardSelectPopup.tsx" "react-hooks/set-state-in-effect" 1
@@ -146,7 +145,7 @@
146145
"../../src/components/TextInput/BaseTextInput/implementation/index.native.tsx" "react-hooks/static-components" 1
147146
"../../src/components/TextInput/BaseTextInput/implementation/index.tsx" "react-hooks/refs" 2
148147
"../../src/components/TextInput/BaseTextInput/implementation/index.tsx" "react-hooks/static-components" 1
149-
"../../src/components/TextInput/BaseTextInput/isTextInputFocused.ts" "@typescript-eslint/no-deprecated" 1
148+
"../../src/components/TextInput/BaseTextInput/isTextInputFocused.ts" "@typescript-eslint/no-deprecated/React.MutableRefObject" 1
150149
"../../src/components/TextInput/index.tsx" "no-restricted-syntax" 1
151150
"../../src/components/TextLinkBlock.tsx" "no-restricted-syntax" 1
152151
"../../src/components/TextPicker/TextSelectorModal.tsx" "react-hooks/set-state-in-effect" 1
@@ -171,7 +170,7 @@
171170
"../../src/hooks/useCancellationType.ts" "react-hooks/set-state-in-effect" 1
172171
"../../src/hooks/useDebouncedState.ts" "react-hooks/refs" 2
173172
"../../src/hooks/useDomainGroupFilter.ts" "react-hooks/set-state-in-effect" 1
174-
"../../src/hooks/useDragAndDrop/types.ts" "@typescript-eslint/no-deprecated" 1
173+
"../../src/hooks/useDragAndDrop/types.ts" "@typescript-eslint/no-deprecated/React.MutableRefObject" 1
175174
"../../src/hooks/useInitial.ts" "react-hooks/refs" 4
176175
"../../src/hooks/useIsBlockedToAddFeed.ts" "react-hooks/set-state-in-effect" 1
177176
"../../src/hooks/useIsOwnWorkspaceChatRef.ts" "react-hooks/refs" 2
@@ -194,7 +193,7 @@
194193
"../../src/hooks/useSingleExecution/index.native.ts" "react-hooks/refs" 1
195194
"../../src/hooks/useStepFormSubmit.ts" "no-restricted-syntax" 1
196195
"../../src/hooks/useSubStep/index.ts" "react-hooks/refs" 2
197-
"../../src/libs/Accessibility/moveAccessibilityFocus/types.ts" "@typescript-eslint/no-deprecated" 1
196+
"../../src/libs/Accessibility/moveAccessibilityFocus/types.ts" "@typescript-eslint/no-deprecated/ElementRef" 1
198197
"../../src/libs/Clipboard/index.ts" "no-restricted-syntax" 1
199198
"../../src/libs/ComposerUtils/index.ts" "no-restricted-syntax" 1
200199
"../../src/libs/CurrencyUtils.ts" "rulesdir/no-onyx-connect" 1
@@ -218,7 +217,7 @@
218217
"../../src/libs/Navigation/AppNavigator/Navigators/SearchFullscreenNavigator.tsx" "no-restricted-syntax" 1
219218
"../../src/libs/Navigation/AppNavigator/Navigators/TestToolsModalNavigator.tsx" "no-restricted-syntax" 1
220219
"../../src/libs/Navigation/AppNavigator/UserStatusHandler.tsx" "no-restricted-syntax" 1
221-
"../../src/libs/Navigation/helpers/createNormalizedConfigs.ts" "@typescript-eslint/no-deprecated" 1
220+
"../../src/libs/Navigation/helpers/createNormalizedConfigs.ts" "@typescript-eslint/no-deprecated/escape" 1
222221
"../../src/libs/Network/enhanceParameters.ts" "no-restricted-syntax" 1
223222
"../../src/libs/Network/index.ts" "no-restricted-syntax" 1
224223
"../../src/libs/Notification/LocalNotification/BrowserNotifications.ts" "no-restricted-syntax" 2
@@ -332,22 +331,21 @@
332331
"../../src/pages/AddressPage.tsx" "react-hooks/set-state-in-effect" 1
333332
"../../src/pages/Debug/Transaction/DebugTransactionViolations.tsx" "no-restricted-syntax" 1
334333
"../../src/pages/DynamicReportChangeApproverPage.tsx" "react-hooks/set-state-in-effect" 1
335-
"../../src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx" "@typescript-eslint/no-deprecated" 1
336-
"../../src/pages/EnablePayments/FeesAndTerms/FeesAndTerms.tsx" "@typescript-eslint/no-deprecated" 1
334+
"../../src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx" "@typescript-eslint/no-deprecated/useSubStep" 1
335+
"../../src/pages/EnablePayments/FeesAndTerms/FeesAndTerms.tsx" "@typescript-eslint/no-deprecated/useSubStep" 1
337336
"../../src/pages/EnablePayments/FeesAndTerms/FeesAndTerms.tsx" "no-restricted-syntax" 2
338337
"../../src/pages/EnablePayments/FeesAndTerms/FeesAndTerms.tsx" "rulesdir/no-default-id-values" 1
339338
"../../src/pages/EnablePayments/FeesAndTerms/substeps/TermsStep.tsx" "react-hooks/set-state-in-effect" 1
340-
"../../src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx" "@typescript-eslint/no-deprecated" 1
339+
"../../src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx" "@typescript-eslint/no-deprecated/useSubStep" 1
341340
"../../src/pages/MissingPersonalDetails/subPages/Address.tsx" "react-hooks/refs" 4
342341
"../../src/pages/MultifactorAuthentication/ValidateCodePage.tsx" "react-hooks/set-state-in-effect" 1
343342
"../../src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx" "react-hooks/preserve-manual-memoization" 2
344-
"../../src/pages/ReimbursementAccount/EnterSignerInfo/index.tsx" "@typescript-eslint/no-deprecated" 1
343+
"../../src/pages/ReimbursementAccount/EnterSignerInfo/index.tsx" "@typescript-eslint/no-deprecated/useSubStep" 1
345344
"../../src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx" "react-hooks/refs" 3
346345
"../../src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx" "react-hooks/set-state-in-effect" 4
347346
"../../src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/ConfirmationUBO.tsx" "no-restricted-syntax" 1
348347
"../../src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IndustryCode/IndustryCodeSelector.tsx" "react-hooks/set-state-in-effect" 1
349348
"../../src/pages/ReportDescriptionPage.tsx" "no-restricted-syntax" 1
350-
"../../src/pages/ReportDetailsPage.tsx" "react-hooks/exhaustive-deps" 1
351349
"../../src/pages/ReportDetailsPage.tsx" "react-hooks/preserve-manual-memoization" 4
352350
"../../src/pages/ReportParticipantsPage.tsx" "react-hooks/set-state-in-effect" 1
353351
"../../src/pages/RoomMembersPage.tsx" "react-hooks/preserve-manual-memoization" 1
@@ -532,4 +530,4 @@
532530
"../../src/types/onyx/WalletOnfido.ts" "no-restricted-syntax" 1
533531
"../../src/types/onyx/WalletTerms.ts" "no-restricted-syntax" 1
534532
"../../src/types/onyx/WalletTransfer.ts" "no-restricted-syntax" 1
535-
"../../tests/unit/useSubStepTest.tsx" "@typescript-eslint/no-deprecated" 13
533+
"../../tests/unit/useSubStepTest.tsx" "@typescript-eslint/no-deprecated/useSubStep" 13

config/eslint/processors/eslint-processor-expensify.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
import seatbelt from 'eslint-seatbelt';
66
import reactCompilerCompatProcessor from './eslint-processor-react-compiler-compat.mjs';
7+
import stratifyNoDeprecatedProcessor from './eslint-processor-stratify-no-deprecated.mjs';
78

89
const seatbeltProcessor = seatbelt.processors.seatbelt;
910

@@ -13,11 +14,13 @@ export default {
1314

1415
preprocess(text, filename) {
1516
const [textResult] = reactCompilerCompatProcessor.preprocess(text, filename);
16-
return seatbeltProcessor.preprocess(textResult, filename);
17+
const [afterStratifyPreprocess] = stratifyNoDeprecatedProcessor.preprocess(textResult, filename);
18+
return seatbeltProcessor.preprocess(afterStratifyPreprocess, filename);
1719
},
1820

1921
postprocess(messagesPerBlock, filename) {
2022
const afterCompilerFilter = reactCompilerCompatProcessor.postprocess(messagesPerBlock, filename);
21-
return seatbeltProcessor.postprocess([afterCompilerFilter], filename);
23+
const afterStratify = stratifyNoDeprecatedProcessor.postprocess([afterCompilerFilter], filename);
24+
return seatbeltProcessor.postprocess([afterStratify], filename);
2225
},
2326
};
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Rewrites `@typescript-eslint/no-deprecated` messages into per-API rule IDs
3+
* (e.g. `@typescript-eslint/no-deprecated/StyleSheet.absoluteFillObject`) so
4+
* eslint-seatbelt can ratchet each deprecated API independently.
5+
*/
6+
import {parse} from '@babel/parser';
7+
8+
const NO_DEPRECATED_RULE_ID = '@typescript-eslint/no-deprecated';
9+
10+
// AST keys to ignore while walking children: positions, comments, etc.
11+
const NON_CHILD_KEYS = new Set(['loc', 'start', 'end', 'extra', 'leadingComments', 'trailingComments', 'innerComments']);
12+
13+
// Node types whose children are part of a single dotted/qualified expression
14+
// (e.g. `Foo.bar`, `Foo?.bar`, `Foo.bar` in TS type position).
15+
const MEMBER_LIKE_TYPES = new Set(['MemberExpression', 'OptionalMemberExpression', 'TSQualifiedName']);
16+
17+
const sourceByFilename = new Map();
18+
19+
const isAstNode = (value) => !!value && typeof value === 'object' && typeof value.type === 'string' && typeof value.start === 'number' && typeof value.end === 'number';
20+
21+
/** Iterate over a node's direct AST children, skipping non-child metadata. */
22+
function* astChildren(node) {
23+
for (const [key, value] of Object.entries(node)) {
24+
if (NON_CHILD_KEYS.has(key)) {
25+
continue;
26+
}
27+
for (const child of Array.isArray(value) ? value : [value]) {
28+
if (isAstNode(child)) {
29+
yield child;
30+
}
31+
}
32+
}
33+
}
34+
35+
/** Convert ESLint's 1-based (line, column) into a 0-based source offset, or -1 if line is out of range. */
36+
function lineColumnToOffset(source, line, column) {
37+
let lineStart = 0;
38+
for (let currentLine = 1; currentLine < line; currentLine++) {
39+
const nextNewline = source.indexOf('\n', lineStart);
40+
if (nextNewline < 0) {
41+
return -1;
42+
}
43+
lineStart = nextNewline + 1;
44+
}
45+
return lineStart + column - 1;
46+
}
47+
48+
/**
49+
* Walk down the AST following children whose range contains `offset`.
50+
* Returns the ancestor path (root → deepest) or `null` if the offset is out of range.
51+
*/
52+
function findAstPathAtOffset(root, offset) {
53+
if (offset < 0 || offset < root.start || offset > root.end) {
54+
return null;
55+
}
56+
const path = [root];
57+
while (true) {
58+
const current = path.at(-1);
59+
let descended = false;
60+
for (const child of astChildren(current)) {
61+
if (offset >= child.start && offset <= child.end) {
62+
path.push(child);
63+
descended = true;
64+
break;
65+
}
66+
}
67+
if (!descended) {
68+
return path;
69+
}
70+
}
71+
}
72+
73+
/** Walk a path upward through any wrapping member/qualified expression and return the topmost. */
74+
function topOfMemberChain(path) {
75+
let topIndex = path.length - 1;
76+
while (topIndex > 0 && MEMBER_LIKE_TYPES.has(path.at(topIndex - 1).type)) {
77+
topIndex--;
78+
}
79+
return path.at(topIndex);
80+
}
81+
82+
function parseSourceOrNull(source) {
83+
try {
84+
return parse(source, {sourceType: 'module', plugins: ['typescript', 'jsx']});
85+
} catch {
86+
return null;
87+
}
88+
}
89+
90+
/** Slice the full deprecated expression (e.g. `StyleSheet.absoluteFillObject`) at the lint location, or null on miss. */
91+
function getDeprecatedExpressionFromSource(source, ast, message) {
92+
const offset = lineColumnToOffset(source, message.line, message.column);
93+
const path = findAstPathAtOffset(ast, offset);
94+
if (!path) {
95+
return null;
96+
}
97+
const top = topOfMemberChain(path);
98+
return source.slice(top.start, top.end);
99+
}
100+
101+
/** Fallback: parse the symbol name out of the lint message text. */
102+
function getSymbolNameFromMessage(message) {
103+
const match = /^`([^`]+)`/.exec(message.message);
104+
return match ? match.at(1) : null;
105+
}
106+
107+
/** Trim; collapse whitespace and `/` to `_`. Preserves `.`, `#`, `$`, `@`. */
108+
function toRuleIdSuffix(apiName) {
109+
return apiName.trim().replaceAll(/[\s/]+/g, '_');
110+
}
111+
112+
/**
113+
* @param {import('eslint').Linter.LintMessage[]} messages
114+
* @param {string | null} source
115+
* @returns {import('eslint').Linter.LintMessage[]}
116+
*/
117+
function stratifyMessages(messages, source) {
118+
const hasNoDeprecatedMessages = messages.some((message) => message.ruleId === NO_DEPRECATED_RULE_ID);
119+
const ast = source && hasNoDeprecatedMessages ? parseSourceOrNull(source) : null;
120+
121+
return messages.map((message) => {
122+
if (message.ruleId !== NO_DEPRECATED_RULE_ID) {
123+
return message;
124+
}
125+
const apiName = (ast && getDeprecatedExpressionFromSource(source, ast, message)) || getSymbolNameFromMessage(message);
126+
if (!apiName) {
127+
return message;
128+
}
129+
return {...message, ruleId: `${NO_DEPRECATED_RULE_ID}/${toRuleIdSuffix(apiName)}`};
130+
});
131+
}
132+
133+
const processor = {
134+
meta: {
135+
name: 'stratify-no-deprecated',
136+
version: '1.0.0',
137+
},
138+
supportsAutofix: true,
139+
140+
preprocess(text, filename) {
141+
sourceByFilename.set(filename, text);
142+
return [text];
143+
},
144+
145+
postprocess(messagesPerBlock, filename) {
146+
const source = sourceByFilename.get(filename) ?? null;
147+
sourceByFilename.delete(filename);
148+
return stratifyMessages(messagesPerBlock[0], source);
149+
},
150+
};
151+
152+
export default processor;

0 commit comments

Comments
 (0)