|
3 | 3 | */ |
4 | 4 |
|
5 | 5 | import javascript |
| 6 | +private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps |
| 7 | +private import semmle.javascript.dataflow.internal.PreCallGraphStep |
6 | 8 |
|
7 | 9 | /** |
8 | 10 | * Gets a reference to the 'React' object. |
@@ -548,3 +550,228 @@ private class ReactJSXElement extends JSXElement { |
548 | 550 | */ |
549 | 551 | ReactComponent getComponent() { result = component } |
550 | 552 | } |
| 553 | + |
| 554 | +/** |
| 555 | + * Step through the state variable of a `useState` call. |
| 556 | + * |
| 557 | + * It returns a pair of the current state, and a callback to change the state. |
| 558 | + * |
| 559 | + * For example: |
| 560 | + * ```js |
| 561 | + * let [state, setState] = useState(initialValue); |
| 562 | + * let [state, setState] = useState(() => initialValue); // lazy initial state |
| 563 | + * |
| 564 | + * setState(newState); |
| 565 | + * setState(prevState => { ... }); |
| 566 | + * ``` |
| 567 | + */ |
| 568 | +private class UseStateStep extends PreCallGraphStep { |
| 569 | + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { |
| 570 | + exists(DataFlow::CallNode call | call = react().getAMemberCall("useState") | |
| 571 | + pred = |
| 572 | + [call.getArgument(0), // initial state |
| 573 | + call.getCallback(0).getReturnNode(), // lazy initial state |
| 574 | + call.getAPropertyRead("1").getACall().getArgument(0), // setState invocation |
| 575 | + call.getAPropertyRead("1").getACall().getCallback(0).getReturnNode()] and // setState with callback |
| 576 | + succ = call.getAPropertyRead("0") |
| 577 | + or |
| 578 | + // Propagate current state into the callback argument of `setState(prevState => { ... })` |
| 579 | + pred = call.getAPropertyRead("0") and |
| 580 | + succ = call.getAPropertyRead("1").getACall().getCallback(0).getParameter(0) |
| 581 | + ) |
| 582 | + } |
| 583 | +} |
| 584 | + |
| 585 | +/** |
| 586 | + * A step through a React context object. |
| 587 | + * |
| 588 | + * For example: |
| 589 | + * ```js |
| 590 | + * let MyContext = React.createContext('foo'); |
| 591 | + * |
| 592 | + * <MyContext.Provider value={pred}> |
| 593 | + * <Foo/> |
| 594 | + * </MyContext.Provider> |
| 595 | + * |
| 596 | + * function Foo() { |
| 597 | + * let succ = useContext(MyContext); |
| 598 | + * } |
| 599 | + */ |
| 600 | +private class UseContextStep extends PreCallGraphStep { |
| 601 | + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { |
| 602 | + exists(DataFlow::CallNode context | |
| 603 | + pred = getAContextInput(context) and |
| 604 | + succ = getAContextOutput(context) |
| 605 | + ) |
| 606 | + } |
| 607 | +} |
| 608 | + |
| 609 | +/** |
| 610 | + * Gets a data flow node referring to the result of the given `createContext` call. |
| 611 | + */ |
| 612 | +private DataFlow::SourceNode getAContextRef(DataFlow::CallNode createContext) { |
| 613 | + createContext = react().getAMemberCall("createContext") and |
| 614 | + result = createContext |
| 615 | + or |
| 616 | + // Track through imports/exports, but not full type tracking, so this can be used as a PreCallGraphStep. |
| 617 | + exists(DataFlow::Node mid | |
| 618 | + getAContextRef(createContext).flowsTo(mid) and |
| 619 | + FlowSteps::propertyFlowStep(mid, result) |
| 620 | + ) |
| 621 | +} |
| 622 | + |
| 623 | +/** |
| 624 | + * Gets a data flow node whose value is provided to the given context object. |
| 625 | + * |
| 626 | + * For example: |
| 627 | + * ```jsx |
| 628 | + * React.createContext(x); |
| 629 | + * <MyContext.Provider value={x}> |
| 630 | + * ``` |
| 631 | + */ |
| 632 | +pragma[nomagic] |
| 633 | +private DataFlow::Node getAContextInput(DataFlow::CallNode createContext) { |
| 634 | + result = createContext.getArgument(0) // initial value |
| 635 | + or |
| 636 | + exists(JSXElement provider | |
| 637 | + getAContextRef(createContext) |
| 638 | + .getAPropertyRead("Provider") |
| 639 | + .flowsTo(provider.getNameExpr().flow()) and |
| 640 | + result = provider.getAttributeByName("value").getValue().flow() |
| 641 | + ) |
| 642 | +} |
| 643 | + |
| 644 | +/** |
| 645 | + * Gets a data flow node whose value is obtained from the given context object. |
| 646 | + * |
| 647 | + * For example: |
| 648 | + * ```js |
| 649 | + * let value = useContext(MyContext); |
| 650 | + * ``` |
| 651 | + */ |
| 652 | +pragma[nomagic] |
| 653 | +private DataFlow::CallNode getAContextOutput(DataFlow::CallNode createContext) { |
| 654 | + result = react().getAMemberCall("useContext") and |
| 655 | + getAContextRef(createContext).flowsTo(result.getArgument(0)) |
| 656 | + or |
| 657 | + exists(DataFlow::ClassNode cls | |
| 658 | + getAContextRef(createContext).flowsTo(cls.getAPropertyWrite("contextType").getRhs()) and |
| 659 | + result = cls.getAReceiverNode().getAPropertyRead("context") |
| 660 | + ) |
| 661 | +} |
| 662 | + |
| 663 | +/** |
| 664 | + * A step through a `useMemo` call; for example: |
| 665 | + * ```js |
| 666 | + * let succ = useMemo(() => pred, []); |
| 667 | + * ``` |
| 668 | + */ |
| 669 | +private class UseMemoStep extends PreCallGraphStep { |
| 670 | + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { |
| 671 | + exists(DataFlow::CallNode call | |
| 672 | + call = react().getAMemberCall("useMemo") |
| 673 | + | |
| 674 | + pred = call.getCallback(0).getReturnNode() and |
| 675 | + succ = call |
| 676 | + ) |
| 677 | + } |
| 678 | +} |
| 679 | + |
| 680 | +private DataFlow::SourceNode reactRouterDom() { |
| 681 | + result = DataFlow::moduleImport("react-router-dom") |
| 682 | +} |
| 683 | + |
| 684 | +private class ReactRouterSource extends RemoteFlowSource { |
| 685 | + ReactRouterSource() { |
| 686 | + this = reactRouterDom().getAMemberCall("useParams") |
| 687 | + or |
| 688 | + this = reactRouterDom().getAMemberCall("useRouteMatch").getAPropertyRead(["params", "url"]) |
| 689 | + } |
| 690 | + |
| 691 | + override string getSourceType() { |
| 692 | + result = "react-router path parameters" |
| 693 | + } |
| 694 | +} |
| 695 | + |
| 696 | +/** |
| 697 | + * Holds if `mod` transitively depends on `react-router-dom`. |
| 698 | + * |
| 699 | + * We assume any React component in such a file may be used in a context where react-router |
| 700 | + * injects the `location` property in its `props` object. |
| 701 | + */ |
| 702 | +private predicate dependsOnReactRouter(Module mod) { |
| 703 | + mod.getAnImport().getImportedPath().getValue() = "react-router-dom" |
| 704 | + or |
| 705 | + dependsOnReactRouter(mod.getAnImportedModule()) |
| 706 | +} |
| 707 | + |
| 708 | +/** |
| 709 | + * A reference to the DOM location obtained through `react-router-dom` |
| 710 | + * |
| 711 | + * For example: |
| 712 | + * ```js |
| 713 | + * let location = useLocation(); |
| 714 | + * |
| 715 | + * function MyComponent(props) { |
| 716 | + * props.location; |
| 717 | + * } |
| 718 | + * export default withRouter(MyComponent); |
| 719 | + */ |
| 720 | +private class ReactRouterLocationSource extends DOM::LocationSource::Range { |
| 721 | + ReactRouterLocationSource() { |
| 722 | + this = reactRouterDom().getAMemberCall("useLocation") |
| 723 | + or |
| 724 | + exists(ReactComponent component | |
| 725 | + dependsOnReactRouter(component.getTopLevel()) and |
| 726 | + this = component.getAPropRead("location") |
| 727 | + ) |
| 728 | + } |
| 729 | +} |
| 730 | + |
| 731 | +/** |
| 732 | + * Gets a reference to a function which, if called with a React component, returns wrapped |
| 733 | + * version of that component, which we model as a direct reference to the underlying component. |
| 734 | + */ |
| 735 | +private DataFlow::SourceNode higherOrderComponentBuilder() { |
| 736 | + result = react().getAPropertyRead("memo") |
| 737 | + or |
| 738 | + result = DataFlow::moduleMember("react-redux", "connect").getACall() |
| 739 | + or |
| 740 | + result = reactRouterDom().getAPropertyRead("withRouter") |
| 741 | + or |
| 742 | + exists(FunctionCompositionCall compose | |
| 743 | + higherOrderComponentBuilder().flowsTo(compose.getAnOperandNode()) and |
| 744 | + result = compose |
| 745 | + ) |
| 746 | +} |
| 747 | + |
| 748 | +private class HigherOrderComponentStep extends PreCallGraphStep { |
| 749 | + override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) { |
| 750 | + // `lazy(() => P)` returns a proxy for the component eventually returned by |
| 751 | + // the promise P. We model this call as simply returning the value in P. |
| 752 | + // It is primarily used for lazy-loading of React components. |
| 753 | + exists(DataFlow::CallNode call | |
| 754 | + call = react().getAMemberCall("lazy") and |
| 755 | + pred = call.getCallback(0).getReturnNode() and |
| 756 | + succ = call and |
| 757 | + prop = Promises::valueProp() |
| 758 | + ) |
| 759 | + } |
| 760 | + |
| 761 | + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { |
| 762 | + // `memo(f)` returns a function behaves as `f` but caches results |
| 763 | + // It is sometimes used to wrap an entire functional component. |
| 764 | + exists(DataFlow::CallNode call | |
| 765 | + call = higherOrderComponentBuilder().getACall() and |
| 766 | + pred = call.getArgument(0) and |
| 767 | + succ = call |
| 768 | + ) |
| 769 | + or |
| 770 | + exists(TaggedTemplateExpr expr, DataFlow::CallNode call | |
| 771 | + call = DataFlow::moduleImport("styled-components").getACall() and |
| 772 | + pred = call.getArgument(0) and |
| 773 | + call.flowsTo(expr.getTag().flow()) and |
| 774 | + succ = expr.flow() |
| 775 | + ) |
| 776 | + } |
| 777 | +} |
0 commit comments