@agent-native/core-corpus 0.217.0 → 0.219.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/hubspot/SKILL.md +5 -3
- package/corpus/templates/analytics/CHANGELOG.md +2 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
- package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/assets/CHANGELOG.md +6 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
- package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
- package/corpus/templates/brain/app/routes/sources.tsx +1 -1
- package/corpus/templates/brain/vitest.config.ts +7 -0
- package/corpus/templates/calendar/CHANGELOG.md +6 -0
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
- package/corpus/templates/clips/CHANGELOG.md +7 -20
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
- package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
- package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/CHANGELOG.md +7 -0
- package/corpus/templates/content/app/components/editor/CommentStack.tsx +301 -0
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +770 -179
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +75 -27
- package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
- package/corpus/templates/content/app/components/editor/block-field-comment-threads.ts +15 -0
- package/corpus/templates/content/app/components/editor/block-field-comments.tsx +5 -9
- package/corpus/templates/content/app/components/editor/comment-stacks.ts +143 -0
- package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
- package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
- package/corpus/templates/content/app/global.css +31 -0
- package/corpus/templates/content/app/i18n-data.ts +8 -0
- package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-blocks-with-several-comment-threads-fold-into-one-stack-in-t.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-opening-a-page-no-longer-saves-an-edit-again-after-another-o.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +8 -1
- package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
- package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
- package/corpus/templates/content/shared/suggestion-text.ts +20 -5
- package/corpus/templates/design/.generated/bridge/interact-links.generated.ts +8 -8
- package/corpus/templates/design/CHANGELOG.md +8 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +18 -1
- package/corpus/templates/design/app/components/design/bridge/interact-links.bridge.ts +8 -8
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +15 -13
- package/corpus/templates/design/app/i18n/en-US.ts +1 -1
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +18 -64
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +15 -6
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +29 -5
- package/corpus/templates/design/app/pages/design-editor/prototype-navigation-transition.ts +423 -0
- package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +35 -9
- package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-the-design-full-app-toggle-on-the-home-page-no-longer-hides-.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-prototype-links-can-play-code-authored-screen-transitions.md +6 -0
- package/corpus/templates/design/shared/motion-authored-css-import.ts +938 -7
- package/corpus/templates/design/shared/motion-html-source.ts +1689 -135
- package/corpus/templates/design/shared/motion-prototype-transition.ts +329 -0
- package/corpus/templates/design/shared/motion-react-source.ts +2154 -126
- package/corpus/templates/design/shared/motion-text-runtime-v1.fixture.txt +221 -0
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
- package/corpus/templates/plan/CHANGELOG.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
- package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +1 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +7 -2
- package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
- package/package.json +1 -1
- package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
- package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
|
@@ -441,6 +441,14 @@ export function applyMotionReactSourceEdit(
|
|
|
441
441
|
export function readMotionReactSource(
|
|
442
442
|
source: string,
|
|
443
443
|
modulePath?: string,
|
|
444
|
+
): MotionReactSourceReadResult {
|
|
445
|
+
return readMotionReactSourceInternal(source, modulePath);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function readMotionReactSourceInternal(
|
|
449
|
+
source: string,
|
|
450
|
+
modulePath: string | undefined,
|
|
451
|
+
options: { allowUnsupportedRuntimeCallbacks?: boolean } = {},
|
|
444
452
|
): MotionReactSourceReadResult {
|
|
445
453
|
try {
|
|
446
454
|
const sourceFile = parseTsx(source);
|
|
@@ -502,6 +510,11 @@ export function readMotionReactSource(
|
|
|
502
510
|
modulePath,
|
|
503
511
|
managedStyles,
|
|
504
512
|
managedTargetNodeIds,
|
|
513
|
+
{
|
|
514
|
+
allowManagedMotionWithUnresolvedStylesheets: true,
|
|
515
|
+
allowUnsupportedRuntimeCallbacks:
|
|
516
|
+
options.allowUnsupportedRuntimeCallbacks,
|
|
517
|
+
},
|
|
505
518
|
);
|
|
506
519
|
if (authoredMotion.document) {
|
|
507
520
|
try {
|
|
@@ -599,6 +612,12 @@ export function inspectMotionReactSourceWriteCapability(args: {
|
|
|
599
612
|
try {
|
|
600
613
|
if (read.status === "invalid") throw read.error;
|
|
601
614
|
const sourceFile = parseTsx(args.source);
|
|
615
|
+
if (targetNodeIds.length > 0 && hasUnresolvedReactStylesheet(sourceFile)) {
|
|
616
|
+
throw new MotionReactSourceError(
|
|
617
|
+
"unsupported-authored-motion",
|
|
618
|
+
"Resolve external stylesheets before editing or adding Motion.",
|
|
619
|
+
);
|
|
620
|
+
}
|
|
602
621
|
if (targetNodeIds.length === 0) {
|
|
603
622
|
const canEditMetadata =
|
|
604
623
|
read.status === "valid" &&
|
|
@@ -629,6 +648,11 @@ export function inspectMotionReactSourceWriteCapability(args: {
|
|
|
629
648
|
}
|
|
630
649
|
} else {
|
|
631
650
|
const targets = findTargets(sourceFile, targetNodeIds, args.modulePath);
|
|
651
|
+
assertNoUnsupportedReactStylePropAnimations(
|
|
652
|
+
sourceFile,
|
|
653
|
+
targetNodeIds,
|
|
654
|
+
args.modulePath,
|
|
655
|
+
);
|
|
632
656
|
const targetRoots = new Set(
|
|
633
657
|
targets.map((target) => target.componentRoot),
|
|
634
658
|
);
|
|
@@ -679,7 +703,9 @@ export function removeMotionReactSource(
|
|
|
679
703
|
source: string,
|
|
680
704
|
modulePath?: string,
|
|
681
705
|
): MotionReactSourceRemoval {
|
|
682
|
-
const read =
|
|
706
|
+
const read = readMotionReactSourceInternal(source, modulePath, {
|
|
707
|
+
allowUnsupportedRuntimeCallbacks: true,
|
|
708
|
+
});
|
|
683
709
|
if (read.status === "invalid") throw read.error;
|
|
684
710
|
if (read.status === "absent") return { source, changed: false };
|
|
685
711
|
|
|
@@ -689,6 +715,10 @@ export function removeMotionReactSource(
|
|
|
689
715
|
modulePath,
|
|
690
716
|
findManagedStyles(originalSourceFile),
|
|
691
717
|
read.status === "valid" ? read.targetNodeIds : [],
|
|
718
|
+
{
|
|
719
|
+
allowManagedMotionWithUnresolvedStylesheets: true,
|
|
720
|
+
allowUnsupportedRuntimeCallbacks: true,
|
|
721
|
+
},
|
|
692
722
|
);
|
|
693
723
|
const withoutAuthored = removeAuthoredCssSourceRanges(
|
|
694
724
|
source,
|
|
@@ -756,7 +786,22 @@ function readAuthoredReactMotion(
|
|
|
756
786
|
modulePath: string | undefined,
|
|
757
787
|
managedStyles: readonly ManagedStyle[],
|
|
758
788
|
managedTargetNodeIds: readonly string[] = [],
|
|
789
|
+
options: {
|
|
790
|
+
allowManagedMotionWithUnresolvedStylesheets?: boolean;
|
|
791
|
+
allowUnsupportedRuntimeCallbacks?: boolean;
|
|
792
|
+
} = {},
|
|
759
793
|
): AuthoredReactMotion {
|
|
794
|
+
if (
|
|
795
|
+
!options.allowManagedMotionWithUnresolvedStylesheets &&
|
|
796
|
+
managedTargetNodeIds.length > 0 &&
|
|
797
|
+
hasUnresolvedReactStylesheet(sourceFile)
|
|
798
|
+
) {
|
|
799
|
+
throw new MotionReactSourceError(
|
|
800
|
+
"unsupported-authored-motion",
|
|
801
|
+
"Resolve external stylesheets before editing or adding Motion.",
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
760
805
|
const managedElements = new Set(managedStyles.map((style) => style.element));
|
|
761
806
|
const authoredStyles: TypeScript.JsxElement[] = [];
|
|
762
807
|
const visit = (node: TypeScript.Node): void => {
|
|
@@ -773,6 +818,10 @@ function readAuthoredReactMotion(
|
|
|
773
818
|
visit(sourceFile);
|
|
774
819
|
|
|
775
820
|
const hosts = findReactAuthoredHosts(sourceFile);
|
|
821
|
+
const transitionCssBlocks = authoredStyles.flatMap((style) => {
|
|
822
|
+
const content = readStaticReactStyleContent(sourceFile, style);
|
|
823
|
+
return content ? [content.css] : [];
|
|
824
|
+
});
|
|
776
825
|
const tracks: NonNullable<MotionCodeDocument["tracks"]> = [];
|
|
777
826
|
const rangesToRemove: MotionAuthoredCssSourceRange[] = [];
|
|
778
827
|
const importedTargetNodeIds = new Set<string>();
|
|
@@ -795,27 +844,31 @@ function readAuthoredReactMotion(
|
|
|
795
844
|
continue;
|
|
796
845
|
}
|
|
797
846
|
|
|
798
|
-
const result = importAuthoredCssAnimations(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
styleRoot
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
847
|
+
const result = importAuthoredCssAnimations(
|
|
848
|
+
content.css,
|
|
849
|
+
(selector) => {
|
|
850
|
+
let styleRoot: TypeScript.JsxElement | TypeScript.JsxFragment;
|
|
851
|
+
try {
|
|
852
|
+
styleRoot = findComponentRoot(sourceFile, style);
|
|
853
|
+
} catch (error) {
|
|
854
|
+
return {
|
|
855
|
+
status: "unsupported",
|
|
856
|
+
reason:
|
|
857
|
+
error instanceof Error
|
|
858
|
+
? error.message
|
|
859
|
+
: "React inline style animations must belong to one static component root.",
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
return resolveAuthoredReactTarget(
|
|
863
|
+
selector,
|
|
864
|
+
hosts,
|
|
865
|
+
modulePath,
|
|
866
|
+
sourceFile,
|
|
867
|
+
styleRoot,
|
|
868
|
+
);
|
|
869
|
+
},
|
|
870
|
+
{ transitionCssBlocks },
|
|
871
|
+
);
|
|
819
872
|
if (result.status === "absent") {
|
|
820
873
|
for (const name of result.definedKeyframeNames) {
|
|
821
874
|
keyframeNameCounts.set(name, (keyframeNameCounts.get(name) ?? 0) + 1);
|
|
@@ -828,10 +881,10 @@ function readAuthoredReactMotion(
|
|
|
828
881
|
result.reason,
|
|
829
882
|
);
|
|
830
883
|
}
|
|
831
|
-
if (
|
|
884
|
+
if (hasUnresolvedReactStylesheet(sourceFile)) {
|
|
832
885
|
throw new MotionReactSourceError(
|
|
833
886
|
"unsupported-authored-motion",
|
|
834
|
-
"Inline CSS animations cannot be imported while
|
|
887
|
+
"Inline CSS animations cannot be imported while stylesheet dependencies remain unresolved.",
|
|
835
888
|
);
|
|
836
889
|
}
|
|
837
890
|
for (const name of result.definedKeyframeNames) {
|
|
@@ -861,11 +914,13 @@ function readAuthoredReactMotion(
|
|
|
861
914
|
);
|
|
862
915
|
}
|
|
863
916
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
917
|
+
if (!options.allowUnsupportedRuntimeCallbacks) {
|
|
918
|
+
assertNoUnsupportedReactStylePropAnimations(
|
|
919
|
+
sourceFile,
|
|
920
|
+
[...managedTargetNodeIds, ...importedTargetNodeIds],
|
|
921
|
+
modulePath,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
869
924
|
if (tracks.length === 0) return { document: null, rangesToRemove: [] };
|
|
870
925
|
for (const name of usedKeyframeNames) {
|
|
871
926
|
if ((keyframeNameCounts.get(name) ?? 0) > 1) {
|
|
@@ -917,13 +972,117 @@ function hasImportedStylesheet(sourceFile: TypeScript.SourceFile): boolean {
|
|
|
917
972
|
ts.isIdentifier(node.expression) && node.expression.text === "require";
|
|
918
973
|
const isDynamicImport =
|
|
919
974
|
node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
975
|
+
const isImportMetaGlob =
|
|
976
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
977
|
+
ts.isMetaProperty(node.expression.expression) &&
|
|
978
|
+
node.expression.expression.keywordToken ===
|
|
979
|
+
ts.SyntaxKind.ImportKeyword &&
|
|
980
|
+
node.expression.expression.name.text === "meta" &&
|
|
981
|
+
["glob", "globEager"].includes(node.expression.name.text);
|
|
920
982
|
if (
|
|
921
983
|
(isRequire || isDynamicImport) &&
|
|
922
|
-
|
|
984
|
+
isPotentialCssModuleSpecifier(node.arguments[0]!)
|
|
923
985
|
) {
|
|
924
986
|
found = true;
|
|
925
987
|
return;
|
|
926
988
|
}
|
|
989
|
+
if (isImportMetaGlob && isPotentialCssGlobPattern(node.arguments[0]!)) {
|
|
990
|
+
found = true;
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (!found) ts.forEachChild(node, visit);
|
|
995
|
+
};
|
|
996
|
+
visit(sourceFile);
|
|
997
|
+
return found;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function hasUnresolvedReactStylesheet(
|
|
1001
|
+
sourceFile: TypeScript.SourceFile,
|
|
1002
|
+
): boolean {
|
|
1003
|
+
return (
|
|
1004
|
+
hasImportedStylesheet(sourceFile) || hasLinkedReactStylesheet(sourceFile)
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function isPotentialCssGlobPattern(node: TypeScript.Expression): boolean {
|
|
1009
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
1010
|
+
return node.elements.some(isPotentialCssGlobPattern);
|
|
1011
|
+
}
|
|
1012
|
+
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const pattern = node.text;
|
|
1017
|
+
if (/\.(?:css|scss|sass|less|styl)(?:$|[?#*{}])/i.test(pattern)) {
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
const extensionList = pattern.match(/\.\{([^}]+)\}(?:[?#].*)?$/);
|
|
1021
|
+
if (extensionList) {
|
|
1022
|
+
return extensionList[1]!
|
|
1023
|
+
.split(",")
|
|
1024
|
+
.some((extension) => /^(?:css|scss|sass|less|styl)$/i.test(extension));
|
|
1025
|
+
}
|
|
1026
|
+
return !/\.[a-z0-9]+(?:[?#].*)?$/i.test(pattern);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function hasLinkedReactStylesheet(sourceFile: TypeScript.SourceFile): boolean {
|
|
1030
|
+
let found = false;
|
|
1031
|
+
const visit = (node: TypeScript.Node): void => {
|
|
1032
|
+
if (found) return;
|
|
1033
|
+
const opening = ts.isJsxElement(node)
|
|
1034
|
+
? node.openingElement
|
|
1035
|
+
: ts.isJsxSelfClosingElement(node)
|
|
1036
|
+
? node
|
|
1037
|
+
: null;
|
|
1038
|
+
if (
|
|
1039
|
+
opening &&
|
|
1040
|
+
ts.isIdentifier(opening.tagName) &&
|
|
1041
|
+
opening.tagName.text === "link"
|
|
1042
|
+
) {
|
|
1043
|
+
const properties = opening.attributes.properties;
|
|
1044
|
+
const attributes = new Map<string, TypeScript.JsxAttribute>();
|
|
1045
|
+
let hasSpread = false;
|
|
1046
|
+
for (const property of properties) {
|
|
1047
|
+
if (ts.isJsxSpreadAttribute(property)) {
|
|
1048
|
+
hasSpread = true;
|
|
1049
|
+
} else {
|
|
1050
|
+
attributes.set(
|
|
1051
|
+
property.name.getText(sourceFile).toLowerCase(),
|
|
1052
|
+
property,
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const relAttribute = attributes.get("rel");
|
|
1057
|
+
const hrefAttribute = attributes.get("href");
|
|
1058
|
+
const rel = relAttribute
|
|
1059
|
+
? readStaticJsxAttributeValue(relAttribute)
|
|
1060
|
+
: null;
|
|
1061
|
+
const relationTokens =
|
|
1062
|
+
rel?.status === "static"
|
|
1063
|
+
? rel.value.split(/[\t\n\f\r ]+/).map((value) => value.toLowerCase())
|
|
1064
|
+
: [];
|
|
1065
|
+
const href = hrefAttribute
|
|
1066
|
+
? readStaticJsxAttributeValue(hrefAttribute)
|
|
1067
|
+
: null;
|
|
1068
|
+
const asAttribute = attributes.get("as");
|
|
1069
|
+
const as = asAttribute ? readStaticJsxAttributeValue(asAttribute) : null;
|
|
1070
|
+
const hasHref =
|
|
1071
|
+
href?.status === "dynamic" ||
|
|
1072
|
+
(href?.status === "static" && href.value.trim().length > 0);
|
|
1073
|
+
const relMayBeStylesheet =
|
|
1074
|
+
rel?.status === "dynamic" && hrefAttribute !== undefined;
|
|
1075
|
+
const preloadMayBecomeStylesheet =
|
|
1076
|
+
relationTokens.includes("preload") &&
|
|
1077
|
+
(as?.status === "dynamic" ||
|
|
1078
|
+
(as?.status === "static" && as.value.toLowerCase() === "style")) &&
|
|
1079
|
+
attributes.has("onload");
|
|
1080
|
+
found =
|
|
1081
|
+
hasSpread ||
|
|
1082
|
+
(hasHref &&
|
|
1083
|
+
(relationTokens.includes("stylesheet") ||
|
|
1084
|
+
relMayBeStylesheet ||
|
|
1085
|
+
preloadMayBecomeStylesheet));
|
|
927
1086
|
}
|
|
928
1087
|
if (!found) ts.forEachChild(node, visit);
|
|
929
1088
|
};
|
|
@@ -931,6 +1090,13 @@ function hasImportedStylesheet(sourceFile: TypeScript.SourceFile): boolean {
|
|
|
931
1090
|
return found;
|
|
932
1091
|
}
|
|
933
1092
|
|
|
1093
|
+
function isPotentialCssModuleSpecifier(node: TypeScript.Expression): boolean {
|
|
1094
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
1095
|
+
return isCssModuleSpecifier(node);
|
|
1096
|
+
}
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
934
1100
|
function isCssModuleSpecifier(node: TypeScript.Expression): boolean {
|
|
935
1101
|
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
936
1102
|
return false;
|
|
@@ -943,34 +1109,199 @@ function assertNoUnsupportedReactStylePropAnimations(
|
|
|
943
1109
|
targetNodeIds: readonly string[],
|
|
944
1110
|
modulePath: string | undefined,
|
|
945
1111
|
): void {
|
|
946
|
-
const
|
|
1112
|
+
const targets =
|
|
947
1113
|
targetNodeIds.length > 0
|
|
948
|
-
? findStaticTargets(
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1114
|
+
? findStaticTargets(sourceFile, [...new Set(targetNodeIds)], modulePath)
|
|
1115
|
+
: [];
|
|
1116
|
+
const targetElements = new Set(targets.map((target) => target.element));
|
|
1117
|
+
const capturedMotionRefNames = new Set<string>();
|
|
1118
|
+
for (const target of targets) {
|
|
1119
|
+
const attributes = ts.isJsxElement(target.element)
|
|
1120
|
+
? target.element.openingElement.attributes
|
|
1121
|
+
: target.element.attributes;
|
|
1122
|
+
for (const property of attributes.properties) {
|
|
1123
|
+
if (
|
|
1124
|
+
!ts.isJsxAttribute(property) ||
|
|
1125
|
+
property.name.getText(sourceFile) !== "ref" ||
|
|
1126
|
+
!property.initializer ||
|
|
1127
|
+
!ts.isJsxExpression(property.initializer) ||
|
|
1128
|
+
!property.initializer.expression
|
|
1129
|
+
) {
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
for (const name of collectReactCapturedRefNames(
|
|
1133
|
+
property.initializer.expression,
|
|
1134
|
+
sourceFile,
|
|
1135
|
+
new Set(),
|
|
1136
|
+
)) {
|
|
1137
|
+
capturedMotionRefNames.add(name);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const targetRoots = new Set(
|
|
1142
|
+
targets.map((target) => findComponentRoot(sourceFile, target.element)),
|
|
954
1143
|
);
|
|
1144
|
+
const isWithinTargetRoot = (node: TypeScript.Node): boolean => {
|
|
1145
|
+
for (
|
|
1146
|
+
let current: TypeScript.Node | undefined = node;
|
|
1147
|
+
current;
|
|
1148
|
+
current = current.parent
|
|
1149
|
+
) {
|
|
1150
|
+
if (
|
|
1151
|
+
targetRoots.has(
|
|
1152
|
+
current as TypeScript.JsxElement | TypeScript.JsxFragment,
|
|
1153
|
+
)
|
|
1154
|
+
) {
|
|
1155
|
+
return true;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return false;
|
|
1159
|
+
};
|
|
1160
|
+
const componentFunctions = new Set<
|
|
1161
|
+
| TypeScript.FunctionDeclaration
|
|
1162
|
+
| TypeScript.FunctionExpression
|
|
1163
|
+
| TypeScript.ArrowFunction
|
|
1164
|
+
| TypeScript.MethodDeclaration
|
|
1165
|
+
>();
|
|
1166
|
+
for (const root of targetRoots) {
|
|
1167
|
+
for (
|
|
1168
|
+
let current: TypeScript.Node | undefined = root.parent;
|
|
1169
|
+
current;
|
|
1170
|
+
current = current.parent
|
|
1171
|
+
) {
|
|
1172
|
+
if (isFunctionBoundary(current)) {
|
|
1173
|
+
if (
|
|
1174
|
+
ts.isFunctionDeclaration(current) ||
|
|
1175
|
+
ts.isFunctionExpression(current) ||
|
|
1176
|
+
ts.isArrowFunction(current) ||
|
|
1177
|
+
ts.isMethodDeclaration(current)
|
|
1178
|
+
) {
|
|
1179
|
+
componentFunctions.add(current);
|
|
1180
|
+
}
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
for (const component of componentFunctions) {
|
|
1186
|
+
if (!component.body) continue;
|
|
1187
|
+
const inspectLifecycleEffects = (node: TypeScript.Node): void => {
|
|
1188
|
+
if (node !== component.body && isFunctionBoundary(node)) return;
|
|
1189
|
+
if (
|
|
1190
|
+
ts.isCallExpression(node) &&
|
|
1191
|
+
isReactLifecycleEffectCall(node, sourceFile)
|
|
1192
|
+
) {
|
|
1193
|
+
const callback = node.arguments[0];
|
|
1194
|
+
const inspection = callback
|
|
1195
|
+
? inspectReactStyleMutationCallback(
|
|
1196
|
+
callback,
|
|
1197
|
+
sourceFile,
|
|
1198
|
+
new Set(),
|
|
1199
|
+
false,
|
|
1200
|
+
capturedMotionRefNames,
|
|
1201
|
+
true,
|
|
1202
|
+
)
|
|
1203
|
+
: "unknown";
|
|
1204
|
+
if (inspection !== "safe") {
|
|
1205
|
+
throw new MotionReactSourceError(
|
|
1206
|
+
"unsupported-authored-motion",
|
|
1207
|
+
"React effect callbacks in a Motion component can access or mutate target styles and cannot be proven safe.",
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
ts.forEachChild(node, inspectLifecycleEffects);
|
|
1213
|
+
};
|
|
1214
|
+
inspectLifecycleEffects(component.body);
|
|
1215
|
+
}
|
|
1216
|
+
for (const root of targetRoots) {
|
|
1217
|
+
const visitRoot = (node: TypeScript.Node): void => {
|
|
1218
|
+
if (
|
|
1219
|
+
ts.isJsxAttribute(node) &&
|
|
1220
|
+
node.name.getText(sourceFile) === "dangerouslySetInnerHTML"
|
|
1221
|
+
) {
|
|
1222
|
+
throw new MotionReactSourceError(
|
|
1223
|
+
"unsupported-authored-motion",
|
|
1224
|
+
"React dangerouslySetInnerHTML in a Motion component can hide CSS from source inspection.",
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
ts.forEachChild(node, visitRoot);
|
|
1228
|
+
};
|
|
1229
|
+
visitRoot(root);
|
|
1230
|
+
}
|
|
955
1231
|
const visit = (node: TypeScript.Node): void => {
|
|
956
|
-
if (ts.
|
|
1232
|
+
if (ts.isJsxSpreadAttribute(node)) {
|
|
1233
|
+
const opening = node.parent.parent;
|
|
1234
|
+
const host = ts.isJsxOpeningElement(opening) ? opening.parent : opening;
|
|
1235
|
+
const isRelevantTargetProp =
|
|
1236
|
+
targetElements.has(host) || isWithinTargetRoot(host);
|
|
1237
|
+
if (isRelevantTargetProp) {
|
|
1238
|
+
const inspection = inspectReactJsxSpreadExpression(
|
|
1239
|
+
node.expression,
|
|
1240
|
+
sourceFile,
|
|
1241
|
+
new Set(),
|
|
1242
|
+
capturedMotionRefNames,
|
|
1243
|
+
);
|
|
1244
|
+
if (inspection !== "safe") {
|
|
1245
|
+
throw new MotionReactSourceError(
|
|
1246
|
+
"unsupported-authored-motion",
|
|
1247
|
+
"React JSX spread props in a Motion component can supply style declarations or callbacks that mutate target styles and cannot be proven safe.",
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
ts.forEachChild(node, visit);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (ts.isJsxAttribute(node)) {
|
|
1256
|
+
const attributeName = node.name.getText(sourceFile);
|
|
957
1257
|
const initializer = node.initializer;
|
|
958
|
-
if (!initializer || !ts.isJsxExpression(initializer)) return;
|
|
959
|
-
if (!initializer.expression) return;
|
|
960
|
-
const inspection = inspectReactStyleExpression(
|
|
961
|
-
initializer.expression,
|
|
962
|
-
sourceFile,
|
|
963
|
-
new Set(),
|
|
964
|
-
);
|
|
965
1258
|
const attributes = node.parent;
|
|
966
1259
|
const opening = attributes.parent;
|
|
967
1260
|
const host = ts.isJsxOpeningElement(opening) ? opening.parent : opening;
|
|
968
|
-
|
|
1261
|
+
const isRelevantTargetProp =
|
|
1262
|
+
targetElements.has(host) ||
|
|
1263
|
+
(attributeName !== "style" && isWithinTargetRoot(host));
|
|
1264
|
+
const expression =
|
|
1265
|
+
initializer && ts.isJsxExpression(initializer) && initializer.expression
|
|
1266
|
+
? initializer.expression
|
|
1267
|
+
: null;
|
|
1268
|
+
|
|
1269
|
+
if (!["style", "css", "sx"].includes(attributeName)) {
|
|
1270
|
+
if (
|
|
1271
|
+
isRelevantTargetProp &&
|
|
1272
|
+
(attributeName === "ref" || /^on[A-Za-z]/.test(attributeName))
|
|
1273
|
+
) {
|
|
1274
|
+
const inspection = expression
|
|
1275
|
+
? inspectReactStyleMutationCallback(
|
|
1276
|
+
expression,
|
|
1277
|
+
sourceFile,
|
|
1278
|
+
new Set(),
|
|
1279
|
+
attributeName === "ref",
|
|
1280
|
+
capturedMotionRefNames,
|
|
1281
|
+
)
|
|
1282
|
+
: "unknown";
|
|
1283
|
+
if (inspection !== "safe") {
|
|
1284
|
+
throw new MotionReactSourceError(
|
|
1285
|
+
"unsupported-authored-motion",
|
|
1286
|
+
`React ${attributeName} callbacks on a Motion component can access or mutate target styles and cannot be proven safe.`,
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
ts.forEachChild(node, visit);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (!isRelevantTargetProp) return;
|
|
1294
|
+
const inspection = expression
|
|
1295
|
+
? attributeName === "style"
|
|
1296
|
+
? inspectReactStyleExpression(expression, sourceFile, new Set())
|
|
1297
|
+
: inspectReactCssExpression(expression, sourceFile, new Set(), true)
|
|
1298
|
+
: "unknown";
|
|
1299
|
+
if (inspection !== "safe") {
|
|
969
1300
|
throw new MotionReactSourceError(
|
|
970
1301
|
"unsupported-authored-motion",
|
|
971
1302
|
inspection === "motion"
|
|
972
|
-
?
|
|
973
|
-
:
|
|
1303
|
+
? `React ${attributeName} prop animations and transitions cannot be imported losslessly; use a supported static JSX <style> animation rule.`
|
|
1304
|
+
: `A React dynamic ${attributeName} prop on a Motion target cannot be proven free of animation or transition declarations.`,
|
|
974
1305
|
);
|
|
975
1306
|
}
|
|
976
1307
|
}
|
|
@@ -980,12 +1311,60 @@ function assertNoUnsupportedReactStylePropAnimations(
|
|
|
980
1311
|
}
|
|
981
1312
|
|
|
982
1313
|
type ReactStyleInspection = "safe" | "motion" | "unknown";
|
|
1314
|
+
const REACT_EVENT_TARGET_PROPERTIES = new Set([
|
|
1315
|
+
"currentTarget",
|
|
1316
|
+
"target",
|
|
1317
|
+
"srcElement",
|
|
1318
|
+
]);
|
|
1319
|
+
const REACT_EVENT_TARGET_SCALAR_PROPERTIES = new Set([
|
|
1320
|
+
"alt",
|
|
1321
|
+
"checked",
|
|
1322
|
+
"disabled",
|
|
1323
|
+
"height",
|
|
1324
|
+
"htmlFor",
|
|
1325
|
+
"id",
|
|
1326
|
+
"innerText",
|
|
1327
|
+
"max",
|
|
1328
|
+
"maxLength",
|
|
1329
|
+
"min",
|
|
1330
|
+
"minLength",
|
|
1331
|
+
"multiple",
|
|
1332
|
+
"name",
|
|
1333
|
+
"placeholder",
|
|
1334
|
+
"readOnly",
|
|
1335
|
+
"required",
|
|
1336
|
+
"role",
|
|
1337
|
+
"scrollLeft",
|
|
1338
|
+
"scrollTop",
|
|
1339
|
+
"selected",
|
|
1340
|
+
"selectedIndex",
|
|
1341
|
+
"selectionEnd",
|
|
1342
|
+
"selectionStart",
|
|
1343
|
+
"tabIndex",
|
|
1344
|
+
"textContent",
|
|
1345
|
+
"title",
|
|
1346
|
+
"type",
|
|
1347
|
+
"value",
|
|
1348
|
+
"width",
|
|
1349
|
+
]);
|
|
1350
|
+
const REACT_EVENT_TARGET_SCALAR_METHODS = new Set(["trim"]);
|
|
1351
|
+
const REACT_REF_READ_METHODS = new Set([
|
|
1352
|
+
"closest",
|
|
1353
|
+
"contains",
|
|
1354
|
+
"getAttribute",
|
|
1355
|
+
"getBoundingClientRect",
|
|
1356
|
+
"getClientRects",
|
|
1357
|
+
"hasAttribute",
|
|
1358
|
+
"matches",
|
|
1359
|
+
"querySelector",
|
|
1360
|
+
"querySelectorAll",
|
|
1361
|
+
]);
|
|
983
1362
|
|
|
984
|
-
function
|
|
1363
|
+
function collectReactCapturedRefNames(
|
|
985
1364
|
expression: TypeScript.Expression,
|
|
986
1365
|
sourceFile: TypeScript.SourceFile,
|
|
987
1366
|
resolving: Set<string>,
|
|
988
|
-
):
|
|
1367
|
+
): Set<string> {
|
|
989
1368
|
while (
|
|
990
1369
|
ts.isParenthesizedExpression(expression) ||
|
|
991
1370
|
ts.isAsExpression(expression) ||
|
|
@@ -995,95 +1374,1654 @@ function inspectReactStyleExpression(
|
|
|
995
1374
|
expression = expression.expression;
|
|
996
1375
|
}
|
|
997
1376
|
|
|
998
|
-
if (expression
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if (!text) return "unknown";
|
|
1377
|
+
if (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression)) {
|
|
1378
|
+
if (!ts.isIdentifier(expression)) return new Set();
|
|
1379
|
+
|
|
1380
|
+
const resolved = resolveReactStyleExpression(expression, sourceFile);
|
|
1381
|
+
if (resolved) {
|
|
1382
|
+
const declarationKey = `${resolved.start}:${resolved.end}`;
|
|
1383
|
+
if (resolving.has(declarationKey)) return new Set();
|
|
1384
|
+
resolving.add(declarationKey);
|
|
1385
|
+
const names = collectReactCapturedRefNames(
|
|
1386
|
+
resolved.expression,
|
|
1387
|
+
sourceFile,
|
|
1388
|
+
resolving,
|
|
1389
|
+
);
|
|
1390
|
+
resolving.delete(declarationKey);
|
|
1391
|
+
return names;
|
|
1014
1392
|
}
|
|
1015
|
-
|
|
1393
|
+
|
|
1394
|
+
const functions: TypeScript.FunctionDeclaration[] = [];
|
|
1395
|
+
const findFunction = (node: TypeScript.Node): void => {
|
|
1396
|
+
if (
|
|
1397
|
+
ts.isFunctionDeclaration(node) &&
|
|
1398
|
+
node.name?.text === expression.text
|
|
1399
|
+
) {
|
|
1400
|
+
functions.push(node);
|
|
1401
|
+
}
|
|
1402
|
+
ts.forEachChild(node, findFunction);
|
|
1403
|
+
};
|
|
1404
|
+
findFunction(sourceFile);
|
|
1405
|
+
const callback = functions.length === 1 ? functions[0] : null;
|
|
1406
|
+
if (!callback?.body) return new Set();
|
|
1407
|
+
return collectReactCapturedRefNamesFromCallback(
|
|
1408
|
+
callback.parameters,
|
|
1409
|
+
callback.body,
|
|
1410
|
+
);
|
|
1016
1411
|
}
|
|
1017
1412
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (resolving.has(declarationKey)) return "unknown";
|
|
1022
|
-
resolving.add(declarationKey);
|
|
1023
|
-
const result = inspectReactStyleExpression(
|
|
1024
|
-
resolved.expression,
|
|
1025
|
-
sourceFile,
|
|
1026
|
-
resolving,
|
|
1413
|
+
return collectReactCapturedRefNamesFromCallback(
|
|
1414
|
+
expression.parameters,
|
|
1415
|
+
expression.body,
|
|
1027
1416
|
);
|
|
1028
|
-
resolving.delete(declarationKey);
|
|
1029
|
-
return result;
|
|
1030
1417
|
}
|
|
1031
1418
|
|
|
1032
|
-
function
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
):
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
ts.isIdentifier(expression.expression)
|
|
1040
|
-
? expression.expression.text
|
|
1041
|
-
: null;
|
|
1042
|
-
if (!name) return null;
|
|
1043
|
-
|
|
1044
|
-
if (hasReactStyleParameterShadow(expression, sourceFile, name)) return null;
|
|
1419
|
+
function collectReactCapturedRefNamesFromCallback(
|
|
1420
|
+
parameters: TypeScript.NodeArray<TypeScript.ParameterDeclaration>,
|
|
1421
|
+
body: TypeScript.ConciseBody,
|
|
1422
|
+
): Set<string> {
|
|
1423
|
+
const capturedNames = new Set<string>();
|
|
1424
|
+
const parameterNames = getReactCallbackParameterNames(parameters);
|
|
1425
|
+
if (parameterNames.size === 0) return capturedNames;
|
|
1045
1426
|
|
|
1046
|
-
const matches: TypeScript.VariableDeclaration[] = [];
|
|
1047
1427
|
const visit = (node: TypeScript.Node): void => {
|
|
1048
1428
|
if (
|
|
1049
|
-
ts.
|
|
1050
|
-
|
|
1051
|
-
node.
|
|
1429
|
+
ts.isBinaryExpression(node) &&
|
|
1430
|
+
isReactAssignmentOperator(node.operatorToken.getText()) &&
|
|
1431
|
+
ts.isIdentifier(node.left) &&
|
|
1432
|
+
!parameterNames.has(node.left.text) &&
|
|
1433
|
+
!isReactNameBoundWithinCallback(node, node.left.text, parameters, body) &&
|
|
1434
|
+
isReactCallbackParameterDerivedExpression(
|
|
1435
|
+
node.right,
|
|
1436
|
+
parameterNames,
|
|
1437
|
+
body,
|
|
1438
|
+
new Set(),
|
|
1439
|
+
)
|
|
1052
1440
|
) {
|
|
1053
|
-
|
|
1441
|
+
capturedNames.add(node.left.text);
|
|
1054
1442
|
}
|
|
1055
1443
|
ts.forEachChild(node, visit);
|
|
1056
1444
|
};
|
|
1057
|
-
visit(
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
(ts.isStringLiteral(property.name) &&
|
|
1075
|
-
property.name.text === propertyName)),
|
|
1445
|
+
visit(body);
|
|
1446
|
+
return capturedNames;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function isReactNameBoundWithinCallback(
|
|
1450
|
+
assignment: TypeScript.BinaryExpression,
|
|
1451
|
+
name: string,
|
|
1452
|
+
callbackParameters: TypeScript.NodeArray<TypeScript.ParameterDeclaration>,
|
|
1453
|
+
callbackBody: TypeScript.ConciseBody,
|
|
1454
|
+
): boolean {
|
|
1455
|
+
const bindingContainsName = (
|
|
1456
|
+
bindingName: TypeScript.BindingName,
|
|
1457
|
+
): boolean => {
|
|
1458
|
+
if (ts.isIdentifier(bindingName)) return bindingName.text === name;
|
|
1459
|
+
return bindingName.elements.some(
|
|
1460
|
+
(element) =>
|
|
1461
|
+
!ts.isOmittedExpression(element) && bindingContainsName(element.name),
|
|
1076
1462
|
);
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1463
|
+
};
|
|
1464
|
+
const hasFunctionScopedBinding = (
|
|
1465
|
+
functionNode: TypeScript.FunctionLikeDeclaration,
|
|
1466
|
+
): boolean => {
|
|
1467
|
+
if (
|
|
1468
|
+
functionNode.parameters.some((parameter) =>
|
|
1469
|
+
bindingContainsName(parameter.name),
|
|
1470
|
+
)
|
|
1471
|
+
) {
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
if (
|
|
1475
|
+
(ts.isFunctionExpression(functionNode) ||
|
|
1476
|
+
ts.isFunctionDeclaration(functionNode)) &&
|
|
1477
|
+
functionNode.name?.text === name
|
|
1478
|
+
) {
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
let found = false;
|
|
1483
|
+
const visit = (node: TypeScript.Node): void => {
|
|
1484
|
+
if (found || (node !== functionNode.body && isFunctionBoundary(node))) {
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
if (
|
|
1488
|
+
ts.isVariableDeclaration(node) &&
|
|
1489
|
+
ts.isIdentifier(node.name) &&
|
|
1490
|
+
node.name.text === name &&
|
|
1491
|
+
node.parent &&
|
|
1492
|
+
ts.isVariableDeclarationList(node.parent) &&
|
|
1493
|
+
(node.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0
|
|
1494
|
+
) {
|
|
1495
|
+
found = true;
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
ts.forEachChild(node, visit);
|
|
1499
|
+
};
|
|
1500
|
+
if (functionNode.body) visit(functionNode.body);
|
|
1501
|
+
return found;
|
|
1502
|
+
};
|
|
1503
|
+
const hasBlockScopedBinding = (block: TypeScript.Block): boolean => {
|
|
1504
|
+
let found = false;
|
|
1505
|
+
const visit = (node: TypeScript.Node): void => {
|
|
1506
|
+
if (
|
|
1507
|
+
found ||
|
|
1508
|
+
(node !== block && (ts.isBlock(node) || isFunctionBoundary(node)))
|
|
1509
|
+
) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if (
|
|
1513
|
+
ts.isVariableDeclaration(node) &&
|
|
1514
|
+
ts.isIdentifier(node.name) &&
|
|
1515
|
+
node.name.text === name &&
|
|
1516
|
+
node.parent &&
|
|
1517
|
+
ts.isVariableDeclarationList(node.parent) &&
|
|
1518
|
+
(node.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0
|
|
1519
|
+
) {
|
|
1520
|
+
found = true;
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (
|
|
1524
|
+
(ts.isFunctionDeclaration(node) ||
|
|
1525
|
+
ts.isClassDeclaration(node) ||
|
|
1526
|
+
ts.isEnumDeclaration(node)) &&
|
|
1527
|
+
node.name?.text === name
|
|
1528
|
+
) {
|
|
1529
|
+
found = true;
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
ts.forEachChild(node, visit);
|
|
1533
|
+
};
|
|
1534
|
+
visit(block);
|
|
1535
|
+
return found;
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
if (
|
|
1539
|
+
callbackParameters.some((parameter) => bindingContainsName(parameter.name))
|
|
1540
|
+
) {
|
|
1541
|
+
return true;
|
|
1542
|
+
}
|
|
1543
|
+
for (
|
|
1544
|
+
let current: TypeScript.Node | undefined = assignment.parent;
|
|
1545
|
+
current;
|
|
1546
|
+
current = current.parent
|
|
1547
|
+
) {
|
|
1548
|
+
if (ts.isBlock(current) && hasBlockScopedBinding(current)) return true;
|
|
1549
|
+
if (
|
|
1550
|
+
isFunctionBoundary(current) &&
|
|
1551
|
+
hasFunctionScopedBinding(current as TypeScript.FunctionLikeDeclaration)
|
|
1552
|
+
) {
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1555
|
+
if (current === callbackBody) {
|
|
1556
|
+
const callbackFunction = current.parent;
|
|
1557
|
+
if (
|
|
1558
|
+
callbackFunction &&
|
|
1559
|
+
isFunctionBoundary(callbackFunction) &&
|
|
1560
|
+
hasFunctionScopedBinding(
|
|
1561
|
+
callbackFunction as TypeScript.FunctionLikeDeclaration,
|
|
1562
|
+
)
|
|
1563
|
+
) {
|
|
1564
|
+
return true;
|
|
1565
|
+
}
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function inspectReactStyleMutationCallback(
|
|
1573
|
+
expression: TypeScript.Expression,
|
|
1574
|
+
sourceFile: TypeScript.SourceFile,
|
|
1575
|
+
resolving: Set<string>,
|
|
1576
|
+
isRefCallback: boolean,
|
|
1577
|
+
capturedMotionRefNames: ReadonlySet<string> = new Set(),
|
|
1578
|
+
inspectLifecycleCalls = false,
|
|
1579
|
+
): ReactStyleInspection {
|
|
1580
|
+
while (
|
|
1581
|
+
ts.isParenthesizedExpression(expression) ||
|
|
1582
|
+
ts.isAsExpression(expression) ||
|
|
1583
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
1584
|
+
ts.isSatisfiesExpression(expression)
|
|
1585
|
+
) {
|
|
1586
|
+
expression = expression.expression;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression)) {
|
|
1590
|
+
if (ts.isIdentifier(expression)) {
|
|
1591
|
+
const resolved = resolveReactStyleExpression(expression, sourceFile);
|
|
1592
|
+
if (resolved) {
|
|
1593
|
+
const declarationKey = `${resolved.start}:${resolved.end}`;
|
|
1594
|
+
if (resolving.has(declarationKey)) return "unknown";
|
|
1595
|
+
resolving.add(declarationKey);
|
|
1596
|
+
const result = inspectReactStyleMutationCallback(
|
|
1597
|
+
resolved.expression,
|
|
1598
|
+
sourceFile,
|
|
1599
|
+
resolving,
|
|
1600
|
+
isRefCallback,
|
|
1601
|
+
capturedMotionRefNames,
|
|
1602
|
+
inspectLifecycleCalls,
|
|
1603
|
+
);
|
|
1604
|
+
resolving.delete(declarationKey);
|
|
1605
|
+
return result;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const handlerName = expression.text;
|
|
1609
|
+
const functions: TypeScript.FunctionDeclaration[] = [];
|
|
1610
|
+
const findFunction = (node: TypeScript.Node): void => {
|
|
1611
|
+
if (ts.isFunctionDeclaration(node) && node.name?.text === handlerName) {
|
|
1612
|
+
functions.push(node);
|
|
1613
|
+
}
|
|
1614
|
+
ts.forEachChild(node, findFunction);
|
|
1615
|
+
};
|
|
1616
|
+
findFunction(sourceFile);
|
|
1617
|
+
if (functions.length === 1 && functions[0]!.body) {
|
|
1618
|
+
if (
|
|
1619
|
+
isRefCallback &&
|
|
1620
|
+
functions[0]!.parameters.some(
|
|
1621
|
+
(parameter) => !ts.isIdentifier(parameter.name),
|
|
1622
|
+
)
|
|
1623
|
+
) {
|
|
1624
|
+
return "unknown";
|
|
1625
|
+
}
|
|
1626
|
+
const callback = functions[0]!;
|
|
1627
|
+
const declarationKey = `${callback.getStart(sourceFile)}:${callback.end}`;
|
|
1628
|
+
if (resolving.has(declarationKey)) return "unknown";
|
|
1629
|
+
resolving.add(declarationKey);
|
|
1630
|
+
const callbackParameterNames = getReactCallbackParameterNames(
|
|
1631
|
+
callback.parameters,
|
|
1632
|
+
);
|
|
1633
|
+
const eventTargetBindings = isRefCallback
|
|
1634
|
+
? { targetNames: new Set<string>(), scalarNames: new Set<string>() }
|
|
1635
|
+
: getReactCallbackEventTargetBindings(
|
|
1636
|
+
callback.parameters,
|
|
1637
|
+
callback.body!,
|
|
1638
|
+
callbackParameterNames,
|
|
1639
|
+
);
|
|
1640
|
+
if (!isRefCallback) {
|
|
1641
|
+
for (const name of capturedMotionRefNames) {
|
|
1642
|
+
eventTargetBindings.targetNames.add(name);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
const result = inspectReactStyleMutationBody(
|
|
1646
|
+
callback.body!,
|
|
1647
|
+
callbackParameterNames,
|
|
1648
|
+
isRefCallback,
|
|
1649
|
+
sourceFile,
|
|
1650
|
+
eventTargetBindings.targetNames,
|
|
1651
|
+
eventTargetBindings.scalarNames,
|
|
1652
|
+
resolving,
|
|
1653
|
+
inspectLifecycleCalls,
|
|
1654
|
+
);
|
|
1655
|
+
resolving.delete(declarationKey);
|
|
1656
|
+
return result;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
return "unknown";
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (
|
|
1663
|
+
isRefCallback &&
|
|
1664
|
+
expression.parameters.some((parameter) => !ts.isIdentifier(parameter.name))
|
|
1665
|
+
) {
|
|
1666
|
+
return "unknown";
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const callbackParameterNames = getReactCallbackParameterNames(
|
|
1670
|
+
expression.parameters,
|
|
1671
|
+
);
|
|
1672
|
+
const eventTargetBindings = isRefCallback
|
|
1673
|
+
? { targetNames: new Set<string>(), scalarNames: new Set<string>() }
|
|
1674
|
+
: getReactCallbackEventTargetBindings(
|
|
1675
|
+
expression.parameters,
|
|
1676
|
+
expression.body,
|
|
1677
|
+
callbackParameterNames,
|
|
1678
|
+
);
|
|
1679
|
+
if (!isRefCallback) {
|
|
1680
|
+
for (const name of capturedMotionRefNames) {
|
|
1681
|
+
eventTargetBindings.targetNames.add(name);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return inspectReactStyleMutationBody(
|
|
1685
|
+
expression.body,
|
|
1686
|
+
callbackParameterNames,
|
|
1687
|
+
isRefCallback,
|
|
1688
|
+
sourceFile,
|
|
1689
|
+
eventTargetBindings.targetNames,
|
|
1690
|
+
eventTargetBindings.scalarNames,
|
|
1691
|
+
resolving,
|
|
1692
|
+
inspectLifecycleCalls,
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function inspectReactStyleMutationBody(
|
|
1697
|
+
body: TypeScript.ConciseBody,
|
|
1698
|
+
callbackParameterNames: Set<string>,
|
|
1699
|
+
isRefCallback: boolean,
|
|
1700
|
+
sourceFile: TypeScript.SourceFile,
|
|
1701
|
+
eventTargetNames: Set<string>,
|
|
1702
|
+
eventTargetScalarNames: Set<string>,
|
|
1703
|
+
resolving: Set<string>,
|
|
1704
|
+
inspectLifecycleCalls: boolean,
|
|
1705
|
+
): ReactStyleInspection {
|
|
1706
|
+
let inspection: ReactStyleInspection = "safe";
|
|
1707
|
+
const visit = (node: TypeScript.Node): void => {
|
|
1708
|
+
if (inspection !== "safe") return;
|
|
1709
|
+
|
|
1710
|
+
if (isReactGlobalDocumentReference(node, sourceFile)) {
|
|
1711
|
+
inspection = "unknown";
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
if (inspectLifecycleCalls && ts.isCallExpression(node)) {
|
|
1716
|
+
const callInspection = inspectReactLifecycleHelperCall(
|
|
1717
|
+
node,
|
|
1718
|
+
sourceFile,
|
|
1719
|
+
resolving,
|
|
1720
|
+
eventTargetNames,
|
|
1721
|
+
);
|
|
1722
|
+
if (callInspection !== "safe") {
|
|
1723
|
+
inspection = callInspection;
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
if (
|
|
1729
|
+
isRefCallback &&
|
|
1730
|
+
ts.isBinaryExpression(node) &&
|
|
1731
|
+
isReactAssignmentOperator(node.operatorToken.getText()) &&
|
|
1732
|
+
containsReactCallbackParameterReference(
|
|
1733
|
+
node.right,
|
|
1734
|
+
callbackParameterNames,
|
|
1735
|
+
) &&
|
|
1736
|
+
(!ts.isIdentifier(node.left) ||
|
|
1737
|
+
!isReactCallbackParameterDerivedExpression(
|
|
1738
|
+
node.right,
|
|
1739
|
+
callbackParameterNames,
|
|
1740
|
+
body,
|
|
1741
|
+
new Set(),
|
|
1742
|
+
))
|
|
1743
|
+
) {
|
|
1744
|
+
inspection = "unknown";
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
if (
|
|
1749
|
+
isRefCallback &&
|
|
1750
|
+
ts.isVariableDeclaration(node) &&
|
|
1751
|
+
!ts.isIdentifier(node.name) &&
|
|
1752
|
+
node.initializer &&
|
|
1753
|
+
isReactCallbackParameterDerivedExpression(
|
|
1754
|
+
node.initializer,
|
|
1755
|
+
callbackParameterNames,
|
|
1756
|
+
body,
|
|
1757
|
+
new Set(),
|
|
1758
|
+
)
|
|
1759
|
+
) {
|
|
1760
|
+
inspection = "unknown";
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (
|
|
1765
|
+
isRefCallback &&
|
|
1766
|
+
((ts.isCallExpression(node) &&
|
|
1767
|
+
isReactRefTargetMethodCall(node, callbackParameterNames, body)) ||
|
|
1768
|
+
isReactRefTargetMemberAccess(node, callbackParameterNames, body) ||
|
|
1769
|
+
(ts.isBinaryExpression(node) &&
|
|
1770
|
+
isReactRefTargetMemberAssignment(node, callbackParameterNames, body)))
|
|
1771
|
+
) {
|
|
1772
|
+
inspection = "unknown";
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
if (
|
|
1777
|
+
!isRefCallback &&
|
|
1778
|
+
isReactEventTargetMutation(
|
|
1779
|
+
node,
|
|
1780
|
+
callbackParameterNames,
|
|
1781
|
+
body,
|
|
1782
|
+
eventTargetNames,
|
|
1783
|
+
)
|
|
1784
|
+
) {
|
|
1785
|
+
inspection = "unknown";
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
if (
|
|
1790
|
+
!isRefCallback &&
|
|
1791
|
+
ts.isIdentifier(node) &&
|
|
1792
|
+
eventTargetNames.has(node.text) &&
|
|
1793
|
+
!isReactStaticPropertyName(node) &&
|
|
1794
|
+
!isReactBindingNameIdentifier(node)
|
|
1795
|
+
) {
|
|
1796
|
+
const access = getReactMemberAccessFollowing(node);
|
|
1797
|
+
if (
|
|
1798
|
+
!access ||
|
|
1799
|
+
!isReactSafeEventTargetScalarValue(
|
|
1800
|
+
access,
|
|
1801
|
+
callbackParameterNames,
|
|
1802
|
+
body,
|
|
1803
|
+
eventTargetNames,
|
|
1804
|
+
eventTargetScalarNames,
|
|
1805
|
+
)
|
|
1806
|
+
) {
|
|
1807
|
+
inspection = "unknown";
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
if (
|
|
1813
|
+
ts.isPropertyAccessExpression(node) &&
|
|
1814
|
+
["style", ...REACT_EVENT_TARGET_PROPERTIES, "current"].includes(
|
|
1815
|
+
node.name.text,
|
|
1816
|
+
)
|
|
1817
|
+
) {
|
|
1818
|
+
inspection =
|
|
1819
|
+
node.name.text === "style"
|
|
1820
|
+
? "motion"
|
|
1821
|
+
: !isRefCallback &&
|
|
1822
|
+
isReactSafeEventTargetScalarRead(
|
|
1823
|
+
node,
|
|
1824
|
+
callbackParameterNames,
|
|
1825
|
+
body,
|
|
1826
|
+
eventTargetNames,
|
|
1827
|
+
)
|
|
1828
|
+
? "safe"
|
|
1829
|
+
: "unknown";
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (
|
|
1833
|
+
ts.isElementAccessExpression(node) &&
|
|
1834
|
+
node.argumentExpression &&
|
|
1835
|
+
ts.isStringLiteralLike(node.argumentExpression) &&
|
|
1836
|
+
["style", ...REACT_EVENT_TARGET_PROPERTIES, "current"].includes(
|
|
1837
|
+
node.argumentExpression.text,
|
|
1838
|
+
)
|
|
1839
|
+
) {
|
|
1840
|
+
inspection =
|
|
1841
|
+
node.argumentExpression.text === "style"
|
|
1842
|
+
? "motion"
|
|
1843
|
+
: !isRefCallback &&
|
|
1844
|
+
isReactSafeEventTargetScalarRead(
|
|
1845
|
+
node,
|
|
1846
|
+
callbackParameterNames,
|
|
1847
|
+
body,
|
|
1848
|
+
eventTargetNames,
|
|
1849
|
+
)
|
|
1850
|
+
? "safe"
|
|
1851
|
+
: "unknown";
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
if (
|
|
1856
|
+
ts.isCallExpression(node) &&
|
|
1857
|
+
node.arguments.some(
|
|
1858
|
+
(argument) =>
|
|
1859
|
+
containsReactCallbackParameterReference(
|
|
1860
|
+
argument,
|
|
1861
|
+
callbackParameterNames,
|
|
1862
|
+
) &&
|
|
1863
|
+
!isReactSafeEventTargetScalarValue(
|
|
1864
|
+
argument,
|
|
1865
|
+
callbackParameterNames,
|
|
1866
|
+
body,
|
|
1867
|
+
eventTargetNames,
|
|
1868
|
+
eventTargetScalarNames,
|
|
1869
|
+
),
|
|
1870
|
+
)
|
|
1871
|
+
) {
|
|
1872
|
+
inspection = "unknown";
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
ts.forEachChild(node, visit);
|
|
1877
|
+
};
|
|
1878
|
+
visit(body);
|
|
1879
|
+
return inspection;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function inspectReactLifecycleHelperCall(
|
|
1883
|
+
call: TypeScript.CallExpression,
|
|
1884
|
+
sourceFile: TypeScript.SourceFile,
|
|
1885
|
+
resolving: Set<string>,
|
|
1886
|
+
capturedMotionRefNames: ReadonlySet<string>,
|
|
1887
|
+
): ReactStyleInspection {
|
|
1888
|
+
if (call.arguments.length > 0) return "unknown";
|
|
1889
|
+
const callee = unwrapReactMotionExpression(call.expression);
|
|
1890
|
+
if (!ts.isIdentifier(callee)) return "unknown";
|
|
1891
|
+
return inspectReactStyleMutationCallback(
|
|
1892
|
+
callee,
|
|
1893
|
+
sourceFile,
|
|
1894
|
+
resolving,
|
|
1895
|
+
false,
|
|
1896
|
+
capturedMotionRefNames,
|
|
1897
|
+
true,
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function isReactGlobalDocumentReference(
|
|
1902
|
+
node: TypeScript.Node,
|
|
1903
|
+
sourceFile: TypeScript.SourceFile,
|
|
1904
|
+
resolving = new Set<string>(),
|
|
1905
|
+
): boolean {
|
|
1906
|
+
if (ts.isIdentifier(node) && node.text === "document") {
|
|
1907
|
+
return !isReactStaticPropertyName(node);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
const propertyAccess = ts.isPropertyAccessExpression(node) ? node : null;
|
|
1911
|
+
const elementAccess = ts.isElementAccessExpression(node) ? node : null;
|
|
1912
|
+
const receiver =
|
|
1913
|
+
propertyAccess?.expression ?? elementAccess?.expression ?? null;
|
|
1914
|
+
const propertyName = propertyAccess
|
|
1915
|
+
? propertyAccess.name.text
|
|
1916
|
+
: elementAccess?.argumentExpression &&
|
|
1917
|
+
ts.isStringLiteralLike(elementAccess.argumentExpression)
|
|
1918
|
+
? elementAccess.argumentExpression.text
|
|
1919
|
+
: null;
|
|
1920
|
+
if (
|
|
1921
|
+
propertyName === "document" &&
|
|
1922
|
+
receiver !== null &&
|
|
1923
|
+
isReactGlobalObjectReference(receiver, sourceFile, resolving)
|
|
1924
|
+
) {
|
|
1925
|
+
return true;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if (!ts.isIdentifier(node) || isReactStaticPropertyName(node)) return false;
|
|
1929
|
+
const resolved = resolveReactStyleExpression(node, sourceFile);
|
|
1930
|
+
if (!resolved) return false;
|
|
1931
|
+
const declarationKey = `${resolved.start}:${resolved.end}`;
|
|
1932
|
+
if (resolving.has(declarationKey)) return true;
|
|
1933
|
+
resolving.add(declarationKey);
|
|
1934
|
+
|
|
1935
|
+
let found = false;
|
|
1936
|
+
const visit = (current: TypeScript.Node): void => {
|
|
1937
|
+
if (found) return;
|
|
1938
|
+
if (isReactGlobalDocumentReference(current, sourceFile, resolving)) {
|
|
1939
|
+
found = true;
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
ts.forEachChild(current, visit);
|
|
1943
|
+
};
|
|
1944
|
+
visit(resolved.expression);
|
|
1945
|
+
resolving.delete(declarationKey);
|
|
1946
|
+
return found;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
function isReactGlobalObjectReference(
|
|
1950
|
+
expression: TypeScript.Expression,
|
|
1951
|
+
sourceFile: TypeScript.SourceFile,
|
|
1952
|
+
resolving: Set<string>,
|
|
1953
|
+
): boolean {
|
|
1954
|
+
while (
|
|
1955
|
+
ts.isParenthesizedExpression(expression) ||
|
|
1956
|
+
ts.isAsExpression(expression) ||
|
|
1957
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
1958
|
+
ts.isSatisfiesExpression(expression)
|
|
1959
|
+
) {
|
|
1960
|
+
expression = expression.expression;
|
|
1961
|
+
}
|
|
1962
|
+
if (!ts.isIdentifier(expression) || isReactStaticPropertyName(expression)) {
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
if (
|
|
1966
|
+
["globalThis", "self", "window"].includes(expression.text) &&
|
|
1967
|
+
!hasReactStyleParameterShadow(expression, sourceFile, expression.text) &&
|
|
1968
|
+
!resolveReactStyleExpression(expression, sourceFile)
|
|
1969
|
+
) {
|
|
1970
|
+
return true;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
const resolved = resolveReactStyleExpression(expression, sourceFile);
|
|
1974
|
+
if (!resolved) return false;
|
|
1975
|
+
const declarationKey = `${resolved.start}:${resolved.end}`;
|
|
1976
|
+
if (resolving.has(declarationKey)) return false;
|
|
1977
|
+
resolving.add(declarationKey);
|
|
1978
|
+
const isGlobal = isReactGlobalObjectReference(
|
|
1979
|
+
resolved.expression,
|
|
1980
|
+
sourceFile,
|
|
1981
|
+
resolving,
|
|
1982
|
+
);
|
|
1983
|
+
resolving.delete(declarationKey);
|
|
1984
|
+
return isGlobal;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function isReactLifecycleEffectCall(
|
|
1988
|
+
call: TypeScript.CallExpression,
|
|
1989
|
+
sourceFile: TypeScript.SourceFile,
|
|
1990
|
+
): boolean {
|
|
1991
|
+
return isReactLifecycleEffectExpression(call.expression, sourceFile);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function isReactLifecycleEffectExpression(
|
|
1995
|
+
expression: TypeScript.Expression,
|
|
1996
|
+
sourceFile: TypeScript.SourceFile,
|
|
1997
|
+
resolving = new Set<string>(),
|
|
1998
|
+
): boolean {
|
|
1999
|
+
while (
|
|
2000
|
+
ts.isParenthesizedExpression(expression) ||
|
|
2001
|
+
ts.isAsExpression(expression) ||
|
|
2002
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
2003
|
+
ts.isSatisfiesExpression(expression)
|
|
2004
|
+
) {
|
|
2005
|
+
expression = expression.expression;
|
|
2006
|
+
}
|
|
2007
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
2008
|
+
return ["useEffect", "useLayoutEffect", "useInsertionEffect"].includes(
|
|
2009
|
+
expression.name.text,
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
if (!ts.isIdentifier(expression)) return false;
|
|
2013
|
+
if (
|
|
2014
|
+
["useEffect", "useLayoutEffect", "useInsertionEffect"].includes(
|
|
2015
|
+
expression.text,
|
|
2016
|
+
)
|
|
2017
|
+
) {
|
|
2018
|
+
return true;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
const importedReactHook = sourceFile.statements.some((statement) => {
|
|
2022
|
+
if (
|
|
2023
|
+
!ts.isImportDeclaration(statement) ||
|
|
2024
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
2025
|
+
statement.moduleSpecifier.text !== "react"
|
|
2026
|
+
) {
|
|
2027
|
+
return false;
|
|
2028
|
+
}
|
|
2029
|
+
const namedBindings = statement.importClause?.namedBindings;
|
|
2030
|
+
return (
|
|
2031
|
+
!!namedBindings &&
|
|
2032
|
+
ts.isNamedImports(namedBindings) &&
|
|
2033
|
+
namedBindings.elements.some(
|
|
2034
|
+
(element) =>
|
|
2035
|
+
element.name.text === expression.text &&
|
|
2036
|
+
["useEffect", "useLayoutEffect", "useInsertionEffect"].includes(
|
|
2037
|
+
element.propertyName?.text ?? element.name.text,
|
|
2038
|
+
),
|
|
2039
|
+
)
|
|
2040
|
+
);
|
|
2041
|
+
});
|
|
2042
|
+
if (importedReactHook) return true;
|
|
2043
|
+
|
|
2044
|
+
const resolved = resolveReactStyleExpression(expression, sourceFile);
|
|
2045
|
+
if (!resolved) return false;
|
|
2046
|
+
const declarationKey = `${resolved.start}:${resolved.end}`;
|
|
2047
|
+
if (resolving.has(declarationKey)) return false;
|
|
2048
|
+
resolving.add(declarationKey);
|
|
2049
|
+
const isEffect = isReactLifecycleEffectExpression(
|
|
2050
|
+
resolved.expression,
|
|
2051
|
+
sourceFile,
|
|
2052
|
+
resolving,
|
|
2053
|
+
);
|
|
2054
|
+
resolving.delete(declarationKey);
|
|
2055
|
+
return isEffect;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
function inspectReactJsxSpreadExpression(
|
|
2059
|
+
expression: TypeScript.Expression,
|
|
2060
|
+
sourceFile: TypeScript.SourceFile,
|
|
2061
|
+
resolving: Set<string>,
|
|
2062
|
+
capturedMotionRefNames: ReadonlySet<string> = new Set(),
|
|
2063
|
+
): ReactStyleInspection {
|
|
2064
|
+
while (
|
|
2065
|
+
ts.isParenthesizedExpression(expression) ||
|
|
2066
|
+
ts.isAsExpression(expression) ||
|
|
2067
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
2068
|
+
ts.isSatisfiesExpression(expression)
|
|
2069
|
+
) {
|
|
2070
|
+
expression = expression.expression;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
if (!ts.isObjectLiteralExpression(expression)) {
|
|
2074
|
+
const resolved = resolveReactStyleExpression(expression, sourceFile);
|
|
2075
|
+
if (!resolved) return "unknown";
|
|
2076
|
+
const declarationKey = `${resolved.start}:${resolved.end}`;
|
|
2077
|
+
if (resolving.has(declarationKey)) return "unknown";
|
|
2078
|
+
resolving.add(declarationKey);
|
|
2079
|
+
const result = inspectReactJsxSpreadExpression(
|
|
2080
|
+
resolved.expression,
|
|
2081
|
+
sourceFile,
|
|
2082
|
+
resolving,
|
|
2083
|
+
capturedMotionRefNames,
|
|
2084
|
+
);
|
|
2085
|
+
resolving.delete(declarationKey);
|
|
2086
|
+
return result;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
for (const property of expression.properties) {
|
|
2090
|
+
if (ts.isSpreadAssignment(property)) {
|
|
2091
|
+
const inspection = inspectReactJsxSpreadExpression(
|
|
2092
|
+
property.expression,
|
|
2093
|
+
sourceFile,
|
|
2094
|
+
resolving,
|
|
2095
|
+
capturedMotionRefNames,
|
|
2096
|
+
);
|
|
2097
|
+
if (inspection !== "safe") return inspection;
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
const name =
|
|
2102
|
+
ts.isPropertyAssignment(property) ||
|
|
2103
|
+
ts.isShorthandPropertyAssignment(property)
|
|
2104
|
+
? property.name
|
|
2105
|
+
: null;
|
|
2106
|
+
if (!name) return "unknown";
|
|
2107
|
+
const propertyName =
|
|
2108
|
+
ts.isIdentifier(name) || ts.isStringLiteral(name)
|
|
2109
|
+
? name.text
|
|
2110
|
+
: ts.isComputedPropertyName(name) && ts.isStringLiteral(name.expression)
|
|
2111
|
+
? name.expression.text
|
|
2112
|
+
: null;
|
|
2113
|
+
if (!propertyName) return "unknown";
|
|
2114
|
+
|
|
2115
|
+
const value = ts.isPropertyAssignment(property)
|
|
2116
|
+
? property.initializer
|
|
2117
|
+
: ts.isShorthandPropertyAssignment(property)
|
|
2118
|
+
? property.name
|
|
2119
|
+
: null;
|
|
2120
|
+
if (!value) return "unknown";
|
|
2121
|
+
|
|
2122
|
+
let inspection: ReactStyleInspection;
|
|
2123
|
+
if (propertyName === "style") {
|
|
2124
|
+
inspection = inspectReactStyleExpression(value, sourceFile, new Set());
|
|
2125
|
+
} else if (propertyName === "css" || propertyName === "sx") {
|
|
2126
|
+
inspection = inspectReactCssExpression(
|
|
2127
|
+
value,
|
|
2128
|
+
sourceFile,
|
|
2129
|
+
new Set(),
|
|
2130
|
+
true,
|
|
2131
|
+
);
|
|
2132
|
+
} else if (propertyName === "ref" || /^on[A-Z]/.test(propertyName)) {
|
|
2133
|
+
inspection = inspectReactStyleMutationCallback(
|
|
2134
|
+
value,
|
|
2135
|
+
sourceFile,
|
|
2136
|
+
new Set(),
|
|
2137
|
+
propertyName === "ref",
|
|
2138
|
+
capturedMotionRefNames,
|
|
2139
|
+
);
|
|
2140
|
+
} else if (
|
|
2141
|
+
["class", "className", "children", "dangerouslySetInnerHTML"].includes(
|
|
2142
|
+
propertyName,
|
|
2143
|
+
)
|
|
2144
|
+
) {
|
|
2145
|
+
inspection = "unknown";
|
|
2146
|
+
} else {
|
|
2147
|
+
inspection = isStaticReactJsxSpreadValue(value) ? "safe" : "unknown";
|
|
2148
|
+
}
|
|
2149
|
+
if (inspection !== "safe") return inspection;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
return "safe";
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function isStaticReactJsxSpreadValue(
|
|
2156
|
+
expression: TypeScript.Expression,
|
|
2157
|
+
): boolean {
|
|
2158
|
+
while (
|
|
2159
|
+
ts.isParenthesizedExpression(expression) ||
|
|
2160
|
+
ts.isAsExpression(expression) ||
|
|
2161
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
2162
|
+
ts.isSatisfiesExpression(expression)
|
|
2163
|
+
) {
|
|
2164
|
+
expression = expression.expression;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
return (
|
|
2168
|
+
ts.isStringLiteralLike(expression) ||
|
|
2169
|
+
ts.isNumericLiteral(expression) ||
|
|
2170
|
+
expression.kind === ts.SyntaxKind.TrueKeyword ||
|
|
2171
|
+
expression.kind === ts.SyntaxKind.FalseKeyword ||
|
|
2172
|
+
expression.kind === ts.SyntaxKind.NullKeyword ||
|
|
2173
|
+
(ts.isIdentifier(expression) && expression.text === "undefined")
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function isReactRefTargetMethodCall(
|
|
2178
|
+
call: TypeScript.CallExpression,
|
|
2179
|
+
callbackParameterNames: Set<string>,
|
|
2180
|
+
body: TypeScript.ConciseBody,
|
|
2181
|
+
): boolean {
|
|
2182
|
+
const callee = call.expression;
|
|
2183
|
+
const propertyAccess = ts.isPropertyAccessExpression(callee) ? callee : null;
|
|
2184
|
+
const elementAccess = ts.isElementAccessExpression(callee) ? callee : null;
|
|
2185
|
+
const receiver =
|
|
2186
|
+
propertyAccess?.expression ?? elementAccess?.expression ?? null;
|
|
2187
|
+
if (
|
|
2188
|
+
!receiver ||
|
|
2189
|
+
!isReactCallbackParameterDerivedExpression(
|
|
2190
|
+
receiver,
|
|
2191
|
+
callbackParameterNames,
|
|
2192
|
+
body,
|
|
2193
|
+
new Set(),
|
|
2194
|
+
)
|
|
2195
|
+
) {
|
|
2196
|
+
return false;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
const argumentExpression = elementAccess?.argumentExpression;
|
|
2200
|
+
const methodName = propertyAccess
|
|
2201
|
+
? propertyAccess.name.text
|
|
2202
|
+
: argumentExpression && ts.isStringLiteralLike(argumentExpression)
|
|
2203
|
+
? argumentExpression.text
|
|
2204
|
+
: null;
|
|
2205
|
+
return !methodName || !REACT_REF_READ_METHODS.has(methodName);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function isReactRefTargetMemberAccess(
|
|
2209
|
+
node: TypeScript.Node,
|
|
2210
|
+
callbackParameterNames: Set<string>,
|
|
2211
|
+
body: TypeScript.ConciseBody,
|
|
2212
|
+
): boolean {
|
|
2213
|
+
const propertyAccess = ts.isPropertyAccessExpression(node) ? node : null;
|
|
2214
|
+
const elementAccess = ts.isElementAccessExpression(node) ? node : null;
|
|
2215
|
+
const receiver =
|
|
2216
|
+
propertyAccess?.expression ?? elementAccess?.expression ?? null;
|
|
2217
|
+
if (
|
|
2218
|
+
!receiver ||
|
|
2219
|
+
!isReactCallbackParameterDerivedExpression(
|
|
2220
|
+
receiver,
|
|
2221
|
+
callbackParameterNames,
|
|
2222
|
+
body,
|
|
2223
|
+
new Set(),
|
|
2224
|
+
)
|
|
2225
|
+
) {
|
|
2226
|
+
return false;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
const argumentExpression = elementAccess?.argumentExpression;
|
|
2230
|
+
const propertyName = propertyAccess
|
|
2231
|
+
? propertyAccess.name.text
|
|
2232
|
+
: argumentExpression && ts.isStringLiteralLike(argumentExpression)
|
|
2233
|
+
? argumentExpression.text
|
|
2234
|
+
: null;
|
|
2235
|
+
return !propertyName || !REACT_REF_READ_METHODS.has(propertyName);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function isReactRefTargetMemberAssignment(
|
|
2239
|
+
assignment: TypeScript.BinaryExpression,
|
|
2240
|
+
callbackParameterNames: Set<string>,
|
|
2241
|
+
body: TypeScript.ConciseBody,
|
|
2242
|
+
): boolean {
|
|
2243
|
+
if (!isReactAssignmentOperator(assignment.operatorToken.getText())) {
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
const left = assignment.left;
|
|
2248
|
+
const receiver = ts.isPropertyAccessExpression(left)
|
|
2249
|
+
? left.expression
|
|
2250
|
+
: ts.isElementAccessExpression(left)
|
|
2251
|
+
? left.expression
|
|
2252
|
+
: null;
|
|
2253
|
+
return (
|
|
2254
|
+
receiver !== null &&
|
|
2255
|
+
isReactCallbackParameterDerivedExpression(
|
|
2256
|
+
receiver,
|
|
2257
|
+
callbackParameterNames,
|
|
2258
|
+
body,
|
|
2259
|
+
new Set(),
|
|
2260
|
+
)
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
function isReactSafeEventTargetScalarRead(
|
|
2265
|
+
node:
|
|
2266
|
+
| TypeScript.PropertyAccessExpression
|
|
2267
|
+
| TypeScript.ElementAccessExpression,
|
|
2268
|
+
callbackParameterNames: Set<string>,
|
|
2269
|
+
body: TypeScript.ConciseBody,
|
|
2270
|
+
eventTargetNames: Set<string>,
|
|
2271
|
+
): boolean {
|
|
2272
|
+
const parent = getReactMemberAccessFollowing(node);
|
|
2273
|
+
const propertyName = parent
|
|
2274
|
+
? ts.isPropertyAccessExpression(parent)
|
|
2275
|
+
? parent.name.text
|
|
2276
|
+
: parent.argumentExpression &&
|
|
2277
|
+
ts.isStringLiteralLike(parent.argumentExpression)
|
|
2278
|
+
? parent.argumentExpression.text
|
|
2279
|
+
: null
|
|
2280
|
+
: null;
|
|
2281
|
+
return (
|
|
2282
|
+
propertyName !== null &&
|
|
2283
|
+
REACT_EVENT_TARGET_SCALAR_PROPERTIES.has(propertyName) &&
|
|
2284
|
+
isReactEventTargetBaseAccess(
|
|
2285
|
+
node,
|
|
2286
|
+
callbackParameterNames,
|
|
2287
|
+
body,
|
|
2288
|
+
eventTargetNames,
|
|
2289
|
+
)
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
function isReactSafeEventTargetScalarValue(
|
|
2294
|
+
expression: TypeScript.Expression,
|
|
2295
|
+
callbackParameterNames: Set<string>,
|
|
2296
|
+
body: TypeScript.ConciseBody,
|
|
2297
|
+
eventTargetNames: Set<string>,
|
|
2298
|
+
eventTargetScalarNames: Set<string>,
|
|
2299
|
+
): boolean {
|
|
2300
|
+
while (
|
|
2301
|
+
ts.isParenthesizedExpression(expression) ||
|
|
2302
|
+
ts.isAsExpression(expression) ||
|
|
2303
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
2304
|
+
ts.isSatisfiesExpression(expression) ||
|
|
2305
|
+
ts.isNonNullExpression(expression)
|
|
2306
|
+
) {
|
|
2307
|
+
expression = expression.expression;
|
|
2308
|
+
}
|
|
2309
|
+
if (
|
|
2310
|
+
ts.isIdentifier(expression) &&
|
|
2311
|
+
eventTargetScalarNames.has(expression.text)
|
|
2312
|
+
) {
|
|
2313
|
+
return true;
|
|
2314
|
+
}
|
|
2315
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
2316
|
+
return (
|
|
2317
|
+
REACT_EVENT_TARGET_SCALAR_PROPERTIES.has(expression.name.text) &&
|
|
2318
|
+
isReactEventTargetBaseAccess(
|
|
2319
|
+
expression.expression,
|
|
2320
|
+
callbackParameterNames,
|
|
2321
|
+
body,
|
|
2322
|
+
eventTargetNames,
|
|
2323
|
+
)
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
if (ts.isElementAccessExpression(expression)) {
|
|
2327
|
+
const propertyName = expression.argumentExpression;
|
|
2328
|
+
return (
|
|
2329
|
+
!!propertyName &&
|
|
2330
|
+
ts.isStringLiteralLike(propertyName) &&
|
|
2331
|
+
REACT_EVENT_TARGET_SCALAR_PROPERTIES.has(propertyName.text) &&
|
|
2332
|
+
isReactEventTargetBaseAccess(
|
|
2333
|
+
expression.expression,
|
|
2334
|
+
callbackParameterNames,
|
|
2335
|
+
body,
|
|
2336
|
+
eventTargetNames,
|
|
2337
|
+
)
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
if (ts.isCallExpression(expression) && expression.arguments.length === 0) {
|
|
2341
|
+
const callee = unwrapReactMotionExpression(expression.expression);
|
|
2342
|
+
return (
|
|
2343
|
+
ts.isPropertyAccessExpression(callee) &&
|
|
2344
|
+
REACT_EVENT_TARGET_SCALAR_METHODS.has(callee.name.text) &&
|
|
2345
|
+
isReactSafeEventTargetScalarValue(
|
|
2346
|
+
callee.expression,
|
|
2347
|
+
callbackParameterNames,
|
|
2348
|
+
body,
|
|
2349
|
+
eventTargetNames,
|
|
2350
|
+
eventTargetScalarNames,
|
|
2351
|
+
)
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
return false;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
function isReactEventTargetBaseAccess(
|
|
2358
|
+
expression: TypeScript.Expression,
|
|
2359
|
+
callbackParameterNames: Set<string>,
|
|
2360
|
+
body: TypeScript.ConciseBody,
|
|
2361
|
+
eventTargetNames: Set<string>,
|
|
2362
|
+
): boolean {
|
|
2363
|
+
expression = unwrapReactMotionExpression(expression);
|
|
2364
|
+
if (ts.isIdentifier(expression) && eventTargetNames.has(expression.text)) {
|
|
2365
|
+
return true;
|
|
2366
|
+
}
|
|
2367
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
2368
|
+
return (
|
|
2369
|
+
REACT_EVENT_TARGET_PROPERTIES.has(expression.name.text) &&
|
|
2370
|
+
isReactCallbackParameterDerivedExpression(
|
|
2371
|
+
expression.expression,
|
|
2372
|
+
callbackParameterNames,
|
|
2373
|
+
body,
|
|
2374
|
+
new Set(),
|
|
2375
|
+
)
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
if (ts.isElementAccessExpression(expression)) {
|
|
2379
|
+
const propertyName = expression.argumentExpression;
|
|
2380
|
+
return (
|
|
2381
|
+
!!propertyName &&
|
|
2382
|
+
ts.isStringLiteralLike(propertyName) &&
|
|
2383
|
+
REACT_EVENT_TARGET_PROPERTIES.has(propertyName.text) &&
|
|
2384
|
+
isReactCallbackParameterDerivedExpression(
|
|
2385
|
+
expression.expression,
|
|
2386
|
+
callbackParameterNames,
|
|
2387
|
+
body,
|
|
2388
|
+
new Set(),
|
|
2389
|
+
)
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
return false;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
function getReactMemberAccessFollowing(
|
|
2396
|
+
node: TypeScript.Node,
|
|
2397
|
+
):
|
|
2398
|
+
| TypeScript.PropertyAccessExpression
|
|
2399
|
+
| TypeScript.ElementAccessExpression
|
|
2400
|
+
| null {
|
|
2401
|
+
let current: TypeScript.Node = node;
|
|
2402
|
+
let parent = current.parent;
|
|
2403
|
+
while (
|
|
2404
|
+
(ts.isParenthesizedExpression(parent) ||
|
|
2405
|
+
ts.isAsExpression(parent) ||
|
|
2406
|
+
ts.isTypeAssertionExpression(parent) ||
|
|
2407
|
+
ts.isSatisfiesExpression(parent) ||
|
|
2408
|
+
ts.isNonNullExpression(parent)) &&
|
|
2409
|
+
parent.expression === current
|
|
2410
|
+
) {
|
|
2411
|
+
current = parent;
|
|
2412
|
+
parent = current.parent;
|
|
2413
|
+
}
|
|
2414
|
+
return (ts.isPropertyAccessExpression(parent) ||
|
|
2415
|
+
ts.isElementAccessExpression(parent)) &&
|
|
2416
|
+
parent.expression === current
|
|
2417
|
+
? parent
|
|
2418
|
+
: null;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
function unwrapReactMotionExpression(
|
|
2422
|
+
expression: TypeScript.Expression,
|
|
2423
|
+
): TypeScript.Expression {
|
|
2424
|
+
while (
|
|
2425
|
+
ts.isParenthesizedExpression(expression) ||
|
|
2426
|
+
ts.isAsExpression(expression) ||
|
|
2427
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
2428
|
+
ts.isSatisfiesExpression(expression) ||
|
|
2429
|
+
ts.isNonNullExpression(expression)
|
|
2430
|
+
) {
|
|
2431
|
+
expression = expression.expression;
|
|
2432
|
+
}
|
|
2433
|
+
return expression;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
function isReactEventTargetMutation(
|
|
2437
|
+
node: TypeScript.Node,
|
|
2438
|
+
callbackParameterNames: Set<string>,
|
|
2439
|
+
body: TypeScript.ConciseBody,
|
|
2440
|
+
eventTargetNames: Set<string>,
|
|
2441
|
+
): boolean {
|
|
2442
|
+
if (ts.isBinaryExpression(node)) {
|
|
2443
|
+
return (
|
|
2444
|
+
isReactAssignmentOperator(node.operatorToken.getText()) &&
|
|
2445
|
+
containsReactEventTargetBaseAccess(
|
|
2446
|
+
node.left,
|
|
2447
|
+
callbackParameterNames,
|
|
2448
|
+
body,
|
|
2449
|
+
eventTargetNames,
|
|
2450
|
+
)
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
2454
|
+
return (
|
|
2455
|
+
[
|
|
2456
|
+
ts.SyntaxKind.DeleteKeyword,
|
|
2457
|
+
ts.SyntaxKind.PlusPlusToken,
|
|
2458
|
+
ts.SyntaxKind.MinusMinusToken,
|
|
2459
|
+
].includes(node.operator) &&
|
|
2460
|
+
containsReactEventTargetBaseAccess(
|
|
2461
|
+
node.operand,
|
|
2462
|
+
callbackParameterNames,
|
|
2463
|
+
body,
|
|
2464
|
+
eventTargetNames,
|
|
2465
|
+
)
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
if (ts.isPostfixUnaryExpression(node)) {
|
|
2469
|
+
return (
|
|
2470
|
+
[ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(
|
|
2471
|
+
node.operator,
|
|
2472
|
+
) &&
|
|
2473
|
+
containsReactEventTargetBaseAccess(
|
|
2474
|
+
node.operand,
|
|
2475
|
+
callbackParameterNames,
|
|
2476
|
+
body,
|
|
2477
|
+
eventTargetNames,
|
|
2478
|
+
)
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
return false;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
function containsReactEventTargetBaseAccess(
|
|
2485
|
+
expression: TypeScript.Expression,
|
|
2486
|
+
callbackParameterNames: Set<string>,
|
|
2487
|
+
body: TypeScript.ConciseBody,
|
|
2488
|
+
eventTargetNames: Set<string>,
|
|
2489
|
+
): boolean {
|
|
2490
|
+
expression = unwrapReactMotionExpression(expression);
|
|
2491
|
+
if (
|
|
2492
|
+
isReactEventTargetBaseAccess(
|
|
2493
|
+
expression,
|
|
2494
|
+
callbackParameterNames,
|
|
2495
|
+
body,
|
|
2496
|
+
eventTargetNames,
|
|
2497
|
+
)
|
|
2498
|
+
) {
|
|
2499
|
+
return true;
|
|
2500
|
+
}
|
|
2501
|
+
if (
|
|
2502
|
+
ts.isPropertyAccessExpression(expression) ||
|
|
2503
|
+
ts.isElementAccessExpression(expression)
|
|
2504
|
+
) {
|
|
2505
|
+
return containsReactEventTargetBaseAccess(
|
|
2506
|
+
expression.expression,
|
|
2507
|
+
callbackParameterNames,
|
|
2508
|
+
body,
|
|
2509
|
+
eventTargetNames,
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
return false;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
function isReactAssignmentOperator(operator: string): boolean {
|
|
2516
|
+
return [
|
|
2517
|
+
"=",
|
|
2518
|
+
"+=",
|
|
2519
|
+
"-=",
|
|
2520
|
+
"*=",
|
|
2521
|
+
"**=",
|
|
2522
|
+
"/=",
|
|
2523
|
+
"%=",
|
|
2524
|
+
"<<=",
|
|
2525
|
+
">>=",
|
|
2526
|
+
">>>=",
|
|
2527
|
+
"&=",
|
|
2528
|
+
"|=",
|
|
2529
|
+
"^=",
|
|
2530
|
+
"&&=",
|
|
2531
|
+
"||=",
|
|
2532
|
+
"??=",
|
|
2533
|
+
].includes(operator);
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
function isReactCallbackParameterDerivedExpression(
|
|
2537
|
+
expression: TypeScript.Expression,
|
|
2538
|
+
callbackParameterNames: Set<string>,
|
|
2539
|
+
body: TypeScript.ConciseBody,
|
|
2540
|
+
resolving: Set<string>,
|
|
2541
|
+
): boolean {
|
|
2542
|
+
while (
|
|
2543
|
+
ts.isParenthesizedExpression(expression) ||
|
|
2544
|
+
ts.isAsExpression(expression) ||
|
|
2545
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
2546
|
+
ts.isSatisfiesExpression(expression) ||
|
|
2547
|
+
ts.isNonNullExpression(expression)
|
|
2548
|
+
) {
|
|
2549
|
+
expression = expression.expression;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
if (ts.isIdentifier(expression)) {
|
|
2553
|
+
if (callbackParameterNames.has(expression.text)) return true;
|
|
2554
|
+
if (resolving.has(expression.text)) return false;
|
|
2555
|
+
|
|
2556
|
+
const assignments: TypeScript.Expression[] = [];
|
|
2557
|
+
const visit = (node: TypeScript.Node): void => {
|
|
2558
|
+
if (
|
|
2559
|
+
ts.isVariableDeclaration(node) &&
|
|
2560
|
+
ts.isIdentifier(node.name) &&
|
|
2561
|
+
node.name.text === expression.text &&
|
|
2562
|
+
node.initializer
|
|
2563
|
+
) {
|
|
2564
|
+
assignments.push(node.initializer);
|
|
2565
|
+
} else if (
|
|
2566
|
+
ts.isBinaryExpression(node) &&
|
|
2567
|
+
ts.isIdentifier(node.left) &&
|
|
2568
|
+
node.left.text === expression.text &&
|
|
2569
|
+
isReactAssignmentOperator(node.operatorToken.getText())
|
|
2570
|
+
) {
|
|
2571
|
+
assignments.push(node.right);
|
|
2572
|
+
}
|
|
2573
|
+
ts.forEachChild(node, visit);
|
|
2574
|
+
};
|
|
2575
|
+
visit(body);
|
|
2576
|
+
if (assignments.length === 0) return false;
|
|
2577
|
+
|
|
2578
|
+
resolving.add(expression.text);
|
|
2579
|
+
const derived = assignments.some((assignment) =>
|
|
2580
|
+
isReactCallbackParameterDerivedExpression(
|
|
2581
|
+
assignment,
|
|
2582
|
+
callbackParameterNames,
|
|
2583
|
+
body,
|
|
2584
|
+
resolving,
|
|
2585
|
+
),
|
|
2586
|
+
);
|
|
2587
|
+
resolving.delete(expression.text);
|
|
2588
|
+
return derived;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
2592
|
+
return isReactCallbackParameterDerivedExpression(
|
|
2593
|
+
expression.expression,
|
|
2594
|
+
callbackParameterNames,
|
|
2595
|
+
body,
|
|
2596
|
+
resolving,
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
if (ts.isElementAccessExpression(expression)) {
|
|
2600
|
+
return isReactCallbackParameterDerivedExpression(
|
|
2601
|
+
expression.expression,
|
|
2602
|
+
callbackParameterNames,
|
|
2603
|
+
body,
|
|
2604
|
+
resolving,
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
if (ts.isCallExpression(expression)) {
|
|
2608
|
+
const callee = expression.expression;
|
|
2609
|
+
const receiver = ts.isPropertyAccessExpression(callee)
|
|
2610
|
+
? callee.expression
|
|
2611
|
+
: ts.isElementAccessExpression(callee)
|
|
2612
|
+
? callee.expression
|
|
2613
|
+
: null;
|
|
2614
|
+
return (
|
|
2615
|
+
receiver !== null &&
|
|
2616
|
+
isReactCallbackParameterDerivedExpression(
|
|
2617
|
+
receiver,
|
|
2618
|
+
callbackParameterNames,
|
|
2619
|
+
body,
|
|
2620
|
+
resolving,
|
|
2621
|
+
)
|
|
2622
|
+
);
|
|
2623
|
+
}
|
|
2624
|
+
return false;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
function containsReactCallbackParameterReference(
|
|
2628
|
+
node: TypeScript.Node,
|
|
2629
|
+
callbackParameterNames: Set<string>,
|
|
2630
|
+
): boolean {
|
|
2631
|
+
let found = false;
|
|
2632
|
+
const visit = (current: TypeScript.Node): void => {
|
|
2633
|
+
if (found) return;
|
|
2634
|
+
if (
|
|
2635
|
+
ts.isIdentifier(current) &&
|
|
2636
|
+
callbackParameterNames.has(current.text) &&
|
|
2637
|
+
!isReactStaticPropertyName(current)
|
|
2638
|
+
) {
|
|
2639
|
+
found = true;
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
ts.forEachChild(current, visit);
|
|
2643
|
+
};
|
|
2644
|
+
visit(node);
|
|
2645
|
+
return found;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
function isReactStaticPropertyName(node: TypeScript.Identifier): boolean {
|
|
2649
|
+
const parent = node.parent;
|
|
2650
|
+
return (
|
|
2651
|
+
(ts.isPropertyAccessExpression(parent) && parent.name === node) ||
|
|
2652
|
+
((ts.isPropertyAssignment(parent) ||
|
|
2653
|
+
ts.isMethodDeclaration(parent) ||
|
|
2654
|
+
ts.isMethodSignature(parent) ||
|
|
2655
|
+
ts.isPropertyDeclaration(parent) ||
|
|
2656
|
+
ts.isPropertySignature(parent)) &&
|
|
2657
|
+
parent.name === node &&
|
|
2658
|
+
!ts.isComputedPropertyName(parent.name))
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
function isReactBindingNameIdentifier(node: TypeScript.Identifier): boolean {
|
|
2663
|
+
const parent = node.parent;
|
|
2664
|
+
return (
|
|
2665
|
+
(ts.isBindingElement(parent) && parent.name === node) ||
|
|
2666
|
+
(ts.isParameter(parent) && parent.name === node) ||
|
|
2667
|
+
(ts.isVariableDeclaration(parent) && parent.name === node)
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
function getReactCallbackParameterNames(
|
|
2672
|
+
parameters: TypeScript.NodeArray<TypeScript.ParameterDeclaration>,
|
|
2673
|
+
): Set<string> {
|
|
2674
|
+
const names = new Set<string>();
|
|
2675
|
+
const addBindingNames = (bindingName: TypeScript.BindingName): void => {
|
|
2676
|
+
if (ts.isIdentifier(bindingName)) {
|
|
2677
|
+
names.add(bindingName.text);
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
if (ts.isObjectBindingPattern(bindingName)) {
|
|
2681
|
+
for (const element of bindingName.elements) {
|
|
2682
|
+
addBindingNames(element.name);
|
|
2683
|
+
}
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
for (const element of bindingName.elements) {
|
|
2687
|
+
if (!ts.isOmittedExpression(element)) addBindingNames(element.name);
|
|
2688
|
+
}
|
|
2689
|
+
};
|
|
2690
|
+
|
|
2691
|
+
for (const parameter of parameters) addBindingNames(parameter.name);
|
|
2692
|
+
return names;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
interface ReactCallbackEventTargetBindings {
|
|
2696
|
+
targetNames: Set<string>;
|
|
2697
|
+
scalarNames: Set<string>;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
function getReactCallbackEventTargetBindings(
|
|
2701
|
+
parameters: TypeScript.NodeArray<TypeScript.ParameterDeclaration>,
|
|
2702
|
+
body: TypeScript.ConciseBody,
|
|
2703
|
+
callbackParameterNames: Set<string>,
|
|
2704
|
+
): ReactCallbackEventTargetBindings {
|
|
2705
|
+
const targetNames = new Set<string>();
|
|
2706
|
+
const scalarNames = new Set<string>();
|
|
2707
|
+
const eventObjectNames = new Set(
|
|
2708
|
+
parameters.flatMap((parameter) =>
|
|
2709
|
+
ts.isIdentifier(parameter.name) ? [parameter.name.text] : [],
|
|
2710
|
+
),
|
|
2711
|
+
);
|
|
2712
|
+
const localDeclarations: TypeScript.VariableDeclaration[] = [];
|
|
2713
|
+
const addBindingNames = (
|
|
2714
|
+
bindingName: TypeScript.BindingName,
|
|
2715
|
+
into: Set<string>,
|
|
2716
|
+
): boolean => {
|
|
2717
|
+
if (ts.isIdentifier(bindingName)) {
|
|
2718
|
+
const previousSize = into.size;
|
|
2719
|
+
into.add(bindingName.text);
|
|
2720
|
+
return into.size !== previousSize;
|
|
2721
|
+
}
|
|
2722
|
+
let changed = false;
|
|
2723
|
+
if (ts.isObjectBindingPattern(bindingName)) {
|
|
2724
|
+
for (const element of bindingName.elements) {
|
|
2725
|
+
changed = addBindingNames(element.name, into) || changed;
|
|
2726
|
+
}
|
|
2727
|
+
return changed;
|
|
2728
|
+
}
|
|
2729
|
+
for (const element of bindingName.elements) {
|
|
2730
|
+
if (!ts.isOmittedExpression(element)) {
|
|
2731
|
+
changed = addBindingNames(element.name, into) || changed;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
return changed;
|
|
2735
|
+
};
|
|
2736
|
+
|
|
2737
|
+
const addTargetObjectBinding = (
|
|
2738
|
+
bindingName: TypeScript.BindingName,
|
|
2739
|
+
): boolean => {
|
|
2740
|
+
if (!ts.isObjectBindingPattern(bindingName)) {
|
|
2741
|
+
return addBindingNames(bindingName, targetNames);
|
|
2742
|
+
}
|
|
2743
|
+
let changed = false;
|
|
2744
|
+
for (const element of bindingName.elements) {
|
|
2745
|
+
const propertyName = element.propertyName
|
|
2746
|
+
? getReactStaticBindingPropertyName(element.propertyName)
|
|
2747
|
+
: ts.isIdentifier(element.name)
|
|
2748
|
+
? element.name.text
|
|
2749
|
+
: null;
|
|
2750
|
+
if (
|
|
2751
|
+
!element.dotDotDotToken &&
|
|
2752
|
+
propertyName &&
|
|
2753
|
+
REACT_EVENT_TARGET_SCALAR_PROPERTIES.has(propertyName) &&
|
|
2754
|
+
ts.isIdentifier(element.name)
|
|
2755
|
+
) {
|
|
2756
|
+
const previousSize = scalarNames.size;
|
|
2757
|
+
scalarNames.add(element.name.text);
|
|
2758
|
+
changed = scalarNames.size !== previousSize || changed;
|
|
2759
|
+
} else {
|
|
2760
|
+
changed = addBindingNames(element.name, targetNames) || changed;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
return changed;
|
|
2764
|
+
};
|
|
2765
|
+
|
|
2766
|
+
const addEventObjectBinding = (
|
|
2767
|
+
bindingName: TypeScript.BindingName,
|
|
2768
|
+
): boolean => {
|
|
2769
|
+
if (!ts.isObjectBindingPattern(bindingName)) {
|
|
2770
|
+
return addBindingNames(bindingName, targetNames);
|
|
2771
|
+
}
|
|
2772
|
+
let changed = false;
|
|
2773
|
+
for (const element of bindingName.elements) {
|
|
2774
|
+
const propertyName = element.propertyName
|
|
2775
|
+
? getReactStaticBindingPropertyName(element.propertyName)
|
|
2776
|
+
: ts.isIdentifier(element.name)
|
|
2777
|
+
? element.name.text
|
|
2778
|
+
: null;
|
|
2779
|
+
if (
|
|
2780
|
+
!element.dotDotDotToken &&
|
|
2781
|
+
propertyName &&
|
|
2782
|
+
REACT_EVENT_TARGET_PROPERTIES.has(propertyName)
|
|
2783
|
+
) {
|
|
2784
|
+
if (ts.isIdentifier(element.name)) {
|
|
2785
|
+
const previousSize = targetNames.size;
|
|
2786
|
+
targetNames.add(element.name.text);
|
|
2787
|
+
changed = targetNames.size !== previousSize || changed;
|
|
2788
|
+
} else {
|
|
2789
|
+
changed = addTargetObjectBinding(element.name) || changed;
|
|
2790
|
+
}
|
|
2791
|
+
} else if (element.dotDotDotToken) {
|
|
2792
|
+
changed = addBindingNames(element.name, targetNames) || changed;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
return changed;
|
|
2796
|
+
};
|
|
2797
|
+
|
|
2798
|
+
for (const parameter of parameters) {
|
|
2799
|
+
if (!ts.isObjectBindingPattern(parameter.name)) continue;
|
|
2800
|
+
addEventObjectBinding(parameter.name);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
const collectDeclarations = (node: TypeScript.Node): void => {
|
|
2804
|
+
if (ts.isVariableDeclaration(node)) localDeclarations.push(node);
|
|
2805
|
+
ts.forEachChild(node, collectDeclarations);
|
|
2806
|
+
};
|
|
2807
|
+
collectDeclarations(body);
|
|
2808
|
+
|
|
2809
|
+
let changed = true;
|
|
2810
|
+
while (changed) {
|
|
2811
|
+
changed = false;
|
|
2812
|
+
for (const declaration of localDeclarations) {
|
|
2813
|
+
if (!declaration.initializer) continue;
|
|
2814
|
+
const initializer = unwrapReactMotionExpression(declaration.initializer);
|
|
2815
|
+
if (
|
|
2816
|
+
ts.isIdentifier(declaration.name) &&
|
|
2817
|
+
ts.isIdentifier(initializer) &&
|
|
2818
|
+
eventObjectNames.has(initializer.text)
|
|
2819
|
+
) {
|
|
2820
|
+
const previousSize = eventObjectNames.size;
|
|
2821
|
+
eventObjectNames.add(declaration.name.text);
|
|
2822
|
+
changed = eventObjectNames.size !== previousSize || changed;
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
if (
|
|
2826
|
+
ts.isObjectBindingPattern(declaration.name) &&
|
|
2827
|
+
ts.isIdentifier(initializer) &&
|
|
2828
|
+
eventObjectNames.has(initializer.text)
|
|
2829
|
+
) {
|
|
2830
|
+
changed = addEventObjectBinding(declaration.name) || changed;
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
if (
|
|
2834
|
+
ts.isObjectBindingPattern(declaration.name) &&
|
|
2835
|
+
isReactEventTargetBaseAccess(
|
|
2836
|
+
initializer,
|
|
2837
|
+
callbackParameterNames,
|
|
2838
|
+
body,
|
|
2839
|
+
targetNames,
|
|
2840
|
+
)
|
|
2841
|
+
) {
|
|
2842
|
+
changed = addTargetObjectBinding(declaration.name) || changed;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
return { targetNames, scalarNames };
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
function getReactStaticBindingPropertyName(
|
|
2850
|
+
name: TypeScript.PropertyName,
|
|
2851
|
+
): string | null {
|
|
2852
|
+
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text;
|
|
2853
|
+
return ts.isComputedPropertyName(name) &&
|
|
2854
|
+
ts.isStringLiteralLike(name.expression)
|
|
2855
|
+
? name.expression.text
|
|
2856
|
+
: null;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
function inspectReactStyleExpression(
|
|
2860
|
+
expression: TypeScript.Expression,
|
|
2861
|
+
sourceFile: TypeScript.SourceFile,
|
|
2862
|
+
resolving: Set<string>,
|
|
2863
|
+
): ReactStyleInspection {
|
|
2864
|
+
while (
|
|
2865
|
+
ts.isParenthesizedExpression(expression) ||
|
|
2866
|
+
ts.isAsExpression(expression) ||
|
|
2867
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
2868
|
+
ts.isSatisfiesExpression(expression)
|
|
2869
|
+
) {
|
|
2870
|
+
expression = expression.expression;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
if (expression.kind === ts.SyntaxKind.NullKeyword) return "safe";
|
|
2874
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
2875
|
+
for (const property of expression.properties) {
|
|
2876
|
+
if (ts.isSpreadAssignment(property)) return "unknown";
|
|
2877
|
+
const name = ts.isPropertyAssignment(property)
|
|
2878
|
+
? property.name
|
|
2879
|
+
: ts.isShorthandPropertyAssignment(property)
|
|
2880
|
+
? property.name
|
|
2881
|
+
: null;
|
|
2882
|
+
const text = name
|
|
2883
|
+
? ts.isIdentifier(name) || ts.isStringLiteral(name)
|
|
2884
|
+
? name.text
|
|
2885
|
+
: null
|
|
2886
|
+
: null;
|
|
2887
|
+
if (text && isReactMotionStylePropName(text)) return "motion";
|
|
2888
|
+
if (!text) return "unknown";
|
|
2889
|
+
}
|
|
2890
|
+
return "safe";
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
const resolved = resolveReactStyleExpression(expression, sourceFile);
|
|
2894
|
+
if (!resolved) return "unknown";
|
|
2895
|
+
const declarationKey = `${resolved.start}:${resolved.end}`;
|
|
2896
|
+
if (resolving.has(declarationKey)) return "unknown";
|
|
2897
|
+
resolving.add(declarationKey);
|
|
2898
|
+
const result = inspectReactStyleExpression(
|
|
2899
|
+
resolved.expression,
|
|
2900
|
+
sourceFile,
|
|
2901
|
+
resolving,
|
|
2902
|
+
);
|
|
2903
|
+
resolving.delete(declarationKey);
|
|
2904
|
+
return result;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
function inspectReactCssExpression(
|
|
2908
|
+
expression: TypeScript.Expression,
|
|
2909
|
+
sourceFile: TypeScript.SourceFile,
|
|
2910
|
+
resolving: Set<string>,
|
|
2911
|
+
inspectRootCssString = false,
|
|
2912
|
+
): ReactStyleInspection {
|
|
2913
|
+
while (
|
|
2914
|
+
ts.isParenthesizedExpression(expression) ||
|
|
2915
|
+
ts.isAsExpression(expression) ||
|
|
2916
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
2917
|
+
ts.isSatisfiesExpression(expression)
|
|
2918
|
+
) {
|
|
2919
|
+
expression = expression.expression;
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
if (inspectRootCssString && isReactCssStringLiteral(expression)) {
|
|
2923
|
+
return inspectReactCssString(expression.text);
|
|
2924
|
+
}
|
|
2925
|
+
if (isStaticReactCssValueExpression(expression)) return "safe";
|
|
2926
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
2927
|
+
for (const property of expression.properties) {
|
|
2928
|
+
if (ts.isSpreadAssignment(property)) return "unknown";
|
|
2929
|
+
if (
|
|
2930
|
+
!ts.isPropertyAssignment(property) &&
|
|
2931
|
+
!ts.isShorthandPropertyAssignment(property)
|
|
2932
|
+
) {
|
|
2933
|
+
return "unknown";
|
|
2934
|
+
}
|
|
2935
|
+
const name = property.name;
|
|
2936
|
+
const text =
|
|
2937
|
+
ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
|
|
2938
|
+
if (!text) return "unknown";
|
|
2939
|
+
if (isReactMotionStylePropName(text)) return "motion";
|
|
2940
|
+
const valueExpression = ts.isPropertyAssignment(property)
|
|
2941
|
+
? property.initializer
|
|
2942
|
+
: property.name;
|
|
2943
|
+
if (!isStaticReactCssValueExpression(valueExpression)) {
|
|
2944
|
+
const nested = inspectReactCssExpression(
|
|
2945
|
+
valueExpression,
|
|
2946
|
+
sourceFile,
|
|
2947
|
+
resolving,
|
|
2948
|
+
);
|
|
2949
|
+
if (nested !== "safe") return nested;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
return "safe";
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
const resolved = resolveReactStyleExpression(expression, sourceFile);
|
|
2956
|
+
if (!resolved) return "unknown";
|
|
2957
|
+
const declarationKey = `${resolved.start}:${resolved.end}`;
|
|
2958
|
+
if (resolving.has(declarationKey)) return "unknown";
|
|
2959
|
+
resolving.add(declarationKey);
|
|
2960
|
+
const result = inspectReactCssExpression(
|
|
2961
|
+
resolved.expression,
|
|
2962
|
+
sourceFile,
|
|
2963
|
+
resolving,
|
|
2964
|
+
inspectRootCssString,
|
|
2965
|
+
);
|
|
2966
|
+
resolving.delete(declarationKey);
|
|
2967
|
+
return result;
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
function resolveReactStyleExpression(
|
|
2971
|
+
expression: TypeScript.Expression,
|
|
2972
|
+
sourceFile: TypeScript.SourceFile,
|
|
2973
|
+
): { expression: TypeScript.Expression; start: number; end: number } | null {
|
|
2974
|
+
const name = ts.isIdentifier(expression)
|
|
2975
|
+
? expression.text
|
|
2976
|
+
: ts.isPropertyAccessExpression(expression) &&
|
|
2977
|
+
ts.isIdentifier(expression.expression)
|
|
2978
|
+
? expression.expression.text
|
|
2979
|
+
: null;
|
|
2980
|
+
if (!name) return null;
|
|
2981
|
+
|
|
2982
|
+
if (hasReactStyleParameterShadow(expression, sourceFile, name)) return null;
|
|
2983
|
+
|
|
2984
|
+
const matches: TypeScript.VariableDeclaration[] = [];
|
|
2985
|
+
const visit = (node: TypeScript.Node): void => {
|
|
2986
|
+
if (
|
|
2987
|
+
ts.isVariableDeclaration(node) &&
|
|
2988
|
+
ts.isIdentifier(node.name) &&
|
|
2989
|
+
node.name.text === name
|
|
2990
|
+
) {
|
|
2991
|
+
matches.push(node);
|
|
2992
|
+
}
|
|
2993
|
+
ts.forEachChild(node, visit);
|
|
2994
|
+
};
|
|
2995
|
+
visit(sourceFile);
|
|
2996
|
+
if (matches.length !== 1) return null;
|
|
2997
|
+
const declaration = matches[0]!;
|
|
2998
|
+
const scope = reactVariableDeclarationScope(declaration);
|
|
2999
|
+
if (!scope || !isReactNodeWithinScope(scope, expression)) return null;
|
|
3000
|
+
const initializer = declaration.initializer;
|
|
3001
|
+
if (
|
|
3002
|
+
ts.isPropertyAccessExpression(expression) &&
|
|
3003
|
+
initializer &&
|
|
3004
|
+
ts.isObjectLiteralExpression(initializer)
|
|
3005
|
+
) {
|
|
3006
|
+
const propertyName = expression.name.text;
|
|
3007
|
+
const properties = initializer.properties.filter(
|
|
3008
|
+
(property): property is TypeScript.PropertyAssignment =>
|
|
3009
|
+
ts.isPropertyAssignment(property) &&
|
|
3010
|
+
((ts.isIdentifier(property.name) &&
|
|
3011
|
+
property.name.text === propertyName) ||
|
|
3012
|
+
(ts.isStringLiteral(property.name) &&
|
|
3013
|
+
property.name.text === propertyName)),
|
|
3014
|
+
);
|
|
3015
|
+
if (properties.length === 1) {
|
|
3016
|
+
return {
|
|
3017
|
+
expression: properties[0]!.initializer,
|
|
3018
|
+
start: properties[0]!.getStart(sourceFile),
|
|
3019
|
+
end: properties[0]!.end,
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
return initializer
|
|
3024
|
+
? {
|
|
1087
3025
|
expression: initializer,
|
|
1088
3026
|
start: initializer.getStart(sourceFile),
|
|
1089
3027
|
end: initializer.end,
|
|
@@ -1172,12 +3110,102 @@ function isReactMotionStylePropName(name: string): boolean {
|
|
|
1172
3110
|
/^(?:webkit|moz|ms|o)?animation(?:name|duration|delay|timingfunction|fillmode|iterationcount|direction|playstate|composition|timeline|range(?:start|end)?)?$/.test(
|
|
1173
3111
|
normalized,
|
|
1174
3112
|
) ||
|
|
1175
|
-
/^(?:webkit|moz|ms|o)?transition(?:property|duration|delay|timingfunction)?$/.test(
|
|
3113
|
+
/^(?:webkit|moz|ms|o)?transition(?:property|duration|delay|timingfunction|behavior)?$/.test(
|
|
1176
3114
|
normalized,
|
|
1177
3115
|
)
|
|
1178
3116
|
);
|
|
1179
3117
|
}
|
|
1180
3118
|
|
|
3119
|
+
function isReactCssStringLiteral(
|
|
3120
|
+
expression: TypeScript.Expression,
|
|
3121
|
+
): expression is
|
|
3122
|
+
| TypeScript.StringLiteral
|
|
3123
|
+
| TypeScript.NoSubstitutionTemplateLiteral {
|
|
3124
|
+
return (
|
|
3125
|
+
ts.isStringLiteral(expression) ||
|
|
3126
|
+
ts.isNoSubstitutionTemplateLiteral(expression)
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
function inspectReactCssString(css: string): ReactStyleInspection {
|
|
3131
|
+
const inspected = maskReactCssCommentsAndStrings(css);
|
|
3132
|
+
if (inspected.includes("\\")) return "unknown";
|
|
3133
|
+
const declarations = inspected.matchAll(/(?:^|[;{}\s])\s*([\w-]+)\s*:/g);
|
|
3134
|
+
let foundDeclaration = false;
|
|
3135
|
+
for (const declaration of declarations) {
|
|
3136
|
+
foundDeclaration = true;
|
|
3137
|
+
if (isReactMotionStylePropName(declaration[1]!)) return "motion";
|
|
3138
|
+
}
|
|
3139
|
+
if (foundDeclaration || inspected.trim().length === 0) return "safe";
|
|
3140
|
+
return "unknown";
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function maskReactCssCommentsAndStrings(css: string): string {
|
|
3144
|
+
let result = "";
|
|
3145
|
+
let quote: "'" | '"' | null = null;
|
|
3146
|
+
for (let index = 0; index < css.length; index++) {
|
|
3147
|
+
const character = css[index]!;
|
|
3148
|
+
if (quote) {
|
|
3149
|
+
if (character === "\\") {
|
|
3150
|
+
result += " ";
|
|
3151
|
+
const escaped = css[index + 1];
|
|
3152
|
+
if (escaped !== undefined) {
|
|
3153
|
+
result += escaped === "\n" ? "\n" : " ";
|
|
3154
|
+
index++;
|
|
3155
|
+
}
|
|
3156
|
+
} else {
|
|
3157
|
+
if (character === quote) quote = null;
|
|
3158
|
+
result += character === "\n" ? "\n" : " ";
|
|
3159
|
+
}
|
|
3160
|
+
continue;
|
|
3161
|
+
}
|
|
3162
|
+
if (character === "/" && css[index + 1] === "*") {
|
|
3163
|
+
result += " ";
|
|
3164
|
+
index++;
|
|
3165
|
+
while (
|
|
3166
|
+
index + 1 < css.length &&
|
|
3167
|
+
!(css[index] === "*" && css[index + 1] === "/")
|
|
3168
|
+
) {
|
|
3169
|
+
index++;
|
|
3170
|
+
result += css[index] === "\n" ? "\n" : " ";
|
|
3171
|
+
}
|
|
3172
|
+
if (index + 1 < css.length) {
|
|
3173
|
+
result += " ";
|
|
3174
|
+
index++;
|
|
3175
|
+
}
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
if (character === "'" || character === '"') {
|
|
3179
|
+
quote = character;
|
|
3180
|
+
result += " ";
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
result += character;
|
|
3184
|
+
}
|
|
3185
|
+
return result;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
function isStaticReactCssValueExpression(
|
|
3189
|
+
expression: TypeScript.Expression,
|
|
3190
|
+
): boolean {
|
|
3191
|
+
while (
|
|
3192
|
+
ts.isParenthesizedExpression(expression) ||
|
|
3193
|
+
ts.isAsExpression(expression) ||
|
|
3194
|
+
ts.isTypeAssertionExpression(expression) ||
|
|
3195
|
+
ts.isSatisfiesExpression(expression)
|
|
3196
|
+
) {
|
|
3197
|
+
expression = expression.expression;
|
|
3198
|
+
}
|
|
3199
|
+
return (
|
|
3200
|
+
ts.isStringLiteral(expression) ||
|
|
3201
|
+
ts.isNoSubstitutionTemplateLiteral(expression) ||
|
|
3202
|
+
ts.isNumericLiteral(expression) ||
|
|
3203
|
+
expression.kind === ts.SyntaxKind.TrueKeyword ||
|
|
3204
|
+
expression.kind === ts.SyntaxKind.FalseKeyword ||
|
|
3205
|
+
expression.kind === ts.SyntaxKind.NullKeyword
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3208
|
+
|
|
1181
3209
|
function readStaticReactStyleContent(
|
|
1182
3210
|
sourceFile: TypeScript.SourceFile,
|
|
1183
3211
|
style: TypeScript.JsxElement,
|