1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  */
    
  7. 
    
  8. import {ATTRIBUTE_NAME_CHAR} from './isAttributeNameSafe';
    
  9. import isCustomElement from './isCustomElement';
    
  10. import possibleStandardNames from './possibleStandardNames';
    
  11. import hasOwnProperty from 'shared/hasOwnProperty';
    
  12. import {
    
  13.   enableCustomElementPropertySupport,
    
  14.   enableFormActions,
    
  15. } from 'shared/ReactFeatureFlags';
    
  16. 
    
  17. const warnedProperties = {};
    
  18. const EVENT_NAME_REGEX = /^on./;
    
  19. const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
    
  20. const rARIA = __DEV__
    
  21.   ? new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$')
    
  22.   : null;
    
  23. const rARIACamel = __DEV__
    
  24.   ? new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$')
    
  25.   : null;
    
  26. 
    
  27. function validateProperty(tagName, name, value, eventRegistry) {
    
  28.   if (__DEV__) {
    
  29.     if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) {
    
  30.       return true;
    
  31.     }
    
  32. 
    
  33.     const lowerCasedName = name.toLowerCase();
    
  34.     if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') {
    
  35.       console.error(
    
  36.         'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
    
  37.           'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
    
  38.           'are not needed/supported by React.',
    
  39.       );
    
  40.       warnedProperties[name] = true;
    
  41.       return true;
    
  42.     }
    
  43. 
    
  44.     if (enableFormActions) {
    
  45.       // Actions are special because unlike events they can have other value types.
    
  46.       if (typeof value === 'function') {
    
  47.         if (tagName === 'form' && name === 'action') {
    
  48.           return true;
    
  49.         }
    
  50.         if (tagName === 'input' && name === 'formAction') {
    
  51.           return true;
    
  52.         }
    
  53.         if (tagName === 'button' && name === 'formAction') {
    
  54.           return true;
    
  55.         }
    
  56.       }
    
  57.     }
    
  58. 
    
  59.     // We can't rely on the event system being injected on the server.
    
  60.     if (eventRegistry != null) {
    
  61.       const {registrationNameDependencies, possibleRegistrationNames} =
    
  62.         eventRegistry;
    
  63.       if (registrationNameDependencies.hasOwnProperty(name)) {
    
  64.         return true;
    
  65.       }
    
  66.       const registrationName = possibleRegistrationNames.hasOwnProperty(
    
  67.         lowerCasedName,
    
  68.       )
    
  69.         ? possibleRegistrationNames[lowerCasedName]
    
  70.         : null;
    
  71.       if (registrationName != null) {
    
  72.         console.error(
    
  73.           'Invalid event handler property `%s`. Did you mean `%s`?',
    
  74.           name,
    
  75.           registrationName,
    
  76.         );
    
  77.         warnedProperties[name] = true;
    
  78.         return true;
    
  79.       }
    
  80.       if (EVENT_NAME_REGEX.test(name)) {
    
  81.         console.error(
    
  82.           'Unknown event handler property `%s`. It will be ignored.',
    
  83.           name,
    
  84.         );
    
  85.         warnedProperties[name] = true;
    
  86.         return true;
    
  87.       }
    
  88.     } else if (EVENT_NAME_REGEX.test(name)) {
    
  89.       // If no event plugins have been injected, we are in a server environment.
    
  90.       // So we can't tell if the event name is correct for sure, but we can filter
    
  91.       // out known bad ones like `onclick`. We can't suggest a specific replacement though.
    
  92.       if (INVALID_EVENT_NAME_REGEX.test(name)) {
    
  93.         console.error(
    
  94.           'Invalid event handler property `%s`. ' +
    
  95.             'React events use the camelCase naming convention, for example `onClick`.',
    
  96.           name,
    
  97.         );
    
  98.       }
    
  99.       warnedProperties[name] = true;
    
  100.       return true;
    
  101.     }
    
  102. 
    
  103.     // Let the ARIA attribute hook validate ARIA attributes
    
  104.     if (rARIA.test(name) || rARIACamel.test(name)) {
    
  105.       return true;
    
  106.     }
    
  107. 
    
  108.     if (lowerCasedName === 'innerhtml') {
    
  109.       console.error(
    
  110.         'Directly setting property `innerHTML` is not permitted. ' +
    
  111.           'For more information, lookup documentation on `dangerouslySetInnerHTML`.',
    
  112.       );
    
  113.       warnedProperties[name] = true;
    
  114.       return true;
    
  115.     }
    
  116. 
    
  117.     if (lowerCasedName === 'aria') {
    
  118.       console.error(
    
  119.         'The `aria` attribute is reserved for future use in React. ' +
    
  120.           'Pass individual `aria-` attributes instead.',
    
  121.       );
    
  122.       warnedProperties[name] = true;
    
  123.       return true;
    
  124.     }
    
  125. 
    
  126.     if (
    
  127.       lowerCasedName === 'is' &&
    
  128.       value !== null &&
    
  129.       value !== undefined &&
    
  130.       typeof value !== 'string'
    
  131.     ) {
    
  132.       console.error(
    
  133.         'Received a `%s` for a string attribute `is`. If this is expected, cast ' +
    
  134.           'the value to a string.',
    
  135.         typeof value,
    
  136.       );
    
  137.       warnedProperties[name] = true;
    
  138.       return true;
    
  139.     }
    
  140. 
    
  141.     if (typeof value === 'number' && isNaN(value)) {
    
  142.       console.error(
    
  143.         'Received NaN for the `%s` attribute. If this is expected, cast ' +
    
  144.           'the value to a string.',
    
  145.         name,
    
  146.       );
    
  147.       warnedProperties[name] = true;
    
  148.       return true;
    
  149.     }
    
  150. 
    
  151.     // Known attributes should match the casing specified in the property config.
    
  152.     if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
    
  153.       const standardName = possibleStandardNames[lowerCasedName];
    
  154.       if (standardName !== name) {
    
  155.         console.error(
    
  156.           'Invalid DOM property `%s`. Did you mean `%s`?',
    
  157.           name,
    
  158.           standardName,
    
  159.         );
    
  160.         warnedProperties[name] = true;
    
  161.         return true;
    
  162.       }
    
  163.     } else if (name !== lowerCasedName) {
    
  164.       // Unknown attributes should have lowercase casing since that's how they
    
  165.       // will be cased anyway with server rendering.
    
  166.       console.error(
    
  167.         'React does not recognize the `%s` prop on a DOM element. If you ' +
    
  168.           'intentionally want it to appear in the DOM as a custom ' +
    
  169.           'attribute, spell it as lowercase `%s` instead. ' +
    
  170.           'If you accidentally passed it from a parent component, remove ' +
    
  171.           'it from the DOM element.',
    
  172.         name,
    
  173.         lowerCasedName,
    
  174.       );
    
  175.       warnedProperties[name] = true;
    
  176.       return true;
    
  177.     }
    
  178. 
    
  179.     // Now that we've validated casing, do not validate
    
  180.     // data types for reserved props
    
  181.     switch (name) {
    
  182.       case 'dangerouslySetInnerHTML':
    
  183.       case 'children':
    
  184.       case 'style':
    
  185.       case 'suppressContentEditableWarning':
    
  186.       case 'suppressHydrationWarning':
    
  187.       case 'defaultValue': // Reserved
    
  188.       case 'defaultChecked':
    
  189.       case 'innerHTML': {
    
  190.         return true;
    
  191.       }
    
  192.       case 'innerText': // Properties
    
  193.       case 'textContent':
    
  194.         if (enableCustomElementPropertySupport) {
    
  195.           return true;
    
  196.         }
    
  197.     }
    
  198. 
    
  199.     switch (typeof value) {
    
  200.       case 'boolean': {
    
  201.         switch (name) {
    
  202.           case 'autoFocus':
    
  203.           case 'checked':
    
  204.           case 'multiple':
    
  205.           case 'muted':
    
  206.           case 'selected':
    
  207.           case 'contentEditable':
    
  208.           case 'spellCheck':
    
  209.           case 'draggable':
    
  210.           case 'value':
    
  211.           case 'autoReverse':
    
  212.           case 'externalResourcesRequired':
    
  213.           case 'focusable':
    
  214.           case 'preserveAlpha':
    
  215.           case 'allowFullScreen':
    
  216.           case 'async':
    
  217.           case 'autoPlay':
    
  218.           case 'controls':
    
  219.           case 'default':
    
  220.           case 'defer':
    
  221.           case 'disabled':
    
  222.           case 'disablePictureInPicture':
    
  223.           case 'disableRemotePlayback':
    
  224.           case 'formNoValidate':
    
  225.           case 'hidden':
    
  226.           case 'loop':
    
  227.           case 'noModule':
    
  228.           case 'noValidate':
    
  229.           case 'open':
    
  230.           case 'playsInline':
    
  231.           case 'readOnly':
    
  232.           case 'required':
    
  233.           case 'reversed':
    
  234.           case 'scoped':
    
  235.           case 'seamless':
    
  236.           case 'itemScope':
    
  237.           case 'capture':
    
  238.           case 'download': {
    
  239.             // Boolean properties can accept boolean values
    
  240.             return true;
    
  241.           }
    
  242.           default: {
    
  243.             const prefix = name.toLowerCase().slice(0, 5);
    
  244.             if (prefix === 'data-' || prefix === 'aria-') {
    
  245.               return true;
    
  246.             }
    
  247.             if (value) {
    
  248.               console.error(
    
  249.                 'Received `%s` for a non-boolean attribute `%s`.\n\n' +
    
  250.                   'If you want to write it to the DOM, pass a string instead: ' +
    
  251.                   '%s="%s" or %s={value.toString()}.',
    
  252.                 value,
    
  253.                 name,
    
  254.                 name,
    
  255.                 value,
    
  256.                 name,
    
  257.               );
    
  258.             } else {
    
  259.               console.error(
    
  260.                 'Received `%s` for a non-boolean attribute `%s`.\n\n' +
    
  261.                   'If you want to write it to the DOM, pass a string instead: ' +
    
  262.                   '%s="%s" or %s={value.toString()}.\n\n' +
    
  263.                   'If you used to conditionally omit it with %s={condition && value}, ' +
    
  264.                   'pass %s={condition ? value : undefined} instead.',
    
  265.                 value,
    
  266.                 name,
    
  267.                 name,
    
  268.                 value,
    
  269.                 name,
    
  270.                 name,
    
  271.                 name,
    
  272.               );
    
  273.             }
    
  274.             warnedProperties[name] = true;
    
  275.             return true;
    
  276.           }
    
  277.         }
    
  278.       }
    
  279.       case 'function':
    
  280.       case 'symbol': // eslint-disable-line
    
  281.         // Warn when a known attribute is a bad type
    
  282.         warnedProperties[name] = true;
    
  283.         return false;
    
  284.       case 'string': {
    
  285.         // Warn when passing the strings 'false' or 'true' into a boolean prop
    
  286.         if (value === 'false' || value === 'true') {
    
  287.           switch (name) {
    
  288.             case 'checked':
    
  289.             case 'selected':
    
  290.             case 'multiple':
    
  291.             case 'muted':
    
  292.             case 'allowFullScreen':
    
  293.             case 'async':
    
  294.             case 'autoPlay':
    
  295.             case 'controls':
    
  296.             case 'default':
    
  297.             case 'defer':
    
  298.             case 'disabled':
    
  299.             case 'disablePictureInPicture':
    
  300.             case 'disableRemotePlayback':
    
  301.             case 'formNoValidate':
    
  302.             case 'hidden':
    
  303.             case 'loop':
    
  304.             case 'noModule':
    
  305.             case 'noValidate':
    
  306.             case 'open':
    
  307.             case 'playsInline':
    
  308.             case 'readOnly':
    
  309.             case 'required':
    
  310.             case 'reversed':
    
  311.             case 'scoped':
    
  312.             case 'seamless':
    
  313.             case 'itemScope': {
    
  314.               break;
    
  315.             }
    
  316.             default: {
    
  317.               return true;
    
  318.             }
    
  319.           }
    
  320.           console.error(
    
  321.             'Received the string `%s` for the boolean attribute `%s`. ' +
    
  322.               '%s ' +
    
  323.               'Did you mean %s={%s}?',
    
  324.             value,
    
  325.             name,
    
  326.             value === 'false'
    
  327.               ? 'The browser will interpret it as a truthy value.'
    
  328.               : 'Although this works, it will not work as expected if you pass the string "false".',
    
  329.             name,
    
  330.             value,
    
  331.           );
    
  332.           warnedProperties[name] = true;
    
  333.           return true;
    
  334.         }
    
  335.       }
    
  336.     }
    
  337.     return true;
    
  338.   }
    
  339. }
    
  340. 
    
  341. function warnUnknownProperties(type, props, eventRegistry) {
    
  342.   if (__DEV__) {
    
  343.     const unknownProps = [];
    
  344.     for (const key in props) {
    
  345.       const isValid = validateProperty(type, key, props[key], eventRegistry);
    
  346.       if (!isValid) {
    
  347.         unknownProps.push(key);
    
  348.       }
    
  349.     }
    
  350. 
    
  351.     const unknownPropString = unknownProps
    
  352.       .map(prop => '`' + prop + '`')
    
  353.       .join(', ');
    
  354.     if (unknownProps.length === 1) {
    
  355.       console.error(
    
  356.         'Invalid value for prop %s on <%s> tag. Either remove it from the element, ' +
    
  357.           'or pass a string or number value to keep it in the DOM. ' +
    
  358.           'For details, see https://reactjs.org/link/attribute-behavior ',
    
  359.         unknownPropString,
    
  360.         type,
    
  361.       );
    
  362.     } else if (unknownProps.length > 1) {
    
  363.       console.error(
    
  364.         'Invalid values for props %s on <%s> tag. Either remove them from the element, ' +
    
  365.           'or pass a string or number value to keep them in the DOM. ' +
    
  366.           'For details, see https://reactjs.org/link/attribute-behavior ',
    
  367.         unknownPropString,
    
  368.         type,
    
  369.       );
    
  370.     }
    
  371.   }
    
  372. }
    
  373. 
    
  374. export function validateProperties(type, props, eventRegistry) {
    
  375.   if (isCustomElement(type, props) || typeof props.is === 'string') {
    
  376.     return;
    
  377.   }
    
  378.   warnUnknownProperties(type, props, eventRegistry);
    
  379. }