diff --git a/dev/tools/phan/config.php b/dev/tools/phan/config.php index 1dc71344d2d..b5ac3bfafd8 100644 --- a/dev/tools/phan/config.php +++ b/dev/tools/phan/config.php @@ -4,6 +4,187 @@ define('DOL_PROJECT_ROOT', __DIR__.'/../../..'); define('DOL_DOCUMENT_ROOT', DOL_PROJECT_ROOT.'/htdocs'); define('PHAN_DIR', __DIR__); +$sanitizeRegex + = '/^(array:)?(?:'.implode( + '|', + array( + // Documented: + 'none', + 'array', + 'int', + 'intcomma', + 'alpha', + 'alphawithlgt', + 'alphanohtml', + 'MS', + 'aZ', + 'aZ09', + 'aZ09arobase', + 'aZ09comma', + 'san_alpha', + 'restricthtml', + 'nohtml', + 'custom', + // Not documented: + 'email', + 'restricthtmlallowclass', + 'restricthtmlallowunvalid', + 'restricthtmlnolink', + //'ascii', + //'categ_id', + //'chaine', + + //'html', + //'boolean', + //'double', + //'float', + //'string', + ) + ).')*$/'; + +/** + * Map deprecated module names to new module names + */ +$DEPRECATED_MODULE_MAPPING = array( + 'actioncomm' => 'agenda', + 'adherent' => 'member', + 'adherent_type' => 'member_type', + 'banque' => 'bank', + 'categorie' => 'category', + 'commande' => 'order', + 'contrat' => 'contract', + 'entrepot' => 'stock', + 'expedition' => 'delivery_note', + 'facture' => 'invoice', + 'ficheinter' => 'intervention', + 'product_fournisseur_price' => 'productsupplierprice', + 'product_price' => 'productprice', + 'projet' => 'project', + 'propale' => 'propal', + 'socpeople' => 'contact', +); + +/** + * Map module names to the 'class' name (the class is: mod) + * Value is null when the module is not internal to the default + * Dolibarr setup. + */ +$VALID_MODULE_MAPPING = array( + 'accounting' => 'Accounting', + 'agenda' => 'Agenda', + 'ai' => 'Ai', + 'anothermodule' => null, + 'api' => 'Api', + 'asset' => 'Asset', + 'bank' => 'Banque', + 'barcode' => 'Barcode', + 'blockedlog' => 'BlockedLog', + 'bom' => 'Bom', + 'bookcal' => 'BookCal', + 'bookmark' => 'Bookmark', + 'cashdesk' => null, // TODO: fill in proper class + 'category' => 'Categorie', + 'clicktodial' => 'ClickToDial', + 'collab' => 'Collab', + 'comptabilite' => 'Comptabilite', + 'contact' => null, // TODO: fill in proper class + 'contract' => 'Contrat', + 'cron' => 'Cron', + 'datapolicy' => 'DataPolicy', + 'dav' => 'Dav', + 'debugbar' => 'DebugBar', + 'delivery_note' => 'Expedition', + 'deplacement' => 'Deplacement', + "documentgeneration" => 'DocumentGeneration', + 'don' => 'Don', + 'dynamicprices' => 'DynamicPrices', + 'ecm' => 'ECM', + 'ecotax' => null, // TODO: External module ? + 'emailcollector' => 'EmailCollector', + 'eventorganization' => 'EventOrganization', + 'expensereport' => 'ExpenseReport', + 'export' => 'Export', + 'externalrss' => 'ExternalRss', + 'externalsite' => 'ExternalSite', + 'fckeditor' => 'Fckeditor', + 'fournisseur' => 'Fournisseur', + 'ftp' => 'FTP', + 'geoipmaxmind' => 'GeoIPMaxmind', + 'google' => null, // External ? + 'gravatar' => 'Gravatar', + 'holiday' => 'Holiday', + 'hrm' => 'HRM', + 'import' => 'Import', + 'incoterm' => 'Incoterm', + 'intervention' => 'Ficheinter', + 'intracommreport' => 'Intracommreport', + 'invoice' => 'Facture', + 'knowledgemanagement' => 'KnowledgeManagement', + 'label' => 'Label', + 'ldap' => 'Ldap', + 'loan' => 'Loan', + 'mailing' => 'Mailing', + 'mailman' => null, // Same module as mailmanspip -> MailmanSpip ?? + 'mailmanspip' => 'MailmanSpip', + 'margin' => 'Margin', + 'member' => 'Adherent', + 'memcached' => null, // TODO: External module? + 'modulebuilder' => 'ModuleBuilder', + 'mrp' => 'Mrp', + 'multicompany' => null, // Not provided by default, no module tests + 'multicurrency' => 'MultiCurrency', + 'mymodule' => null, // modMyModule - Name used in module builder (avoid false positives) + 'notification' => 'Notification', + 'numberwords' => null, // Not provided by default, no module tests + 'oauth' => 'Oauth', + 'openstreetmap' => null, // External module? + 'opensurvey' => 'OpenSurvey', + 'order' => 'Commande', + 'partnership' => 'Partnership', + 'paybox' => 'Paybox', + 'paymentbybanktransfer' => 'PaymentByBankTransfer', + 'paypal' => 'Paypal', + 'paypalplus' => null, + 'prelevement' => 'Prelevement', + 'printing' => 'Printing', + 'product' => 'Product', + 'productbatch' => 'ProductBatch', + 'productprice' => null, + 'productsupplierprice' => null, + 'project' => 'Projet', + 'propal' => 'Propale', + 'receiptprinter' => 'ReceiptPrinter', + 'reception' => 'Reception', + 'recruitment' => 'Recruitment', + 'resource' => 'Resource', + 'salaries' => 'Salaries', + 'service' => 'Service', + 'socialnetworks' => 'SocialNetworks', + 'societe' => 'Societe', + 'stock' => 'Stock', + 'stocktransfer' => 'StockTransfer', + 'stripe' => 'Stripe', + 'supplier_invoice' => null, // Special case, uses invoice + 'supplier_order' => null, // Special case, uses invoice + 'supplier_proposal' => 'SupplierProposal', + 'syslog' => 'Syslog', + 'takepos' => 'TakePos', + 'tax' => 'Tax', + 'ticket' => 'Ticket', + 'user' => 'User', + 'variants' => 'Variants', + 'webhook' => 'Webhook', + 'webportal' => 'WebPortal', + 'webservices' => 'WebServices', + 'webservicesclient' => 'WebServicesClient', + 'website' => 'Website', + 'workflow' => 'Workflow', + 'workstation' => 'Workstation', + 'zapier' => 'Zapier', +); + +$moduleNameRegex = '/^(?:'.implode('|', array_merge(array_keys($DEPRECATED_MODULE_MAPPING), array_keys($VALID_MODULE_MAPPING), array('\$modulename'))).')$/'; + /** * This configuration will be read and overlaid on top of the * default configuration. Command line arguments will be applied @@ -71,6 +252,7 @@ return [ .'|htdocs/includes/restler/.*' // @phpstan-ignore-line // Included as stub (did not seem properly analysed by phan without it) .'|htdocs/includes/stripe/.*' // @phpstan-ignore-line + // .'|htdocs/[^h].*/.*' // For testing @phpstan-ignore-line .')@', // @phpstan-ignore-line // A list of plugin files to execute. @@ -82,8 +264,14 @@ return [ // // Alternately, you can pass in the full path to a PHP file // with the plugin's implementation (e.g. 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php') + 'ParamMatchRegexPlugin' => [ + '/^GETPOST$/' => [1, $sanitizeRegex], + '/^isModEnabled$/' => [0, $moduleNameRegex], + '/^sanitizeVal$/' => [1, $sanitizeRegex], + ], 'plugins' => [ __DIR__.'/plugins/NoVarDumpPlugin.php', + __DIR__.'/plugins/ParamMatchRegexPlugin.php', // checks if a function, closure or method unconditionally returns. // can also be written as 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php' //'DeprecateAliasPlugin', diff --git a/dev/tools/phan/config_extended.php b/dev/tools/phan/config_extended.php index 9a721355daf..3b46fa7a49d 100644 --- a/dev/tools/phan/config_extended.php +++ b/dev/tools/phan/config_extended.php @@ -4,6 +4,187 @@ define('DOL_PROJECT_ROOT', __DIR__.'/../../..'); define('DOL_DOCUMENT_ROOT', DOL_PROJECT_ROOT.'/htdocs'); define('PHAN_DIR', __DIR__); +$sanitizeRegex + = '/^(array:)?(?:'.implode( + '|', + array( + // Documented: + 'none', + 'array', + 'int', + 'intcomma', + 'alpha', + 'alphawithlgt', + 'alphanohtml', + 'MS', + 'aZ', + 'aZ09', + 'aZ09arobase', + 'aZ09comma', + 'san_alpha', + 'restricthtml', + 'nohtml', + 'custom', + // Not documented: + 'email', + 'restricthtmlallowclass', + 'restricthtmlallowunvalid', + 'restricthtmlnolink', + //'ascii', + //'categ_id', + //'chaine', + + //'html', + //'boolean', + //'double', + //'float', + //'string', + ) + ).')*$/'; + +/** + * Map deprecated module names to new module names + */ +$DEPRECATED_MODULE_MAPPING = array( + 'actioncomm' => 'agenda', + 'adherent' => 'member', + 'adherent_type' => 'member_type', + 'banque' => 'bank', + 'categorie' => 'category', + 'commande' => 'order', + 'contrat' => 'contract', + 'entrepot' => 'stock', + 'expedition' => 'delivery_note', + 'facture' => 'invoice', + 'ficheinter' => 'intervention', + 'product_fournisseur_price' => 'productsupplierprice', + 'product_price' => 'productprice', + 'projet' => 'project', + 'propale' => 'propal', + 'socpeople' => 'contact', +); + +/** + * Map module names to the 'class' name (the class is: mod) + * Value is null when the module is not internal to the default + * Dolibarr setup. + */ +$VALID_MODULE_MAPPING = array( + 'accounting' => 'Accounting', + 'agenda' => 'Agenda', + 'ai' => 'Ai', + 'anothermodule' => null, + 'api' => 'Api', + 'asset' => 'Asset', + 'bank' => 'Banque', + 'barcode' => 'Barcode', + 'blockedlog' => 'BlockedLog', + 'bom' => 'Bom', + 'bookcal' => 'BookCal', + 'bookmark' => 'Bookmark', + 'cashdesk' => null, // TODO: fill in proper class + 'category' => 'Categorie', + 'clicktodial' => 'ClickToDial', + 'collab' => 'Collab', + 'comptabilite' => 'Comptabilite', + 'contact' => null, // TODO: fill in proper class + 'contract' => 'Contrat', + 'cron' => 'Cron', + 'datapolicy' => 'DataPolicy', + 'dav' => 'Dav', + 'debugbar' => 'DebugBar', + 'delivery_note' => 'Expedition', + 'deplacement' => 'Deplacement', + "documentgeneration" => 'DocumentGeneration', + 'don' => 'Don', + 'dynamicprices' => 'DynamicPrices', + 'ecm' => 'ECM', + 'ecotax' => null, // TODO: External module ? + 'emailcollector' => 'EmailCollector', + 'eventorganization' => 'EventOrganization', + 'expensereport' => 'ExpenseReport', + 'export' => 'Export', + 'externalrss' => 'ExternalRss', + 'externalsite' => 'ExternalSite', + 'fckeditor' => 'Fckeditor', + 'fournisseur' => 'Fournisseur', + 'ftp' => 'FTP', + 'geoipmaxmind' => 'GeoIPMaxmind', + 'google' => null, // External ? + 'gravatar' => 'Gravatar', + 'holiday' => 'Holiday', + 'hrm' => 'HRM', + 'import' => 'Import', + 'incoterm' => 'Incoterm', + 'intervention' => 'Ficheinter', + 'intracommreport' => 'Intracommreport', + 'invoice' => 'Facture', + 'knowledgemanagement' => 'KnowledgeManagement', + 'label' => 'Label', + 'ldap' => 'Ldap', + 'loan' => 'Loan', + 'mailing' => 'Mailing', + 'mailman' => null, // Same module as mailmanspip -> MailmanSpip ?? + 'mailmanspip' => 'MailmanSpip', + 'margin' => 'Margin', + 'member' => 'Adherent', + 'memcached' => null, // TODO: External module? + 'modulebuilder' => 'ModuleBuilder', + 'mrp' => 'Mrp', + 'multicompany' => null, // Not provided by default, no module tests + 'multicurrency' => 'MultiCurrency', + 'mymodule' => null, // modMyModule - Name used in module builder (avoid false positives) + 'notification' => 'Notification', + 'numberwords' => null, // Not provided by default, no module tests + 'oauth' => 'Oauth', + 'openstreetmap' => null, // External module? + 'opensurvey' => 'OpenSurvey', + 'order' => 'Commande', + 'partnership' => 'Partnership', + 'paybox' => 'Paybox', + 'paymentbybanktransfer' => 'PaymentByBankTransfer', + 'paypal' => 'Paypal', + 'paypalplus' => null, + 'prelevement' => 'Prelevement', + 'printing' => 'Printing', + 'product' => 'Product', + 'productbatch' => 'ProductBatch', + 'productprice' => null, + 'productsupplierprice' => null, + 'project' => 'Projet', + 'propal' => 'Propale', + 'receiptprinter' => 'ReceiptPrinter', + 'reception' => 'Reception', + 'recruitment' => 'Recruitment', + 'resource' => 'Resource', + 'salaries' => 'Salaries', + 'service' => 'Service', + 'socialnetworks' => 'SocialNetworks', + 'societe' => 'Societe', + 'stock' => 'Stock', + 'stocktransfer' => 'StockTransfer', + 'stripe' => 'Stripe', + 'supplier_invoice' => null, // Special case, uses invoice + 'supplier_order' => null, // Special case, uses invoice + 'supplier_proposal' => 'SupplierProposal', + 'syslog' => 'Syslog', + 'takepos' => 'TakePos', + 'tax' => 'Tax', + 'ticket' => 'Ticket', + 'user' => 'User', + 'variants' => 'Variants', + 'webhook' => 'Webhook', + 'webportal' => 'WebPortal', + 'webservices' => 'WebServices', + 'webservicesclient' => 'WebServicesClient', + 'website' => 'Website', + 'workflow' => 'Workflow', + 'workstation' => 'Workstation', + 'zapier' => 'Zapier', +); + +$moduleNameRegex = '/^(?:'.implode('|', array_merge(array_keys($DEPRECATED_MODULE_MAPPING), array_keys($VALID_MODULE_MAPPING))).')$/'; + /** * This configuration will be read and overlaid on top of the * default configuration. Command line arguments will be applied @@ -82,8 +263,14 @@ return [ // // Alternately, you can pass in the full path to a PHP file // with the plugin's implementation (e.g. 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php') + 'ParamMatchRegexPlugin' => [ + '/^GETPOST$/' => [1, $sanitizeRegex], + '/^isModEnabled$/' => [0, $moduleNameRegex], + '/^sanitizeVal$/' => [1, $sanitizeRegex], + ], 'plugins' => [ __DIR__.'/plugins/NoVarDumpPlugin.php', + __DIR__.'/plugins/ParamMatchRegexPlugin.php', 'DeprecateAliasPlugin', //'EmptyMethodAndFunctionPlugin', 'InvalidVariableIssetPlugin', diff --git a/dev/tools/phan/config_fixer.php b/dev/tools/phan/config_fixer.php new file mode 100644 index 00000000000..d5607844f26 --- /dev/null +++ b/dev/tools/phan/config_fixer.php @@ -0,0 +1,203 @@ + + */ +define('DOL_PROJECT_ROOT', __DIR__.'/../../..'); +define('DOL_DOCUMENT_ROOT', DOL_PROJECT_ROOT.'/htdocs'); +define('PHAN_DIR', __DIR__); +/** + * This configuration will be read and overlaid on top of the + * default configuration. Command line arguments will be applied + * after this file is read. + */ +return [ + // 'processes' => 6, + 'backward_compatibility_checks' => false, + 'simplify_ast' => true, + 'analyzed_file_extensions' => ['php','inc'], + 'globals_type_map' => [ + 'conf' => '\Conf', + 'db' => '\DoliDB', + 'extrafields' => '\ExtraFields', + 'hookmanager' => '\HookManager', + 'langs' => '\Translate', + 'mysoc' => '\Societe', + 'nblines' => '\int', + 'user' => '\User', + ], + + // Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`, `null`. + // If this is set to `null`, + // then Phan assumes the PHP version which is closest to the minor version + // of the php executable used to execute Phan. + //"target_php_version" => null, + "target_php_version" => '8.2', + //"target_php_version" => '7.3', + //"target_php_version" => '5.6', + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in exclude_analysis_directory_list, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'htdocs', + PHAN_DIR . '/stubs/', + ], + + // A directory list that defines files that will be excluded + // from static analysis, but whose class and method + // information should be included. + // + // Generally, you'll want to include the directories for + // third-party code (such as "vendor/") in this list. + // + // n.b.: If you'd like to parse but not analyze 3rd + // party code, directories containing that code + // should be added to the `directory_list` as + // to `exclude_analysis_directory_list`. + "exclude_analysis_directory_list" => [ + 'htdocs/includes/', + 'htdocs/core/class/lessc.class.php', // External library + PHAN_DIR . '/stubs/', + ], + //'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', + 'exclude_file_regex' => '@^(' // @phpstan-ignore-line + .'dummy' // @phpstan-ignore-line + .'|htdocs/.*/canvas/.*/tpl/.*.tpl.php' // @phpstan-ignore-line + .'|htdocs/modulebuilder/template/.*' // @phpstan-ignore-line + // Included as stub (old version + incompatible typing hints) + .'|htdocs/includes/restler/.*' // @phpstan-ignore-line + // Included as stub (did not seem properly analysed by phan without it) + .'|htdocs/includes/stripe/.*' // @phpstan-ignore-line + .')@', // @phpstan-ignore-line + + // A list of plugin files to execute. + // Plugins which are bundled with Phan can be added here by providing their name + // (e.g. 'AlwaysReturnPlugin') + // + // Documentation about available bundled plugins can be found + // at https://github.com/phan/phan/tree/master/.phan/plugins + // + // Alternately, you can pass in the full path to a PHP file + // with the plugin's implementation (e.g. 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php') + 'plugins' => [ + //'DeprecateAliasPlugin', + // __DIR__.'/plugins/NoVarDumpPlugin.php', + //'PHPDocToRealTypesPlugin', + + /* + //'EmptyMethodAndFunctionPlugin', + 'InvalidVariableIssetPlugin', + //'MoreSpecificElementTypePlugin', + 'NoAssertPlugin', + 'NotFullyQualifiedUsagePlugin', + 'PHPDocRedundantPlugin', + 'PHPUnitNotDeadCodePlugin', + //'PossiblyStaticMethodPlugin', + 'PreferNamespaceUsePlugin', + 'PrintfCheckerPlugin', + 'RedundantAssignmentPlugin', + + 'ConstantVariablePlugin', // Warns about values that are actually constant + //'HasPHPDocPlugin', // Requires PHPDoc + 'InlineHTMLPlugin', // html in PHP file, or at end of file + 'NonBoolBranchPlugin', // Requires test on bool, nont on ints + 'NonBoolInLogicalArithPlugin', + 'NumericalComparisonPlugin', + 'PHPDocToRealTypesPlugin', + 'PHPDocInWrongCommentPlugin', // Missing /** (/* was used) + //'ShortArrayPlugin', // Checks that [] is used + //'StrictLiteralComparisonPlugin', + 'UnknownClassElementAccessPlugin', + 'UnknownElementTypePlugin', + 'WhitespacePlugin', + //'RemoveDebugStatementPlugin', // Reports echo, print, ... + //'StrictComparisonPlugin', // Expects === + 'SuspiciousParamOrderPlugin', + 'UnsafeCodePlugin', + //'UnusedSuppressionPlugin', + + 'AlwaysReturnPlugin', + //'DollarDollarPlugin', + 'DuplicateArrayKeyPlugin', + 'DuplicateExpressionPlugin', + 'PregRegexCheckerPlugin', + 'PrintfCheckerPlugin', + 'SleepCheckerPlugin', + // Checks for syntactically unreachable statements in + // the global scope or function bodies. + 'UnreachableCodePlugin', + 'UseReturnValuePlugin', + 'EmptyStatementListPlugin', + 'LoopVariableReusePlugin', + */ + ], + + // Add any issue types (such as 'PhanUndeclaredMethod') + // here to inhibit them from being reported + 'suppress_issue_types' => [ + 'PhanPluginWhitespaceTab', // Dolibarr used tabs + 'PhanPluginCanUsePHP71Void', // Dolibarr is maintaining 7.0 compatibility + 'PhanPluginShortArray', // Dolibarr uses array() + 'PhanPluginShortArrayList', // Dolibarr uses array() + // The following may require that --quick is not used + // Fixers From PHPDocToRealTypesPlugin: + 'PhanPluginCanUseParamType', // Fixer - Report/Add types in the function definition (function abc(string $var) (adds string) + 'PhanPluginCanUseReturnType', // Fixer - Report/Add return types in the function definition (function abc(string $var) (adds string) + 'PhanPluginCanUseNullableParamType', // Fixer - Report/Add nullable parameter types in the function definition + 'PhanPluginCanUseNullableReturnType', // Fixer - Report/Add nullable return types in the function definition + + 'PhanPluginNonBoolBranch', // Not essential - 31240+ occurrences + 'PhanPluginNumericalComparison', // Not essential - 19870+ occurrences + 'PhanTypeMismatchArgument', // Not essential - 12300+ occurrences + 'PhanPluginNonBoolInLogicalArith', // Not essential - 11040+ occurrences + 'PhanPluginConstantVariableScalar', // Not essential - 5180+ occurrences + 'PhanPluginDuplicateConditionalTernaryDuplication', // 2750+ occurrences + 'PhanPluginDuplicateConditionalNullCoalescing', // Not essential - 990+ occurrences + + ], + // You can put relative paths to internal stubs in this config option. + // Phan will continue using its detailed type annotations, + // but load the constants, classes, functions, and classes (and their Reflection types) + // from these stub files (doubling as valid php files). + // Use a different extension from php (and preferably a separate folder) + // to avoid accidentally parsing these as PHP (includes projects depending on this). + // The 'mkstubs' script can be used to generate your own stubs (compatible with php 7.0+ right now) + // Note: The array key must be the same as the extension name reported by `php -m`, + // so that phan can skip loading the stubs if the extension is actually available. + 'autoload_internal_extension_signatures' => [ + // Stubs may be available at https://github.com/JetBrains/phpstorm-stubs/tree/master + + // Xdebug stubs are bundled with Phan 0.10.1+/0.8.9+ for usage, + // because Phan disables xdebug by default. + //'xdebug' => 'vendor/phan/phan/.phan/internal_stubs/xdebug.phan_php', + //'memcached' => PHAN_DIR . '/your_internal_stubs_folder_name/memcached.phan_php', + //'PDO' => PHAN_DIR . '/stubs/PDO.phan_php', + 'brotli' => PHAN_DIR . '/stubs/brotli.phan_php', + 'curl' => PHAN_DIR . '/stubs/curl.phan_php', + 'calendar' => PHAN_DIR . '/stubs/calendar.phan_php', + 'fileinfo' => PHAN_DIR . '/stubs/fileinfo.phan_php', + 'ftp' => PHAN_DIR . '/stubs/ftp.phan_php', + 'gd' => PHAN_DIR . '/stubs/gd.phan_php', + 'geoip' => PHAN_DIR . '/stubs/geoip.phan_php', + 'imap' => PHAN_DIR . '/stubs/imap.phan_php', + 'intl' => PHAN_DIR . '/stubs/intl.phan_php', + 'ldap' => PHAN_DIR . '/stubs/ldap.phan_php', + 'mcrypt' => PHAN_DIR . '/stubs/mcrypt.phan_php', + 'memcache' => PHAN_DIR . '/stubs/memcache.phan_php', + 'mysqli' => PHAN_DIR . '/stubs/mysqli.phan_php', + 'pdo_cubrid' => PHAN_DIR . '/stubs/pdo_cubrid.phan_php', + 'pdo_mysql' => PHAN_DIR . '/stubs/pdo_mysql.phan_php', + 'pdo_pgsql' => PHAN_DIR . '/stubs/pdo_pgsql.phan_php', + 'pdo_sqlite' => PHAN_DIR . '/stubs/pdo_sqlite.phan_php', + 'pgsql' => PHAN_DIR . '/stubs/pgsql.phan_php', + 'session' => PHAN_DIR . '/stubs/session.phan_php', + 'simplexml' => PHAN_DIR . '/stubs/SimpleXML.phan_php', + 'soap' => PHAN_DIR . '/stubs/soap.phan_php', + 'sockets' => PHAN_DIR . '/stubs/sockets.phan_php', + 'zip' => PHAN_DIR . '/stubs/zip.phan_php', + ], + + ]; diff --git a/dev/tools/phan/plugins/ParamMatchRegexPlugin.php b/dev/tools/phan/plugins/ParamMatchRegexPlugin.php new file mode 100644 index 00000000000..248ed72843e --- /dev/null +++ b/dev/tools/phan/plugins/ParamMatchRegexPlugin.php @@ -0,0 +1,248 @@ + + * + * Phan Plugin to validate that arguments match a regex + * + * + * "ParamMatchRegexPlugin" => [ + * "/^test1$/" => [ 0, "/^OK$/"], // Argument 0 must be 'OK' + * "/^test2$/" => [ 1, "/^NOK$/", "Test2Arg1NokError"], // Argument 1 must be 'NOK', error code + * "/^\\MyTest::mymethod$/" => [ 0, "/^NOK$/"], // Argument 0 must be 'NOK' + * ], + * 'plugins' => [ + * ".phan/plugins/ParamMatchRegexPlugin.php", + * // [...] + * ], + */ +declare(strict_types=1); + + +use ast\Node; +use Phan\Config; +use Phan\AST\UnionTypeVisitor; +//use Phan\Language\Element\FunctionInterface; +use Phan\Language\UnionType; +use Phan\Language\Type; +use Phan\PluginV3; +use Phan\PluginV3\PluginAwarePostAnalysisVisitor; +use Phan\PluginV3\PostAnalyzeNodeCapability; +use Phan\Exception\NodeException; +use Phan\Language\FQSEN\FullyQualifiedClassName; +use Phan\Exception\FQSENException; + +/** + * ParamMatchPlugin hooks into one event: + * + * - getPostAnalyzeNodeVisitorClassName + * This method returns a visitor that is called on every AST node from every + * file being analyzed + * + * A plugin file must + * + * - Contain a class that inherits from \Phan\PluginV3 + * + * - End by returning an instance of that class. + * + * It is assumed without being checked that plugins aren't + * mangling state within the passed code base or context. + * + * Note: When adding new plugins, + * add them to the corresponding section of README.md + */ +class ParamMatchPlugin extends PluginV3 implements PostAnalyzeNodeCapability +{ + /** + * @return string - name of PluginAwarePostAnalysisVisitor subclass + */ + public static function getPostAnalyzeNodeVisitorClassName(): string + { + return ParamMatchVisitor::class; + } +} + +/** + * When __invoke on this class is called with a node, a method + * will be dispatched based on the `kind` of the given node. + * + * Visitors such as this are useful for defining lots of different + * checks on a node based on its kind. + */ +class ParamMatchVisitor extends PluginAwarePostAnalysisVisitor +{ + // A plugin's visitors should not override visit() unless they need to. + + /** + * @override + * @param Node $node Node to analyze + * + * @return void + */ + public function visitMethodCall(Node $node): void + { + $method_name = $node->children['method'] ?? null; + if (!\is_string($method_name)) { + return; // Not handled, TODO: handle variable(?) methods + // throw new NodeException($node); + } + try { + // Fetch the list of valid classes, and warn about any undefined classes. + $union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']); + } catch (Exception $_) { + // Phan should already throw for this + return; + } + + $class_list = []; + foreach ($union_type->getTypeSet() as $type) { + $class_fqsen = "NoFSQENType"; + if ($type instanceof Type) { + try { + $class_fqsen = (string) FullyQualifiedClassName::fromFullyQualifiedString($type->getName()); + } catch (FQSENException $_) { + // var_dump([$_, $node]); + continue; + } + } else { + // var_dump( $type) ; + continue; + } + $class_name = (string) $class_fqsen; + $class_list[] = $class_name; + } + + /* May need to check list of classes + */ + + /* + if (!$class->hasMethodWithName($this->code_base, $method_name, true)) { + throw new NodeException($expr, 'does not have method'); + } + $class_name = $class->getName(); + */ + foreach ($class_list as $class_name) { + $this->checkRule($node, "$class_name::$method_name"); + } + } + + /** + * @override + * @param Node $node Node to analyze + * + * @return void + */ + public function visitStaticCall(Node $node): void + { + $class_name = $node->children['class']->children['name'] ?? null; + if (!\is_string($class_name)) { + throw new NodeException($expr, 'does not have class'); + } + try { + $class_name = (string) FullyQualifiedClassName::fromFullyQualifiedString($class_name); + } catch (FQSENException $_) { + } + $method_name = $node->children['method'] ?? null; + + if (!\is_string($method_name)) { + return; + } + $this->checkRule($node, "$class_name::$method_name"); + } + /** + * @override + * + * @param Node $node A node to analyze + * + * @return void + */ + public function visitCall(Node $node): void + { + $name = $node->children['expr']->children['name'] ?? null; + if (!\is_string($name)) { + return; + } + + + $this->checkRule($node, $name); + } + + /** + * + * @param Node $node A node to analyze + * @param string $name function name or fqsn of class:: + * + * @return void + */ + public function checkRule(Node $node, string $name) + { + $rules = Config::getValue('ParamMatchRegexPlugin'); + foreach ($rules as $regex => $rule) { + if (preg_match($regex, $name)) { + $this->checkParam($node, $rule[0], $rule[1], $name, $rule[2] ?? null); + } + } + } + + /** + * Check that argument matches regex at node + * + * @param Node $node Visited node for which to verify arguments match regex + * @param int $argPosition Position of argument to check + * @param string $argRegex Regex to validate against argument + * @param string $functionName Function name for report + * @param string $messageCode Message code to provide in message + * + * @return void + */ + public function checkParam(Node $node, int $argPosition, string $argRegex, $functionName, $messageCode = null): void + { + $args = $node->children['args']->children; + + if (!array_key_exists($argPosition, $args)) { + /* + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'ParamMatchMissingArgument', + "Argument at %s for %s is missing", + [$argPosition, $function_name] + ); + */ + return; + } + $expr = $args[$argPosition]; + try { + $expr_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr, false); + } catch (Exception $_) { + return; + } + + $expr_value = $expr_type->getRealUnionType()->asValueOrNullOrSelf(); + if (!is_object($expr_value)) { + $list = [(string) $expr_value]; + } elseif ($expr_value instanceof UnionType) { + $list = $expr_value->asScalarValues(); + } else { + // Note: maybe more types could be supported + return; + } + + foreach ($list as $argValue) { + if (!\preg_match($argRegex, (string) $argValue)) { + // Emit an issue if the argument does not match the expected regex pattern + // var_dump([$node,$expr_value,$expr_type->getRealUnionType()]); // Information about node + $this->emitPluginIssue( + $this->code_base, + $this->context, + $messageCode ?? 'ParamMatchRegexError', + "Argument {INDEX} function {FUNCTION} can have value {STRING_LITERAL} that does not match the expected pattern '{STRING_LITERAL}'", + [$argPosition, $functionName, json_encode($argValue), $argRegex] + ); + } + } + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new ParamMatchPlugin(); diff --git a/htdocs/core/lib/functions.lib.php b/htdocs/core/lib/functions.lib.php index 42d1f1df716..40e1aa39219 100644 --- a/htdocs/core/lib/functions.lib.php +++ b/htdocs/core/lib/functions.lib.php @@ -932,6 +932,7 @@ function GETPOST($paramname, $check = 'alphanohtml', $method = 0, $filter = null $out = preg_replace('/([<>])([-+]?\d)/', '\1 \2', $out); } + // @phan-suppress-next-line ParamMatchRegexError $out = sanitizeVal($out, $check, $filter, $options); } diff --git a/htdocs/modulebuilder/index.php b/htdocs/modulebuilder/index.php index aa0d49399a4..61c41d0782c 100644 --- a/htdocs/modulebuilder/index.php +++ b/htdocs/modulebuilder/index.php @@ -4186,6 +4186,7 @@ if ($module == 'initmodule') { print ''.img_picto($langs->trans("Delete"), 'delete').''; print $form->textwithpicto('', $langs->trans("InfoForApiFile"), 1, 'warning'); print '   '; + // @phan-suppress-next-line ParamMatchRegexError if (!isModEnabled($modulelowercase)) { // If module is not activated print ''.$langs->trans("ApiExplorer").''; } else {