diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -9,14 +9,20 @@ phutil_register_library_map(array( '__library_version__' => 2, 'class' => array( + 'ArcanistAliasFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAliasFunctionXHPASTLinterRule.php', 'ArcanistAliasWorkflow' => 'workflow/ArcanistAliasWorkflow.php', 'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php', 'ArcanistAnoidWorkflow' => 'workflow/ArcanistAnoidWorkflow.php', + 'ArcanistArrayIndexSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistArrayIndexSpacingXHPASTLinterRule.php', + 'ArcanistArraySeparatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistArraySeparatorXHPASTLinterRule.php', 'ArcanistBackoutWorkflow' => 'workflow/ArcanistBackoutWorkflow.php', 'ArcanistBaseCommitParser' => 'parser/ArcanistBaseCommitParser.php', 'ArcanistBaseCommitParserTestCase' => 'parser/__tests__/ArcanistBaseCommitParserTestCase.php', 'ArcanistBaseXHPASTLinter' => 'lint/linter/ArcanistBaseXHPASTLinter.php', + 'ArcanistBinaryExpressionSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBinaryExpressionSpacingXHPASTLinterRule.php', + 'ArcanistBlacklistedFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBlacklistedFunctionXHPASTLinterRule.php', 'ArcanistBookmarkWorkflow' => 'workflow/ArcanistBookmarkWorkflow.php', + 'ArcanistBraceFormattingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBraceFormattingXHPASTLinterRule.php', 'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php', 'ArcanistBritishTestCase' => 'configuration/__tests__/ArcanistBritishTestCase.php', 'ArcanistBrowseWorkflow' => 'workflow/ArcanistBrowseWorkflow.php', @@ -26,30 +32,42 @@ 'ArcanistCSSLintLinterTestCase' => 'lint/linter/__tests__/ArcanistCSSLintLinterTestCase.php', 'ArcanistCSharpLinter' => 'lint/linter/ArcanistCSharpLinter.php', 'ArcanistCallConduitWorkflow' => 'workflow/ArcanistCallConduitWorkflow.php', + 'ArcanistCallTimePassByReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCallTimePassByReferenceXHPASTLinterRule.php', 'ArcanistCapabilityNotSupportedException' => 'workflow/exception/ArcanistCapabilityNotSupportedException.php', + 'ArcanistCastSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCastSpacingXHPASTLinterRule.php', 'ArcanistCheckstyleXMLLintRenderer' => 'lint/renderer/ArcanistCheckstyleXMLLintRenderer.php', 'ArcanistChmodLinter' => 'lint/linter/ArcanistChmodLinter.php', 'ArcanistChmodLinterTestCase' => 'lint/linter/__tests__/ArcanistChmodLinterTestCase.php', + 'ArcanistClassFilenameMismatchXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClassFilenameMismatchXHPASTLinterRule.php', + 'ArcanistClassNameLiteralXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClassNameLiteralXHPASTLinterRule.php', 'ArcanistCloseRevisionWorkflow' => 'workflow/ArcanistCloseRevisionWorkflow.php', 'ArcanistCloseWorkflow' => 'workflow/ArcanistCloseWorkflow.php', + 'ArcanistClosingCallParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClosingCallParenthesesXHPASTLinterRule.php', + 'ArcanistClosingDeclarationParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClosingDeclarationParenthesesXHPASTLinterRule.php', 'ArcanistClosureLinter' => 'lint/linter/ArcanistClosureLinter.php', 'ArcanistClosureLinterTestCase' => 'lint/linter/__tests__/ArcanistClosureLinterTestCase.php', 'ArcanistCoffeeLintLinter' => 'lint/linter/ArcanistCoffeeLintLinter.php', 'ArcanistCoffeeLintLinterTestCase' => 'lint/linter/__tests__/ArcanistCoffeeLintLinterTestCase.php', 'ArcanistCommentRemover' => 'parser/ArcanistCommentRemover.php', 'ArcanistCommentRemoverTestCase' => 'parser/__tests__/ArcanistCommentRemoverTestCase.php', + 'ArcanistCommentSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCommentSpacingXHPASTLinterRule.php', + 'ArcanistCommentStyleXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCommentStyleXHPASTLinterRule.php', 'ArcanistCommitWorkflow' => 'workflow/ArcanistCommitWorkflow.php', 'ArcanistCompilerLintRenderer' => 'lint/renderer/ArcanistCompilerLintRenderer.php', 'ArcanistComprehensiveLintEngine' => 'lint/engine/ArcanistComprehensiveLintEngine.php', + 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConcatenationOperatorXHPASTLinterRule.php', 'ArcanistConfiguration' => 'configuration/ArcanistConfiguration.php', 'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.php', 'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php', 'ArcanistConsoleLintRenderer' => 'lint/renderer/ArcanistConsoleLintRenderer.php', + 'ArcanistConstructorParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConstructorParenthesesXHPASTLinterRule.php', + 'ArcanistControlStatementSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistControlStatementSpacingXHPASTLinterRule.php', 'ArcanistCoverWorkflow' => 'workflow/ArcanistCoverWorkflow.php', 'ArcanistCppcheckLinter' => 'lint/linter/ArcanistCppcheckLinter.php', 'ArcanistCppcheckLinterTestCase' => 'lint/linter/__tests__/ArcanistCppcheckLinterTestCase.php', 'ArcanistCpplintLinter' => 'lint/linter/ArcanistCpplintLinter.php', 'ArcanistCpplintLinterTestCase' => 'lint/linter/__tests__/ArcanistCpplintLinterTestCase.php', + 'ArcanistDefaultParametersXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDefaultParametersXHPASTLinterRule.php', 'ArcanistDiffChange' => 'parser/diff/ArcanistDiffChange.php', 'ArcanistDiffChangeType' => 'parser/diff/ArcanistDiffChangeType.php', 'ArcanistDiffHunk' => 'parser/diff/ArcanistDiffHunk.php', @@ -63,11 +81,19 @@ 'ArcanistDifferentialDependencyGraph' => 'differential/ArcanistDifferentialDependencyGraph.php', 'ArcanistDifferentialRevisionHash' => 'differential/constants/ArcanistDifferentialRevisionHash.php', 'ArcanistDifferentialRevisionStatus' => 'differential/constants/ArcanistDifferentialRevisionStatus.php', + 'ArcanistDoubleQuoteXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDoubleQuoteXHPASTLinterRule.php', 'ArcanistDownloadWorkflow' => 'workflow/ArcanistDownloadWorkflow.php', + 'ArcanistDuplicateKeysInArrayXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDuplicateKeysInArrayXHPASTLinterRule.php', + 'ArcanistDuplicateSwitchCaseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDuplicateSwitchCaseXHPASTLinterRule.php', + 'ArcanistDynamicDefineXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDynamicDefineXHPASTLinterRule.php', + 'ArcanistElseIfUsageXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistElseIfUsageXHPASTLinterRule.php', + 'ArcanistEmptyStatementXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistEmptyStatementXHPASTLinterRule.php', 'ArcanistEventType' => 'events/constant/ArcanistEventType.php', + 'ArcanistExitExpressionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistExitExpressionXHPASTLinterRule.php', 'ArcanistExportWorkflow' => 'workflow/ArcanistExportWorkflow.php', 'ArcanistExternalLinter' => 'lint/linter/ArcanistExternalLinter.php', 'ArcanistExternalLinterTestCase' => 'lint/linter/__tests__/ArcanistExternalLinterTestCase.php', + 'ArcanistExtractUseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistExtractUseXHPASTLinterRule.php', 'ArcanistFeatureWorkflow' => 'workflow/ArcanistFeatureWorkflow.php', 'ArcanistFileDataRef' => 'upload/ArcanistFileDataRef.php', 'ArcanistFileUploader' => 'upload/ArcanistFileUploader.php', @@ -76,6 +102,7 @@ 'ArcanistFlagWorkflow' => 'workflow/ArcanistFlagWorkflow.php', 'ArcanistFlake8Linter' => 'lint/linter/ArcanistFlake8Linter.php', 'ArcanistFlake8LinterTestCase' => 'lint/linter/__tests__/ArcanistFlake8LinterTestCase.php', + 'ArcanistFormattedStringXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistFormattedStringXHPASTLinterRule.php', 'ArcanistFutureLinter' => 'lint/linter/ArcanistFutureLinter.php', 'ArcanistGeneratedLinter' => 'lint/linter/ArcanistGeneratedLinter.php', 'ArcanistGeneratedLinterTestCase' => 'lint/linter/__tests__/ArcanistGeneratedLinterTestCase.php', @@ -92,7 +119,14 @@ 'ArcanistHgProxyClient' => 'hgdaemon/ArcanistHgProxyClient.php', 'ArcanistHgProxyServer' => 'hgdaemon/ArcanistHgProxyServer.php', 'ArcanistHgServerChannel' => 'hgdaemon/ArcanistHgServerChannel.php', + 'ArcanistImplicitConstructorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistImplicitConstructorXHPASTLinterRule.php', + 'ArcanistImplicitFallthroughXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistImplicitFallthroughXHPASTLinterRule.php', + 'ArcanistImplicitVisibilityXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistImplicitVisibilityXHPASTLinterRule.php', + 'ArcanistInnerFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInnerFunctionXHPASTLinterRule.php', 'ArcanistInstallCertificateWorkflow' => 'workflow/ArcanistInstallCertificateWorkflow.php', + 'ArcanistInstanceOfOperatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInstanceOfOperatorXHPASTLinterRule.php', + 'ArcanistInvalidDefaultParameterXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInvalidDefaultParameterXHPASTLinterRule.php', + 'ArcanistInvalidModifiersXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInvalidModifiersXHPASTLinterRule.php', 'ArcanistJSHintLinter' => 'lint/linter/ArcanistJSHintLinter.php', 'ArcanistJSHintLinterTestCase' => 'lint/linter/__tests__/ArcanistJSHintLinterTestCase.php', 'ArcanistJSONLintLinter' => 'lint/linter/ArcanistJSONLintLinter.php', @@ -102,7 +136,10 @@ 'ArcanistJSONLinterTestCase' => 'lint/linter/__tests__/ArcanistJSONLinterTestCase.php', 'ArcanistJscsLinter' => 'lint/linter/ArcanistJscsLinter.php', 'ArcanistJscsLinterTestCase' => 'lint/linter/__tests__/ArcanistJscsLinterTestCase.php', + 'ArcanistKeywordCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php', + 'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLambdaFuncFunctionXHPASTLinterRule.php', 'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php', + 'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLanguageConstructParenthesesXHPASTLinterRule.php', 'ArcanistLesscLinter' => 'lint/linter/ArcanistLesscLinter.php', 'ArcanistLesscLinterTestCase' => 'lint/linter/__tests__/ArcanistLesscLinterTestCase.php', 'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php', @@ -118,19 +155,30 @@ 'ArcanistLinterTestCase' => 'lint/linter/__tests__/ArcanistLinterTestCase.php', 'ArcanistLintersWorkflow' => 'workflow/ArcanistLintersWorkflow.php', 'ArcanistListWorkflow' => 'workflow/ArcanistListWorkflow.php', + 'ArcanistLogicalOperatorsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLogicalOperatorsXHPASTLinterRule.php', + 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLowercaseFunctionsXHPASTLinterRule.php', 'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php', 'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php', 'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php', 'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php', 'ArcanistMergeConflictLinterTestCase' => 'lint/linter/__tests__/ArcanistMergeConflictLinterTestCase.php', 'ArcanistMissingLinterException' => 'lint/linter/exception/ArcanistMissingLinterException.php', + 'ArcanistModifierOrderingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistModifierOrderingXHPASTLinterRule.php', + 'ArcanistNamingConventionsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistNamingConventionsXHPASTLinterRule.php', 'ArcanistNoEffectException' => 'exception/usage/ArcanistNoEffectException.php', 'ArcanistNoEngineException' => 'exception/usage/ArcanistNoEngineException.php', 'ArcanistNoLintLinter' => 'lint/linter/ArcanistNoLintLinter.php', 'ArcanistNoLintLinterTestCase' => 'lint/linter/__tests__/ArcanistNoLintLinterTestCase.php', + 'ArcanistNoParentScopeXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistNoParentScopeXHPASTLinterRule.php', 'ArcanistNoneLintRenderer' => 'lint/renderer/ArcanistNoneLintRenderer.php', 'ArcanistPEP8Linter' => 'lint/linter/ArcanistPEP8Linter.php', 'ArcanistPEP8LinterTestCase' => 'lint/linter/__tests__/ArcanistPEP8LinterTestCase.php', + 'ArcanistPHPCloseTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPCloseTagXHPASTLinterRule.php', + 'ArcanistPHPCompatibilityXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php', + 'ArcanistPHPEchoTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPEchoTagXHPASTLinterRule.php', + 'ArcanistPHPOpenTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPOpenTagXHPASTLinterRule.php', + 'ArcanistPHPShortTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPShortTagXHPASTLinterRule.php', + 'ArcanistParenthesesSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistParenthesesSpacingXHPASTLinterRule.php', 'ArcanistPasteWorkflow' => 'workflow/ArcanistPasteWorkflow.php', 'ArcanistPatchWorkflow' => 'workflow/ArcanistPatchWorkflow.php', 'ArcanistPhpLinter' => 'lint/linter/ArcanistPhpLinter.php', @@ -142,6 +190,8 @@ 'ArcanistPhutilLibraryLinter' => 'lint/linter/ArcanistPhutilLibraryLinter.php', 'ArcanistPhutilXHPASTLinter' => 'lint/linter/ArcanistPhutilXHPASTLinter.php', 'ArcanistPhutilXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistPhutilXHPASTLinterTestCase.php', + 'ArcanistPlusOperatorOnStringsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPlusOperatorOnStringsXHPASTLinterRule.php', + 'ArcanistPregQuoteMisuseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPregQuoteMisuseXHPASTLinterRule.php', 'ArcanistPuppetLintLinter' => 'lint/linter/ArcanistPuppetLintLinter.php', 'ArcanistPuppetLintLinterTestCase' => 'lint/linter/__tests__/ArcanistPuppetLintLinterTestCase.php', 'ArcanistPyFlakesLinter' => 'lint/linter/ArcanistPyFlakesLinter.php', @@ -151,40 +201,57 @@ 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', 'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php', 'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php', + 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php', + 'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php', + 'ArcanistReusedIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorXHPASTLinterRule.php', 'ArcanistRevertWorkflow' => 'workflow/ArcanistRevertWorkflow.php', 'ArcanistRuboCopLinter' => 'lint/linter/ArcanistRuboCopLinter.php', 'ArcanistRuboCopLinterTestCase' => 'lint/linter/__tests__/ArcanistRuboCopLinterTestCase.php', 'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php', 'ArcanistRubyLinterTestCase' => 'lint/linter/__tests__/ArcanistRubyLinterTestCase.php', 'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php', + 'ArcanistSelfMemberReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSelfMemberReferenceXHPASTLinterRule.php', + 'ArcanistSemicolonSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSemicolonSpacingXHPASTLinterRule.php', 'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php', 'ArcanistSettings' => 'configuration/ArcanistSettings.php', 'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php', 'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php', + 'ArcanistSlownessXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSlownessXHPASTLinterRule.php', 'ArcanistSpellingLinter' => 'lint/linter/ArcanistSpellingLinter.php', 'ArcanistSpellingLinterTestCase' => 'lint/linter/__tests__/ArcanistSpellingLinterTestCase.php', 'ArcanistStartWorkflow' => 'workflow/ArcanistStartWorkflow.php', + 'ArcanistStaticThisXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistStaticThisXHPASTLinterRule.php', 'ArcanistStopWorkflow' => 'workflow/ArcanistStopWorkflow.php', 'ArcanistSubversionAPI' => 'repository/api/ArcanistSubversionAPI.php', 'ArcanistSummaryLintRenderer' => 'lint/renderer/ArcanistSummaryLintRenderer.php', + 'ArcanistSyntaxErrorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSyntaxErrorXHPASTLinterRule.php', 'ArcanistTasksWorkflow' => 'workflow/ArcanistTasksWorkflow.php', + 'ArcanistTautologicalExpressionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistTautologicalExpressionXHPASTLinterRule.php', 'ArcanistTestResultParser' => 'unit/parser/ArcanistTestResultParser.php', 'ArcanistTestXHPASTLintSwitchHook' => 'lint/linter/__tests__/ArcanistTestXHPASTLintSwitchHook.php', 'ArcanistTextLinter' => 'lint/linter/ArcanistTextLinter.php', 'ArcanistTextLinterTestCase' => 'lint/linter/__tests__/ArcanistTextLinterTestCase.php', 'ArcanistTimeWorkflow' => 'workflow/ArcanistTimeWorkflow.php', + 'ArcanistToStringExceptionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistToStringExceptionXHPASTLinterRule.php', + 'ArcanistTodoCommentXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistTodoCommentXHPASTLinterRule.php', 'ArcanistTodoWorkflow' => 'workflow/ArcanistTodoWorkflow.php', 'ArcanistUSEnglishTranslation' => 'internationalization/ArcanistUSEnglishTranslation.php', + 'ArcanistUnableToParseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnableToParseXHPASTLinterRule.php', + 'ArcanistUndeclaredVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php', 'ArcanistUnitConsoleRenderer' => 'unit/renderer/ArcanistUnitConsoleRenderer.php', 'ArcanistUnitRenderer' => 'unit/renderer/ArcanistUnitRenderer.php', 'ArcanistUnitTestEngine' => 'unit/engine/ArcanistUnitTestEngine.php', 'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php', 'ArcanistUnitTestableLintEngine' => 'lint/engine/ArcanistUnitTestableLintEngine.php', 'ArcanistUnitWorkflow' => 'workflow/ArcanistUnitWorkflow.php', + 'ArcanistUnnecessaryFinalModifierXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnnecessaryFinalModifierXHPASTLinterRule.php', + 'ArcanistUnnecessarySemicolonXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnnecessarySemicolonXHPASTLinterRule.php', 'ArcanistUpgradeWorkflow' => 'workflow/ArcanistUpgradeWorkflow.php', 'ArcanistUploadWorkflow' => 'workflow/ArcanistUploadWorkflow.php', 'ArcanistUsageException' => 'exception/ArcanistUsageException.php', + 'ArcanistUselessOverridingMethodXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUselessOverridingMethodXHPASTLinterRule.php', 'ArcanistUserAbortException' => 'exception/usage/ArcanistUserAbortException.php', + 'ArcanistVariableVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistVariableVariableXHPASTLinterRule.php', 'ArcanistVersionWorkflow' => 'workflow/ArcanistVersionWorkflow.php', 'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php', 'ArcanistWorkflow' => 'workflow/ArcanistWorkflow.php', @@ -193,6 +260,7 @@ 'ArcanistXHPASTLintNamingHookTestCase' => 'lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php', 'ArcanistXHPASTLintSwitchHook' => 'lint/linter/xhpast/ArcanistXHPASTLintSwitchHook.php', 'ArcanistXHPASTLinter' => 'lint/linter/ArcanistXHPASTLinter.php', + 'ArcanistXHPASTLinterRule' => 'lint/linter/xhpast/ArcanistXHPASTLinterRule.php', 'ArcanistXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php', 'ArcanistXMLLinter' => 'lint/linter/ArcanistXMLLinter.php', 'ArcanistXMLLinterTestCase' => 'lint/linter/__tests__/ArcanistXMLLinterTestCase.php', @@ -213,13 +281,19 @@ ), 'function' => array(), 'xmap' => array( + 'ArcanistAliasFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAliasWorkflow' => 'ArcanistWorkflow', 'ArcanistAmendWorkflow' => 'ArcanistWorkflow', 'ArcanistAnoidWorkflow' => 'ArcanistWorkflow', + 'ArcanistArrayIndexSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistArraySeparatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBackoutWorkflow' => 'ArcanistWorkflow', 'ArcanistBaseCommitParserTestCase' => 'PhutilTestCase', 'ArcanistBaseXHPASTLinter' => 'ArcanistFutureLinter', + 'ArcanistBinaryExpressionSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistBlacklistedFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBookmarkWorkflow' => 'ArcanistFeatureWorkflow', + 'ArcanistBraceFormattingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBranchWorkflow' => 'ArcanistFeatureWorkflow', 'ArcanistBritishTestCase' => 'PhutilTestCase', 'ArcanistBrowseWorkflow' => 'ArcanistWorkflow', @@ -228,37 +302,57 @@ 'ArcanistCSSLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCSharpLinter' => 'ArcanistLinter', 'ArcanistCallConduitWorkflow' => 'ArcanistWorkflow', + 'ArcanistCallTimePassByReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCapabilityNotSupportedException' => 'Exception', + 'ArcanistCastSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCheckstyleXMLLintRenderer' => 'ArcanistLintRenderer', 'ArcanistChmodLinter' => 'ArcanistLinter', 'ArcanistChmodLinterTestCase' => 'ArcanistLinterTestCase', + 'ArcanistClassFilenameMismatchXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistClassNameLiteralXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCloseRevisionWorkflow' => 'ArcanistWorkflow', 'ArcanistCloseWorkflow' => 'ArcanistWorkflow', + 'ArcanistClosingCallParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistClosingDeclarationParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistClosureLinter' => 'ArcanistExternalLinter', 'ArcanistClosureLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCoffeeLintLinter' => 'ArcanistExternalLinter', 'ArcanistCoffeeLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCommentRemoverTestCase' => 'PhutilTestCase', + 'ArcanistCommentSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistCommentStyleXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCommitWorkflow' => 'ArcanistWorkflow', 'ArcanistCompilerLintRenderer' => 'ArcanistLintRenderer', 'ArcanistComprehensiveLintEngine' => 'ArcanistLintEngine', + 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine', 'ArcanistConsoleLintRenderer' => 'ArcanistLintRenderer', + 'ArcanistConstructorParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistControlStatementSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCoverWorkflow' => 'ArcanistWorkflow', 'ArcanistCppcheckLinter' => 'ArcanistExternalLinter', 'ArcanistCppcheckLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCpplintLinter' => 'ArcanistExternalLinter', 'ArcanistCpplintLinterTestCase' => 'ArcanistExternalLinterTestCase', + 'ArcanistDefaultParametersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDiffParserTestCase' => 'PhutilTestCase', 'ArcanistDiffUtilsTestCase' => 'PhutilTestCase', 'ArcanistDiffWorkflow' => 'ArcanistWorkflow', 'ArcanistDifferentialCommitMessageParserException' => 'Exception', 'ArcanistDifferentialDependencyGraph' => 'AbstractDirectedGraph', + 'ArcanistDoubleQuoteXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDownloadWorkflow' => 'ArcanistWorkflow', + 'ArcanistDuplicateKeysInArrayXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistDuplicateSwitchCaseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistDynamicDefineXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistElseIfUsageXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistEmptyStatementXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistEventType' => 'PhutilEventType', + 'ArcanistExitExpressionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistExportWorkflow' => 'ArcanistWorkflow', 'ArcanistExternalLinter' => 'ArcanistFutureLinter', 'ArcanistExternalLinterTestCase' => 'ArcanistLinterTestCase', + 'ArcanistExtractUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistFeatureWorkflow' => 'ArcanistWorkflow', 'ArcanistFileDataRef' => 'Phobject', 'ArcanistFileUploader' => 'Phobject', @@ -267,6 +361,7 @@ 'ArcanistFlagWorkflow' => 'ArcanistWorkflow', 'ArcanistFlake8Linter' => 'ArcanistExternalLinter', 'ArcanistFlake8LinterTestCase' => 'ArcanistExternalLinterTestCase', + 'ArcanistFormattedStringXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistFutureLinter' => 'ArcanistLinter', 'ArcanistGeneratedLinter' => 'ArcanistLinter', 'ArcanistGeneratedLinterTestCase' => 'ArcanistLinterTestCase', @@ -281,7 +376,14 @@ 'ArcanistHelpWorkflow' => 'ArcanistWorkflow', 'ArcanistHgClientChannel' => 'PhutilProtocolChannel', 'ArcanistHgServerChannel' => 'PhutilProtocolChannel', + 'ArcanistImplicitConstructorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistImplicitFallthroughXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistImplicitVisibilityXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistInnerFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistInstallCertificateWorkflow' => 'ArcanistWorkflow', + 'ArcanistInstanceOfOperatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistInvalidDefaultParameterXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistInvalidModifiersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistJSHintLinter' => 'ArcanistExternalLinter', 'ArcanistJSHintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistJSONLintLinter' => 'ArcanistExternalLinter', @@ -291,7 +393,10 @@ 'ArcanistJSONLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistJscsLinter' => 'ArcanistExternalLinter', 'ArcanistJscsLinterTestCase' => 'ArcanistExternalLinterTestCase', + 'ArcanistKeywordCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLandWorkflow' => 'ArcanistWorkflow', + 'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLesscLinter' => 'ArcanistExternalLinter', 'ArcanistLesscLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistLiberateWorkflow' => 'ArcanistWorkflow', @@ -300,18 +405,29 @@ 'ArcanistLinterTestCase' => 'PhutilTestCase', 'ArcanistLintersWorkflow' => 'ArcanistWorkflow', 'ArcanistListWorkflow' => 'ArcanistWorkflow', + 'ArcanistLogicalOperatorsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI', 'ArcanistMercurialParserTestCase' => 'PhutilTestCase', 'ArcanistMergeConflictLinter' => 'ArcanistLinter', 'ArcanistMergeConflictLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistMissingLinterException' => 'Exception', + 'ArcanistModifierOrderingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistNamingConventionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistNoEffectException' => 'ArcanistUsageException', 'ArcanistNoEngineException' => 'ArcanistUsageException', 'ArcanistNoLintLinter' => 'ArcanistLinter', 'ArcanistNoLintLinterTestCase' => 'ArcanistLinterTestCase', + 'ArcanistNoParentScopeXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistNoneLintRenderer' => 'ArcanistLintRenderer', 'ArcanistPEP8Linter' => 'ArcanistExternalLinter', 'ArcanistPEP8LinterTestCase' => 'ArcanistExternalLinterTestCase', + 'ArcanistPHPCloseTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistPHPCompatibilityXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistPHPEchoTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistPHPOpenTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistPHPShortTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistParenthesesSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPasteWorkflow' => 'ArcanistWorkflow', 'ArcanistPatchWorkflow' => 'ArcanistWorkflow', 'ArcanistPhpLinter' => 'ArcanistExternalLinter', @@ -323,6 +439,8 @@ 'ArcanistPhutilLibraryLinter' => 'ArcanistLinter', 'ArcanistPhutilXHPASTLinter' => 'ArcanistBaseXHPASTLinter', 'ArcanistPhutilXHPASTLinterTestCase' => 'ArcanistLinterTestCase', + 'ArcanistPlusOperatorOnStringsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistPregQuoteMisuseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPuppetLintLinter' => 'ArcanistExternalLinter', 'ArcanistPuppetLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPyFlakesLinter' => 'ArcanistExternalLinter', @@ -331,35 +449,52 @@ 'ArcanistPyLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase', 'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase', + 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistReusedIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistRevertWorkflow' => 'ArcanistWorkflow', 'ArcanistRuboCopLinter' => 'ArcanistExternalLinter', 'ArcanistRuboCopLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRubyLinter' => 'ArcanistExternalLinter', 'ArcanistRubyLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistScriptAndRegexLinter' => 'ArcanistLinter', + 'ArcanistSelfMemberReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistSemicolonSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSetConfigWorkflow' => 'ArcanistWorkflow', 'ArcanistShellCompleteWorkflow' => 'ArcanistWorkflow', 'ArcanistSingleLintEngine' => 'ArcanistLintEngine', + 'ArcanistSlownessXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSpellingLinter' => 'ArcanistLinter', 'ArcanistSpellingLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistStartWorkflow' => 'ArcanistPhrequentWorkflow', + 'ArcanistStaticThisXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistStopWorkflow' => 'ArcanistPhrequentWorkflow', 'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI', 'ArcanistSummaryLintRenderer' => 'ArcanistLintRenderer', + 'ArcanistSyntaxErrorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistTasksWorkflow' => 'ArcanistWorkflow', + 'ArcanistTautologicalExpressionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistTestXHPASTLintSwitchHook' => 'ArcanistXHPASTLintSwitchHook', 'ArcanistTextLinter' => 'ArcanistLinter', 'ArcanistTextLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistTimeWorkflow' => 'ArcanistPhrequentWorkflow', + 'ArcanistToStringExceptionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistTodoCommentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistTodoWorkflow' => 'ArcanistWorkflow', 'ArcanistUSEnglishTranslation' => 'PhutilTranslation', + 'ArcanistUnableToParseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistUndeclaredVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnitConsoleRenderer' => 'ArcanistUnitRenderer', 'ArcanistUnitTestableLintEngine' => 'ArcanistLintEngine', 'ArcanistUnitWorkflow' => 'ArcanistWorkflow', + 'ArcanistUnnecessaryFinalModifierXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistUnnecessarySemicolonXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUpgradeWorkflow' => 'ArcanistWorkflow', 'ArcanistUploadWorkflow' => 'ArcanistWorkflow', 'ArcanistUsageException' => 'Exception', + 'ArcanistUselessOverridingMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUserAbortException' => 'ArcanistUsageException', + 'ArcanistVariableVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistVersionWorkflow' => 'ArcanistWorkflow', 'ArcanistWhichWorkflow' => 'ArcanistWorkflow', 'ArcanistWorkflow' => 'Phobject', diff --git a/src/lint/linter/ArcanistBaseXHPASTLinter.php b/src/lint/linter/ArcanistBaseXHPASTLinter.php --- a/src/lint/linter/ArcanistBaseXHPASTLinter.php +++ b/src/lint/linter/ArcanistBaseXHPASTLinter.php @@ -201,7 +201,7 @@ } -/* -( Utility )------------------------------------------------------------ */ +/* -( Deprecated )--------------------------------------------------------- */ /** * Retrieve all calls to some specified function(s). diff --git a/src/lint/linter/ArcanistXHPASTLinter.php b/src/lint/linter/ArcanistXHPASTLinter.php --- a/src/lint/linter/ArcanistXHPASTLinter.php +++ b/src/lint/linter/ArcanistXHPASTLinter.php @@ -5,80 +5,20 @@ */ final class ArcanistXHPASTLinter extends ArcanistBaseXHPASTLinter { - const LINT_PHP_SYNTAX_ERROR = 1; - const LINT_UNABLE_TO_PARSE = 2; - const LINT_VARIABLE_VARIABLE = 3; - const LINT_EXTRACT_USE = 4; - const LINT_UNDECLARED_VARIABLE = 5; - const LINT_PHP_SHORT_TAG = 6; - const LINT_PHP_ECHO_TAG = 7; - const LINT_PHP_CLOSE_TAG = 8; - const LINT_NAMING_CONVENTIONS = 9; - const LINT_IMPLICIT_CONSTRUCTOR = 10; - const LINT_DYNAMIC_DEFINE = 12; - const LINT_STATIC_THIS = 13; - const LINT_PREG_QUOTE_MISUSE = 14; - const LINT_PHP_OPEN_TAG = 15; - const LINT_TODO_COMMENT = 16; - const LINT_EXIT_EXPRESSION = 17; - const LINT_COMMENT_STYLE = 18; - const LINT_CLASS_FILENAME_MISMATCH = 19; - const LINT_TAUTOLOGICAL_EXPRESSION = 20; - const LINT_PLUS_OPERATOR_ON_STRINGS = 21; - const LINT_DUPLICATE_KEYS_IN_ARRAY = 22; - const LINT_REUSED_ITERATORS = 23; - const LINT_BRACE_FORMATTING = 24; - const LINT_PARENTHESES_SPACING = 25; - const LINT_CONTROL_STATEMENT_SPACING = 26; - const LINT_BINARY_EXPRESSION_SPACING = 27; - const LINT_ARRAY_INDEX_SPACING = 28; - const LINT_IMPLICIT_FALLTHROUGH = 30; - const LINT_REUSED_AS_ITERATOR = 32; - const LINT_COMMENT_SPACING = 34; - const LINT_SLOWNESS = 36; - const LINT_CLOSING_CALL_PAREN = 37; - const LINT_CLOSING_DECL_PAREN = 38; - const LINT_REUSED_ITERATOR_REFERENCE = 39; - const LINT_KEYWORD_CASING = 40; - const LINT_DOUBLE_QUOTE = 41; - const LINT_ELSEIF_USAGE = 42; - const LINT_SEMICOLON_SPACING = 43; - const LINT_CONCATENATION_OPERATOR = 44; - const LINT_PHP_COMPATIBILITY = 45; - const LINT_LANGUAGE_CONSTRUCT_PAREN = 46; - const LINT_EMPTY_STATEMENT = 47; - const LINT_ARRAY_SEPARATOR = 48; - const LINT_CONSTRUCTOR_PARENTHESES = 49; - const LINT_DUPLICATE_SWITCH_CASE = 50; - const LINT_BLACKLISTED_FUNCTION = 51; - const LINT_IMPLICIT_VISIBILITY = 52; - const LINT_CALL_TIME_PASS_BY_REF = 53; - const LINT_FORMATTED_STRING = 54; - const LINT_UNNECESSARY_FINAL_MODIFIER = 55; - const LINT_UNNECESSARY_SEMICOLON = 56; - const LINT_SELF_MEMBER_REFERENCE = 57; - const LINT_LOGICAL_OPERATORS = 58; - const LINT_INNER_FUNCTION = 59; - const LINT_DEFAULT_PARAMETERS = 60; - const LINT_LOWERCASE_FUNCTIONS = 61; - const LINT_CLASS_NAME_LITERAL = 62; - const LINT_USELESS_OVERRIDING_METHOD = 63; - const LINT_NO_PARENT_SCOPE = 64; - const LINT_ALIAS_FUNCTION = 65; - const LINT_CAST_SPACING = 66; - const LINT_TOSTRING_EXCEPTION = 67; - const LINT_LAMBDA_FUNC_FUNCTION = 68; - const LINT_INSTANCEOF_OPERATOR = 69; - const LINT_INVALID_DEFAULT_PARAMETER = 70; - const LINT_MODIFIER_ORDERING = 71; - const LINT_INVALID_MODIFIERS = 72; + private $rules = array(); - private $blacklistedFunctions = array(); - private $naminghook; - private $printfFunctions = array(); - private $switchhook; - private $version; - private $windowsVersion; + public function __construct() { + $this->rules = $this->getLinterRules(); + } + + public function __clone() { + $rules = $this->rules; + + $this->rules = array(); + foreach ($rules as $rule) { + $this->rules[] = clone $rule; + } + } public function getInfoName() { return pht('XHPAST Lint'); @@ -88,145 +28,6 @@ return pht('Use XHPAST to enforce coding conventions on PHP source files.'); } - public function getLintNameMap() { - return array( - self::LINT_PHP_SYNTAX_ERROR - => pht('PHP Syntax Error!'), - self::LINT_UNABLE_TO_PARSE - => pht('Unable to Parse'), - self::LINT_VARIABLE_VARIABLE - => pht('Use of Variable Variable'), - self::LINT_EXTRACT_USE - => pht('Use of %s', 'extract()'), - self::LINT_UNDECLARED_VARIABLE - => pht('Use of Undeclared Variable'), - self::LINT_PHP_SHORT_TAG - => pht('Use of Short Tag "%s"', ' pht('Use of Echo Tag "%s"', ' pht('Use of Close Tag "%s"', '?>'), - self::LINT_NAMING_CONVENTIONS - => pht('Naming Conventions'), - self::LINT_IMPLICIT_CONSTRUCTOR - => pht('Implicit Constructor'), - self::LINT_DYNAMIC_DEFINE - => pht('Dynamic %s', 'define()'), - self::LINT_STATIC_THIS - => pht('Use of %s in Static Context', '$this'), - self::LINT_PREG_QUOTE_MISUSE - => pht('Misuse of %s', 'preg_quote()'), - self::LINT_PHP_OPEN_TAG - => pht('Expected Open Tag'), - self::LINT_TODO_COMMENT - => pht('TODO Comment'), - self::LINT_EXIT_EXPRESSION - => pht('Exit Used as Expression'), - self::LINT_COMMENT_STYLE - => pht('Comment Style'), - self::LINT_CLASS_FILENAME_MISMATCH - => pht('Class-Filename Mismatch'), - self::LINT_TAUTOLOGICAL_EXPRESSION - => pht('Tautological Expression'), - self::LINT_PLUS_OPERATOR_ON_STRINGS - => pht('Not String Concatenation'), - self::LINT_DUPLICATE_KEYS_IN_ARRAY - => pht('Duplicate Keys in Array'), - self::LINT_REUSED_ITERATORS - => pht('Reuse of Iterator Variable'), - self::LINT_BRACE_FORMATTING - => pht('Brace Placement'), - self::LINT_PARENTHESES_SPACING - => pht('Spaces Inside Parentheses'), - self::LINT_CONTROL_STATEMENT_SPACING - => pht('Space After Control Statement'), - self::LINT_BINARY_EXPRESSION_SPACING - => pht('Space Around Binary Operator'), - self::LINT_ARRAY_INDEX_SPACING - => pht('Spacing Before Array Index'), - self::LINT_IMPLICIT_FALLTHROUGH - => pht('Implicit Fallthrough'), - self::LINT_REUSED_AS_ITERATOR - => pht('Variable Reused As Iterator'), - self::LINT_COMMENT_SPACING - => pht('Comment Spaces'), - self::LINT_SLOWNESS - => pht('Slow Construct'), - self::LINT_CLOSING_CALL_PAREN - => pht('Call Formatting'), - self::LINT_CLOSING_DECL_PAREN - => pht('Declaration Formatting'), - self::LINT_REUSED_ITERATOR_REFERENCE - => pht('Reuse of Iterator References'), - self::LINT_KEYWORD_CASING - => pht('Keyword Conventions'), - self::LINT_DOUBLE_QUOTE - => pht('Unnecessary Double Quotes'), - self::LINT_ELSEIF_USAGE - => pht('ElseIf Usage'), - self::LINT_SEMICOLON_SPACING - => pht('Semicolon Spacing'), - self::LINT_CONCATENATION_OPERATOR - => pht('Concatenation Spacing'), - self::LINT_PHP_COMPATIBILITY - => pht('PHP Compatibility'), - self::LINT_LANGUAGE_CONSTRUCT_PAREN - => pht('Language Construct Parentheses'), - self::LINT_EMPTY_STATEMENT - => pht('Empty Block Statement'), - self::LINT_ARRAY_SEPARATOR - => pht('Array Separator'), - self::LINT_CONSTRUCTOR_PARENTHESES - => pht('Constructor Parentheses'), - self::LINT_DUPLICATE_SWITCH_CASE - => pht('Duplicate Case Statements'), - self::LINT_BLACKLISTED_FUNCTION - => pht('Use of Blacklisted Function'), - self::LINT_IMPLICIT_VISIBILITY - => pht('Implicit Method Visibility'), - self::LINT_CALL_TIME_PASS_BY_REF - => pht('Call-Time Pass-By-Reference'), - self::LINT_FORMATTED_STRING - => pht('Formatted String'), - self::LINT_UNNECESSARY_FINAL_MODIFIER - => pht('Unnecessary Final Modifier'), - self::LINT_UNNECESSARY_SEMICOLON - => pht('Unnecessary Semicolon'), - self::LINT_SELF_MEMBER_REFERENCE - => pht('Self Member Reference'), - self::LINT_LOGICAL_OPERATORS - => pht('Logical Operators'), - self::LINT_INNER_FUNCTION - => pht('Inner Functions'), - self::LINT_DEFAULT_PARAMETERS - => pht('Default Parameters'), - self::LINT_LOWERCASE_FUNCTIONS - => pht('Lowercase Functions'), - self::LINT_CLASS_NAME_LITERAL - => pht('Class Name Literal'), - self::LINT_USELESS_OVERRIDING_METHOD - => pht('Useless Overriding Method'), - self::LINT_NO_PARENT_SCOPE - => pht('No Parent Scope'), - self::LINT_ALIAS_FUNCTION - => pht('Alias Functions'), - self::LINT_CAST_SPACING - => pht('Cast Spacing'), - self::LINT_TOSTRING_EXCEPTION - => pht('Throwing Exception in %s Method', '__toString'), - self::LINT_LAMBDA_FUNC_FUNCTION - => pht('%s Function', '__lambda_func'), - self::LINT_INSTANCEOF_OPERATOR - => pht('%s Operator', 'instanceof'), - self::LINT_INVALID_DEFAULT_PARAMETER - => pht('Invalid Default Parameter'), - self::LINT_MODIFIER_ORDERING - => pht('Modifier Ordering'), - self::LINT_INVALID_MODIFIERS - => pht('Invalid Modifiers'), - ); - } - public function getLinterName() { return 'XHP'; } @@ -235,117 +36,60 @@ return 'xhpast'; } - public function getLintSeverityMap() { - $disabled = ArcanistLintSeverity::SEVERITY_DISABLED; - $advice = ArcanistLintSeverity::SEVERITY_ADVICE; - $warning = ArcanistLintSeverity::SEVERITY_WARNING; + public function getLintNameMap() { + return mpull($this->rules, 'getLintName', 'getLintID'); + } - return array( - self::LINT_TODO_COMMENT => $disabled, - self::LINT_UNABLE_TO_PARSE => $warning, - self::LINT_NAMING_CONVENTIONS => $warning, - self::LINT_PREG_QUOTE_MISUSE => $advice, - self::LINT_BRACE_FORMATTING => $warning, - self::LINT_PARENTHESES_SPACING => $warning, - self::LINT_CONTROL_STATEMENT_SPACING => $warning, - self::LINT_BINARY_EXPRESSION_SPACING => $warning, - self::LINT_ARRAY_INDEX_SPACING => $warning, - self::LINT_IMPLICIT_FALLTHROUGH => $warning, - self::LINT_SLOWNESS => $warning, - self::LINT_COMMENT_SPACING => $advice, - self::LINT_CLOSING_CALL_PAREN => $warning, - self::LINT_CLOSING_DECL_PAREN => $warning, - self::LINT_REUSED_ITERATOR_REFERENCE => $warning, - self::LINT_KEYWORD_CASING => $warning, - self::LINT_DOUBLE_QUOTE => $advice, - self::LINT_ELSEIF_USAGE => $advice, - self::LINT_SEMICOLON_SPACING => $advice, - self::LINT_CONCATENATION_OPERATOR => $warning, - self::LINT_LANGUAGE_CONSTRUCT_PAREN => $warning, - self::LINT_EMPTY_STATEMENT => $advice, - self::LINT_ARRAY_SEPARATOR => $advice, - self::LINT_CONSTRUCTOR_PARENTHESES => $advice, - self::LINT_IMPLICIT_VISIBILITY => $advice, - self::LINT_UNNECESSARY_FINAL_MODIFIER => $advice, - self::LINT_UNNECESSARY_SEMICOLON => $advice, - self::LINT_SELF_MEMBER_REFERENCE => $advice, - self::LINT_LOGICAL_OPERATORS => $advice, - self::LINT_INNER_FUNCTION => $warning, - self::LINT_DEFAULT_PARAMETERS => $warning, - self::LINT_LOWERCASE_FUNCTIONS => $advice, - self::LINT_CLASS_NAME_LITERAL => $advice, - self::LINT_USELESS_OVERRIDING_METHOD => $advice, - self::LINT_ALIAS_FUNCTION => $advice, - self::LINT_CAST_SPACING => $advice, - self::LINT_MODIFIER_ORDERING => $advice, - ); + public function getLintSeverityMap() { + return mpull($this->rules, 'getLintSeverity', 'getLintID'); } public function getLinterConfigurationOptions() { - return parent::getLinterConfigurationOptions() + array( - 'xhpast.blacklisted.function' => array( - 'type' => 'optional map', - 'help' => pht('Blacklisted functions which should not be used.'), - ), - 'xhpast.naminghook' => array( - 'type' => 'optional string', - 'help' => pht( - 'Name of a concrete subclass of ArcanistXHPASTLintNamingHook which '. - 'enforces more granular naming convention rules for symbols.'), - ), - 'xhpast.printf-functions' => array( - 'type' => 'optional map', - 'help' => pht( - '%s-style functions which take a format string and list of values '. - 'as arguments. The value for the mapping is the start index of the '. - 'function parameters (the index of the format string parameter).', - 'printf()'), - ), - 'xhpast.switchhook' => array( - 'type' => 'optional string', - 'help' => pht( - 'Name of a concrete subclass of ArcanistXHPASTLintSwitchHook which '. - 'tunes the analysis of switch() statements for this linter.'), - ), - 'xhpast.php-version' => array( - 'type' => 'optional string', - 'help' => pht('PHP version to target.'), - ), - 'xhpast.php-version.windows' => array( - 'type' => 'optional string', - 'help' => pht('PHP version to target on Windows.'), - ), - ); + return parent::getLinterConfigurationOptions() + array_mergev( + mpull($this->rules, 'getLinterConfigurationOptions')); } public function setLinterConfigurationValue($key, $value) { - switch ($key) { - case 'xhpast.blacklisted.function': - $this->blacklistedFunctions = $value; - return; - case 'xhpast.naminghook': - $this->naminghook = $value; - return; - case 'xhpast.printf-functions': - $this->printfFunctions = $value; - return; - case 'xhpast.switchhook': - $this->switchhook = $value; - return; - case 'xhpast.php-version': - $this->version = $value; - return; - case 'xhpast.php-version.windows': - $this->windowsVersion = $value; - return; + foreach ($this->rules as $rule) { + foreach ($rule->getLinterConfigurationOptions() as $k => $spec) { + if ($k == $key) { + return $rule->setLinterConfigurationValue($key, $value); + } + } } return parent::setLinterConfigurationValue($key, $value); } public function getVersion() { - // The version number should be incremented whenever a new rule is added. - return '34'; + // TODO: Improve this. + return count($this->rules); + } + + public function getLinterRules() { + $rules = array(); + + $symbols = id(new PhutilSymbolLoader()) + ->setAncestorClass('ArcanistXHPASTLinterRule') + ->loadObjects(); + + foreach ($symbols as $class => $rule) { + $id = $rule->getLintID(); + + if (isset($rules[$id])) { + throw new Exception( + pht( + 'Two linter rules (`%s`, `%s`) share the same lint ID (%d). '. + 'Each linter rule must have a unique ID.', + $class, + get_class($rules[$id]), + $id)); + } + + $rules[$id] = $rule; + } + + return $rules; } protected function resolveFuture($path, Future $future) { @@ -356,4119 +100,24 @@ $this->raiseLintAtLine( $ex->getErrorLine(), 1, - self::LINT_PHP_SYNTAX_ERROR, + ArcanistSyntaxErrorXHPASTLinterRule::ID, pht( 'This file contains a syntax error: %s', $ex->getMessage())); } else if ($ex instanceof Exception) { - $this->raiseLintAtPath(self::LINT_UNABLE_TO_PARSE, $ex->getMessage()); + $this->raiseLintAtPath( + ArcanistUnableToParseXHPASTLinterRule::ID, + $ex->getMessage()); } return; } $root = $tree->getRootNode(); - $method_codes = array( - 'lintStrstrUsedForCheck' => self::LINT_SLOWNESS, - 'lintStrposUsedForStart' => self::LINT_SLOWNESS, - 'lintImplicitFallthrough' => self::LINT_IMPLICIT_FALLTHROUGH, - 'lintBraceFormatting' => self::LINT_BRACE_FORMATTING, - 'lintTautologicalExpressions' => self::LINT_TAUTOLOGICAL_EXPRESSION, - 'lintCommentSpaces' => self::LINT_COMMENT_SPACING, - 'lintHashComments' => self::LINT_COMMENT_STYLE, - 'lintReusedIterators' => self::LINT_REUSED_ITERATORS, - 'lintReusedIteratorReferences' => self::LINT_REUSED_ITERATOR_REFERENCE, - 'lintVariableVariables' => self::LINT_VARIABLE_VARIABLE, - 'lintUndeclaredVariables' => array( - self::LINT_EXTRACT_USE, - self::LINT_REUSED_AS_ITERATOR, - self::LINT_UNDECLARED_VARIABLE, - ), - 'lintPHPTagUse' => array( - self::LINT_PHP_SHORT_TAG, - self::LINT_PHP_ECHO_TAG, - self::LINT_PHP_OPEN_TAG, - self::LINT_PHP_CLOSE_TAG, - ), - 'lintNamingConventions' => self::LINT_NAMING_CONVENTIONS, - 'lintSurpriseConstructors' => self::LINT_IMPLICIT_CONSTRUCTOR, - 'lintParenthesesShouldHugExpressions' => self::LINT_PARENTHESES_SPACING, - 'lintSpaceAfterControlStatementKeywords' => - self::LINT_CONTROL_STATEMENT_SPACING, - 'lintSpaceAroundBinaryOperators' => self::LINT_BINARY_EXPRESSION_SPACING, - 'lintDynamicDefines' => self::LINT_DYNAMIC_DEFINE, - 'lintUseOfThisInStaticMethods' => self::LINT_STATIC_THIS, - 'lintPregQuote' => self::LINT_PREG_QUOTE_MISUSE, - 'lintExitExpressions' => self::LINT_EXIT_EXPRESSION, - 'lintArrayIndexWhitespace' => self::LINT_ARRAY_INDEX_SPACING, - 'lintTodoComments' => self::LINT_TODO_COMMENT, - 'lintPrimaryDeclarationFilenameMatch' => - self::LINT_CLASS_FILENAME_MISMATCH, - 'lintPlusOperatorOnStrings' => self::LINT_PLUS_OPERATOR_ON_STRINGS, - 'lintDuplicateKeysInArray' => self::LINT_DUPLICATE_KEYS_IN_ARRAY, - 'lintClosingCallParen' => self::LINT_CLOSING_CALL_PAREN, - 'lintClosingDeclarationParen' => self::LINT_CLOSING_DECL_PAREN, - 'lintKeywordCasing' => self::LINT_KEYWORD_CASING, - 'lintStrings' => self::LINT_DOUBLE_QUOTE, - 'lintElseIfStatements' => self::LINT_ELSEIF_USAGE, - 'lintSemicolons' => self::LINT_SEMICOLON_SPACING, - 'lintSpaceAroundConcatenationOperators' => - self::LINT_CONCATENATION_OPERATOR, - 'lintPHPCompatibility' => self::LINT_PHP_COMPATIBILITY, - 'lintLanguageConstructParentheses' => self::LINT_LANGUAGE_CONSTRUCT_PAREN, - 'lintEmptyBlockStatements' => self::LINT_EMPTY_STATEMENT, - 'lintArraySeparator' => self::LINT_ARRAY_SEPARATOR, - 'lintConstructorParentheses' => self::LINT_CONSTRUCTOR_PARENTHESES, - 'lintSwitchStatements' => self::LINT_DUPLICATE_SWITCH_CASE, - 'lintBlacklistedFunction' => self::LINT_BLACKLISTED_FUNCTION, - 'lintMethodVisibility' => self::LINT_IMPLICIT_VISIBILITY, - 'lintPropertyVisibility' => self::LINT_IMPLICIT_VISIBILITY, - 'lintCallTimePassByReference' => self::LINT_CALL_TIME_PASS_BY_REF, - 'lintFormattedString' => self::LINT_FORMATTED_STRING, - 'lintUnnecessaryFinalModifier' => self::LINT_UNNECESSARY_FINAL_MODIFIER, - 'lintUnnecessarySemicolons' => self::LINT_UNNECESSARY_SEMICOLON, - 'lintConstantDefinitions' => self::LINT_NAMING_CONVENTIONS, - 'lintSelfMemberReference' => self::LINT_SELF_MEMBER_REFERENCE, - 'lintLogicalOperators' => self::LINT_LOGICAL_OPERATORS, - 'lintInnerFunctions' => self::LINT_INNER_FUNCTION, - 'lintDefaultParameters' => self::LINT_DEFAULT_PARAMETERS, - 'lintLowercaseFunctions' => self::LINT_LOWERCASE_FUNCTIONS, - 'lintClassNameLiteral' => self::LINT_CLASS_NAME_LITERAL, - 'lintUselessOverridingMethods' => self::LINT_USELESS_OVERRIDING_METHOD, - 'lintNoParentScope' => self::LINT_NO_PARENT_SCOPE, - 'lintAliasFunctions' => self::LINT_ALIAS_FUNCTION, - 'lintCastSpacing' => self::LINT_CAST_SPACING, - 'lintThrowExceptionInToStringMethod' => self::LINT_TOSTRING_EXCEPTION, - 'lintLambdaFuncFunction' => self::LINT_LAMBDA_FUNC_FUNCTION, - 'lintInstanceOfOperator' => self::LINT_INSTANCEOF_OPERATOR, - 'lintInvalidDefaultParameters' => self::LINT_INVALID_DEFAULT_PARAMETER, - 'lintMethodModifierOrdering' => self::LINT_MODIFIER_ORDERING, - 'lintPropertyModifierOrdering' => self::LINT_MODIFIER_ORDERING, - 'lintInvalidModifiers' => self::LINT_INVALID_MODIFIERS, - ); - - foreach ($method_codes as $method => $codes) { - foreach ((array)$codes as $code) { - if ($this->isCodeEnabled($code)) { - call_user_func(array($this, $method), $root); - break; - } - } - } - } - - private function lintStrstrUsedForCheck(XHPASTNode $root) { - $expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); - foreach ($expressions as $expression) { - $operator = $expression->getChildOfType(1, 'n_OPERATOR'); - $operator = $operator->getConcreteString(); - - if ($operator !== '===' && $operator !== '!==') { - continue; - } - - $false = $expression->getChildByIndex(0); - if ($false->getTypeName() === 'n_SYMBOL_NAME' && - $false->getConcreteString() === 'false') { - $strstr = $expression->getChildByIndex(2); - } else { - $strstr = $false; - $false = $expression->getChildByIndex(2); - if ($false->getTypeName() !== 'n_SYMBOL_NAME' || - $false->getConcreteString() !== 'false') { - continue; - } - } - - if ($strstr->getTypeName() !== 'n_FUNCTION_CALL') { - continue; - } - - $name = strtolower($strstr->getChildByIndex(0)->getConcreteString()); - if ($name === 'strstr' || $name === 'strchr') { - $this->raiseLintAtNode( - $strstr, - self::LINT_SLOWNESS, - pht( - 'Use %s for checking if the string contains something.', - 'strpos()')); - } else if ($name === 'stristr') { - $this->raiseLintAtNode( - $strstr, - self::LINT_SLOWNESS, - pht( - 'Use %s for checking if the string contains something.', - 'stripos()')); - } - } - } - - private function lintStrposUsedForStart(XHPASTNode $root) { - $expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); - foreach ($expressions as $expression) { - $operator = $expression->getChildOfType(1, 'n_OPERATOR'); - $operator = $operator->getConcreteString(); - - if ($operator !== '===' && $operator !== '!==') { - continue; - } - - $zero = $expression->getChildByIndex(0); - if ($zero->getTypeName() === 'n_NUMERIC_SCALAR' && - $zero->getConcreteString() === '0') { - $strpos = $expression->getChildByIndex(2); - } else { - $strpos = $zero; - $zero = $expression->getChildByIndex(2); - if ($zero->getTypeName() !== 'n_NUMERIC_SCALAR' || - $zero->getConcreteString() !== '0') { - continue; - } - } - - if ($strpos->getTypeName() !== 'n_FUNCTION_CALL') { - continue; - } - - $name = strtolower($strpos->getChildByIndex(0)->getConcreteString()); - if ($name === 'strpos') { - $this->raiseLintAtNode( - $strpos, - self::LINT_SLOWNESS, - pht( - 'Use %s for checking if the string starts with something.', - 'strncmp()')); - } else if ($name === 'stripos') { - $this->raiseLintAtNode( - $strpos, - self::LINT_SLOWNESS, - pht( - 'Use %s for checking if the string starts with something.', - 'strncasecmp()')); - } - } - } - - private function lintPHPCompatibility(XHPASTNode $root) { - static $compat_info; - - if (!$this->version) { - return; - } - - if ($compat_info === null) { - $target = phutil_get_library_root('phutil'). - '/../resources/php_compat_info.json'; - $compat_info = phutil_json_decode(Filesystem::readFile($target)); - } - - // Create a whitelist for symbols which are being used conditionally. - $whitelist = array( - 'class' => array(), - 'function' => array(), - ); - - $conditionals = $root->selectDescendantsOfType('n_IF'); - foreach ($conditionals as $conditional) { - $condition = $conditional->getChildOfType(0, 'n_CONTROL_CONDITION'); - $function = $condition->getChildByIndex(0); - - if ($function->getTypeName() != 'n_FUNCTION_CALL') { - continue; - } - - $function_token = $function - ->getChildByIndex(0); - - if ($function_token->getTypeName() != 'n_SYMBOL_NAME') { - // This may be `Class::method(...)` or `$var(...)`. - continue; - } - - $function_name = $function_token->getConcreteString(); - - switch ($function_name) { - case 'class_exists': - case 'function_exists': - case 'interface_exists': - $type = null; - switch ($function_name) { - case 'class_exists': - $type = 'class'; - break; - - case 'function_exists': - $type = 'function'; - break; - - case 'interface_exists': - $type = 'interface'; - break; - } - - $params = $function->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); - $symbol = $params->getChildByIndex(0); - - if (!$symbol->isStaticScalar()) { - continue; - } - - $symbol_name = $symbol->evalStatic(); - if (!idx($whitelist[$type], $symbol_name)) { - $whitelist[$type][$symbol_name] = array(); - } - - $span = $conditional - ->getChildByIndex(1) - ->getTokens(); - - $whitelist[$type][$symbol_name][] = range( - head_key($span), - last_key($span)); - break; - } - } - - $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); - foreach ($calls as $call) { - $node = $call->getChildByIndex(0); - $name = $node->getConcreteString(); - - $version = idx($compat_info['functions'], $name, array()); - $min = idx($version, 'php.min'); - $max = idx($version, 'php.max'); - - // Check if whitelisted. - $whitelisted = false; - foreach (idx($whitelist['function'], $name, array()) as $range) { - if (array_intersect($range, array_keys($node->getTokens()))) { - $whitelisted = true; - break; - } - } - - if ($whitelisted) { - continue; - } - - if ($min && version_compare($min, $this->version, '>')) { - $this->raiseLintAtNode( - $node, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but `%s()` was not '. - 'introduced until PHP %s.', - $this->version, - $name, - $min)); - } else if ($max && version_compare($max, $this->version, '<')) { - $this->raiseLintAtNode( - $node, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but `%s()` was '. - 'removed in PHP %s.', - $this->version, - $name, - $max)); - } else if (array_key_exists($name, $compat_info['params'])) { - $params = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); - foreach (array_values($params->getChildren()) as $i => $param) { - $version = idx($compat_info['params'][$name], $i); - if ($version && version_compare($version, $this->version, '>')) { - $this->raiseLintAtNode( - $param, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but parameter %d '. - 'of `%s()` was not introduced until PHP %s.', - $this->version, - $i + 1, - $name, - $version)); - } - } - } - - if ($this->windowsVersion) { - $windows = idx($compat_info['functions_windows'], $name); - - if ($windows === false) { - $this->raiseLintAtNode( - $node, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s on Windows, '. - 'but `%s()` is not available there.', - $this->windowsVersion, - $name)); - } else if (version_compare($windows, $this->windowsVersion, '>')) { - $this->raiseLintAtNode( - $node, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s on Windows, '. - 'but `%s()` is not available there until PHP %s.', - $this->windowsVersion, - $name, - $windows)); - } - } - } - - $classes = $root->selectDescendantsOfType('n_CLASS_NAME'); - foreach ($classes as $node) { - $name = $node->getConcreteString(); - $version = idx($compat_info['interfaces'], $name, array()); - $version = idx($compat_info['classes'], $name, $version); - $min = idx($version, 'php.min'); - $max = idx($version, 'php.max'); - // Check if whitelisted. - $whitelisted = false; - foreach (idx($whitelist['class'], $name, array()) as $range) { - if (array_intersect($range, array_keys($node->getTokens()))) { - $whitelisted = true; - break; - } - } - - if ($whitelisted) { - continue; - } - - if ($min && version_compare($min, $this->version, '>')) { - $this->raiseLintAtNode( - $node, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but `%s` was not '. - 'introduced until PHP %s.', - $this->version, - $name, - $min)); - } else if ($max && version_compare($max, $this->version, '<')) { - $this->raiseLintAtNode( - $node, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but `%s` was '. - 'removed in PHP %s.', - $this->version, - $name, - $max)); - } - } - - // TODO: Technically, this will include function names. This is unlikely to - // cause any issues (unless, of course, there existed a function that had - // the same name as some constant). - $constants = $root->selectDescendantsOfTypes(array( - 'n_SYMBOL_NAME', - 'n_MAGIC_SCALAR', - )); - foreach ($constants as $node) { - $name = $node->getConcreteString(); - $version = idx($compat_info['constants'], $name, array()); - $min = idx($version, 'php.min'); - $max = idx($version, 'php.max'); - - if ($min && version_compare($min, $this->version, '>')) { - $this->raiseLintAtNode( - $node, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but `%s` was not '. - 'introduced until PHP %s.', - $this->version, - $name, - $min)); - } else if ($max && version_compare($max, $this->version, '<')) { - $this->raiseLintAtNode( - $node, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but `%s` was '. - 'removed in PHP %s.', - $this->version, - $name, - $max)); - } - } - - if (version_compare($this->version, '5.3.0') < 0) { - $this->lintPHP53Features($root); - } else { - $this->lintPHP53Incompatibilities($root); - } - - if (version_compare($this->version, '5.4.0') < 0) { - $this->lintPHP54Features($root); - } else { - $this->lintPHP54Incompatibilities($root); - } - } - - private function lintPHP53Features(XHPASTNode $root) { - $functions = $root->selectTokensOfType('T_FUNCTION'); - foreach ($functions as $function) { - $next = $function->getNextToken(); - while ($next) { - if ($next->isSemantic()) { - break; - } - $next = $next->getNextToken(); - } - - if ($next) { - if ($next->getTypeName() === '(') { - $this->raiseLintAtToken( - $function, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but anonymous '. - 'functions were not introduced until PHP 5.3.', - $this->version)); - } - } - } - - $namespaces = $root->selectTokensOfType('T_NAMESPACE'); - foreach ($namespaces as $namespace) { - $this->raiseLintAtToken( - $namespace, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but namespaces were not '. - 'introduced until PHP 5.3.', - $this->version)); - } - - // NOTE: This is only "use x;", in anonymous functions the node type is - // n_LEXICAL_VARIABLE_LIST even though both tokens are T_USE. - - // TODO: We parse n_USE in a slightly crazy way right now; that would be - // a better selector once it's fixed. - - $uses = $root->selectDescendantsOfType('n_USE_LIST'); - foreach ($uses as $use) { - $this->raiseLintAtNode( - $use, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but namespaces were not '. - 'introduced until PHP 5.3.', - $this->version)); - } - - $statics = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); - foreach ($statics as $static) { - $name = $static->getChildByIndex(0); - if ($name->getTypeName() != 'n_CLASS_NAME') { - continue; - } - if ($name->getConcreteString() === 'static') { - $this->raiseLintAtNode( - $name, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but `static::` was not '. - 'introduced until PHP 5.3.', - $this->version)); - } - } - - $ternaries = $root->selectDescendantsOfType('n_TERNARY_EXPRESSION'); - foreach ($ternaries as $ternary) { - $yes = $ternary->getChildByIndex(1); - if ($yes->getTypeName() === 'n_EMPTY') { - $this->raiseLintAtNode( - $ternary, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but short ternary was '. - 'not introduced until PHP 5.3.', - $this->version)); - } - } - - $heredocs = $root->selectDescendantsOfType('n_HEREDOC'); - foreach ($heredocs as $heredoc) { - if (preg_match('/^<<<[\'"]/', $heredoc->getConcreteString())) { - $this->raiseLintAtNode( - $heredoc, - self::LINT_PHP_COMPATIBILITY, - pht( - 'This codebase targets PHP %s, but nowdoc was not '. - 'introduced until PHP 5.3.', - $this->version)); - } - } - } - - private function lintPHP53Incompatibilities(XHPASTNode $root) {} - - private function lintPHP54Features(XHPASTNode $root) { - $indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS'); - foreach ($indexes as $index) { - switch ($index->getChildByIndex(0)->getTypeName()) { - case 'n_FUNCTION_CALL': - case 'n_METHOD_CALL': - $this->raiseLintAtNode( - $index->getChildByIndex(1), - self::LINT_PHP_COMPATIBILITY, - pht( - 'The `%s` syntax was not introduced until PHP 5.4, but this '. - 'codebase targets an earlier version of PHP. You can rewrite '. - 'this expression using `%s`.', - 'f()[...]', - 'idx()')); - break; - } - } - } - - private function lintPHP54Incompatibilities(XHPASTNode $root) { - $breaks = $root->selectDescendantsOfTypes(array('n_BREAK', 'n_CONTINUE')); - foreach ($breaks as $break) { - $arg = $break->getChildByIndex(0); - - switch ($arg->getTypeName()) { - case 'n_EMPTY': - break; - - case 'n_NUMERIC_SCALAR': - if ($arg->getConcreteString() != '0') { - break; - } - - default: - $this->raiseLintAtNode( - $break->getChildByIndex(0), - self::LINT_PHP_COMPATIBILITY, - pht( - 'The `%s` and `%s` statements no longer accept '. - 'variable arguments.', - 'break', - 'continue')); - break; - } - } - } - - private function lintImplicitFallthrough(XHPASTNode $root) { - $hook_obj = null; - - $hook_class = $this->switchhook; - if ($hook_class) { - $hook_obj = newv($hook_class, array()); - assert_instances_of(array($hook_obj), 'ArcanistXHPASTLintSwitchHook'); - } - - $switches = $root->selectDescendantsOfType('n_SWITCH'); - foreach ($switches as $switch) { - $blocks = array(); - - $cases = $switch->selectDescendantsOfType('n_CASE'); - foreach ($cases as $case) { - $blocks[] = $case; - } - - $defaults = $switch->selectDescendantsOfType('n_DEFAULT'); - foreach ($defaults as $default) { - $blocks[] = $default; - } - - - foreach ($blocks as $key => $block) { - // Collect all the tokens in this block which aren't at top level. - // We want to ignore "break", and "continue" in these blocks. - $lower_level = $block->selectDescendantsOfTypes(array( - 'n_WHILE', - 'n_DO_WHILE', - 'n_FOR', - 'n_FOREACH', - 'n_SWITCH', - )); - $lower_level_tokens = array(); - foreach ($lower_level as $lower_level_block) { - $lower_level_tokens += $lower_level_block->getTokens(); - } - - // Collect all the tokens in this block which aren't in this scope - // (because they're inside class, function or interface declarations). - // We want to ignore all of these tokens. - $decls = $block->selectDescendantsOfTypes(array( - 'n_FUNCTION_DECLARATION', - 'n_CLASS_DECLARATION', - - // For completeness; these can't actually have anything. - 'n_INTERFACE_DECLARATION', - )); - - $different_scope_tokens = array(); - foreach ($decls as $decl) { - $different_scope_tokens += $decl->getTokens(); - } - - $lower_level_tokens += $different_scope_tokens; - - // Get all the trailing nonsemantic tokens, since we need to look for - // "fallthrough" comments past the end of the semantic block. - - $tokens = $block->getTokens(); - $last = end($tokens); - while ($last && $last = $last->getNextToken()) { - if ($last->isSemantic()) { - break; - } - $tokens[$last->getTokenID()] = $last; - } - - $blocks[$key] = array( - $tokens, - $lower_level_tokens, - $different_scope_tokens, - ); - } - - foreach ($blocks as $token_lists) { - list( - $tokens, - $lower_level_tokens, - $different_scope_tokens) = $token_lists; - - // Test each block (case or default statement) to see if it's OK. It's - // OK if: - // - // - it is empty; or - // - it ends in break, return, throw, continue or exit at top level; or - // - it has a comment with "fallthrough" in its text. - - // Empty blocks are OK, so we start this at `true` and only set it to - // false if we find a statement. - $block_ok = true; - - // Keeps track of whether the current statement is one that validates - // the block (break, return, throw, continue) or something else. - $statement_ok = false; - - foreach ($tokens as $token_id => $token) { - if (!$token->isSemantic()) { - // Liberally match "fall" in the comment text so that comments like - // "fallthru", "fall through", "fallthrough", etc., are accepted. - if (preg_match('/fall/i', $token->getValue())) { - $block_ok = true; - break; - } - continue; - } - - $tok_type = $token->getTypeName(); - - if ($tok_type === 'T_FUNCTION' || - $tok_type === 'T_CLASS' || - $tok_type === 'T_INTERFACE') { - // These aren't statements, but mark the block as nonempty anyway. - $block_ok = false; - continue; - } - - if ($tok_type === ';') { - if ($statement_ok) { - $statment_ok = false; - } else { - $block_ok = false; - } - continue; - } - - if ($tok_type === 'T_BREAK' || $tok_type === 'T_CONTINUE') { - if (empty($lower_level_tokens[$token_id])) { - $statement_ok = true; - $block_ok = true; - } - continue; - } - - if ($tok_type === 'T_RETURN' || - $tok_type === 'T_THROW' || - $tok_type === 'T_EXIT' || - ($hook_obj && $hook_obj->checkSwitchToken($token))) { - if (empty($different_scope_tokens[$token_id])) { - $statement_ok = true; - $block_ok = true; - } - continue; - } - } - - if (!$block_ok) { - $this->raiseLintAtToken( - head($tokens), - self::LINT_IMPLICIT_FALLTHROUGH, - pht( - "This '%s' or '%s' has a nonempty block which does not end ". - "with '%s', '%s', '%s', '%s' or '%s'. Did you forget to add ". - "one of those? If you intend to fall through, add a '%s' ". - "comment to silence this warning.", - 'case', - 'default', - 'break', - 'continue', - 'return', - 'throw', - 'exit', - '// fallthrough')); - } - } - } - } - - private function lintBraceFormatting(XHPASTNode $root) { - foreach ($root->selectDescendantsOfType('n_STATEMENT_LIST') as $list) { - $tokens = $list->getTokens(); - if (!$tokens || head($tokens)->getValue() != '{') { - continue; - } - list($before, $after) = $list->getSurroundingNonsemanticTokens(); - if (!$before) { - $first = head($tokens); - - // Only insert the space if we're after a closing parenthesis. If - // we're in a construct like "else{}", other rules will insert space - // after the 'else' correctly. - $prev = $first->getPrevToken(); - if (!$prev || $prev->getValue() !== ')') { - continue; - } - - $this->raiseLintAtToken( - $first, - self::LINT_BRACE_FORMATTING, - pht( - 'Put opening braces on the same line as control statements and '. - 'declarations, with a single space before them.'), - ' '.$first->getValue()); - } else if (count($before) === 1) { - $before = reset($before); - if ($before->getValue() !== ' ') { - $this->raiseLintAtToken( - $before, - self::LINT_BRACE_FORMATTING, - pht( - 'Put opening braces on the same line as control statements and '. - 'declarations, with a single space before them.'), - ' '); - } - } - } - - $nodes = $root->selectDescendantsOfType('n_STATEMENT'); - foreach ($nodes as $node) { - $parent = $node->getParentNode(); - - if (!$parent) { - continue; - } - - $type = $parent->getTypeName(); - if ($type != 'n_STATEMENT_LIST' && $type != 'n_DECLARE') { - $this->raiseLintAtNode( - $node, - self::LINT_BRACE_FORMATTING, - pht('Use braces to surround a statement block.')); - } - } - - $nodes = $root->selectDescendantsOfTypes(array( - 'n_DO_WHILE', - 'n_ELSE', - 'n_ELSEIF', - )); - foreach ($nodes as $list) { - $tokens = $list->getTokens(); - if (!$tokens || last($tokens)->getValue() != '}') { - continue; - } - list($before, $after) = $list->getSurroundingNonsemanticTokens(); - if (!$before) { - $first = last($tokens); - - $this->raiseLintAtToken( - $first, - self::LINT_BRACE_FORMATTING, - pht( - 'Put opening braces on the same line as control statements and '. - 'declarations, with a single space before them.'), - ' '.$first->getValue()); - } else if (count($before) === 1) { - $before = reset($before); - if ($before->getValue() !== ' ') { - $this->raiseLintAtToken( - $before, - self::LINT_BRACE_FORMATTING, - pht( - 'Put opening braces on the same line as control statements and '. - 'declarations, with a single space before them.'), - ' '); - } - } - } - } - - private function lintTautologicalExpressions(XHPASTNode $root) { - $expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); - - static $operators = array( - '-' => true, - '/' => true, - '-=' => true, - '/=' => true, - '<=' => true, - '<' => true, - '==' => true, - '===' => true, - '!=' => true, - '!==' => true, - '>=' => true, - '>' => true, - ); - - static $logical = array( - '||' => true, - '&&' => true, - ); - - foreach ($expressions as $expr) { - $operator = $expr->getChildByIndex(1)->getConcreteString(); - if (!empty($operators[$operator])) { - $left = $expr->getChildByIndex(0)->getSemanticString(); - $right = $expr->getChildByIndex(2)->getSemanticString(); - - if ($left === $right) { - $this->raiseLintAtNode( - $expr, - self::LINT_TAUTOLOGICAL_EXPRESSION, - pht( - 'Both sides of this expression are identical, so it always '. - 'evaluates to a constant.')); - } - } - - if (!empty($logical[$operator])) { - $left = $expr->getChildByIndex(0)->getSemanticString(); - $right = $expr->getChildByIndex(2)->getSemanticString(); - - // NOTE: These will be null to indicate "could not evaluate". - $left = $this->evaluateStaticBoolean($left); - $right = $this->evaluateStaticBoolean($right); - - if (($operator === '||' && ($left === true || $right === true)) || - ($operator === '&&' && ($left === false || $right === false))) { - $this->raiseLintAtNode( - $expr, - self::LINT_TAUTOLOGICAL_EXPRESSION, - pht( - 'The logical value of this expression is static. '. - 'Did you forget to remove some debugging code?')); - } - } - } - } - - /** - * Statically evaluate a boolean value from an XHP tree. - * - * TODO: Improve this and move it to XHPAST proper? - * - * @param string The "semantic string" of a single value. - * @return mixed ##true## or ##false## if the value could be evaluated - * statically; ##null## if static evaluation was not possible. - */ - private function evaluateStaticBoolean($string) { - switch (strtolower($string)) { - case '0': - case 'null': - case 'false': - return false; - case '1': - case 'true': - return true; - } - return null; - } - - protected function lintCommentSpaces(XHPASTNode $root) { - foreach ($root->selectTokensOfType('T_COMMENT') as $comment) { - $value = $comment->getValue(); - if ($value[0] !== '#') { - $match = null; - if (preg_match('@^(/[/*]+)[^/*\s]@', $value, $match)) { - $this->raiseLintAtOffset( - $comment->getOffset(), - self::LINT_COMMENT_SPACING, - pht('Put space after comment start.'), - $match[1], - $match[1].' '); - } - } - } - } - - protected function lintHashComments(XHPASTNode $root) { - foreach ($root->selectTokensOfType('T_COMMENT') as $comment) { - $value = $comment->getValue(); - if ($value[0] !== '#') { - continue; - } - - $this->raiseLintAtOffset( - $comment->getOffset(), - self::LINT_COMMENT_STYLE, - pht('Use "%s" single-line comments, not "%s".', '//', '#'), - '#', - (preg_match('/^#\S/', $value) ? '// ' : '//')); - } - } - - /** - * Find cases where loops get nested inside each other but use the same - * iterator variable. For example: - * - * COUNTEREXAMPLE - * foreach ($list as $thing) { - * foreach ($stuff as $thing) { // <-- Raises an error for reuse of $thing - * // ... - * } - * } - * - */ - private function lintReusedIterators(XHPASTNode $root) { - $used_vars = array(); - - $for_loops = $root->selectDescendantsOfType('n_FOR'); - foreach ($for_loops as $for_loop) { - $var_map = array(); - - // Find all the variables that are assigned to in the for() expression. - $for_expr = $for_loop->getChildOfType(0, 'n_FOR_EXPRESSION'); - $bin_exprs = $for_expr->selectDescendantsOfType('n_BINARY_EXPRESSION'); - foreach ($bin_exprs as $bin_expr) { - if ($bin_expr->getChildByIndex(1)->getConcreteString() === '=') { - $var = $bin_expr->getChildByIndex(0); - $var_map[$var->getConcreteString()] = $var; - } - } - - $used_vars[$for_loop->getID()] = $var_map; - } - - $foreach_loops = $root->selectDescendantsOfType('n_FOREACH'); - foreach ($foreach_loops as $foreach_loop) { - $var_map = array(); - - $foreach_expr = $foreach_loop->getChildOfType(0, 'n_FOREACH_EXPRESSION'); - - // We might use one or two vars, i.e. "foreach ($x as $y => $z)" or - // "foreach ($x as $y)". - $possible_used_vars = array( - $foreach_expr->getChildByIndex(1), - $foreach_expr->getChildByIndex(2), - ); - foreach ($possible_used_vars as $var) { - if ($var->getTypeName() === 'n_EMPTY') { - continue; - } - $name = $var->getConcreteString(); - $name = trim($name, '&'); // Get rid of ref silliness. - $var_map[$name] = $var; - } - - $used_vars[$foreach_loop->getID()] = $var_map; - } - - $all_loops = $for_loops->add($foreach_loops); - foreach ($all_loops as $loop) { - $child_loops = $loop->selectDescendantsOfTypes(array( - 'n_FOR', - 'n_FOREACH', - )); - - $outer_vars = $used_vars[$loop->getID()]; - foreach ($child_loops as $inner_loop) { - $inner_vars = $used_vars[$inner_loop->getID()]; - $shared = array_intersect_key($outer_vars, $inner_vars); - if ($shared) { - $shared_desc = implode(', ', array_keys($shared)); - $message = $this->raiseLintAtNode( - $inner_loop->getChildByIndex(0), - self::LINT_REUSED_ITERATORS, - pht( - 'This loop reuses iterator variables (%s) from an '. - 'outer loop. You might be clobbering the outer iterator. '. - 'Change the inner loop to use a different iterator name.', - $shared_desc)); - - $locations = array(); - foreach ($shared as $var) { - $locations[] = $this->getOtherLocation($var->getOffset()); - } - $message->setOtherLocations($locations); - } - } - } - } - - /** - * Find cases where a foreach loop is being iterated using a variable - * reference and the same variable is used outside of the loop without - * calling unset() or reassigning the variable to another variable - * reference. - * - * COUNTEREXAMPLE - * foreach ($ar as &$a) { - * // ... - * } - * $a = 1; // <-- Raises an error for using $a - * - */ - protected function lintReusedIteratorReferences(XHPASTNode $root) { - $defs = $root->selectDescendantsOfTypes(array( - 'n_FUNCTION_DECLARATION', - 'n_METHOD_DECLARATION', - )); - - foreach ($defs as $def) { - - $body = $def->getChildByIndex(5); - if ($body->getTypeName() === 'n_EMPTY') { - // Abstract method declaration. - continue; - } - - $exclude = array(); - - // Exclude uses of variables, unsets, and foreach loops - // within closures - they are checked on their own - $func_defs = $body->selectDescendantsOfType('n_FUNCTION_DECLARATION'); - foreach ($func_defs as $func_def) { - $vars = $func_def->selectDescendantsOfType('n_VARIABLE'); - foreach ($vars as $var) { - $exclude[$var->getID()] = true; - } - - $unset_lists = $func_def->selectDescendantsOfType('n_UNSET_LIST'); - foreach ($unset_lists as $unset_list) { - $exclude[$unset_list->getID()] = true; - } - - $foreaches = $func_def->selectDescendantsOfType('n_FOREACH'); - foreach ($foreaches as $foreach) { - $exclude[$foreach->getID()] = true; - } - } - - // Find all variables that are unset within the scope - $unset_vars = array(); - $unset_lists = $body->selectDescendantsOfType('n_UNSET_LIST'); - foreach ($unset_lists as $unset_list) { - if (isset($exclude[$unset_list->getID()])) { - continue; - } - - $unset_list_vars = $unset_list->selectDescendantsOfType('n_VARIABLE'); - foreach ($unset_list_vars as $var) { - $concrete = $this->getConcreteVariableString($var); - $unset_vars[$concrete][] = $var->getOffset(); - $exclude[$var->getID()] = true; - } - } - - // Find all reference variables in foreach expressions - $reference_vars = array(); - $foreaches = $body->selectDescendantsOfType('n_FOREACH'); - foreach ($foreaches as $foreach) { - if (isset($exclude[$foreach->getID()])) { - continue; - } - - $foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION'); - $var = $foreach_expr->getChildByIndex(2); - if ($var->getTypeName() !== 'n_VARIABLE_REFERENCE') { - continue; - } - - $reference = $var->getChildByIndex(0); - if ($reference->getTypeName() !== 'n_VARIABLE') { - continue; - } - - $reference_name = $this->getConcreteVariableString($reference); - $reference_vars[$reference_name][] = $reference->getOffset(); - $exclude[$reference->getID()] = true; - - // Exclude uses of the reference variable within the foreach loop - $foreach_vars = $foreach->selectDescendantsOfType('n_VARIABLE'); - foreach ($foreach_vars as $var) { - $name = $this->getConcreteVariableString($var); - if ($name === $reference_name) { - $exclude[$var->getID()] = true; - } - } - } - - // Allow usage if the reference variable is assigned to another - // reference variable - $binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION'); - foreach ($binary as $expr) { - if ($expr->getChildByIndex(1)->getConcreteString() !== '=') { - continue; - } - $lval = $expr->getChildByIndex(0); - if ($lval->getTypeName() !== 'n_VARIABLE') { - continue; - } - $rval = $expr->getChildByIndex(2); - if ($rval->getTypeName() !== 'n_VARIABLE_REFERENCE') { - continue; - } - - // Counts as unsetting a variable - $concrete = $this->getConcreteVariableString($lval); - $unset_vars[$concrete][] = $lval->getOffset(); - $exclude[$lval->getID()] = true; - } - - $all_vars = array(); - $all = $body->selectDescendantsOfType('n_VARIABLE'); - foreach ($all as $var) { - if (isset($exclude[$var->getID()])) { - continue; - } - - $name = $this->getConcreteVariableString($var); - - if (!isset($reference_vars[$name])) { - continue; - } - - // Find the closest reference offset to this variable - $reference_offset = null; - foreach ($reference_vars[$name] as $offset) { - if ($offset < $var->getOffset()) { - $reference_offset = $offset; - } else { - break; - } - } - if (!$reference_offset) { - continue; - } - - // Check if an unset exists between reference and usage of this - // variable - $warn = true; - if (isset($unset_vars[$name])) { - foreach ($unset_vars[$name] as $unset_offset) { - if ($unset_offset > $reference_offset && - $unset_offset < $var->getOffset()) { - $warn = false; - break; - } - } - } - if ($warn) { - $this->raiseLintAtNode( - $var, - self::LINT_REUSED_ITERATOR_REFERENCE, - pht( - 'This variable was used already as a by-reference iterator '. - 'variable. Such variables survive outside the foreach loop, '. - 'do not reuse.')); - } - } - - } - } - - protected function lintVariableVariables(XHPASTNode $root) { - $vvars = $root->selectDescendantsOfType('n_VARIABLE_VARIABLE'); - foreach ($vvars as $vvar) { - $this->raiseLintAtNode( - $vvar, - self::LINT_VARIABLE_VARIABLE, - pht( - 'Rewrite this code to use an array. Variable variables are unclear '. - 'and hinder static analysis.')); - } - } - - private function lintUndeclaredVariables(XHPASTNode $root) { - // These things declare variables in a function: - // Explicit parameters - // Assignment - // Assignment via list() - // Static - // Global - // Lexical vars - // Builtins ($this) - // foreach() - // catch - // - // These things make lexical scope unknowable: - // Use of extract() - // Assignment to variable variables ($$x) - // Global with variable variables - // - // These things don't count as "using" a variable: - // isset() - // empty() - // Static class variables - // - // The general approach here is to find each function/method declaration, - // then: - // - // 1. Identify all the variable declarations, and where they first occur - // in the function/method declaration. - // 2. Identify all the uses that don't really count (as above). - // 3. Everything else must be a use of a variable. - // 4. For each variable, check if any uses occur before the declaration - // and warn about them. - // - // We also keep track of where lexical scope becomes unknowable (e.g., - // because the function calls extract() or uses dynamic variables, - // preventing us from keeping track of which variables are defined) so we - // can stop issuing warnings after that. - // - // TODO: Support functions defined inside other functions which is commonly - // used with anonymous functions. - - $defs = $root->selectDescendantsOfTypes(array( - 'n_FUNCTION_DECLARATION', - 'n_METHOD_DECLARATION', - )); - - foreach ($defs as $def) { - - // We keep track of the first offset where scope becomes unknowable, and - // silence any warnings after that. Default it to INT_MAX so we can min() - // it later to keep track of the first problem we encounter. - $scope_destroyed_at = PHP_INT_MAX; - - $declarations = array( - '$this' => 0, - ) + array_fill_keys($this->getSuperGlobalNames(), 0); - $declaration_tokens = array(); - $exclude_tokens = array(); - $vars = array(); - - // First up, find all the different kinds of declarations, as explained - // above. Put the tokens into the $vars array. - - $param_list = $def->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST'); - $param_vars = $param_list->selectDescendantsOfType('n_VARIABLE'); - foreach ($param_vars as $var) { - $vars[] = $var; - } - - // This is PHP5.3 closure syntax: function () use ($x) {}; - $lexical_vars = $def - ->getChildByIndex(4) - ->selectDescendantsOfType('n_VARIABLE'); - foreach ($lexical_vars as $var) { - $vars[] = $var; - } - - $body = $def->getChildByIndex(5); - if ($body->getTypeName() === 'n_EMPTY') { - // Abstract method declaration. - continue; - } - - $static_vars = $body - ->selectDescendantsOfType('n_STATIC_DECLARATION') - ->selectDescendantsOfType('n_VARIABLE'); - foreach ($static_vars as $var) { - $vars[] = $var; - } - - - $global_vars = $body - ->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST'); - foreach ($global_vars as $var_list) { - foreach ($var_list->getChildren() as $var) { - if ($var->getTypeName() === 'n_VARIABLE') { - $vars[] = $var; - } else { - // Dynamic global variable, i.e. "global $$x;". - $scope_destroyed_at = min($scope_destroyed_at, $var->getOffset()); - // An error is raised elsewhere, no need to raise here. - } - } - } - - // Include "catch (Exception $ex)", but not variables in the body of the - // catch block. - $catches = $body->selectDescendantsOfType('n_CATCH'); - foreach ($catches as $catch) { - $vars[] = $catch->getChildOfType(1, 'n_VARIABLE'); - } - - $binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION'); - foreach ($binary as $expr) { - if ($expr->getChildByIndex(1)->getConcreteString() !== '=') { - continue; - } - $lval = $expr->getChildByIndex(0); - if ($lval->getTypeName() === 'n_VARIABLE') { - $vars[] = $lval; - } else if ($lval->getTypeName() === 'n_LIST') { - // Recursivey grab everything out of list(), since the grammar - // permits list() to be nested. Also note that list() is ONLY valid - // as an lval assignments, so we could safely lift this out of the - // n_BINARY_EXPRESSION branch. - $assign_vars = $lval->selectDescendantsOfType('n_VARIABLE'); - foreach ($assign_vars as $var) { - $vars[] = $var; - } - } - - if ($lval->getTypeName() === 'n_VARIABLE_VARIABLE') { - $scope_destroyed_at = min($scope_destroyed_at, $lval->getOffset()); - // No need to raise here since we raise an error elsewhere. - } - } - - $calls = $body->selectDescendantsOfType('n_FUNCTION_CALL'); - foreach ($calls as $call) { - $name = strtolower($call->getChildByIndex(0)->getConcreteString()); - - if ($name === 'empty' || $name === 'isset') { - $params = $call - ->getChildOfType(1, 'n_CALL_PARAMETER_LIST') - ->selectDescendantsOfType('n_VARIABLE'); - foreach ($params as $var) { - $exclude_tokens[$var->getID()] = true; - } - continue; - } - if ($name !== 'extract') { - continue; - } - $scope_destroyed_at = min($scope_destroyed_at, $call->getOffset()); - $this->raiseLintAtNode( - $call, - self::LINT_EXTRACT_USE, - pht( - 'Avoid %s. It is confusing and hinders static analysis.', - 'extract()')); - } - - // Now we have every declaration except foreach(), handled below. Build - // two maps, one which just keeps track of which tokens are part of - // declarations ($declaration_tokens) and one which has the first offset - // where a variable is declared ($declarations). - - foreach ($vars as $var) { - $concrete = $this->getConcreteVariableString($var); - $declarations[$concrete] = min( - idx($declarations, $concrete, PHP_INT_MAX), - $var->getOffset()); - $declaration_tokens[$var->getID()] = true; - } - - // Excluded tokens are ones we don't "count" as being used, described - // above. Put them into $exclude_tokens. - - $class_statics = $body - ->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); - $class_static_vars = $class_statics - ->selectDescendantsOfType('n_VARIABLE'); - foreach ($class_static_vars as $var) { - $exclude_tokens[$var->getID()] = true; - } - - - // Find all the variables in scope, and figure out where they are used. - // We want to find foreach() iterators which are both declared before and - // used after the foreach() loop. - - $uses = array(); - - $all_vars = $body->selectDescendantsOfType('n_VARIABLE'); - $all = array(); - - // NOTE: $all_vars is not a real array so we can't unset() it. - foreach ($all_vars as $var) { - - // Be strict since it's easier; we don't let you reuse an iterator you - // declared before a loop after the loop, even if you're just assigning - // to it. - - $concrete = $this->getConcreteVariableString($var); - $uses[$concrete][$var->getID()] = $var->getOffset(); - - if (isset($declaration_tokens[$var->getID()])) { - // We know this is part of a declaration, so it's fine. - continue; - } - if (isset($exclude_tokens[$var->getID()])) { - // We know this is part of isset() or similar, so it's fine. - continue; - } - - $all[$var->getOffset()] = $concrete; - } - - - // Do foreach() last, we want to handle implicit redeclaration of a - // variable already in scope since this probably means we're ovewriting a - // local. - - // NOTE: Processing foreach expressions in order allows programs which - // reuse iterator variables in other foreach() loops -- this is fine. We - // have a separate warning to prevent nested loops from reusing the same - // iterators. - - $foreaches = $body->selectDescendantsOfType('n_FOREACH'); - $all_foreach_vars = array(); - foreach ($foreaches as $foreach) { - $foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION'); - - $foreach_vars = array(); - - // Determine the end of the foreach() loop. - $foreach_tokens = $foreach->getTokens(); - $last_token = end($foreach_tokens); - $foreach_end = $last_token->getOffset(); - - $key_var = $foreach_expr->getChildByIndex(1); - if ($key_var->getTypeName() === 'n_VARIABLE') { - $foreach_vars[] = $key_var; - } - - $value_var = $foreach_expr->getChildByIndex(2); - if ($value_var->getTypeName() === 'n_VARIABLE') { - $foreach_vars[] = $value_var; - } else { - // The root-level token may be a reference, as in: - // foreach ($a as $b => &$c) { ... } - // Reach into the n_VARIABLE_REFERENCE node to grab the n_VARIABLE - // node. - $var = $value_var->getChildByIndex(0); - if ($var->getTypeName() === 'n_VARIABLE_VARIABLE') { - $var = $var->getChildByIndex(0); - } - $foreach_vars[] = $var; - } - - // Remove all uses of the iterators inside of the foreach() loop from - // the $uses map. - - foreach ($foreach_vars as $var) { - $concrete = $this->getConcreteVariableString($var); - $offset = $var->getOffset(); - - foreach ($uses[$concrete] as $id => $use_offset) { - if (($use_offset >= $offset) && ($use_offset < $foreach_end)) { - unset($uses[$concrete][$id]); - } - } - - $all_foreach_vars[] = $var; - } - } - - foreach ($all_foreach_vars as $var) { - $concrete = $this->getConcreteVariableString($var); - $offset = $var->getOffset(); - - // If a variable was declared before a foreach() and is used after - // it, raise a message. - - if (isset($declarations[$concrete])) { - if ($declarations[$concrete] < $offset) { - if (!empty($uses[$concrete]) && - max($uses[$concrete]) > $offset) { - $message = $this->raiseLintAtNode( - $var, - self::LINT_REUSED_AS_ITERATOR, - pht( - 'This iterator variable is a previously declared local '. - 'variable. To avoid overwriting locals, do not reuse them '. - 'as iterator variables.')); - $message->setOtherLocations(array( - $this->getOtherLocation($declarations[$concrete]), - $this->getOtherLocation(max($uses[$concrete])), - )); - } - } - } - - // This is a declaration, exclude it from the "declare variables prior - // to use" check below. - unset($all[$var->getOffset()]); - - $vars[] = $var; - } - - // Now rebuild declarations to include foreach(). - - foreach ($vars as $var) { - $concrete = $this->getConcreteVariableString($var); - $declarations[$concrete] = min( - idx($declarations, $concrete, PHP_INT_MAX), - $var->getOffset()); - $declaration_tokens[$var->getID()] = true; - } - - foreach (array('n_STRING_SCALAR', 'n_HEREDOC') as $type) { - foreach ($body->selectDescendantsOfType($type) as $string) { - foreach ($string->getStringVariables() as $offset => $var) { - $all[$string->getOffset() + $offset - 1] = '$'.$var; - } - } - } - - // Issue a warning for every variable token, unless it appears in a - // declaration, we know about a prior declaration, we have explicitly - // exlcuded it, or scope has been made unknowable before it appears. - - $issued_warnings = array(); - foreach ($all as $offset => $concrete) { - if ($offset >= $scope_destroyed_at) { - // This appears after an extract() or $$var so we have no idea - // whether it's legitimate or not. We raised a harshly-worded warning - // when scope was made unknowable, so just ignore anything we can't - // figure out. - continue; - } - if ($offset >= idx($declarations, $concrete, PHP_INT_MAX)) { - // The use appears after the variable is declared, so it's fine. - continue; - } - if (!empty($issued_warnings[$concrete])) { - // We've already issued a warning for this variable so we don't need - // to issue another one. - continue; - } - $this->raiseLintAtOffset( - $offset, - self::LINT_UNDECLARED_VARIABLE, - pht( - 'Declare variables prior to use (even if you are passing them '. - 'as reference parameters). You may have misspelled this '. - 'variable name.'), - $concrete); - $issued_warnings[$concrete] = true; - } - } - } - - private function getConcreteVariableString(XHPASTNode $var) { - $concrete = $var->getConcreteString(); - // Strip off curly braces as in $obj->{$property}. - $concrete = trim($concrete, '{}'); - return $concrete; - } - - private function lintPHPTagUse(XHPASTNode $root) { - $tokens = $root->getTokens(); - foreach ($tokens as $token) { - if ($token->getTypeName() === 'T_OPEN_TAG') { - if (trim($token->getValue()) === 'raiseLintAtToken( - $token, - self::LINT_PHP_SHORT_TAG, - pht( - 'Use the full form of the PHP open tag, "%s".', - 'getTypeName() === 'T_OPEN_TAG_WITH_ECHO') { - $this->raiseLintAtToken( - $token, - self::LINT_PHP_ECHO_TAG, - pht('Avoid the PHP echo short form, "%s".', 'getValue())) { - $this->raiseLintAtToken( - $token, - self::LINT_PHP_OPEN_TAG, - pht( - 'PHP files should start with "%s", which may be preceded by '. - 'a "%s" line for scripts.', - 'selectTokensOfType('T_CLOSE_TAG') as $token) { - $this->raiseLintAtToken( - $token, - self::LINT_PHP_CLOSE_TAG, - pht('Do not use the PHP closing tag, "%s".', '?>')); - } - } - - private function lintNamingConventions(XHPASTNode $root) { - // We're going to build up a list of tuples - // and then try to instantiate a hook class which has the opportunity to - // override us. - $names = array(); - - $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); - foreach ($classes as $class) { - $name_token = $class->getChildByIndex(1); - $name_string = $name_token->getConcreteString(); - - $names[] = array( - 'class', - $name_string, - $name_token, - ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string) - ? null - : pht( - 'Follow naming conventions: classes should be named using '. - 'UpperCamelCase.'), - ); - } - - $ifaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); - foreach ($ifaces as $iface) { - $name_token = $iface->getChildByIndex(1); - $name_string = $name_token->getConcreteString(); - $names[] = array( - 'interface', - $name_string, - $name_token, - ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string) - ? null - : pht( - 'Follow naming conventions: interfaces should be named using '. - 'UpperCamelCase.'), - ); - } - - - $functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); - foreach ($functions as $function) { - $name_token = $function->getChildByIndex(2); - if ($name_token->getTypeName() === 'n_EMPTY') { - // Unnamed closure. - continue; - } - $name_string = $name_token->getConcreteString(); - $names[] = array( - 'function', - $name_string, - $name_token, - ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores( - ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string)) - ? null - : pht( - 'Follow naming conventions: functions should be named using '. - 'lowercase_with_underscores.'), - ); - } - - - $methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION'); - foreach ($methods as $method) { - $name_token = $method->getChildByIndex(2); - $name_string = $name_token->getConcreteString(); - $names[] = array( - 'method', - $name_string, - $name_token, - ArcanistXHPASTLintNamingHook::isLowerCamelCase( - ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string)) - ? null - : pht( - 'Follow naming conventions: methods should be named using '. - 'lowerCamelCase.'), - ); - } - - $param_tokens = array(); - - $params = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST'); - foreach ($params as $param_list) { - foreach ($param_list->getChildren() as $param) { - $name_token = $param->getChildByIndex(1); - if ($name_token->getTypeName() === 'n_VARIABLE_REFERENCE') { - $name_token = $name_token->getChildOfType(0, 'n_VARIABLE'); - } - $param_tokens[$name_token->getID()] = true; - $name_string = $name_token->getConcreteString(); - - $names[] = array( - 'parameter', - $name_string, - $name_token, - ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores( - ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string)) - ? null - : pht( - 'Follow naming conventions: parameters should be named using '. - 'lowercase_with_underscores.'), - ); - } - } - - - $constants = $root->selectDescendantsOfType( - 'n_CLASS_CONSTANT_DECLARATION_LIST'); - foreach ($constants as $constant_list) { - foreach ($constant_list->getChildren() as $constant) { - $name_token = $constant->getChildByIndex(0); - $name_string = $name_token->getConcreteString(); - $names[] = array( - 'constant', - $name_string, - $name_token, - ArcanistXHPASTLintNamingHook::isUppercaseWithUnderscores($name_string) - ? null - : pht( - 'Follow naming conventions: class constants should be named '. - 'using UPPERCASE_WITH_UNDERSCORES.'), - ); - } - } - - $member_tokens = array(); - - $props = $root->selectDescendantsOfType('n_CLASS_MEMBER_DECLARATION_LIST'); - foreach ($props as $prop_list) { - foreach ($prop_list->getChildren() as $token_id => $prop) { - if ($prop->getTypeName() === 'n_CLASS_MEMBER_MODIFIER_LIST') { - continue; - } - - $name_token = $prop->getChildByIndex(0); - $member_tokens[$name_token->getID()] = true; - - $name_string = $name_token->getConcreteString(); - $names[] = array( - 'member', - $name_string, - $name_token, - ArcanistXHPASTLintNamingHook::isLowerCamelCase( - ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string)) - ? null - : pht( - 'Follow naming conventions: class properties should be named '. - 'using lowerCamelCase.'), - ); - } - } - - $superglobal_map = array_fill_keys( - $this->getSuperGlobalNames(), - true); - - - $defs = $root->selectDescendantsOfTypes(array( - 'n_FUNCTION_DECLARATION', - 'n_METHOD_DECLARATION', - )); - - foreach ($defs as $def) { - $globals = $def->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST'); - $globals = $globals->selectDescendantsOfType('n_VARIABLE'); - - $globals_map = array(); - foreach ($globals as $global) { - $global_string = $global->getConcreteString(); - $globals_map[$global_string] = true; - $names[] = array( - 'user', - $global_string, - $global, - - // No advice for globals, but hooks have an option to provide some. - null, - ); - } - - // Exclude access of static properties, since lint will be raised at - // their declaration if they're invalid and they may not conform to - // variable rules. This is slightly overbroad (includes the entire - // RHS of a "Class::..." token) to cover cases like "Class:$x[0]". These - // variables are simply made exempt from naming conventions. - $exclude_tokens = array(); - $statics = $def->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); - foreach ($statics as $static) { - $rhs = $static->getChildByIndex(1); - if ($rhs->getTypeName() == 'n_VARIABLE') { - $exclude_tokens[$rhs->getID()] = true; - } else { - $rhs_vars = $rhs->selectDescendantsOfType('n_VARIABLE'); - foreach ($rhs_vars as $var) { - $exclude_tokens[$var->getID()] = true; - } - } - } - - $vars = $def->selectDescendantsOfType('n_VARIABLE'); - foreach ($vars as $token_id => $var) { - if (isset($member_tokens[$token_id])) { - continue; - } - if (isset($param_tokens[$token_id])) { - continue; - } - if (isset($exclude_tokens[$token_id])) { - continue; - } - - $var_string = $var->getConcreteString(); - - // Awkward artifact of "$o->{$x}". - $var_string = trim($var_string, '{}'); - - if (isset($superglobal_map[$var_string])) { - continue; - } - if (isset($globals_map[$var_string])) { - continue; - } - - $names[] = array( - 'variable', - $var_string, - $var, - ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores( - ArcanistXHPASTLintNamingHook::stripPHPVariable($var_string)) - ? null - : pht( - 'Follow naming conventions: variables should be named using '. - 'lowercase_with_underscores.'), - ); - } - } - - // If a naming hook is configured, give it a chance to override the - // default results for all the symbol names. - $hook_class = $this->naminghook; - if ($hook_class) { - $hook_obj = newv($hook_class, array()); - foreach ($names as $k => $name_attrs) { - list($type, $name, $token, $default) = $name_attrs; - $result = $hook_obj->lintSymbolName($type, $name, $default); - $names[$k][3] = $result; - } - } - - // Raise anything we're left with. - foreach ($names as $k => $name_attrs) { - list($type, $name, $token, $result) = $name_attrs; - if ($result) { - $this->raiseLintAtNode( - $token, - self::LINT_NAMING_CONVENTIONS, - $result); - } - } - } - - private function lintSurpriseConstructors(XHPASTNode $root) { - $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); - foreach ($classes as $class) { - $class_name = $class->getChildByIndex(1)->getConcreteString(); - $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); - foreach ($methods as $method) { - $method_name_token = $method->getChildByIndex(2); - $method_name = $method_name_token->getConcreteString(); - if (strtolower($class_name) === strtolower($method_name)) { - $this->raiseLintAtNode( - $method_name_token, - self::LINT_IMPLICIT_CONSTRUCTOR, - pht( - 'Name constructors %s explicitly. This method is a constructor '. - ' because it has the same name as the class it is defined in.', - '__construct()')); - } - } - } - } - - private function lintParenthesesShouldHugExpressions(XHPASTNode $root) { - $all_paren_groups = $root->selectDescendantsOfTypes(array( - 'n_CALL_PARAMETER_LIST', - 'n_CONTROL_CONDITION', - 'n_FOR_EXPRESSION', - 'n_FOREACH_EXPRESSION', - 'n_DECLARATION_PARAMETER_LIST', - )); - - foreach ($all_paren_groups as $group) { - $tokens = $group->getTokens(); - - $token_o = array_shift($tokens); - $token_c = array_pop($tokens); - if ($token_o->getTypeName() !== '(') { - throw new Exception(pht('Expected open parentheses.')); - } - if ($token_c->getTypeName() !== ')') { - throw new Exception(pht('Expected close parentheses.')); - } - - $nonsem_o = $token_o->getNonsemanticTokensAfter(); - $nonsem_c = $token_c->getNonsemanticTokensBefore(); - - if (!$nonsem_o) { - continue; - } - - $raise = array(); - - $string_o = implode('', mpull($nonsem_o, 'getValue')); - if (preg_match('/^[ ]+$/', $string_o)) { - $raise[] = array($nonsem_o, $string_o); - } - - if ($nonsem_o !== $nonsem_c) { - $string_c = implode('', mpull($nonsem_c, 'getValue')); - if (preg_match('/^[ ]+$/', $string_c)) { - $raise[] = array($nonsem_c, $string_c); - } - } - - foreach ($raise as $warning) { - list($tokens, $string) = $warning; - $this->raiseLintAtOffset( - reset($tokens)->getOffset(), - self::LINT_PARENTHESES_SPACING, - pht('Parentheses should hug their contents.'), - $string, - ''); - } - } - } - - private function lintSpaceAfterControlStatementKeywords(XHPASTNode $root) { - foreach ($root->getTokens() as $id => $token) { - switch ($token->getTypeName()) { - case 'T_IF': - case 'T_ELSE': - case 'T_FOR': - case 'T_FOREACH': - case 'T_WHILE': - case 'T_DO': - case 'T_SWITCH': - $after = $token->getNonsemanticTokensAfter(); - if (empty($after)) { - $this->raiseLintAtToken( - $token, - self::LINT_CONTROL_STATEMENT_SPACING, - pht('Convention: put a space after control statements.'), - $token->getValue().' '); - } else if (count($after) === 1) { - $space = head($after); - - // If we have an else clause with braces, $space may not be - // a single white space. e.g., - // - // if ($x) - // echo 'foo' - // else // <- $space is not " " but "\n ". - // echo 'bar' - // - // We just require it starts with either a whitespace or a newline. - if ($token->getTypeName() === 'T_ELSE' || - $token->getTypeName() === 'T_DO') { - break; - } - - if ($space->isAnyWhitespace() && $space->getValue() !== ' ') { - $this->raiseLintAtToken( - $space, - self::LINT_CONTROL_STATEMENT_SPACING, - pht('Convention: put a single space after control statements.'), - ' '); - } - } - break; - } - } - } - - private function lintSpaceAroundBinaryOperators(XHPASTNode $root) { - $expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); - foreach ($expressions as $expression) { - $operator = $expression->getChildByIndex(1); - $operator_value = $operator->getConcreteString(); - list($before, $after) = $operator->getSurroundingNonsemanticTokens(); - - $replace = null; - if (empty($before) && empty($after)) { - $replace = " {$operator_value} "; - } else if (empty($before)) { - $replace = " {$operator_value}"; - } else if (empty($after)) { - $replace = "{$operator_value} "; - } - - if ($replace !== null) { - $this->raiseLintAtNode( - $operator, - self::LINT_BINARY_EXPRESSION_SPACING, - pht( - 'Convention: logical and arithmetic operators should be '. - 'surrounded by whitespace.'), - $replace); - } - } - - $tokens = $root->selectTokensOfType(','); - foreach ($tokens as $token) { - $next = $token->getNextToken(); - switch ($next->getTypeName()) { - case ')': - case 'T_WHITESPACE': - break; - default: - $this->raiseLintAtToken( - $token, - self::LINT_BINARY_EXPRESSION_SPACING, - pht('Convention: comma should be followed by space.'), - ', '); - break; - } - } - - $tokens = $root->selectTokensOfType('T_DOUBLE_ARROW'); - foreach ($tokens as $token) { - $prev = $token->getPrevToken(); - $next = $token->getNextToken(); - - $prev_type = $prev->getTypeName(); - $next_type = $next->getTypeName(); - - $prev_space = ($prev_type === 'T_WHITESPACE'); - $next_space = ($next_type === 'T_WHITESPACE'); - - $replace = null; - if (!$prev_space && !$next_space) { - $replace = ' => '; - } else if ($prev_space && !$next_space) { - $replace = '=> '; - } else if (!$prev_space && $next_space) { - $replace = ' =>'; - } - - if ($replace !== null) { - $this->raiseLintAtToken( - $token, - self::LINT_BINARY_EXPRESSION_SPACING, - pht('Convention: double arrow should be surrounded by whitespace.'), - $replace); - } - } - - $parameters = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER'); - foreach ($parameters as $parameter) { - if ($parameter->getChildByIndex(2)->getTypeName() == 'n_EMPTY') { - continue; - } - - $operator = head($parameter->selectTokensOfType('=')); - $before = $operator->getNonsemanticTokensBefore(); - $after = $operator->getNonsemanticTokensAfter(); - - $replace = null; - if (empty($before) && empty($after)) { - $replace = ' = '; - } else if (empty($before)) { - $replace = ' ='; - } else if (empty($after)) { - $replace = '= '; - } - - if ($replace !== null) { - $this->raiseLintAtToken( - $operator, - self::LINT_BINARY_EXPRESSION_SPACING, - pht( - 'Convention: logical and arithmetic operators should be '. - 'surrounded by whitespace.'), - $replace); - } - } - } - - private function lintSpaceAroundConcatenationOperators(XHPASTNode $root) { - $tokens = $root->selectTokensOfType('.'); - foreach ($tokens as $token) { - $prev = $token->getPrevToken(); - $next = $token->getNextToken(); - - foreach (array('prev' => $prev, 'next' => $next) as $wtoken) { - if ($wtoken->getTypeName() !== 'T_WHITESPACE') { - continue; - } - - $value = $wtoken->getValue(); - if (strpos($value, "\n") !== false) { - // If the whitespace has a newline, it's conventional. - continue; - } - - $next = $wtoken->getNextToken(); - if ($next && $next->getTypeName() === 'T_COMMENT') { - continue; - } - - $this->raiseLintAtToken( - $wtoken, - self::LINT_CONCATENATION_OPERATOR, - pht( - 'Convention: no spaces around "%s" '. - '(string concatenation) operator.', - '.'), - ''); - } - } - } - - private function lintDynamicDefines(XHPASTNode $root) { - $calls = $this->getFunctionCalls($root, array('define')); - - foreach ($calls as $call) { - $parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); - $defined = $parameter_list->getChildByIndex(0); - if (!$defined->isStaticScalar()) { - $this->raiseLintAtNode( - $defined, - self::LINT_DYNAMIC_DEFINE, - pht( - 'First argument to %s must be a string literal.', - 'define()')); - } - } - } - - private function lintUseOfThisInStaticMethods(XHPASTNode $root) { - $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); - foreach ($classes as $class) { - $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); - foreach ($methods as $method) { - - $attributes = $method - ->getChildByIndex(0, 'n_METHOD_MODIFIER_LIST') - ->selectDescendantsOfType('n_STRING'); - - $method_is_static = false; - $method_is_abstract = false; - foreach ($attributes as $attribute) { - if (strtolower($attribute->getConcreteString()) === 'static') { - $method_is_static = true; - } - if (strtolower($attribute->getConcreteString()) === 'abstract') { - $method_is_abstract = true; - } - } - - if ($method_is_abstract) { - continue; - } - - if (!$method_is_static) { - continue; - } - - $body = $method->getChildOfType(5, 'n_STATEMENT_LIST'); - - $variables = $body->selectDescendantsOfType('n_VARIABLE'); - foreach ($variables as $variable) { - if ($method_is_static && - strtolower($variable->getConcreteString()) === '$this') { - $this->raiseLintAtNode( - $variable, - self::LINT_STATIC_THIS, - pht( - 'You can not reference `%s` inside a static method.', - '$this')); - } - } - } - } - } - - /** - * preg_quote() takes two arguments, but the second one is optional because - * it is possible to use (), [] or {} as regular expression delimiters. If - * you don't pass a second argument, you're probably going to get something - * wrong. - */ - private function lintPregQuote(XHPASTNode $root) { - $function_calls = $this->getFunctionCalls($root, array('preg_quote')); - - foreach ($function_calls as $call) { - $parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); - if (count($parameter_list->getChildren()) !== 2) { - $this->raiseLintAtNode( - $call, - self::LINT_PREG_QUOTE_MISUSE, - pht( - 'If you use pattern delimiters that require escaping '. - '(such as `%s`, but not `%s`) then you should pass two '. - 'arguments to %s, so that %s knows which delimiter to escape.', - '//', - '()', - 'preg_quote()', - 'preg_quote()')); - } - } - } - - /** - * Exit is parsed as an expression, but using it as such is almost always - * wrong. That is, this is valid: - * - * strtoupper(33 * exit - 6); - * - * When exit is used as an expression, it causes the program to terminate with - * exit code 0. This is likely not what is intended; these statements have - * different effects: - * - * exit(-1); - * exit -1; - * - * The former exits with a failure code, the latter with a success code! - */ - private function lintExitExpressions(XHPASTNode $root) { - $unaries = $root->selectDescendantsOfType('n_UNARY_PREFIX_EXPRESSION'); - foreach ($unaries as $unary) { - $operator = $unary->getChildByIndex(0)->getConcreteString(); - if (strtolower($operator) === 'exit') { - if ($unary->getParentNode()->getTypeName() !== 'n_STATEMENT') { - $this->raiseLintAtNode( - $unary, - self::LINT_EXIT_EXPRESSION, - pht('Use `%s` as a statement, not an expression.', 'exit')); - } - } - } - } - - private function lintArrayIndexWhitespace(XHPASTNode $root) { - $indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS'); - foreach ($indexes as $index) { - $tokens = $index->getChildByIndex(0)->getTokens(); - $last = array_pop($tokens); - $trailing = $last->getNonsemanticTokensAfter(); - $trailing_text = implode('', mpull($trailing, 'getValue')); - if (preg_match('/^ +$/', $trailing_text)) { - $this->raiseLintAtOffset( - $last->getOffset() + strlen($last->getValue()), - self::LINT_ARRAY_INDEX_SPACING, - pht('Convention: no spaces before index access.'), - $trailing_text, - ''); - } - } - } - - private function lintTodoComments(XHPASTNode $root) { - $comments = $root->selectTokensOfTypes(array( - 'T_COMMENT', - 'T_DOC_COMMENT', - )); - - foreach ($comments as $token) { - $value = $token->getValue(); - if ($token->getTypeName() === 'T_DOC_COMMENT') { - $regex = '/(TODO|@todo)/'; - } else { - $regex = '/TODO/'; - } - - $matches = null; - $preg = preg_match_all( - $regex, - $value, - $matches, - PREG_OFFSET_CAPTURE); - - foreach ($matches[0] as $match) { - list($string, $offset) = $match; - $this->raiseLintAtOffset( - $token->getOffset() + $offset, - self::LINT_TODO_COMMENT, - pht('This comment has a TODO.'), - $string); - } - } - } - - /** - * Lint that if the file declares exactly one interface or class, - * the name of the file matches the name of the class, - * unless the classname is funky like an XHP element. - */ - private function lintPrimaryDeclarationFilenameMatch(XHPASTNode $root) { - $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); - $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); - - if (count($classes) + count($interfaces) !== 1) { - return; - } - - $declarations = count($classes) ? $classes : $interfaces; - $declarations->rewind(); - $declaration = $declarations->current(); - - $decl_name = $declaration->getChildByIndex(1); - $decl_string = $decl_name->getConcreteString(); - - // Exclude strangely named classes, e.g. XHP tags. - if (!preg_match('/^\w+$/', $decl_string)) { - return; - } - - $rename = $decl_string.'.php'; - - $path = $this->getActivePath(); - $filename = basename($path); - - if ($rename === $filename) { - return; - } - - $this->raiseLintAtNode( - $decl_name, - self::LINT_CLASS_FILENAME_MISMATCH, - pht( - "The name of this file differs from the name of the ". - "class or interface it declares. Rename the file to '%s'.", - $rename)); - } - - private function lintPlusOperatorOnStrings(XHPASTNode $root) { - $binops = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); - foreach ($binops as $binop) { - $op = $binop->getChildByIndex(1); - if ($op->getConcreteString() !== '+') { - continue; - } - - $left = $binop->getChildByIndex(0); - $right = $binop->getChildByIndex(2); - if (($left->getTypeName() === 'n_STRING_SCALAR') || - ($right->getTypeName() === 'n_STRING_SCALAR')) { - $this->raiseLintAtNode( - $binop, - self::LINT_PLUS_OPERATOR_ON_STRINGS, - pht( - "In PHP, '%s' is the string concatenation operator, not '%s'. ". - "This expression uses '+' with a string literal as an operand.", - '.', - '+')); - } - } - } - - /** - * Finds duplicate keys in array initializers, as in - * array(1 => 'anything', 1 => 'foo'). Since the first entry is ignored, - * this is almost certainly an error. - */ - private function lintDuplicateKeysInArray(XHPASTNode $root) { - $array_literals = $root->selectDescendantsOfType('n_ARRAY_LITERAL'); - foreach ($array_literals as $array_literal) { - $nodes_by_key = array(); - $keys_warn = array(); - $list_node = $array_literal->getChildByIndex(0); - foreach ($list_node->getChildren() as $array_entry) { - $key_node = $array_entry->getChildByIndex(0); - - switch ($key_node->getTypeName()) { - case 'n_STRING_SCALAR': - case 'n_NUMERIC_SCALAR': - // Scalars: array(1 => 'v1', '1' => 'v2'); - $key = 'scalar:'.(string)$key_node->evalStatic(); - break; - - case 'n_SYMBOL_NAME': - case 'n_VARIABLE': - case 'n_CLASS_STATIC_ACCESS': - // Constants: array(CONST => 'v1', CONST => 'v2'); - // Variables: array($a => 'v1', $a => 'v2'); - // Class constants and vars: array(C::A => 'v1', C::A => 'v2'); - $key = $key_node->getTypeName().':'.$key_node->getConcreteString(); - break; - - default: - $key = null; - break; - } - - if ($key !== null) { - if (isset($nodes_by_key[$key])) { - $keys_warn[$key] = true; - } - $nodes_by_key[$key][] = $key_node; - } - } - - foreach ($keys_warn as $key => $_) { - $node = array_pop($nodes_by_key[$key]); - $message = $this->raiseLintAtNode( - $node, - self::LINT_DUPLICATE_KEYS_IN_ARRAY, - pht( - 'Duplicate key in array initializer. PHP will ignore all '. - 'but the last entry.')); - - $locations = array(); - foreach ($nodes_by_key[$key] as $node) { - $locations[] = $this->getOtherLocation($node->getOffset()); - } - $message->setOtherLocations($locations); - } - } - } - - private function lintClosingCallParen(XHPASTNode $root) { - $calls = $root->selectDescendantsOfTypes(array( - 'n_FUNCTION_CALL', - 'n_METHOD_CALL', - )); - - foreach ($calls as $call) { - // If the last parameter of a call is a HEREDOC, don't apply this rule. - $params = $call - ->getChildOfType(1, 'n_CALL_PARAMETER_LIST') - ->getChildren(); - - if ($params) { - $last_param = last($params); - if ($last_param->getTypeName() === 'n_HEREDOC') { - continue; - } - } - - $tokens = $call->getTokens(); - $last = array_pop($tokens); - - $trailing = $last->getNonsemanticTokensBefore(); - $trailing_text = implode('', mpull($trailing, 'getValue')); - if (preg_match('/^\s+$/', $trailing_text)) { - $this->raiseLintAtOffset( - $last->getOffset() - strlen($trailing_text), - self::LINT_CLOSING_CALL_PAREN, - pht('Convention: no spaces before closing parenthesis in calls.'), - $trailing_text, - ''); - } - } - } - - private function lintClosingDeclarationParen(XHPASTNode $root) { - $decs = $root->selectDescendantsOfTypes(array( - 'n_FUNCTION_DECLARATION', - 'n_METHOD_DECLARATION', - )); - - foreach ($decs as $dec) { - $params = $dec->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST'); - $tokens = $params->getTokens(); - $last = array_pop($tokens); - - $trailing = $last->getNonsemanticTokensBefore(); - $trailing_text = implode('', mpull($trailing, 'getValue')); - if (preg_match('/^\s+$/', $trailing_text)) { - $this->raiseLintAtOffset( - $last->getOffset() - strlen($trailing_text), - self::LINT_CLOSING_DECL_PAREN, - pht( - 'Convention: no spaces before closing parenthesis in '. - 'function and method declarations.'), - $trailing_text, - ''); - } - } - } - - private function lintKeywordCasing(XHPASTNode $root) { - $keywords = $root->selectTokensOfTypes(array( - 'T_REQUIRE_ONCE', - 'T_REQUIRE', - 'T_EVAL', - 'T_INCLUDE_ONCE', - 'T_INCLUDE', - 'T_LOGICAL_OR', - 'T_LOGICAL_XOR', - 'T_LOGICAL_AND', - 'T_PRINT', - 'T_INSTANCEOF', - 'T_CLONE', - 'T_NEW', - 'T_EXIT', - 'T_IF', - 'T_ELSEIF', - 'T_ELSE', - 'T_ENDIF', - 'T_ECHO', - 'T_DO', - 'T_WHILE', - 'T_ENDWHILE', - 'T_FOR', - 'T_ENDFOR', - 'T_FOREACH', - 'T_ENDFOREACH', - 'T_DECLARE', - 'T_ENDDECLARE', - 'T_AS', - 'T_SWITCH', - 'T_ENDSWITCH', - 'T_CASE', - 'T_DEFAULT', - 'T_BREAK', - 'T_CONTINUE', - 'T_GOTO', - 'T_FUNCTION', - 'T_CONST', - 'T_RETURN', - 'T_TRY', - 'T_CATCH', - 'T_THROW', - 'T_USE', - 'T_GLOBAL', - 'T_PUBLIC', - 'T_PROTECTED', - 'T_PRIVATE', - 'T_FINAL', - 'T_ABSTRACT', - 'T_STATIC', - 'T_VAR', - 'T_UNSET', - 'T_ISSET', - 'T_EMPTY', - 'T_HALT_COMPILER', - 'T_CLASS', - 'T_INTERFACE', - 'T_EXTENDS', - 'T_IMPLEMENTS', - 'T_LIST', - 'T_ARRAY', - 'T_NAMESPACE', - 'T_INSTEADOF', - 'T_CALLABLE', - 'T_TRAIT', - 'T_YIELD', - 'T_FINALLY', - )); - foreach ($keywords as $keyword) { - $value = $keyword->getValue(); - - if ($value != strtolower($value)) { - $this->raiseLintAtToken( - $keyword, - self::LINT_KEYWORD_CASING, - pht( - "Convention: spell keyword '%s' as '%s'.", - $value, - strtolower($value)), - strtolower($value)); - } - } - - $symbols = $root->selectDescendantsOfType('n_SYMBOL_NAME'); - foreach ($symbols as $symbol) { - static $interesting_symbols = array( - 'false' => true, - 'null' => true, - 'true' => true, - ); - - $symbol_name = $symbol->getConcreteString(); - - if ($symbol->getParentNode()->getTypeName() == 'n_FUNCTION_CALL') { - continue; - } - - if (idx($interesting_symbols, strtolower($symbol_name))) { - if ($symbol_name != strtolower($symbol_name)) { - $this->raiseLintAtNode( - $symbol, - self::LINT_KEYWORD_CASING, - pht( - "Convention: spell keyword '%s' as '%s'.", - $symbol_name, - strtolower($symbol_name)), - strtolower($symbol_name)); - } - } - } - - $magic_constants = $root->selectTokensOfTypes(array( - 'T_CLASS_C', - 'T_METHOD_C', - 'T_FUNC_C', - 'T_LINE', - 'T_FILE', - 'T_NS_C', - 'T_DIR', - 'T_TRAIT_C', - )); - - foreach ($magic_constants as $magic_constant) { - $value = $magic_constant->getValue(); - - if ($value != strtoupper($value)) { - $this->raiseLintAtToken( - $magic_constant, - self::LINT_KEYWORD_CASING, - pht('Magic constants should be uppercase.'), - strtoupper($value)); - } - } - } - - private function lintStrings(XHPASTNode $root) { - $nodes = $root->selectDescendantsOfTypes(array( - 'n_CONCATENATION_LIST', - 'n_STRING_SCALAR', - )); - - foreach ($nodes as $node) { - $strings = array(); - - if ($node->getTypeName() === 'n_CONCATENATION_LIST') { - $strings = $node->selectDescendantsOfType('n_STRING_SCALAR'); - } else if ($node->getTypeName() === 'n_STRING_SCALAR') { - $strings = array($node); - - if ($node->getParentNode()->getTypeName() === 'n_CONCATENATION_LIST') { - continue; - } - } - - $valid = false; - $invalid_nodes = array(); - $fixes = array(); - - foreach ($strings as $string) { - $concrete_string = $string->getConcreteString(); - $single_quoted = ($concrete_string[0] === "'"); - $contents = substr($concrete_string, 1, -1); - - // Double quoted strings are allowed when the string contains the - // following characters. - static $allowed_chars = array( - '\n', - '\r', - '\t', - '\v', - '\e', - '\f', - '\'', - '\0', - '\1', - '\2', - '\3', - '\4', - '\5', - '\6', - '\7', - '\x', - ); - - $contains_special_chars = false; - foreach ($allowed_chars as $allowed_char) { - if (strpos($contents, $allowed_char) !== false) { - $contains_special_chars = true; - } - } - - if (!$string->isConstantString()) { - $valid = true; - } else if ($contains_special_chars && !$single_quoted) { - $valid = true; - } else if (!$contains_special_chars && !$single_quoted) { - $invalid_nodes[] = $string; - $fixes[$string->getID()] = "'".str_replace('\"', '"', $contents)."'"; - } - } - - if (!$valid) { - foreach ($invalid_nodes as $invalid_node) { - $this->raiseLintAtNode( - $invalid_node, - self::LINT_DOUBLE_QUOTE, - pht( - 'String does not require double quotes. For consistency, '. - 'prefer single quotes.'), - $fixes[$invalid_node->getID()]); - } - } - } - } - - protected function lintElseIfStatements(XHPASTNode $root) { - $tokens = $root->selectTokensOfType('T_ELSEIF'); - - foreach ($tokens as $token) { - $this->raiseLintAtToken( - $token, - self::LINT_ELSEIF_USAGE, - pht('Usage of `%s` is preferred over `%s`.', 'else if', 'elseif'), - 'else if'); - } - } - - protected function lintSemicolons(XHPASTNode $root) { - $tokens = $root->selectTokensOfType(';'); - - foreach ($tokens as $token) { - $prev = $token->getPrevToken(); - - if ($prev->isAnyWhitespace()) { - $this->raiseLintAtToken( - $prev, - self::LINT_SEMICOLON_SPACING, - pht('Space found before semicolon.'), - ''); - } - } - } - - protected function lintLanguageConstructParentheses(XHPASTNode $root) { - $nodes = $root->selectDescendantsOfTypes(array( - 'n_INCLUDE_FILE', - 'n_ECHO_LIST', - )); - - foreach ($nodes as $node) { - $child = head($node->getChildren()); - - if ($child->getTypeName() === 'n_PARENTHETICAL_EXPRESSION') { - list($before, $after) = $child->getSurroundingNonsemanticTokens(); - - $replace = preg_replace( - '/^\((.*)\)$/', - '$1', - $child->getConcreteString()); - - if (!$before) { - $replace = ' '.$replace; - } - - $this->raiseLintAtNode( - $child, - self::LINT_LANGUAGE_CONSTRUCT_PAREN, - pht('Language constructs do not require parentheses.'), - $replace); - } - } - } - - protected function lintEmptyBlockStatements(XHPASTNode $root) { - $nodes = $root->selectDescendantsOfType('n_STATEMENT_LIST'); - - foreach ($nodes as $node) { - $tokens = $node->getTokens(); - $token = head($tokens); - - if (count($tokens) <= 2) { - continue; - } - - // Safety check... if the first token isn't an opening brace then - // there's nothing to do here. - if ($token->getTypeName() != '{') { - continue; - } - - $only_whitespace = true; - for ($token = $token->getNextToken(); - $token && $token->getTypeName() != '}'; - $token = $token->getNextToken()) { - $only_whitespace = $only_whitespace && $token->isAnyWhitespace(); - } - - if (count($tokens) > 2 && $only_whitespace) { - $this->raiseLintAtNode( - $node, - self::LINT_EMPTY_STATEMENT, - pht( - "Braces for an empty block statement shouldn't ". - "contain only whitespace."), - '{}'); - } - } - } - - protected function lintArraySeparator(XHPASTNode $root) { - $arrays = $root->selectDescendantsOfType('n_ARRAY_LITERAL'); - - foreach ($arrays as $array) { - $value_list = $array->getChildOfType(0, 'n_ARRAY_VALUE_LIST'); - $values = $value_list->getChildrenOfType('n_ARRAY_VALUE'); - - if (!$values) { - // There is no need to check an empty array. - continue; - } - - $multiline = $array->getLineNumber() != $array->getEndLineNumber(); - - $value = last($values); - $after = last($value->getTokens())->getNextToken(); - - if ($multiline) { - if (!$after || $after->getValue() != ',') { - if ($value->getChildByIndex(1)->getTypeName() == 'n_HEREDOC') { - continue; - } - - list($before, $after) = $value->getSurroundingNonsemanticTokens(); - $after = implode('', mpull($after, 'getValue')); - - $original = $value->getConcreteString(); - $replacement = $value->getConcreteString().','; - - if (strpos($after, "\n") === false) { - $original .= $after; - $replacement .= $after."\n".$array->getIndentation(); - } - - $this->raiseLintAtOffset( - $value->getOffset(), - self::LINT_ARRAY_SEPARATOR, - pht('Multi-lined arrays should have trailing commas.'), - $original, - $replacement); - } else if ($value->getLineNumber() == $array->getEndLineNumber()) { - $close = last($array->getTokens()); - - $this->raiseLintAtToken( - $close, - self::LINT_ARRAY_SEPARATOR, - pht('Closing parenthesis should be on a new line.'), - "\n".$array->getIndentation().$close->getValue()); - } - } else if ($after && $after->getValue() == ',') { - $this->raiseLintAtToken( - $after, - self::LINT_ARRAY_SEPARATOR, - pht('Single lined arrays should not have a trailing comma.'), - ''); - } - } - } - - private function lintConstructorParentheses(XHPASTNode $root) { - $nodes = $root->selectDescendantsOfType('n_NEW'); - - foreach ($nodes as $node) { - $class = $node->getChildByIndex(0); - $params = $node->getChildByIndex(1); - - if ($params->getTypeName() == 'n_EMPTY') { - $this->raiseLintAtNode( - $class, - self::LINT_CONSTRUCTOR_PARENTHESES, - pht('Use parentheses when invoking a constructor.'), - $class->getConcreteString().'()'); - } - } - } - - private function lintSwitchStatements(XHPASTNode $root) { - $switch_statements = $root->selectDescendantsOfType('n_SWITCH'); - - foreach ($switch_statements as $switch_statement) { - $case_statements = $switch_statement - ->getChildOfType(1, 'n_STATEMENT_LIST') - ->getChildrenOfType('n_CASE'); - $nodes_by_case = array(); - - foreach ($case_statements as $case_statement) { - $case = $case_statement - ->getChildByIndex(0) - ->getSemanticString(); - $nodes_by_case[$case][] = $case_statement; - } - - foreach ($nodes_by_case as $case => $nodes) { - if (count($nodes) <= 1) { - continue; - } - - $node = array_pop($nodes_by_case[$case]); - $message = $this->raiseLintAtNode( - $node, - self::LINT_DUPLICATE_SWITCH_CASE, - pht( - 'Duplicate case in switch statement. PHP will ignore all '. - 'but the first case.')); - - $locations = array(); - foreach ($nodes_by_case[$case] as $node) { - $locations[] = $this->getOtherLocation($node->getOffset()); - } - $message->setOtherLocations($locations); - } - } - } - - private function lintBlacklistedFunction(XHPASTNode $root) { - $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); - - foreach ($calls as $call) { - $node = $call->getChildByIndex(0); - $name = $node->getConcreteString(); - - $reason = idx($this->blacklistedFunctions, $name); - - if ($reason) { - $this->raiseLintAtNode( - $node, - self::LINT_BLACKLISTED_FUNCTION, - $reason); - } - } - } - - private function lintMethodVisibility(XHPASTNode $root) { - static $visibilities = array( - 'public', - 'protected', - 'private', - ); - - $methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION'); - - foreach ($methods as $method) { - $modifiers_list = $method->getChildOfType( - 0, - 'n_METHOD_MODIFIER_LIST'); - - foreach ($modifiers_list->getChildren() as $modifier) { - if (in_array($modifier->getConcreteString(), $visibilities)) { - continue 2; - } - } - - if ($modifiers_list->getChildren()) { - $node = $modifiers_list; - } else { - $node = $method; - } - - $this->raiseLintAtNode( - $node, - self::LINT_IMPLICIT_VISIBILITY, - pht('Methods should have their visibility declared explicitly.'), - 'public '.$node->getConcreteString()); - } - } - - private function lintPropertyVisibility(XHPASTNode $root) { - static $visibilities = array( - 'public', - 'protected', - 'private', - ); - - $nodes = $root->selectDescendantsOfType('n_CLASS_MEMBER_MODIFIER_LIST'); - - foreach ($nodes as $node) { - $modifiers = $node->getChildren(); - - foreach ($modifiers as $modifier) { - if ($modifier->getConcreteString() == 'var') { - $this->raiseLintAtNode( - $modifier, - self::LINT_IMPLICIT_VISIBILITY, - pht( - 'Use `%s` instead of `%s` to indicate public visibility.', - 'public', - 'var'), - 'public'); - continue 2; - } - - if (in_array($modifier->getConcreteString(), $visibilities)) { - continue 2; - } - } - - $this->raiseLintAtNode( - $node, - self::LINT_IMPLICIT_VISIBILITY, - pht('Properties should have their visibility declared explicitly.'), - 'public '.$node->getConcreteString()); - } - } - - private function lintCallTimePassByReference(XHPASTNode $root) { - $nodes = $root->selectDescendantsOfType('n_CALL_PARAMETER_LIST'); - - foreach ($nodes as $node) { - $parameters = $node->getChildrenOfType('n_VARIABLE_REFERENCE'); - - foreach ($parameters as $parameter) { - $this->raiseLintAtNode( - $parameter, - self::LINT_CALL_TIME_PASS_BY_REF, - pht('Call-time pass-by-reference calls are prohibited.')); - } - } - } - - private function lintFormattedString(XHPASTNode $root) { - static $functions = array( - // Core PHP - 'fprintf' => 1, - 'printf' => 0, - 'sprintf' => 0, - 'vfprintf' => 1, - - // libphutil - 'csprintf' => 0, - 'execx' => 0, - 'exec_manual' => 0, - 'hgsprintf' => 0, - 'hsprintf' => 0, - 'jsprintf' => 0, - 'pht' => 0, - 'phutil_passthru' => 0, - 'qsprintf' => 1, - 'queryfx' => 1, - 'queryfx_all' => 1, - 'queryfx_one' => 1, - 'vcsprintf' => 0, - 'vqsprintf' => 1, - ); - - $function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); - - foreach ($function_calls as $call) { - $name = $call->getChildByIndex(0)->getConcreteString(); - - $name = strtolower($name); - $start = idx($functions + $this->printfFunctions, $name); - - if ($start === null) { - continue; - } - - $parameters = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); - $argc = count($parameters->getChildren()) - $start; - - if ($argc < 1) { - $this->raiseLintAtNode( - $call, - self::LINT_FORMATTED_STRING, - pht('This function is expected to have a format string.')); - continue; - } - - $format = $parameters->getChildByIndex($start); - if ($format->getTypeName() != 'n_STRING_SCALAR') { - continue; - } - - $argv = array($format->evalStatic()) + array_fill(0, $argc, null); - - try { - xsprintf(null, null, $argv); - } catch (BadFunctionCallException $ex) { - $this->raiseLintAtNode( - $call, - self::LINT_FORMATTED_STRING, - str_replace('xsprintf', $name, $ex->getMessage())); - } catch (InvalidArgumentException $ex) { - // Ignore. - } - } - } - - private function lintUnnecessaryFinalModifier(XHPASTNode $root) { - $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); - - foreach ($classes as $class) { - $attributes = $class->getChildOfType(0, 'n_CLASS_ATTRIBUTES'); - $is_final = false; - - foreach ($attributes->getChildren() as $attribute) { - if ($attribute->getConcreteString() == 'final') { - $is_final = true; - break; - } - } - - if (!$is_final) { - continue; - } - - $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); - foreach ($methods as $method) { - $attributes = $method->getChildOfType(0, 'n_METHOD_MODIFIER_LIST'); - - foreach ($attributes->getChildren() as $attribute) { - if ($attribute->getConcreteString() == 'final') { - $this->raiseLintAtNode( - $attribute, - self::LINT_UNNECESSARY_FINAL_MODIFIER, - pht( - 'Unnecessary %s modifier in %s class.', - 'final', - 'final')); - } - } - } - } - } - - private function lintUnnecessarySemicolons(XHPASTNode $root) { - $statements = $root->selectDescendantsOfType('n_STATEMENT'); - - foreach ($statements as $statement) { - if ($statement->getParentNode()->getTypeName() == 'n_DECLARE') { - continue; - } - - if (count($statement->getChildren()) > 1) { - continue; - } else if ($statement->getChildByIndex(0)->getTypeName() != 'n_EMPTY') { - continue; - } - - if ($statement->getConcreteString() == ';') { - $this->raiseLintAtNode( - $statement, - self::LINT_UNNECESSARY_SEMICOLON, - pht('Unnecessary semicolons after statement.'), - ''); - } - } - } - - private function lintConstantDefinitions(XHPASTNode $root) { - $defines = $this - ->getFunctionCalls($root, array('define')) - ->add($root->selectDescendantsOfTypes(array( - 'n_CLASS_CONSTANT_DECLARATION', - 'n_CONSTANT_DECLARATION', - ))); - - foreach ($defines as $define) { - switch ($define->getTypeName()) { - case 'n_CLASS_CONSTANT_DECLARATION': - case 'n_CONSTANT_DECLARATION': - $constant = $define->getChildByIndex(0); - - if ($constant->getTypeName() !== 'n_STRING') { - $constant = null; - } - - break; - - case 'n_FUNCTION_CALL': - $constant = $define - ->getChildOfType(1, 'n_CALL_PARAMETER_LIST') - ->getChildByIndex(0); - - if ($constant->getTypeName() !== 'n_STRING_SCALAR') { - $constant = null; - } - - break; - - default: - $constant = null; - break; - } - - if (!$constant) { - continue; - } - $constant_name = $constant->getConcreteString(); - - if ($constant_name !== strtoupper($constant_name)) { - $this->raiseLintAtNode( - $constant, - self::LINT_NAMING_CONVENTIONS, - pht('Constants should be uppercase.')); - } - } - } - - private function lintSelfMemberReference(XHPASTNode $root) { - $class_declarations = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); - - foreach ($class_declarations as $class_declaration) { - $class_name = $class_declaration - ->getChildOfType(1, 'n_CLASS_NAME') - ->getConcreteString(); - - $class_static_accesses = $class_declaration - ->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); - - foreach ($class_static_accesses as $class_static_access) { - $double_colons = $class_static_access - ->selectTokensOfType('T_PAAMAYIM_NEKUDOTAYIM'); - $class_ref = $class_static_access->getChildByIndex(0); - - if ($class_ref->getTypeName() != 'n_CLASS_NAME') { - continue; - } - $class_ref_name = $class_ref->getConcreteString(); - - if (strtolower($class_name) == strtolower($class_ref_name)) { - $this->raiseLintAtNode( - $class_ref, - self::LINT_SELF_MEMBER_REFERENCE, - pht('Use `%s` for local static member references.', 'self::'), - 'self'); - } - - static $self_refs = array( - 'parent', - 'self', - 'static', - ); - - if (!in_array(strtolower($class_ref_name), $self_refs)) { - continue; - } - - if ($class_ref_name != strtolower($class_ref_name)) { - $this->raiseLintAtNode( - $class_ref, - self::LINT_SELF_MEMBER_REFERENCE, - pht('PHP keywords should be lowercase.'), - strtolower($class_ref_name)); - } - } - } - - $double_colons = $root - ->selectTokensOfType('T_PAAMAYIM_NEKUDOTAYIM'); - - foreach ($double_colons as $double_colon) { - $tokens = $double_colon->getNonsemanticTokensBefore() + - $double_colon->getNonsemanticTokensAfter(); - - foreach ($tokens as $token) { - if ($token->isAnyWhitespace()) { - if (strpos($token->getValue(), "\n") !== false) { - continue; - } - - $this->raiseLintAtToken( - $token, - self::LINT_SELF_MEMBER_REFERENCE, - pht('Unnecessary whitespace around double colon operator.'), - ''); - } - } - } - } - - private function lintLogicalOperators(XHPASTNode $root) { - $logical_ands = $root->selectTokensOfType('T_LOGICAL_AND'); - $logical_ors = $root->selectTokensOfType('T_LOGICAL_OR'); - - foreach ($logical_ands as $logical_and) { - $this->raiseLintAtToken( - $logical_and, - self::LINT_LOGICAL_OPERATORS, - pht('Use `%s` instead of `%s`.', '&&', 'and'), - '&&'); - } - - foreach ($logical_ors as $logical_or) { - $this->raiseLintAtToken( - $logical_or, - self::LINT_LOGICAL_OPERATORS, - pht('Use `%s` instead of `%s`.', '||', 'or'), - '||'); - } - } - - private function lintInnerFunctions(XHPASTNode $root) { - $function_decls = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); - - foreach ($function_decls as $function_declaration) { - $inner_functions = $function_declaration - ->selectDescendantsOfType('n_FUNCTION_DECLARATION'); - - foreach ($inner_functions as $inner_function) { - if ($inner_function->getChildByIndex(2)->getTypeName() == 'n_EMPTY') { - // Anonymous closure. - continue; - } - - $this->raiseLintAtNode( - $inner_function, - self::LINT_INNER_FUNCTION, - pht('Avoid the use of inner functions.')); - } - } - } - - private function lintDefaultParameters(XHPASTNode $root) { - $parameter_lists = $root->selectDescendantsOfType( - 'n_DECLARATION_PARAMETER_LIST'); - - foreach ($parameter_lists as $parameter_list) { - $default_found = false; - $parameters = $parameter_list->selectDescendantsOfType( - 'n_DECLARATION_PARAMETER'); - - foreach ($parameters as $parameter) { - $default_value = $parameter->getChildByIndex(2); - - if ($default_value->getTypeName() != 'n_EMPTY') { - $default_found = true; - } else if ($default_found) { - $this->raiseLintAtNode( - $parameter_list, - self::LINT_DEFAULT_PARAMETERS, - pht( - 'Arguments with default values must be at the end '. - 'of the argument list.')); - } - } - } - } - - private function lintLowercaseFunctions(XHPASTNode $root) { - static $builtin_functions = null; - - if ($builtin_functions === null) { - $builtin_functions = array_fuse( - idx(get_defined_functions(), 'internal', array())); - } - - $function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); - - foreach ($function_calls as $function_call) { - $function = $function_call->getChildByIndex(0); - - if ($function->getTypeName() != 'n_SYMBOL_NAME') { - continue; - } - - $function_name = $function->getConcreteString(); - - if (!idx($builtin_functions, strtolower($function_name))) { - continue; - } - - if ($function_name != strtolower($function_name)) { - $this->raiseLintAtNode( - $function, - self::LINT_LOWERCASE_FUNCTIONS, - pht('Calls to built-in PHP functions should be lowercase.'), - strtolower($function_name)); - } - } - } - - private function lintClassNameLiteral(XHPASTNode $root) { - $class_declarations = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); - - foreach ($class_declarations as $class_declaration) { - $class_name = $class_declaration - ->getChildOfType(1, 'n_CLASS_NAME') - ->getConcreteString(); - - $strings = $class_declaration->selectDescendantsOfType('n_STRING_SCALAR'); - - foreach ($strings as $string) { - $contents = substr($string->getSemanticString(), 1, -1); - $replacement = null; - - if ($contents == $class_name) { - $replacement = '__CLASS__'; - } - - $regex = '/\b'.preg_quote($class_name, '/').'\b/'; - if (!preg_match($regex, $contents)) { - continue; - } - - $this->raiseLintAtNode( - $string, - self::LINT_CLASS_NAME_LITERAL, - pht( - "Don't hard-code class names, use %s instead.", - '__CLASS__'), - $replacement); - } - } - } - - private function lintUselessOverridingMethods(XHPASTNode $root) { - $methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION'); - - foreach ($methods as $method) { - $method_name = $method - ->getChildOfType(2, 'n_STRING') - ->getConcreteString(); - - $parameter_list = $method - ->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST'); - $parameters = array(); - - foreach ($parameter_list->getChildren() as $parameter) { - $parameter = $parameter->getChildByIndex(1); - - if ($parameter->getTypeName() == 'n_VARIABLE_REFERENCE') { - $parameter = $parameter->getChildOfType(0, 'n_VARIABLE'); - } - - $parameters[] = $parameter->getConcreteString(); - } - - $statements = $method->getChildByIndex(5); - - if ($statements->getTypeName() != 'n_STATEMENT_LIST') { - continue; - } - - if (count($statements->getChildren()) != 1) { - continue; - } - - $statement = $statements - ->getChildOfType(0, 'n_STATEMENT') - ->getChildByIndex(0); - - if ($statement->getTypeName() == 'n_RETURN') { - $statement = $statement->getChildByIndex(0); - } - - if ($statement->getTypeName() != 'n_FUNCTION_CALL') { - continue; - } - - $function = $statement->getChildByIndex(0); - - if ($function->getTypeName() != 'n_CLASS_STATIC_ACCESS') { - continue; - } - - $called_class = $function->getChildOfType(0, 'n_CLASS_NAME'); - $called_method = $function->getChildOfType(1, 'n_STRING'); - - if ($called_class->getConcreteString() != 'parent') { - continue; - } else if ($called_method->getConcreteString() != $method_name) { - continue; - } - - $params = $statement - ->getChildOfType(1, 'n_CALL_PARAMETER_LIST') - ->getChildren(); - - foreach ($params as $param) { - if ($param->getTypeName() != 'n_VARIABLE') { - continue 2; - } - - $expected = array_shift($parameters); - - if ($param->getConcreteString() != $expected) { - continue 2; - } - } - - $this->raiseLintAtNode( - $method, - self::LINT_USELESS_OVERRIDING_METHOD, - pht('Useless overriding method.')); - } - } - - private function lintNoParentScope(XHPASTNode $root) { - $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); - - foreach ($classes as $class) { - $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); - - if ($class->getChildByIndex(2)->getTypeName() == 'n_EXTENDS_LIST') { - continue; - } - - foreach ($methods as $method) { - $static_accesses = $method - ->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); - - foreach ($static_accesses as $static_access) { - $called_class = $static_access->getChildByIndex(0); - - if ($called_class->getTypeName() != 'n_CLASS_NAME') { - continue; - } - - if ($called_class->getConcreteString() == 'parent') { - $this->raiseLintAtNode( - $static_access, - self::LINT_NO_PARENT_SCOPE, - pht( - 'Cannot access %s when current class scope has no parent.', - 'parent::')); - } - } - } - } - } - - private function lintAliasFunctions(XHPASTNode $root) { - static $aliases = array( - '_' => 'gettext', - 'chop' => 'rtrim', - 'close' => 'closedir', - 'com_get' => 'com_propget', - 'com_propset' => 'com_propput', - 'com_set' => 'com_propput', - 'die' => 'exit', - 'diskfreespace' => 'disk_free_space', - 'doubleval' => 'floatval', - 'drawarc' => 'swfshape_drawarc', - 'drawcircle' => 'swfshape_drawcircle', - 'drawcubic' => 'swfshape_drawcubic', - 'drawcubicto' => 'swfshape_drawcubicto', - 'drawcurve' => 'swfshape_drawcurve', - 'drawcurveto' => 'swfshape_drawcurveto', - 'drawglyph' => 'swfshape_drawglyph', - 'drawline' => 'swfshape_drawline', - 'drawlineto' => 'swfshape_drawlineto', - 'fbsql' => 'fbsql_db_query', - 'fputs' => 'fwrite', - 'gzputs' => 'gzwrite', - 'i18n_convert' => 'mb_convert_encoding', - 'i18n_discover_encoding' => 'mb_detect_encoding', - 'i18n_http_input' => 'mb_http_input', - 'i18n_http_output' => 'mb_http_output', - 'i18n_internal_encoding' => 'mb_internal_encoding', - 'i18n_ja_jp_hantozen' => 'mb_convert_kana', - 'i18n_mime_header_decode' => 'mb_decode_mimeheader', - 'i18n_mime_header_encode' => 'mb_encode_mimeheader', - 'imap_create' => 'imap_createmailbox', - 'imap_fetchtext' => 'imap_body', - 'imap_getmailboxes' => 'imap_list_full', - 'imap_getsubscribed' => 'imap_lsub_full', - 'imap_header' => 'imap_headerinfo', - 'imap_listmailbox' => 'imap_list', - 'imap_listsubscribed' => 'imap_lsub', - 'imap_rename' => 'imap_renamemailbox', - 'imap_scan' => 'imap_listscan', - 'imap_scanmailbox' => 'imap_listscan', - 'ini_alter' => 'ini_set', - 'is_double' => 'is_float', - 'is_integer' => 'is_int', - 'is_long' => 'is_int', - 'is_real' => 'is_float', - 'is_writeable' => 'is_writable', - 'join' => 'implode', - 'key_exists' => 'array_key_exists', - 'ldap_close' => 'ldap_unbind', - 'magic_quotes_runtime' => 'set_magic_quotes_runtime', - 'mbstrcut' => 'mb_strcut', - 'mbstrlen' => 'mb_strlen', - 'mbstrpos' => 'mb_strpos', - 'mbstrrpos' => 'mb_strrpos', - 'mbsubstr' => 'mb_substr', - 'ming_setcubicthreshold' => 'ming_setCubicThreshold', - 'ming_setscale' => 'ming_setScale', - 'msql' => 'msql_db_query', - 'msql_createdb' => 'msql_create_db', - 'msql_dbname' => 'msql_result', - 'msql_dropdb' => 'msql_drop_db', - 'msql_fieldflags' => 'msql_field_flags', - 'msql_fieldlen' => 'msql_field_len', - 'msql_fieldname' => 'msql_field_name', - 'msql_fieldtable' => 'msql_field_table', - 'msql_fieldtype' => 'msql_field_type', - 'msql_freeresult' => 'msql_free_result', - 'msql_listdbs' => 'msql_list_dbs', - 'msql_listfields' => 'msql_list_fields', - 'msql_listtables' => 'msql_list_tables', - 'msql_numfields' => 'msql_num_fields', - 'msql_numrows' => 'msql_num_rows', - 'msql_regcase' => 'sql_regcase', - 'msql_selectdb' => 'msql_select_db', - 'msql_tablename' => 'msql_result', - 'mssql_affected_rows' => 'sybase_affected_rows', - 'mssql_close' => 'sybase_close', - 'mssql_connect' => 'sybase_connect', - 'mssql_data_seek' => 'sybase_data_seek', - 'mssql_fetch_array' => 'sybase_fetch_array', - 'mssql_fetch_field' => 'sybase_fetch_field', - 'mssql_fetch_object' => 'sybase_fetch_object', - 'mssql_fetch_row' => 'sybase_fetch_row', - 'mssql_field_seek' => 'sybase_field_seek', - 'mssql_free_result' => 'sybase_free_result', - 'mssql_get_last_message' => 'sybase_get_last_message', - 'mssql_min_client_severity' => 'sybase_min_client_severity', - 'mssql_min_error_severity' => 'sybase_min_error_severity', - 'mssql_min_message_severity' => 'sybase_min_message_severity', - 'mssql_min_server_severity' => 'sybase_min_server_severity', - 'mssql_num_fields' => 'sybase_num_fields', - 'mssql_num_rows' => 'sybase_num_rows', - 'mssql_pconnect' => 'sybase_pconnect', - 'mssql_query' => 'sybase_query', - 'mssql_result' => 'sybase_result', - 'mssql_select_db' => 'sybase_select_db', - 'multcolor' => 'swfdisplayitem_multColor', - 'mysql' => 'mysql_db_query', - 'mysql_createdb' => 'mysql_create_db', - 'mysql_db_name' => 'mysql_result', - 'mysql_dbname' => 'mysql_result', - 'mysql_dropdb' => 'mysql_drop_db', - 'mysql_fieldflags' => 'mysql_field_flags', - 'mysql_fieldlen' => 'mysql_field_len', - 'mysql_fieldname' => 'mysql_field_name', - 'mysql_fieldtable' => 'mysql_field_table', - 'mysql_fieldtype' => 'mysql_field_type', - 'mysql_freeresult' => 'mysql_free_result', - 'mysql_listdbs' => 'mysql_list_dbs', - 'mysql_listfields' => 'mysql_list_fields', - 'mysql_listtables' => 'mysql_list_tables', - 'mysql_numfields' => 'mysql_num_fields', - 'mysql_numrows' => 'mysql_num_rows', - 'mysql_selectdb' => 'mysql_select_db', - 'mysql_tablename' => 'mysql_result', - 'ociassignelem' => 'OCI-Collection::assignElem', - 'ocibindbyname' => 'oci_bind_by_name', - 'ocicancel' => 'oci_cancel', - 'ocicloselob' => 'OCI-Lob::close', - 'ocicollappend' => 'OCI-Collection::append', - 'ocicollassign' => 'OCI-Collection::assign', - 'ocicollmax' => 'OCI-Collection::max', - 'ocicollsize' => 'OCI-Collection::size', - 'ocicolltrim' => 'OCI-Collection::trim', - 'ocicolumnisnull' => 'oci_field_is_null', - 'ocicolumnname' => 'oci_field_name', - 'ocicolumnprecision' => 'oci_field_precision', - 'ocicolumnscale' => 'oci_field_scale', - 'ocicolumnsize' => 'oci_field_size', - 'ocicolumntype' => 'oci_field_type', - 'ocicolumntyperaw' => 'oci_field_type_raw', - 'ocicommit' => 'oci_commit', - 'ocidefinebyname' => 'oci_define_by_name', - 'ocierror' => 'oci_error', - 'ociexecute' => 'oci_execute', - 'ocifetch' => 'oci_fetch', - 'ocifetchinto' => 'oci_fetch_array(),', - 'ocifetchstatement' => 'oci_fetch_all', - 'ocifreecollection' => 'OCI-Collection::free', - 'ocifreecursor' => 'oci_free_statement', - 'ocifreedesc' => 'oci_free_descriptor', - 'ocifreestatement' => 'oci_free_statement', - 'ocigetelem' => 'OCI-Collection::getElem', - 'ociinternaldebug' => 'oci_internal_debug', - 'ociloadlob' => 'OCI-Lob::load', - 'ocilogon' => 'oci_connect', - 'ocinewcollection' => 'oci_new_collection', - 'ocinewcursor' => 'oci_new_cursor', - 'ocinewdescriptor' => 'oci_new_descriptor', - 'ocinlogon' => 'oci_new_connect', - 'ocinumcols' => 'oci_num_fields', - 'ociparse' => 'oci_parse', - 'ocipasswordchange' => 'oci_password_change', - 'ociplogon' => 'oci_pconnect', - 'ociresult' => 'oci_result', - 'ocirollback' => 'oci_rollback', - 'ocisavelob' => 'OCI-Lob::save', - 'ocisavelobfile' => 'OCI-Lob::import', - 'ociserverversion' => 'oci_server_version', - 'ocisetprefetch' => 'oci_set_prefetch', - 'ocistatementtype' => 'oci_statement_type', - 'ociwritelobtofile' => 'OCI-Lob::export', - 'ociwritetemporarylob' => 'OCI-Lob::writeTemporary', - 'odbc_do' => 'odbc_exec', - 'odbc_field_precision' => 'odbc_field_len', - 'pdf_add_outline' => 'pdf_add_bookmark', - 'pg_clientencoding' => 'pg_client_encoding', - 'pg_setclientencoding' => 'pg_set_client_encoding', - 'pos' => 'current', - 'recode' => 'recode_string', - 'show_source' => 'highlight_file', - 'sizeof' => 'count', - 'snmpwalkoid' => 'snmprealwalk', - 'strchr' => 'strstr', - 'streammp3' => 'swfmovie_streamMp3', - 'swfaction' => 'swfaction_init', - 'swfbitmap' => 'swfbitmap_init', - 'swfbutton' => 'swfbutton_init', - 'swffill' => 'swffill_init', - 'swffont' => 'swffont_init', - 'swfgradient' => 'swfgradient_init', - 'swfmorph' => 'swfmorph_init', - 'swfmovie' => 'swfmovie_init', - 'swfshape' => 'swfshape_init', - 'swfsprite' => 'swfsprite_init', - 'swftext' => 'swftext_init', - 'swftextfield' => 'swftextfield_init', - 'xptr_new_context' => 'xpath_new_context', - ); - - $functions = $this->getFunctionCalls($root, array_keys($aliases)); - - foreach ($functions as $function) { - $function_name = $function->getChildByIndex(0); - - $this->raiseLintAtNode( - $function_name, - self::LINT_ALIAS_FUNCTION, - pht('Alias functions should be avoided.'), - $aliases[phutil_utf8_strtolower($function_name->getConcreteString())]); - } - } - - private function lintCastSpacing(XHPASTNode $root) { - $cast_expressions = $root->selectDescendantsOfType('n_CAST_EXPRESSION'); - - foreach ($cast_expressions as $cast_expression) { - $cast = $cast_expression->getChildOfType(0, 'n_CAST'); - - list($before, $after) = $cast->getSurroundingNonsemanticTokens(); - $after = head($after); - - if ($after) { - $this->raiseLintAtToken( - $after, - self::LINT_CAST_SPACING, - pht('A cast statement must not be followed by a space.'), - ''); - } - } - } - - private function lintThrowExceptionInToStringMethod(XHPASTNode $root) { - $methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION'); - - foreach ($methods as $method) { - $name = $method - ->getChildOfType(2, 'n_STRING') - ->getConcreteString(); - - if ($name != '__toString') { - continue; - } - - $statements = $method->getChildByIndex(5); - - if ($statements->getTypeName() != 'n_STATEMENT_LIST') { - continue; - } - - $throws = $statements->selectDescendantsOfType('n_THROW'); - - foreach ($throws as $throw) { - $this->raiseLintAtNode( - $throw, - self::LINT_TOSTRING_EXCEPTION, - pht( - 'It is not possible to throw an %s from within the %s method.', - 'Exception', - '__toString')); - } - } - } - - private function lintLambdaFuncFunction(XHPASTNode $root) { - $function_declarations = $root - ->selectDescendantsOfType('n_FUNCTION_DECLARATION'); - - foreach ($function_declarations as $function_declaration) { - $function_name = $function_declaration->getChildByIndex(2); - - if ($function_name->getTypeName() == 'n_EMPTY') { - // Anonymous closure. - continue; - } - - if ($function_name->getConcreteString() != '__lambda_func') { - continue; - } - - $this->raiseLintAtNode( - $function_declaration, - self::LINT_LAMBDA_FUNC_FUNCTION, - pht( - 'Declaring a function named %s causes any call to %s to fail. '. - 'This is because %s eval-declares the function %s, then '. - 'modifies the symbol table so that the function is instead '. - 'named %s, and returns that name.', - '__lambda_func', - 'create_function', - 'create_function', - '__lambda_func', - '"\0lambda_".(++$i)')); - } - } - - private function lintInstanceOfOperator(XHPASTNode $root) { - $expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); - - foreach ($expressions as $expression) { - $operator = $expression->getChildOfType(1, 'n_OPERATOR'); - - if (strtolower($operator->getConcreteString()) != 'instanceof') { - continue; - } - - $object = $expression->getChildByIndex(0); - - if ($object->isStaticScalar() || - $object->getTypeName() == 'n_SYMBOL_NAME') { - $this->raiseLintAtNode( - $object, - self::LINT_INSTANCEOF_OPERATOR, - pht( - '%s expects an object instance, constant given.', - 'instanceof')); - } - } - } - - private function lintInvalidDefaultParameters(XHPASTNode $root) { - $parameters = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER'); - - foreach ($parameters as $parameter) { - $type = $parameter->getChildByIndex(0); - $default = $parameter->getChildByIndex(2); - - if ($type->getTypeName() == 'n_EMPTY') { - continue; - } - - if ($default->getTypeName() == 'n_EMPTY') { - continue; - } - - $default_is_null = $default->getTypeName() == 'n_SYMBOL_NAME' && - strtolower($default->getConcreteString()) == 'null'; - - switch (strtolower($type->getConcreteString())) { - case 'array': - if ($default->getTypeName() == 'n_ARRAY_LITERAL') { - break; - } - if ($default_is_null) { - break; - } - - $this->raiseLintAtNode( - $default, - self::LINT_INVALID_DEFAULT_PARAMETER, - pht( - 'Default value for parameters with %s type hint '. - 'can only be an %s or %s.', - 'array', - 'array', - 'null')); - break; - - case 'callable': - if ($default_is_null) { - break; - } - - $this->raiseLintAtNode( - $default, - self::LINT_INVALID_DEFAULT_PARAMETER, - pht( - 'Default value for parameters with %s type hint can only be %s.', - 'callable', - 'null')); - break; - - default: - // Class/interface parameter. - if ($default_is_null) { - break; - } - - $this->raiseLintAtNode( - $default, - self::LINT_INVALID_DEFAULT_PARAMETER, - pht( - 'Default value for parameters with a class type hint '. - 'can only be %s.', - 'null')); - break; - } - } - } - - private function lintMethodModifierOrdering(XHPASTNode $root) { - static $modifiers = array( - 'abstract', - 'final', - 'public', - 'protected', - 'private', - 'static', - ); - - $methods = $root->selectDescendantsOfType('n_METHOD_MODIFIER_LIST'); - - foreach ($methods as $method) { - $modifier_ordering = array_values( - mpull($method->getChildren(), 'getConcreteString')); - $expected_modifier_ordering = array_values( - array_intersect( - $modifiers, - $modifier_ordering)); - - if (count($modifier_ordering) != count($expected_modifier_ordering)) { - continue; - } - - if ($modifier_ordering != $expected_modifier_ordering) { - $this->raiseLintAtNode( - $method, - self::LINT_MODIFIER_ORDERING, - pht('Non-conventional modifier ordering.'), - implode(' ', $expected_modifier_ordering)); - } - } - } - - private function lintPropertyModifierOrdering(XHPASTNode $root) { - static $modifiers = array( - 'public', - 'protected', - 'private', - 'static', - ); - - $properties = $root->selectDescendantsOfType( - 'n_CLASS_MEMBER_MODIFIER_LIST'); - - foreach ($properties as $property) { - $modifier_ordering = array_values( - mpull($property->getChildren(), 'getConcreteString')); - $expected_modifier_ordering = array_values( - array_intersect( - $modifiers, - $modifier_ordering)); - - if (count($modifier_ordering) != count($expected_modifier_ordering)) { - continue; - } - - if ($modifier_ordering != $expected_modifier_ordering) { - $this->raiseLintAtNode( - $property, - self::LINT_MODIFIER_ORDERING, - pht('Non-conventional modifier ordering.'), - implode(' ', $expected_modifier_ordering)); - } - } - } - - private function lintInvalidModifiers(XHPASTNode $root) { - $methods = $root->selectDescendantsOfTypes(array( - 'n_CLASS_MEMBER_MODIFIER_LIST', - 'n_METHOD_MODIFIER_LIST', - )); - - foreach ($methods as $method) { - $modifiers = $method->getChildren(); - - $is_abstract = false; - $is_final = false; - $is_static = false; - $visibility = null; - - foreach ($modifiers as $modifier) { - switch ($modifier->getConcreteString()) { - case 'abstract': - if ($method->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') { - $this->raiseLintAtNode( - $modifier, - self::LINT_INVALID_MODIFIERS, - pht( - 'Properties cannot be declared %s.', - 'abstract')); - } - - if ($is_abstract) { - $this->raiseLintAtNode( - $modifier, - self::LINT_INVALID_MODIFIERS, - pht( - 'Multiple %s modifiers are not allowed.', - 'abstract')); - } - - if ($is_final) { - $this->raiseLintAtNode( - $modifier, - self::LINT_INVALID_MODIFIERS, - pht( - 'Cannot use the %s modifier on an %s class member', - 'final', - 'abstract')); - } - - $is_abstract = true; - break; - - case 'final': - if ($is_abstract) { - $this->raiseLintAtNode( - $modifier, - self::LINT_INVALID_MODIFIERS, - pht( - 'Cannot use the %s modifier on an %s class member', - 'final', - 'abstract')); - } - - if ($is_final) { - $this->raiseLintAtNode( - $modifier, - self::LINT_INVALID_MODIFIERS, - pht( - 'Multiple %s modifiers are not allowed.', - 'final')); - } - - $is_final = true; - break; - case 'public': - case 'protected': - case 'private': - if ($visibility) { - $this->raiseLintAtNode( - $modifier, - self::LINT_INVALID_MODIFIERS, - pht('Multiple access type modifiers are not allowed.')); - } - - $visibility = $modifier->getConcreteString(); - break; - - case 'static': - if ($is_static) { - $this->raiseLintAtNode( - $modifier, - self::LINT_INVALID_MODIFIERS, - pht( - 'Multiple %s modifiers are not allowed.', - 'static')); - } - break; - } + foreach ($this->rules as $rule) { + if ($this->isCodeEnabled($rule->getLintID())) { + $rule->setLinter($this); + $rule->process($root); } } } diff --git a/src/lint/linter/__tests__/xhpast/alias-functions.lint-test b/src/lint/linter/__tests__/xhpast/alias-functions.lint-test --- a/src/lint/linter/__tests__/xhpast/alias-functions.lint-test +++ b/src/lint/linter/__tests__/xhpast/alias-functions.lint-test @@ -3,7 +3,7 @@ $x = array(); sizeof($x); die(); -sizeOf($x); +sizeOf($x); // fixme ~~~~~~~~~~ advice:4:1 advice:5:1 @@ -14,4 +14,4 @@ $x = array(); count($x); exit(); -count($x); +sizeof($x); // fixme diff --git a/src/lint/linter/__tests__/xhpast/blacklisted.lint-test b/src/lint/linter/__tests__/xhpast/blacklisted.lint-test --- a/src/lint/linter/__tests__/xhpast/blacklisted.lint-test +++ b/src/lint/linter/__tests__/xhpast/blacklisted.lint-test @@ -4,4 +4,10 @@ error:2:1 ~~~~~~~~~~ ~~~~~~~~~~ -{"config": {"xhpast.blacklisted.function": {"eval": "Evil function"}}} +{ + "config": { + "xhpast.blacklisted.function": { + "eval": "Evil function" + } + } +} diff --git a/src/lint/linter/__tests__/xhpast/conditional-usage.lint-test b/src/lint/linter/__tests__/xhpast/conditional-usage.lint-test --- a/src/lint/linter/__tests__/xhpast/conditional-usage.lint-test +++ b/src/lint/linter/__tests__/xhpast/conditional-usage.lint-test @@ -26,4 +26,8 @@ warning:16:3 ~~~~~~~~~~ ~~~~~~~~~~ -{"config": {"xhpast.php-version": "5.3.0"}} +{ + "config": { + "xhpast.php-version": "5.3.0" + } +} diff --git a/src/lint/linter/__tests__/xhpast/index-function.lint-test b/src/lint/linter/__tests__/xhpast/index-function.lint-test --- a/src/lint/linter/__tests__/xhpast/index-function.lint-test +++ b/src/lint/linter/__tests__/xhpast/index-function.lint-test @@ -4,4 +4,8 @@ error:2:5 ~~~~~~~~~~ ~~~~~~~~~~ -{"config": {"xhpast.php-version": "5.3.0"}} +{ + "config": { + "xhpast.php-version": "5.3.0" + } +} diff --git a/src/lint/linter/__tests__/xhpast/nowdoc.lint-test b/src/lint/linter/__tests__/xhpast/nowdoc.lint-test --- a/src/lint/linter/__tests__/xhpast/nowdoc.lint-test +++ b/src/lint/linter/__tests__/xhpast/nowdoc.lint-test @@ -6,4 +6,8 @@ error:2:6 ~~~~~~~~~~ ~~~~~~~~~~ -{"config": {"xhpast.php-version": "5.2.3"}} +{ + "config": { + "xhpast.php-version": "5.2.3" + } +} diff --git a/src/lint/linter/__tests__/xhpast/php-compatibility.lint-test b/src/lint/linter/__tests__/xhpast/php-compatibility.lint-test --- a/src/lint/linter/__tests__/xhpast/php-compatibility.lint-test +++ b/src/lint/linter/__tests__/xhpast/php-compatibility.lint-test @@ -7,4 +7,8 @@ error:4:10 ~~~~~~~~~~ ~~~~~~~~~~ -{"config": {"xhpast.php-version": "5.4.0"}} +{ + "config": { + "xhpast.php-version": "5.4.0" + } +} diff --git a/src/lint/linter/__tests__/xhpast/php53-features.lint-test b/src/lint/linter/__tests__/xhpast/php53-features.lint-test --- a/src/lint/linter/__tests__/xhpast/php53-features.lint-test +++ b/src/lint/linter/__tests__/xhpast/php53-features.lint-test @@ -22,4 +22,8 @@ error:13:6 ~~~~~~~~~~ ~~~~~~~~~~ -{"config": {"xhpast.php-version": "5.2.3"}} +{ + "config": { + "xhpast.php-version": "5.2.3" + } +} diff --git a/src/lint/linter/__tests__/xhpast/php54-features.lint-test b/src/lint/linter/__tests__/xhpast/php54-features.lint-test --- a/src/lint/linter/__tests__/xhpast/php54-features.lint-test +++ b/src/lint/linter/__tests__/xhpast/php54-features.lint-test @@ -11,4 +11,8 @@ error:4:9 ~~~~~~~~~~ ~~~~~~~~~~ -{"config": {"xhpast.php-version": "5.3.0"}} +{ + "config": { + "xhpast.php-version": "5.3.0" + } +} diff --git a/src/lint/linter/__tests__/xhpast/switches.lint-test b/src/lint/linter/__tests__/xhpast/switches.lint-test --- a/src/lint/linter/__tests__/xhpast/switches.lint-test +++ b/src/lint/linter/__tests__/xhpast/switches.lint-test @@ -93,4 +93,8 @@ warning:75:3 ~~~~~~~~~~ ~~~~~~~~~~ -{"config":{"xhpast.switchhook":"ArcanistTestXHPASTLintSwitchHook"}} +{ + "config":{ + "xhpast.switchhook":"ArcanistTestXHPASTLintSwitchHook" + } +} diff --git a/src/lint/linter/xhpast/ArcanistXHPASTLinterRule.php b/src/lint/linter/xhpast/ArcanistXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/ArcanistXHPASTLinterRule.php @@ -0,0 +1,167 @@ +getConstant('ID'); + if ($const === false) { + throw new Exception( + pht( + '`%s` class `%s` must define an ID constant.', + __CLASS__, + get_class($this))); + } + + if (!is_int($const)) { + throw new Exception( + pht( + '`%s` class `%s` has an invalid ID constant. ID must be an integer.', + __CLASS__, + get_class($this))); + } + + return $const; + } + + abstract public function getLintName(); + + public function getLintSeverity() { + return ArcanistLintSeverity::SEVERITY_ERROR; + } + + public function getLinterConfigurationOptions() { + return array(); + } + + public function setLinterConfigurationValue($key, $value) {} + + abstract public function process(XHPASTNode $root); + + final public function setLinter(ArcanistXHPASTLinter $linter) { + $this->linter = $linter; + } + + /** + * Statically evaluate a boolean value from an XHP tree. + * + * TODO: Improve this and move it to XHPAST proper? + * + * @param string The "semantic string" of a single value. + * @return mixed `true` or `false` if the value could be evaluated + * statically; `null` if static evaluation was not possible. + */ + protected function evaluateStaticBoolean($string) { + switch (strtolower($string)) { + case '0': + case 'null': + case 'false': + return false; + case '1': + case 'true': + return true; + } + return null; + } + + protected function getConcreteVariableString(XHPASTNode $var) { + $concrete = $var->getConcreteString(); + // Strip off curly braces as in `$obj->{$property}`. + $concrete = trim($concrete, '{}'); + return $concrete; + } + + // These methods are proxied to the @{class:ArcanistLinter}. + + final public function getActivePath() { + return $this->linter->getActivePath(); + } + + final public function getOtherLocation($offset, $path = null) { + return $this->linter->getOtherLocation($offset, $path); + } + + final protected function raiseLintAtNode( + XHPASTNode $node, + $desc, + $replace = null) { + + return $this->linter->raiseLintAtNode( + $node, + $this->getLintID(), + $desc, + $replace); + } + + final public function raiseLintAtOffset( + $offset, + $desc, + $text = null, + $replace = null) { + + return $this->linter->raiseLintAtOffset( + $offset, + $this->getLintID(), + $desc, + $text, + $replace); + } + + final protected function raiseLintAtToken( + XHPASTToken $token, + $desc, + $replace = null) { + + return $this->linter->raiseLintAtToken( + $token, + $this->getLintID(), + $desc, + $replace); + } + +/* -( Utility )------------------------------------------------------------ */ + + /** + * Retrieve all calls to some specified function(s). + * + * Returns all descendant nodes which represent a function call to one of the + * specified functions. + * + * @param XHPASTNode Root node. + * @param list Function names. + * @return AASTNodeList + */ + protected function getFunctionCalls(XHPASTNode $root, array $function_names) { + $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); + $nodes = array(); + + foreach ($calls as $call) { + $node = $call->getChildByIndex(0); + $name = strtolower($node->getConcreteString()); + + if (in_array($name, $function_names)) { + $nodes[] = $call; + } + } + + return AASTNodeList::newFromTreeAndNodes($root->getTree(), $nodes); + } + + public function getSuperGlobalNames() { + return array( + '$GLOBALS', + '$_SERVER', + '$_GET', + '$_POST', + '$_FILES', + '$_COOKIE', + '$_SESSION', + '$_REQUEST', + '$_ENV', + ); + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistAliasFunctionXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistAliasFunctionXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistAliasFunctionXHPASTLinterRule.php @@ -0,0 +1,222 @@ +getFunctionAliases(); + $functions = $this->getFunctionCalls($root, array_keys($aliases)); + + foreach ($functions as $function) { + $function_name = $function->getChildByIndex(0); + + $this->raiseLintAtNode( + $function_name, + pht('Alias functions should be avoided.'), + $aliases[phutil_utf8_strtolower($function_name->getConcreteString())]); + } + } + + public function getFunctionAliases() { + return array( + '_' => 'gettext', + 'chop' => 'rtrim', + 'close' => 'closedir', + 'com_get' => 'com_propget', + 'com_propset' => 'com_propput', + 'com_set' => 'com_propput', + 'die' => 'exit', + 'diskfreespace' => 'disk_free_space', + 'doubleval' => 'floatval', + 'drawarc' => 'swfshape_drawarc', + 'drawcircle' => 'swfshape_drawcircle', + 'drawcubic' => 'swfshape_drawcubic', + 'drawcubicto' => 'swfshape_drawcubicto', + 'drawcurve' => 'swfshape_drawcurve', + 'drawcurveto' => 'swfshape_drawcurveto', + 'drawglyph' => 'swfshape_drawglyph', + 'drawline' => 'swfshape_drawline', + 'drawlineto' => 'swfshape_drawlineto', + 'fbsql' => 'fbsql_db_query', + 'fputs' => 'fwrite', + 'gzputs' => 'gzwrite', + 'i18n_convert' => 'mb_convert_encoding', + 'i18n_discover_encoding' => 'mb_detect_encoding', + 'i18n_http_input' => 'mb_http_input', + 'i18n_http_output' => 'mb_http_output', + 'i18n_internal_encoding' => 'mb_internal_encoding', + 'i18n_ja_jp_hantozen' => 'mb_convert_kana', + 'i18n_mime_header_decode' => 'mb_decode_mimeheader', + 'i18n_mime_header_encode' => 'mb_encode_mimeheader', + 'imap_create' => 'imap_createmailbox', + 'imap_fetchtext' => 'imap_body', + 'imap_getmailboxes' => 'imap_list_full', + 'imap_getsubscribed' => 'imap_lsub_full', + 'imap_header' => 'imap_headerinfo', + 'imap_listmailbox' => 'imap_list', + 'imap_listsubscribed' => 'imap_lsub', + 'imap_rename' => 'imap_renamemailbox', + 'imap_scan' => 'imap_listscan', + 'imap_scanmailbox' => 'imap_listscan', + 'ini_alter' => 'ini_set', + 'is_double' => 'is_float', + 'is_integer' => 'is_int', + 'is_long' => 'is_int', + 'is_real' => 'is_float', + 'is_writeable' => 'is_writable', + 'join' => 'implode', + 'key_exists' => 'array_key_exists', + 'ldap_close' => 'ldap_unbind', + 'magic_quotes_runtime' => 'set_magic_quotes_runtime', + 'mbstrcut' => 'mb_strcut', + 'mbstrlen' => 'mb_strlen', + 'mbstrpos' => 'mb_strpos', + 'mbstrrpos' => 'mb_strrpos', + 'mbsubstr' => 'mb_substr', + 'ming_setcubicthreshold' => 'ming_setCubicThreshold', + 'ming_setscale' => 'ming_setScale', + 'msql' => 'msql_db_query', + 'msql_createdb' => 'msql_create_db', + 'msql_dbname' => 'msql_result', + 'msql_dropdb' => 'msql_drop_db', + 'msql_fieldflags' => 'msql_field_flags', + 'msql_fieldlen' => 'msql_field_len', + 'msql_fieldname' => 'msql_field_name', + 'msql_fieldtable' => 'msql_field_table', + 'msql_fieldtype' => 'msql_field_type', + 'msql_freeresult' => 'msql_free_result', + 'msql_listdbs' => 'msql_list_dbs', + 'msql_listfields' => 'msql_list_fields', + 'msql_listtables' => 'msql_list_tables', + 'msql_numfields' => 'msql_num_fields', + 'msql_numrows' => 'msql_num_rows', + 'msql_regcase' => 'sql_regcase', + 'msql_selectdb' => 'msql_select_db', + 'msql_tablename' => 'msql_result', + 'mssql_affected_rows' => 'sybase_affected_rows', + 'mssql_close' => 'sybase_close', + 'mssql_connect' => 'sybase_connect', + 'mssql_data_seek' => 'sybase_data_seek', + 'mssql_fetch_array' => 'sybase_fetch_array', + 'mssql_fetch_field' => 'sybase_fetch_field', + 'mssql_fetch_object' => 'sybase_fetch_object', + 'mssql_fetch_row' => 'sybase_fetch_row', + 'mssql_field_seek' => 'sybase_field_seek', + 'mssql_free_result' => 'sybase_free_result', + 'mssql_get_last_message' => 'sybase_get_last_message', + 'mssql_min_client_severity' => 'sybase_min_client_severity', + 'mssql_min_error_severity' => 'sybase_min_error_severity', + 'mssql_min_message_severity' => 'sybase_min_message_severity', + 'mssql_min_server_severity' => 'sybase_min_server_severity', + 'mssql_num_fields' => 'sybase_num_fields', + 'mssql_num_rows' => 'sybase_num_rows', + 'mssql_pconnect' => 'sybase_pconnect', + 'mssql_query' => 'sybase_query', + 'mssql_result' => 'sybase_result', + 'mssql_select_db' => 'sybase_select_db', + 'multcolor' => 'swfdisplayitem_multColor', + 'mysql' => 'mysql_db_query', + 'mysql_createdb' => 'mysql_create_db', + 'mysql_db_name' => 'mysql_result', + 'mysql_dbname' => 'mysql_result', + 'mysql_dropdb' => 'mysql_drop_db', + 'mysql_fieldflags' => 'mysql_field_flags', + 'mysql_fieldlen' => 'mysql_field_len', + 'mysql_fieldname' => 'mysql_field_name', + 'mysql_fieldtable' => 'mysql_field_table', + 'mysql_fieldtype' => 'mysql_field_type', + 'mysql_freeresult' => 'mysql_free_result', + 'mysql_listdbs' => 'mysql_list_dbs', + 'mysql_listfields' => 'mysql_list_fields', + 'mysql_listtables' => 'mysql_list_tables', + 'mysql_numfields' => 'mysql_num_fields', + 'mysql_numrows' => 'mysql_num_rows', + 'mysql_selectdb' => 'mysql_select_db', + 'mysql_tablename' => 'mysql_result', + 'ociassignelem' => 'OCI-Collection::assignElem', + 'ocibindbyname' => 'oci_bind_by_name', + 'ocicancel' => 'oci_cancel', + 'ocicloselob' => 'OCI-Lob::close', + 'ocicollappend' => 'OCI-Collection::append', + 'ocicollassign' => 'OCI-Collection::assign', + 'ocicollmax' => 'OCI-Collection::max', + 'ocicollsize' => 'OCI-Collection::size', + 'ocicolltrim' => 'OCI-Collection::trim', + 'ocicolumnisnull' => 'oci_field_is_null', + 'ocicolumnname' => 'oci_field_name', + 'ocicolumnprecision' => 'oci_field_precision', + 'ocicolumnscale' => 'oci_field_scale', + 'ocicolumnsize' => 'oci_field_size', + 'ocicolumntype' => 'oci_field_type', + 'ocicolumntyperaw' => 'oci_field_type_raw', + 'ocicommit' => 'oci_commit', + 'ocidefinebyname' => 'oci_define_by_name', + 'ocierror' => 'oci_error', + 'ociexecute' => 'oci_execute', + 'ocifetch' => 'oci_fetch', + 'ocifetchinto' => 'oci_fetch_array(),', + 'ocifetchstatement' => 'oci_fetch_all', + 'ocifreecollection' => 'OCI-Collection::free', + 'ocifreecursor' => 'oci_free_statement', + 'ocifreedesc' => 'oci_free_descriptor', + 'ocifreestatement' => 'oci_free_statement', + 'ocigetelem' => 'OCI-Collection::getElem', + 'ociinternaldebug' => 'oci_internal_debug', + 'ociloadlob' => 'OCI-Lob::load', + 'ocilogon' => 'oci_connect', + 'ocinewcollection' => 'oci_new_collection', + 'ocinewcursor' => 'oci_new_cursor', + 'ocinewdescriptor' => 'oci_new_descriptor', + 'ocinlogon' => 'oci_new_connect', + 'ocinumcols' => 'oci_num_fields', + 'ociparse' => 'oci_parse', + 'ocipasswordchange' => 'oci_password_change', + 'ociplogon' => 'oci_pconnect', + 'ociresult' => 'oci_result', + 'ocirollback' => 'oci_rollback', + 'ocisavelob' => 'OCI-Lob::save', + 'ocisavelobfile' => 'OCI-Lob::import', + 'ociserverversion' => 'oci_server_version', + 'ocisetprefetch' => 'oci_set_prefetch', + 'ocistatementtype' => 'oci_statement_type', + 'ociwritelobtofile' => 'OCI-Lob::export', + 'ociwritetemporarylob' => 'OCI-Lob::writeTemporary', + 'odbc_do' => 'odbc_exec', + 'odbc_field_precision' => 'odbc_field_len', + 'pdf_add_outline' => 'pdf_add_bookmark', + 'pg_clientencoding' => 'pg_client_encoding', + 'pg_setclientencoding' => 'pg_set_client_encoding', + 'pos' => 'current', + 'recode' => 'recode_string', + 'show_source' => 'highlight_file', + 'sizeof' => 'count', + 'snmpwalkoid' => 'snmprealwalk', + 'strchr' => 'strstr', + 'streammp3' => 'swfmovie_streamMp3', + 'swfaction' => 'swfaction_init', + 'swfbitmap' => 'swfbitmap_init', + 'swfbutton' => 'swfbutton_init', + 'swffill' => 'swffill_init', + 'swffont' => 'swffont_init', + 'swfgradient' => 'swfgradient_init', + 'swfmorph' => 'swfmorph_init', + 'swfmovie' => 'swfmovie_init', + 'swfshape' => 'swfshape_init', + 'swfsprite' => 'swfsprite_init', + 'swftext' => 'swftext_init', + 'swftextfield' => 'swftextfield_init', + 'xptr_new_context' => 'xpath_new_context', + ); + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistArrayIndexSpacingXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistArrayIndexSpacingXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistArrayIndexSpacingXHPASTLinterRule.php @@ -0,0 +1,35 @@ +selectDescendantsOfType('n_INDEX_ACCESS'); + + foreach ($indexes as $index) { + $tokens = $index->getChildByIndex(0)->getTokens(); + $last = array_pop($tokens); + $trailing = $last->getNonsemanticTokensAfter(); + $trailing_text = implode('', mpull($trailing, 'getValue')); + + if (preg_match('/^ +$/', $trailing_text)) { + $this->raiseLintAtOffset( + $last->getOffset() + strlen($last->getValue()), + pht('Convention: no spaces before index access.'), + $trailing_text, + ''); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistArraySeparatorXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistArraySeparatorXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistArraySeparatorXHPASTLinterRule.php @@ -0,0 +1,72 @@ +selectDescendantsOfType('n_ARRAY_LITERAL'); + + foreach ($arrays as $array) { + $value_list = $array->getChildOfType(0, 'n_ARRAY_VALUE_LIST'); + $values = $value_list->getChildrenOfType('n_ARRAY_VALUE'); + + if (!$values) { + // There is no need to check an empty array. + continue; + } + + $multiline = $array->getLineNumber() != $array->getEndLineNumber(); + + $value = last($values); + $after = last($value->getTokens())->getNextToken(); + + if ($multiline) { + if (!$after || $after->getValue() != ',') { + if ($value->getChildByIndex(1)->getTypeName() == 'n_HEREDOC') { + continue; + } + + list($before, $after) = $value->getSurroundingNonsemanticTokens(); + $after = implode('', mpull($after, 'getValue')); + + $original = $value->getConcreteString(); + $replacement = $value->getConcreteString().','; + + if (strpos($after, "\n") === false) { + $original .= $after; + $replacement .= $after."\n".$array->getIndentation(); + } + + $this->raiseLintAtOffset( + $value->getOffset(), + pht('Multi-lined arrays should have trailing commas.'), + $original, + $replacement); + } else if ($value->getLineNumber() == $array->getEndLineNumber()) { + $close = last($array->getTokens()); + + $this->raiseLintAtToken( + $close, + pht('Closing parenthesis should be on a new line.'), + "\n".$array->getIndentation().$close->getValue()); + } + } else if ($after && $after->getValue() == ',') { + $this->raiseLintAtToken( + $after, + pht('Single lined arrays should not have a trailing comma.'), + ''); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistBinaryExpressionSpacingXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistBinaryExpressionSpacingXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistBinaryExpressionSpacingXHPASTLinterRule.php @@ -0,0 +1,117 @@ +selectDescendantsOfType('n_BINARY_EXPRESSION'); + + foreach ($expressions as $expression) { + $operator = $expression->getChildByIndex(1); + $operator_value = $operator->getConcreteString(); + list($before, $after) = $operator->getSurroundingNonsemanticTokens(); + + $replace = null; + if (empty($before) && empty($after)) { + $replace = " {$operator_value} "; + } else if (empty($before)) { + $replace = " {$operator_value}"; + } else if (empty($after)) { + $replace = "{$operator_value} "; + } + + if ($replace !== null) { + $this->raiseLintAtNode( + $operator, + pht( + 'Convention: logical and arithmetic operators should be '. + 'surrounded by whitespace.'), + $replace); + } + } + + $tokens = $root->selectTokensOfType(','); + foreach ($tokens as $token) { + $next = $token->getNextToken(); + switch ($next->getTypeName()) { + case ')': + case 'T_WHITESPACE': + break; + default: + $this->raiseLintAtToken( + $token, + pht('Convention: comma should be followed by space.'), + ', '); + break; + } + } + + $tokens = $root->selectTokensOfType('T_DOUBLE_ARROW'); + foreach ($tokens as $token) { + $prev = $token->getPrevToken(); + $next = $token->getNextToken(); + + $prev_type = $prev->getTypeName(); + $next_type = $next->getTypeName(); + + $prev_space = ($prev_type === 'T_WHITESPACE'); + $next_space = ($next_type === 'T_WHITESPACE'); + + $replace = null; + if (!$prev_space && !$next_space) { + $replace = ' => '; + } else if ($prev_space && !$next_space) { + $replace = '=> '; + } else if (!$prev_space && $next_space) { + $replace = ' =>'; + } + + if ($replace !== null) { + $this->raiseLintAtToken( + $token, + pht('Convention: double arrow should be surrounded by whitespace.'), + $replace); + } + } + + $parameters = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER'); + foreach ($parameters as $parameter) { + if ($parameter->getChildByIndex(2)->getTypeName() == 'n_EMPTY') { + continue; + } + + $operator = head($parameter->selectTokensOfType('=')); + $before = $operator->getNonsemanticTokensBefore(); + $after = $operator->getNonsemanticTokensAfter(); + + $replace = null; + if (empty($before) && empty($after)) { + $replace = ' = '; + } else if (empty($before)) { + $replace = ' ='; + } else if (empty($after)) { + $replace = '= '; + } + + if ($replace !== null) { + $this->raiseLintAtToken( + $operator, + pht( + 'Convention: logical and arithmetic operators should be '. + 'surrounded by whitespace.'), + $replace); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistBlacklistedFunctionXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistBlacklistedFunctionXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistBlacklistedFunctionXHPASTLinterRule.php @@ -0,0 +1,49 @@ + array( + 'type' => 'optional map', + 'help' => pht('Blacklisted functions which should not be used.'), + ), + ); + } + + public function setLinterConfigurationValue($key, $value) { + switch ($key) { + case 'xhpast.blacklisted.function': + $this->blacklistedFunctions = $value; + return; + + default: + return parent::getLinterConfigurationOptions(); + } + } + + public function process(XHPASTNode $root) { + $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); + + foreach ($calls as $call) { + $node = $call->getChildByIndex(0); + $name = $node->getConcreteString(); + + $reason = idx($this->blacklistedFunctions, $name); + + if ($reason) { + $this->raiseLintAtNode($node, $reason); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistBraceFormattingXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistBraceFormattingXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistBraceFormattingXHPASTLinterRule.php @@ -0,0 +1,103 @@ +selectDescendantsOfType('n_STATEMENT_LIST') as $list) { + $tokens = $list->getTokens(); + if (!$tokens || head($tokens)->getValue() != '{') { + continue; + } + list($before, $after) = $list->getSurroundingNonsemanticTokens(); + if (!$before) { + $first = head($tokens); + + // Only insert the space if we're after a closing parenthesis. If + // we're in a construct like "else{}", other rules will insert space + // after the 'else' correctly. + $prev = $first->getPrevToken(); + if (!$prev || $prev->getValue() !== ')') { + continue; + } + + $this->raiseLintAtToken( + $first, + pht( + 'Put opening braces on the same line as control statements and '. + 'declarations, with a single space before them.'), + ' '.$first->getValue()); + } else if (count($before) === 1) { + $before = reset($before); + if ($before->getValue() !== ' ') { + $this->raiseLintAtToken( + $before, + pht( + 'Put opening braces on the same line as control statements and '. + 'declarations, with a single space before them.'), + ' '); + } + } + } + + $nodes = $root->selectDescendantsOfType('n_STATEMENT'); + foreach ($nodes as $node) { + $parent = $node->getParentNode(); + + if (!$parent) { + continue; + } + + $type = $parent->getTypeName(); + if ($type != 'n_STATEMENT_LIST' && $type != 'n_DECLARE') { + $this->raiseLintAtNode( + $node, + pht('Use braces to surround a statement block.')); + } + } + + $nodes = $root->selectDescendantsOfTypes(array( + 'n_DO_WHILE', + 'n_ELSE', + 'n_ELSEIF', + )); + foreach ($nodes as $list) { + $tokens = $list->getTokens(); + if (!$tokens || last($tokens)->getValue() != '}') { + continue; + } + list($before, $after) = $list->getSurroundingNonsemanticTokens(); + if (!$before) { + $first = last($tokens); + + $this->raiseLintAtToken( + $first, + pht( + 'Put opening braces on the same line as control statements and '. + 'declarations, with a single space before them.'), + ' '.$first->getValue()); + } else if (count($before) === 1) { + $before = reset($before); + if ($before->getValue() !== ' ') { + $this->raiseLintAtToken( + $before, + pht( + 'Put opening braces on the same line as control statements and '. + 'declarations, with a single space before them.'), + ' '); + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistCallTimePassByReferenceXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistCallTimePassByReferenceXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistCallTimePassByReferenceXHPASTLinterRule.php @@ -0,0 +1,26 @@ +selectDescendantsOfType('n_CALL_PARAMETER_LIST'); + + foreach ($nodes as $node) { + $parameters = $node->getChildrenOfType('n_VARIABLE_REFERENCE'); + + foreach ($parameters as $parameter) { + $this->raiseLintAtNode( + $parameter, + pht('Call-time pass-by-reference calls are prohibited.')); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistCastSpacingXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistCastSpacingXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistCastSpacingXHPASTLinterRule.php @@ -0,0 +1,34 @@ +selectDescendantsOfType('n_CAST_EXPRESSION'); + + foreach ($cast_expressions as $cast_expression) { + $cast = $cast_expression->getChildOfType(0, 'n_CAST'); + + list($before, $after) = $cast->getSurroundingNonsemanticTokens(); + $after = head($after); + + if ($after) { + $this->raiseLintAtToken( + $after, + pht('A cast statement must not be followed by a space.'), + ''); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistClassFilenameMismatchXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistClassFilenameMismatchXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistClassFilenameMismatchXHPASTLinterRule.php @@ -0,0 +1,54 @@ +selectDescendantsOfType('n_CLASS_DECLARATION'); + $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); + + if (count($classes) + count($interfaces) !== 1) { + return; + } + + $declarations = count($classes) ? $classes : $interfaces; + $declarations->rewind(); + $declaration = $declarations->current(); + + $decl_name = $declaration->getChildByIndex(1); + $decl_string = $decl_name->getConcreteString(); + + // Exclude strangely named classes, e.g. XHP tags. + if (!preg_match('/^\w+$/', $decl_string)) { + return; + } + + $rename = $decl_string.'.php'; + + $path = $this->getActivePath(); + $filename = basename($path); + + if ($rename === $filename) { + return; + } + + $this->raiseLintAtNode( + $decl_name, + pht( + "The name of this file differs from the name of the ". + "class or interface it declares. Rename the file to '%s'.", + $rename)); + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistClassNameLiteralXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistClassNameLiteralXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistClassNameLiteralXHPASTLinterRule.php @@ -0,0 +1,49 @@ +selectDescendantsOfType('n_CLASS_DECLARATION'); + + foreach ($class_declarations as $class_declaration) { + $class_name = $class_declaration + ->getChildOfType(1, 'n_CLASS_NAME') + ->getConcreteString(); + + $strings = $class_declaration->selectDescendantsOfType('n_STRING_SCALAR'); + + foreach ($strings as $string) { + $contents = substr($string->getSemanticString(), 1, -1); + $replacement = null; + + if ($contents == $class_name) { + $replacement = '__CLASS__'; + } + + $regex = '/\b'.preg_quote($class_name, '/').'\b/'; + if (!preg_match($regex, $contents)) { + continue; + } + + $this->raiseLintAtNode( + $string, + pht( + "Don't hard-code class names, use %s instead.", + '__CLASS__'), + $replacement); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistClosingCallParenthesesXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistClosingCallParenthesesXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistClosingCallParenthesesXHPASTLinterRule.php @@ -0,0 +1,50 @@ +selectDescendantsOfTypes(array( + 'n_FUNCTION_CALL', + 'n_METHOD_CALL', + )); + + foreach ($calls as $call) { + // If the last parameter of a call is a HEREDOC, don't apply this rule. + $params = $call + ->getChildOfType(1, 'n_CALL_PARAMETER_LIST') + ->getChildren(); + + if ($params) { + $last_param = last($params); + if ($last_param->getTypeName() === 'n_HEREDOC') { + continue; + } + } + + $tokens = $call->getTokens(); + $last = array_pop($tokens); + + $trailing = $last->getNonsemanticTokensBefore(); + $trailing_text = implode('', mpull($trailing, 'getValue')); + if (preg_match('/^\s+$/', $trailing_text)) { + $this->raiseLintAtOffset( + $last->getOffset() - strlen($trailing_text), + pht('Convention: no spaces before closing parenthesis in calls.'), + $trailing_text, + ''); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistClosingDeclarationParenthesesXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistClosingDeclarationParenthesesXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistClosingDeclarationParenthesesXHPASTLinterRule.php @@ -0,0 +1,42 @@ +selectDescendantsOfTypes(array( + 'n_FUNCTION_DECLARATION', + 'n_METHOD_DECLARATION', + )); + + foreach ($decs as $dec) { + $params = $dec->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST'); + $tokens = $params->getTokens(); + $last = array_pop($tokens); + + $trailing = $last->getNonsemanticTokensBefore(); + $trailing_text = implode('', mpull($trailing, 'getValue')); + + if (preg_match('/^\s+$/', $trailing_text)) { + $this->raiseLintAtOffset( + $last->getOffset() - strlen($trailing_text), + pht( + 'Convention: no spaces before closing parenthesis in '. + 'function and method declarations.'), + $trailing_text, + ''); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistCommentSpacingXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistCommentSpacingXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistCommentSpacingXHPASTLinterRule.php @@ -0,0 +1,34 @@ +selectTokensOfType('T_COMMENT') as $comment) { + $value = $comment->getValue(); + + if ($value[0] !== '#') { + $match = null; + + if (preg_match('@^(/[/*]+)[^/*\s]@', $value, $match)) { + $this->raiseLintAtOffset( + $comment->getOffset(), + pht('Put space after comment start.'), + $match[1], + $match[1].' '); + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistCommentStyleXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistCommentStyleXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistCommentStyleXHPASTLinterRule.php @@ -0,0 +1,28 @@ +selectTokensOfType('T_COMMENT') as $comment) { + $value = $comment->getValue(); + + if ($value[0] !== '#') { + continue; + } + + $this->raiseLintAtOffset( + $comment->getOffset(), + pht('Use "%s" single-line comments, not "%s".', '//', '#'), + '#', + preg_match('/^#\S/', $value) ? '// ' : '//'); + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistConcatenationOperatorXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistConcatenationOperatorXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistConcatenationOperatorXHPASTLinterRule.php @@ -0,0 +1,46 @@ +selectTokensOfType('.'); + foreach ($tokens as $token) { + $prev = $token->getPrevToken(); + $next = $token->getNextToken(); + + foreach (array('prev' => $prev, 'next' => $next) as $wtoken) { + if ($wtoken->getTypeName() !== 'T_WHITESPACE') { + continue; + } + + $value = $wtoken->getValue(); + if (strpos($value, "\n") !== false) { + // If the whitespace has a newline, it's conventional. + continue; + } + + $next = $wtoken->getNextToken(); + if ($next && $next->getTypeName() === 'T_COMMENT') { + continue; + } + + $this->raiseLintAtToken( + $wtoken, + pht('Convention: no spaces around string concatenation operator.'), + ''); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistConstructorParenthesesXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistConstructorParenthesesXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistConstructorParenthesesXHPASTLinterRule.php @@ -0,0 +1,32 @@ +selectDescendantsOfType('n_NEW'); + + foreach ($nodes as $node) { + $class = $node->getChildByIndex(0); + $params = $node->getChildByIndex(1); + + if ($params->getTypeName() == 'n_EMPTY') { + $this->raiseLintAtNode( + $class, + pht('Use parentheses when invoking a constructor.'), + $class->getConcreteString().'()'); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistControlStatementSpacingXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistControlStatementSpacingXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistControlStatementSpacingXHPASTLinterRule.php @@ -0,0 +1,61 @@ +getTokens() as $id => $token) { + switch ($token->getTypeName()) { + case 'T_IF': + case 'T_ELSE': + case 'T_FOR': + case 'T_FOREACH': + case 'T_WHILE': + case 'T_DO': + case 'T_SWITCH': + $after = $token->getNonsemanticTokensAfter(); + if (empty($after)) { + $this->raiseLintAtToken( + $token, + pht('Convention: put a space after control statements.'), + $token->getValue().' '); + } else if (count($after) === 1) { + $space = head($after); + + // If we have an else clause with braces, $space may not be + // a single white space. e.g., + // + // if ($x) + // echo 'foo' + // else // <- $space is not " " but "\n ". + // echo 'bar' + // + // We just require it starts with either a whitespace or a newline. + if ($token->getTypeName() === 'T_ELSE' || + $token->getTypeName() === 'T_DO') { + break; + } + + if ($space->isAnyWhitespace() && $space->getValue() !== ' ') { + $this->raiseLintAtToken( + $space, + pht('Convention: put a single space after control statements.'), + ' '); + } + } + break; + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistDefaultParametersXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistDefaultParametersXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistDefaultParametersXHPASTLinterRule.php @@ -0,0 +1,41 @@ +selectDescendantsOfType( + 'n_DECLARATION_PARAMETER_LIST'); + + foreach ($parameter_lists as $parameter_list) { + $default_found = false; + $parameters = $parameter_list->selectDescendantsOfType( + 'n_DECLARATION_PARAMETER'); + + foreach ($parameters as $parameter) { + $default_value = $parameter->getChildByIndex(2); + + if ($default_value->getTypeName() != 'n_EMPTY') { + $default_found = true; + } else if ($default_found) { + $this->raiseLintAtNode( + $parameter_list, + pht( + 'Arguments with default values must be at the end '. + 'of the argument list.')); + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistDoubleQuoteXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistDoubleQuoteXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistDoubleQuoteXHPASTLinterRule.php @@ -0,0 +1,95 @@ +selectDescendantsOfTypes(array( + 'n_CONCATENATION_LIST', + 'n_STRING_SCALAR', + )); + + foreach ($nodes as $node) { + $strings = array(); + + if ($node->getTypeName() === 'n_CONCATENATION_LIST') { + $strings = $node->selectDescendantsOfType('n_STRING_SCALAR'); + } else if ($node->getTypeName() === 'n_STRING_SCALAR') { + $strings = array($node); + + if ($node->getParentNode()->getTypeName() === 'n_CONCATENATION_LIST') { + continue; + } + } + + $valid = false; + $invalid_nodes = array(); + $fixes = array(); + + foreach ($strings as $string) { + $concrete_string = $string->getConcreteString(); + $single_quoted = ($concrete_string[0] === "'"); + $contents = substr($concrete_string, 1, -1); + + // Double quoted strings are allowed when the string contains the + // following characters. + static $allowed_chars = array( + '\n', + '\r', + '\t', + '\v', + '\e', + '\f', + '\'', + '\0', + '\1', + '\2', + '\3', + '\4', + '\5', + '\6', + '\7', + '\x', + ); + + $contains_special_chars = false; + foreach ($allowed_chars as $allowed_char) { + if (strpos($contents, $allowed_char) !== false) { + $contains_special_chars = true; + } + } + + if (!$string->isConstantString()) { + $valid = true; + } else if ($contains_special_chars && !$single_quoted) { + $valid = true; + } else if (!$contains_special_chars && !$single_quoted) { + $invalid_nodes[] = $string; + $fixes[$string->getID()] = "'".str_replace('\"', '"', $contents)."'"; + } + } + + if (!$valid) { + foreach ($invalid_nodes as $invalid_node) { + $this->raiseLintAtNode( + $invalid_node, + pht( + 'String does not require double quotes. For consistency, '. + 'prefer single quotes.'), + $fixes[$invalid_node->getID()]); + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistDuplicateKeysInArrayXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistDuplicateKeysInArrayXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistDuplicateKeysInArrayXHPASTLinterRule.php @@ -0,0 +1,74 @@ + 'anything', 1 => 'foo')`. Since the first entry is ignored, this + * is almost certainly an error. + */ +final class ArcanistDuplicateKeysInArrayXHPASTLinterRule + extends ArcanistXHPASTLinterRule { + + const ID = 22; + + public function getLintName() { + return pht('Duplicate Keys in Array'); + } + + public function process(XHPASTNode $root) { + $array_literals = $root->selectDescendantsOfType('n_ARRAY_LITERAL'); + + foreach ($array_literals as $array_literal) { + $nodes_by_key = array(); + $keys_warn = array(); + $list_node = $array_literal->getChildByIndex(0); + + foreach ($list_node->getChildren() as $array_entry) { + $key_node = $array_entry->getChildByIndex(0); + + switch ($key_node->getTypeName()) { + case 'n_STRING_SCALAR': + case 'n_NUMERIC_SCALAR': + // Scalars: array(1 => 'v1', '1' => 'v2'); + $key = 'scalar:'.(string)$key_node->evalStatic(); + break; + + case 'n_SYMBOL_NAME': + case 'n_VARIABLE': + case 'n_CLASS_STATIC_ACCESS': + // Constants: array(CONST => 'v1', CONST => 'v2'); + // Variables: array($a => 'v1', $a => 'v2'); + // Class constants and vars: array(C::A => 'v1', C::A => 'v2'); + $key = $key_node->getTypeName().':'.$key_node->getConcreteString(); + break; + + default: + $key = null; + break; + } + + if ($key !== null) { + if (isset($nodes_by_key[$key])) { + $keys_warn[$key] = true; + } + $nodes_by_key[$key][] = $key_node; + } + } + + foreach ($keys_warn as $key => $_) { + $node = array_pop($nodes_by_key[$key]); + $message = $this->raiseLintAtNode( + $node, + pht( + 'Duplicate key in array initializer. '. + 'PHP will ignore all but the last entry.')); + + $locations = array(); + foreach ($nodes_by_key[$key] as $node) { + $locations[] = $this->getOtherLocation($node->getOffset()); + } + $message->setOtherLocations($locations); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistDuplicateSwitchCaseXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistDuplicateSwitchCaseXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistDuplicateSwitchCaseXHPASTLinterRule.php @@ -0,0 +1,49 @@ +selectDescendantsOfType('n_SWITCH'); + + foreach ($switch_statements as $switch_statement) { + $case_statements = $switch_statement + ->getChildOfType(1, 'n_STATEMENT_LIST') + ->getChildrenOfType('n_CASE'); + $nodes_by_case = array(); + + foreach ($case_statements as $case_statement) { + $case = $case_statement + ->getChildByIndex(0) + ->getSemanticString(); + $nodes_by_case[$case][] = $case_statement; + } + + foreach ($nodes_by_case as $case => $nodes) { + if (count($nodes) <= 1) { + continue; + } + + $node = array_pop($nodes_by_case[$case]); + $message = $this->raiseLintAtNode( + $node, + pht( + 'Duplicate case in switch statement. PHP will ignore all '. + 'but the first case.')); + + $locations = array(); + foreach ($nodes_by_case[$case] as $node) { + $locations[] = $this->getOtherLocation($node->getOffset()); + } + $message->setOtherLocations($locations); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistDynamicDefineXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistDynamicDefineXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistDynamicDefineXHPASTLinterRule.php @@ -0,0 +1,29 @@ +getFunctionCalls($root, array('define')); + + foreach ($calls as $call) { + $parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); + $defined = $parameter_list->getChildByIndex(0); + + if (!$defined->isStaticScalar()) { + $this->raiseLintAtNode( + $defined, + pht( + 'First argument to %s must be a string literal.', + 'define()')); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistElseIfUsageXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistElseIfUsageXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistElseIfUsageXHPASTLinterRule.php @@ -0,0 +1,27 @@ +selectTokensOfType('T_ELSEIF'); + + foreach ($tokens as $token) { + $this->raiseLintAtToken( + $token, + pht('Usage of `%s` is preferred over `%s`.', 'else if', 'elseif'), + 'else if'); + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistEmptyStatementXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistEmptyStatementXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistEmptyStatementXHPASTLinterRule.php @@ -0,0 +1,51 @@ +selectDescendantsOfType('n_STATEMENT_LIST'); + + foreach ($nodes as $node) { + $tokens = $node->getTokens(); + $token = head($tokens); + + if (count($tokens) <= 2) { + continue; + } + + // Safety check... if the first token isn't an opening brace then + // there's nothing to do here. + if ($token->getTypeName() != '{') { + continue; + } + + $only_whitespace = true; + for ($token = $token->getNextToken(); + $token && $token->getTypeName() != '}'; + $token = $token->getNextToken()) { + $only_whitespace = $only_whitespace && $token->isAnyWhitespace(); + } + + if (count($tokens) > 2 && $only_whitespace) { + $this->raiseLintAtNode( + $node, + pht( + "Braces for an empty block statement shouldn't ". + "contain only whitespace."), + '{}'); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistExitExpressionXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistExitExpressionXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistExitExpressionXHPASTLinterRule.php @@ -0,0 +1,43 @@ +selectDescendantsOfType('n_UNARY_PREFIX_EXPRESSION'); + + foreach ($unaries as $unary) { + $operator = $unary->getChildByIndex(0)->getConcreteString(); + + if (strtolower($operator) === 'exit') { + if ($unary->getParentNode()->getTypeName() !== 'n_STATEMENT') { + $this->raiseLintAtNode( + $unary, + pht('Use `%s` as a statement, not an expression.', 'exit')); + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistExtractUseXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistExtractUseXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistExtractUseXHPASTLinterRule.php @@ -0,0 +1,24 @@ +getFunctionCalls($root, array('extract')); + + foreach ($calls as $call) { + $this->raiseLintAtNode( + $call, + pht( + 'Avoid %s. It is confusing and hinders static analysis.', + 'extract()')); + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistFormattedStringXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistFormattedStringXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistFormattedStringXHPASTLinterRule.php @@ -0,0 +1,103 @@ + array( + 'type' => 'optional map', + 'help' => pht( + '%s-style functions which take a format string and list of values '. + 'as arguments. The value for the mapping is the start index of the '. + 'function parameters (the index of the format string parameter).', + 'printf()'), + ), + ); + } + + public function setLinterConfigurationValue($key, $value) { + switch ($key) { + case 'xhpast.printf-functions': + $this->printfFunctions = $value; + return; + default: + return parent::setLinterConfigurationValue($key, $value); + } + } + + public function process(XHPASTNode $root) { + static $functions = array( + // Core PHP + 'fprintf' => 1, + 'printf' => 0, + 'sprintf' => 0, + 'vfprintf' => 1, + + // libphutil + 'csprintf' => 0, + 'execx' => 0, + 'exec_manual' => 0, + 'hgsprintf' => 0, + 'hsprintf' => 0, + 'jsprintf' => 0, + 'pht' => 0, + 'phutil_passthru' => 0, + 'qsprintf' => 1, + 'queryfx' => 1, + 'queryfx_all' => 1, + 'queryfx_one' => 1, + 'vcsprintf' => 0, + 'vqsprintf' => 1, + ); + + $function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); + + foreach ($function_calls as $call) { + $name = $call->getChildByIndex(0)->getConcreteString(); + + $name = strtolower($name); + $start = idx($functions + $this->printfFunctions, $name); + + if ($start === null) { + continue; + } + + $parameters = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); + $argc = count($parameters->getChildren()) - $start; + + if ($argc < 1) { + $this->raiseLintAtNode( + $call, + pht('This function is expected to have a format string.')); + continue; + } + + $format = $parameters->getChildByIndex($start); + if ($format->getTypeName() != 'n_STRING_SCALAR') { + continue; + } + + $argv = array($format->evalStatic()) + array_fill(0, $argc, null); + + try { + xsprintf(null, null, $argv); + } catch (BadFunctionCallException $ex) { + $this->raiseLintAtNode( + $call, + str_replace('xsprintf', $name, $ex->getMessage())); + } catch (InvalidArgumentException $ex) { + // Ignore. + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistImplicitConstructorXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistImplicitConstructorXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistImplicitConstructorXHPASTLinterRule.php @@ -0,0 +1,35 @@ +selectDescendantsOfType('n_CLASS_DECLARATION'); + + foreach ($classes as $class) { + $class_name = $class->getChildByIndex(1)->getConcreteString(); + $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); + + foreach ($methods as $method) { + $method_name_token = $method->getChildByIndex(2); + $method_name = $method_name_token->getConcreteString(); + + if (strtolower($class_name) === strtolower($method_name)) { + $this->raiseLintAtNode( + $method_name_token, + pht( + 'Name constructors %s explicitly. This method is a constructor '. + ' because it has the same name as the class it is defined in.', + '__construct()')); + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistImplicitFallthroughXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistImplicitFallthroughXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistImplicitFallthroughXHPASTLinterRule.php @@ -0,0 +1,210 @@ + array( + 'type' => 'optional string', + 'help' => pht( + 'Name of a concrete subclass of %s which tunes the '. + 'analysis of %s statements for this linter.', + 'ArcanistXHPASTLintSwitchHook', + 'switch()'), + ), + ); + } + + public function setLinterConfigurationValue($key, $value) { + switch ($key) { + case 'xhpast.switchhook': + $this->switchhook = $value; + return; + + default: + return parent::setLinterConfigurationValue($key, $value); + } + } + + public function process(XHPASTNode $root) { + $hook_obj = null; + $hook_class = $this->switchhook; + + if ($hook_class) { + $hook_obj = newv($hook_class, array()); + assert_instances_of(array($hook_obj), 'ArcanistXHPASTLintSwitchHook'); + } + + $switches = $root->selectDescendantsOfType('n_SWITCH'); + foreach ($switches as $switch) { + $blocks = array(); + + $cases = $switch->selectDescendantsOfType('n_CASE'); + foreach ($cases as $case) { + $blocks[] = $case; + } + + $defaults = $switch->selectDescendantsOfType('n_DEFAULT'); + foreach ($defaults as $default) { + $blocks[] = $default; + } + + + foreach ($blocks as $key => $block) { + // Collect all the tokens in this block which aren't at top level. + // We want to ignore "break", and "continue" in these blocks. + $lower_level = $block->selectDescendantsOfTypes(array( + 'n_WHILE', + 'n_DO_WHILE', + 'n_FOR', + 'n_FOREACH', + 'n_SWITCH', + )); + $lower_level_tokens = array(); + foreach ($lower_level as $lower_level_block) { + $lower_level_tokens += $lower_level_block->getTokens(); + } + + // Collect all the tokens in this block which aren't in this scope + // (because they're inside class, function or interface declarations). + // We want to ignore all of these tokens. + $decls = $block->selectDescendantsOfTypes(array( + 'n_FUNCTION_DECLARATION', + 'n_CLASS_DECLARATION', + + // For completeness; these can't actually have anything. + 'n_INTERFACE_DECLARATION', + )); + + $different_scope_tokens = array(); + foreach ($decls as $decl) { + $different_scope_tokens += $decl->getTokens(); + } + + $lower_level_tokens += $different_scope_tokens; + + // Get all the trailing nonsemantic tokens, since we need to look for + // "fallthrough" comments past the end of the semantic block. + + $tokens = $block->getTokens(); + $last = end($tokens); + while ($last && $last = $last->getNextToken()) { + if ($last->isSemantic()) { + break; + } + $tokens[$last->getTokenID()] = $last; + } + + $blocks[$key] = array( + $tokens, + $lower_level_tokens, + $different_scope_tokens, + ); + } + + foreach ($blocks as $token_lists) { + list( + $tokens, + $lower_level_tokens, + $different_scope_tokens) = $token_lists; + + // Test each block (case or default statement) to see if it's OK. It's + // OK if: + // + // - it is empty; or + // - it ends in break, return, throw, continue or exit at top level; or + // - it has a comment with "fallthrough" in its text. + + // Empty blocks are OK, so we start this at `true` and only set it to + // false if we find a statement. + $block_ok = true; + + // Keeps track of whether the current statement is one that validates + // the block (break, return, throw, continue) or something else. + $statement_ok = false; + + foreach ($tokens as $token_id => $token) { + if (!$token->isSemantic()) { + // Liberally match "fall" in the comment text so that comments like + // "fallthru", "fall through", "fallthrough", etc., are accepted. + if (preg_match('/fall/i', $token->getValue())) { + $block_ok = true; + break; + } + continue; + } + + $tok_type = $token->getTypeName(); + + if ($tok_type === 'T_FUNCTION' || + $tok_type === 'T_CLASS' || + $tok_type === 'T_INTERFACE') { + // These aren't statements, but mark the block as nonempty anyway. + $block_ok = false; + continue; + } + + if ($tok_type === ';') { + if ($statement_ok) { + $statment_ok = false; + } else { + $block_ok = false; + } + continue; + } + + if ($tok_type === 'T_BREAK' || $tok_type === 'T_CONTINUE') { + if (empty($lower_level_tokens[$token_id])) { + $statement_ok = true; + $block_ok = true; + } + continue; + } + + if ($tok_type === 'T_RETURN' || + $tok_type === 'T_THROW' || + $tok_type === 'T_EXIT' || + ($hook_obj && $hook_obj->checkSwitchToken($token))) { + if (empty($different_scope_tokens[$token_id])) { + $statement_ok = true; + $block_ok = true; + } + continue; + } + } + + if (!$block_ok) { + $this->raiseLintAtToken( + head($tokens), + pht( + "This '%s' or '%s' has a nonempty block which does not end ". + "with '%s', '%s', '%s', '%s' or '%s'. Did you forget to add ". + "one of those? If you intend to fall through, add a '%s' ". + "comment to silence this warning.", + 'case', + 'default', + 'break', + 'continue', + 'return', + 'throw', + 'exit', + '// fallthrough')); + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistImplicitVisibilityXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistImplicitVisibilityXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistImplicitVisibilityXHPASTLinterRule.php @@ -0,0 +1,88 @@ +lintMethodVisibility($root); + $this->lintPropertyVisibility($root); + } + + private function lintMethodVisibility(XHPASTNode $root) { + static $visibilities = array( + 'public', + 'protected', + 'private', + ); + + $methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION'); + + foreach ($methods as $method) { + $modifiers_list = $method->getChildOfType(0, 'n_METHOD_MODIFIER_LIST'); + + foreach ($modifiers_list->getChildren() as $modifier) { + if (in_array($modifier->getConcreteString(), $visibilities)) { + continue 2; + } + } + + if ($modifiers_list->getChildren()) { + $node = $modifiers_list; + } else { + $node = $method; + } + + $this->raiseLintAtNode( + $node, + pht('Methods should have their visibility declared explicitly.'), + 'public '.$node->getConcreteString()); + } + } + + private function lintPropertyVisibility(XHPASTNode $root) { + static $visibilities = array( + 'public', + 'protected', + 'private', + ); + + $nodes = $root->selectDescendantsOfType('n_CLASS_MEMBER_MODIFIER_LIST'); + + foreach ($nodes as $node) { + $modifiers = $node->getChildren(); + + foreach ($modifiers as $modifier) { + if ($modifier->getConcreteString() == 'var') { + $this->raiseLintAtNode( + $modifier, + pht( + 'Use `%s` instead of `%s` to indicate public visibility.', + 'public', + 'var'), + 'public'); + continue 2; + } + + if (in_array($modifier->getConcreteString(), $visibilities)) { + continue 2; + } + } + + $this->raiseLintAtNode( + $node, + pht('Properties should have their visibility declared explicitly.'), + 'public '.$node->getConcreteString()); + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistInnerFunctionXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistInnerFunctionXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistInnerFunctionXHPASTLinterRule.php @@ -0,0 +1,36 @@ +selectDescendantsOfType('n_FUNCTION_DECLARATION'); + + foreach ($function_decls as $function_declaration) { + $inner_functions = $function_declaration + ->selectDescendantsOfType('n_FUNCTION_DECLARATION'); + + foreach ($inner_functions as $inner_function) { + if ($inner_function->getChildByIndex(2)->getTypeName() == 'n_EMPTY') { + // Anonymous closure. + continue; + } + + $this->raiseLintAtNode( + $inner_function, + pht('Avoid the use of inner functions.')); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistInstanceOfOperatorXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistInstanceOfOperatorXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistInstanceOfOperatorXHPASTLinterRule.php @@ -0,0 +1,35 @@ +selectDescendantsOfType('n_BINARY_EXPRESSION'); + + foreach ($expressions as $expression) { + $operator = $expression->getChildOfType(1, 'n_OPERATOR'); + + if (strtolower($operator->getConcreteString()) != 'instanceof') { + continue; + } + + $object = $expression->getChildByIndex(0); + + if ($object->isStaticScalar() || + $object->getTypeName() == 'n_SYMBOL_NAME') { + $this->raiseLintAtNode( + $object, + pht( + '%s expects an object instance, constant given.', + 'instanceof')); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistInvalidDefaultParameterXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistInvalidDefaultParameterXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistInvalidDefaultParameterXHPASTLinterRule.php @@ -0,0 +1,79 @@ +selectDescendantsOfType('n_DECLARATION_PARAMETER'); + + foreach ($parameters as $parameter) { + $type = $parameter->getChildByIndex(0); + $default = $parameter->getChildByIndex(2); + + if ($type->getTypeName() == 'n_EMPTY') { + continue; + } + + if ($default->getTypeName() == 'n_EMPTY') { + continue; + } + + $default_is_null = $default->getTypeName() == 'n_SYMBOL_NAME' && + strtolower($default->getConcreteString()) == 'null'; + + switch (strtolower($type->getConcreteString())) { + case 'array': + if ($default->getTypeName() == 'n_ARRAY_LITERAL') { + break; + } + if ($default_is_null) { + break; + } + + $this->raiseLintAtNode( + $default, + pht( + 'Default value for parameters with %s type hint '. + 'can only be an %s or %s.', + 'array', + 'array', + 'null')); + break; + + case 'callable': + if ($default_is_null) { + break; + } + + $this->raiseLintAtNode( + $default, + pht( + 'Default value for parameters with %s type hint can only be %s.', + 'callable', + 'null')); + break; + + default: + // Class/interface parameter. + if ($default_is_null) { + break; + } + + $this->raiseLintAtNode( + $default, + pht( + 'Default value for parameters with a class type hint '. + 'can only be %s.', + 'null')); + break; + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistInvalidModifiersXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistInvalidModifiersXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistInvalidModifiersXHPASTLinterRule.php @@ -0,0 +1,103 @@ +selectDescendantsOfTypes(array( + 'n_CLASS_MEMBER_MODIFIER_LIST', + 'n_METHOD_MODIFIER_LIST', + )); + + foreach ($methods as $method) { + $modifiers = $method->getChildren(); + + $is_abstract = false; + $is_final = false; + $is_static = false; + $visibility = null; + + foreach ($modifiers as $modifier) { + switch ($modifier->getConcreteString()) { + case 'abstract': + if ($method->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') { + $this->raiseLintAtNode( + $modifier, + pht( + 'Properties cannot be declared %s.', + 'abstract')); + } + + if ($is_abstract) { + $this->raiseLintAtNode( + $modifier, + pht( + 'Multiple %s modifiers are not allowed.', + 'abstract')); + } + + if ($is_final) { + $this->raiseLintAtNode( + $modifier, + pht( + 'Cannot use the %s modifier on an %s class member', + 'final', + 'abstract')); + } + + $is_abstract = true; + break; + + case 'final': + if ($is_abstract) { + $this->raiseLintAtNode( + $modifier, + pht( + 'Cannot use the %s modifier on an %s class member', + 'final', + 'abstract')); + } + + if ($is_final) { + $this->raiseLintAtNode( + $modifier, + pht( + 'Multiple %s modifiers are not allowed.', + 'final')); + } + + $is_final = true; + break; + case 'public': + case 'protected': + case 'private': + if ($visibility) { + $this->raiseLintAtNode( + $modifier, + pht('Multiple access type modifiers are not allowed.')); + } + + $visibility = $modifier->getConcreteString(); + break; + + case 'static': + if ($is_static) { + $this->raiseLintAtNode( + $modifier, + pht( + 'Multiple %s modifiers are not allowed.', + 'static')); + } + break; + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php @@ -0,0 +1,149 @@ +selectTokensOfTypes(array( + 'T_REQUIRE_ONCE', + 'T_REQUIRE', + 'T_EVAL', + 'T_INCLUDE_ONCE', + 'T_INCLUDE', + 'T_LOGICAL_OR', + 'T_LOGICAL_XOR', + 'T_LOGICAL_AND', + 'T_PRINT', + 'T_INSTANCEOF', + 'T_CLONE', + 'T_NEW', + 'T_EXIT', + 'T_IF', + 'T_ELSEIF', + 'T_ELSE', + 'T_ENDIF', + 'T_ECHO', + 'T_DO', + 'T_WHILE', + 'T_ENDWHILE', + 'T_FOR', + 'T_ENDFOR', + 'T_FOREACH', + 'T_ENDFOREACH', + 'T_DECLARE', + 'T_ENDDECLARE', + 'T_AS', + 'T_SWITCH', + 'T_ENDSWITCH', + 'T_CASE', + 'T_DEFAULT', + 'T_BREAK', + 'T_CONTINUE', + 'T_GOTO', + 'T_FUNCTION', + 'T_CONST', + 'T_RETURN', + 'T_TRY', + 'T_CATCH', + 'T_THROW', + 'T_USE', + 'T_GLOBAL', + 'T_PUBLIC', + 'T_PROTECTED', + 'T_PRIVATE', + 'T_FINAL', + 'T_ABSTRACT', + 'T_STATIC', + 'T_VAR', + 'T_UNSET', + 'T_ISSET', + 'T_EMPTY', + 'T_HALT_COMPILER', + 'T_CLASS', + 'T_INTERFACE', + 'T_EXTENDS', + 'T_IMPLEMENTS', + 'T_LIST', + 'T_ARRAY', + 'T_NAMESPACE', + 'T_INSTEADOF', + 'T_CALLABLE', + 'T_TRAIT', + 'T_YIELD', + 'T_FINALLY', + )); + foreach ($keywords as $keyword) { + $value = $keyword->getValue(); + + if ($value != strtolower($value)) { + $this->raiseLintAtToken( + $keyword, + pht( + "Convention: spell keyword '%s' as '%s'.", + $value, + strtolower($value)), + strtolower($value)); + } + } + + $symbols = $root->selectDescendantsOfType('n_SYMBOL_NAME'); + foreach ($symbols as $symbol) { + static $interesting_symbols = array( + 'false' => true, + 'null' => true, + 'true' => true, + ); + + $symbol_name = $symbol->getConcreteString(); + + if ($symbol->getParentNode()->getTypeName() == 'n_FUNCTION_CALL') { + continue; + } + + if (idx($interesting_symbols, strtolower($symbol_name))) { + if ($symbol_name != strtolower($symbol_name)) { + $this->raiseLintAtNode( + $symbol, + pht( + "Convention: spell keyword '%s' as '%s'.", + $symbol_name, + strtolower($symbol_name)), + strtolower($symbol_name)); + } + } + } + + $magic_constants = $root->selectTokensOfTypes(array( + 'T_CLASS_C', + 'T_METHOD_C', + 'T_FUNC_C', + 'T_LINE', + 'T_FILE', + 'T_NS_C', + 'T_DIR', + 'T_TRAIT_C', + )); + + foreach ($magic_constants as $magic_constant) { + $value = $magic_constant->getValue(); + + if ($value != strtoupper($value)) { + $this->raiseLintAtToken( + $magic_constant, + pht('Magic constants should be uppercase.'), + strtoupper($value)); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistLambdaFuncFunctionXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistLambdaFuncFunctionXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistLambdaFuncFunctionXHPASTLinterRule.php @@ -0,0 +1,43 @@ +selectDescendantsOfType('n_FUNCTION_DECLARATION'); + + foreach ($function_declarations as $function_declaration) { + $function_name = $function_declaration->getChildByIndex(2); + + if ($function_name->getTypeName() == 'n_EMPTY') { + // Anonymous closure. + continue; + } + + if ($function_name->getConcreteString() != '__lambda_func') { + continue; + } + + $this->raiseLintAtNode( + $function_declaration, + pht( + 'Declaring a function named %s causes any call to %s to fail. '. + 'This is because %s eval-declares the function %s, then '. + 'modifies the symbol table so that the function is instead '. + 'named %s, and returns that name.', + '__lambda_func', + 'create_function', + 'create_function', + '__lambda_func', + '"\0lambda_".(++$i)')); + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistLanguageConstructParenthesesXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistLanguageConstructParenthesesXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistLanguageConstructParenthesesXHPASTLinterRule.php @@ -0,0 +1,45 @@ +selectDescendantsOfTypes(array( + 'n_INCLUDE_FILE', + 'n_ECHO_LIST', + )); + + foreach ($nodes as $node) { + $child = head($node->getChildren()); + + if ($child->getTypeName() === 'n_PARENTHETICAL_EXPRESSION') { + list($before, $after) = $child->getSurroundingNonsemanticTokens(); + + $replace = preg_replace( + '/^\((.*)\)$/', + '$1', + $child->getConcreteString()); + + if (!$before) { + $replace = ' '.$replace; + } + + $this->raiseLintAtNode( + $child, + pht('Language constructs do not require parentheses.'), + $replace); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistLogicalOperatorsXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistLogicalOperatorsXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistLogicalOperatorsXHPASTLinterRule.php @@ -0,0 +1,35 @@ +selectTokensOfType('T_LOGICAL_AND'); + $logical_ors = $root->selectTokensOfType('T_LOGICAL_OR'); + + foreach ($logical_ands as $logical_and) { + $this->raiseLintAtToken( + $logical_and, + pht('Use `%s` instead of `%s`.', '&&', 'and'), + '&&'); + } + + foreach ($logical_ors as $logical_or) { + $this->raiseLintAtToken( + $logical_or, + pht('Use `%s` instead of `%s`.', '||', 'or'), + '||'); + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistLowercaseFunctionsXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistLowercaseFunctionsXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistLowercaseFunctionsXHPASTLinterRule.php @@ -0,0 +1,48 @@ +selectDescendantsOfType('n_FUNCTION_CALL'); + + foreach ($function_calls as $function_call) { + $function = $function_call->getChildByIndex(0); + + if ($function->getTypeName() != 'n_SYMBOL_NAME') { + continue; + } + + $function_name = $function->getConcreteString(); + + if (!idx($builtin_functions, strtolower($function_name))) { + continue; + } + + if ($function_name != strtolower($function_name)) { + $this->raiseLintAtNode( + $function, + pht('Calls to built-in PHP functions should be lowercase.'), + strtolower($function_name)); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistModifierOrderingXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistModifierOrderingXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistModifierOrderingXHPASTLinterRule.php @@ -0,0 +1,86 @@ +lintMethodModifierOrdering($root); + $this->lintPropertyModifierOrdering($root); + } + + private function lintMethodModifierOrdering(XHPASTNode $root) { + static $modifiers = array( + 'abstract', + 'final', + 'public', + 'protected', + 'private', + 'static', + ); + + $methods = $root->selectDescendantsOfType('n_METHOD_MODIFIER_LIST'); + + foreach ($methods as $method) { + $modifier_ordering = array_values( + mpull($method->getChildren(), 'getConcreteString')); + $expected_modifier_ordering = array_values( + array_intersect( + $modifiers, + $modifier_ordering)); + + if (count($modifier_ordering) != count($expected_modifier_ordering)) { + continue; + } + + if ($modifier_ordering != $expected_modifier_ordering) { + $this->raiseLintAtNode( + $method, + pht('Non-conventional modifier ordering.'), + implode(' ', $expected_modifier_ordering)); + } + } + } + + private function lintPropertyModifierOrdering(XHPASTNode $root) { + static $modifiers = array( + 'public', + 'protected', + 'private', + 'static', + ); + + $properties = $root->selectDescendantsOfType( + 'n_CLASS_MEMBER_MODIFIER_LIST'); + + foreach ($properties as $property) { + $modifier_ordering = array_values( + mpull($property->getChildren(), 'getConcreteString')); + $expected_modifier_ordering = array_values( + array_intersect( + $modifiers, + $modifier_ordering)); + + if (count($modifier_ordering) != count($expected_modifier_ordering)) { + continue; + } + + if ($modifier_ordering != $expected_modifier_ordering) { + $this->raiseLintAtNode( + $property, + pht('Non-conventional modifier ordering.'), + implode(' ', $expected_modifier_ordering)); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistNamingConventionsXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistNamingConventionsXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistNamingConventionsXHPASTLinterRule.php @@ -0,0 +1,349 @@ + array( + 'type' => 'optional string', + 'help' => pht( + 'Name of a concrete subclass of %s which enforces more '. + 'granular naming convention rules for symbols.', + 'ArcanistXHPASTLintNamingHook'), + ), + ); + } + + public function setLinterConfigurationValue($key, $value) { + switch ($key) { + case 'xhpast.naminghook': + $this->naminghook = $value; + return; + + default: + return parent::setLinterConfigurationValue($key, $value); + } + } + + public function process(XHPASTNode $root) { + // We're going to build up a list of tuples + // and then try to instantiate a hook class which has the opportunity to + // override us. + $names = array(); + + $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); + foreach ($classes as $class) { + $name_token = $class->getChildByIndex(1); + $name_string = $name_token->getConcreteString(); + + $names[] = array( + 'class', + $name_string, + $name_token, + ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string) + ? null + : pht( + 'Follow naming conventions: classes should be named using '. + 'UpperCamelCase.'), + ); + } + + $ifaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); + foreach ($ifaces as $iface) { + $name_token = $iface->getChildByIndex(1); + $name_string = $name_token->getConcreteString(); + $names[] = array( + 'interface', + $name_string, + $name_token, + ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string) + ? null + : pht( + 'Follow naming conventions: interfaces should be named using '. + 'UpperCamelCase.'), + ); + } + + + $functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); + foreach ($functions as $function) { + $name_token = $function->getChildByIndex(2); + if ($name_token->getTypeName() === 'n_EMPTY') { + // Unnamed closure. + continue; + } + $name_string = $name_token->getConcreteString(); + $names[] = array( + 'function', + $name_string, + $name_token, + ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores( + ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string)) + ? null + : pht( + 'Follow naming conventions: functions should be named using '. + 'lowercase_with_underscores.'), + ); + } + + + $methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION'); + foreach ($methods as $method) { + $name_token = $method->getChildByIndex(2); + $name_string = $name_token->getConcreteString(); + $names[] = array( + 'method', + $name_string, + $name_token, + ArcanistXHPASTLintNamingHook::isLowerCamelCase( + ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string)) + ? null + : pht( + 'Follow naming conventions: methods should be named using '. + 'lowerCamelCase.'), + ); + } + + $param_tokens = array(); + + $params = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST'); + foreach ($params as $param_list) { + foreach ($param_list->getChildren() as $param) { + $name_token = $param->getChildByIndex(1); + if ($name_token->getTypeName() === 'n_VARIABLE_REFERENCE') { + $name_token = $name_token->getChildOfType(0, 'n_VARIABLE'); + } + $param_tokens[$name_token->getID()] = true; + $name_string = $name_token->getConcreteString(); + + $names[] = array( + 'parameter', + $name_string, + $name_token, + ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores( + ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string)) + ? null + : pht( + 'Follow naming conventions: parameters should be named using '. + 'lowercase_with_underscores.'), + ); + } + } + + + $constants = $root->selectDescendantsOfType( + 'n_CLASS_CONSTANT_DECLARATION_LIST'); + foreach ($constants as $constant_list) { + foreach ($constant_list->getChildren() as $constant) { + $name_token = $constant->getChildByIndex(0); + $name_string = $name_token->getConcreteString(); + $names[] = array( + 'constant', + $name_string, + $name_token, + ArcanistXHPASTLintNamingHook::isUppercaseWithUnderscores($name_string) + ? null + : pht( + 'Follow naming conventions: class constants should be named '. + 'using UPPERCASE_WITH_UNDERSCORES.'), + ); + } + } + + $member_tokens = array(); + + $props = $root->selectDescendantsOfType('n_CLASS_MEMBER_DECLARATION_LIST'); + foreach ($props as $prop_list) { + foreach ($prop_list->getChildren() as $token_id => $prop) { + if ($prop->getTypeName() === 'n_CLASS_MEMBER_MODIFIER_LIST') { + continue; + } + + $name_token = $prop->getChildByIndex(0); + $member_tokens[$name_token->getID()] = true; + + $name_string = $name_token->getConcreteString(); + $names[] = array( + 'member', + $name_string, + $name_token, + ArcanistXHPASTLintNamingHook::isLowerCamelCase( + ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string)) + ? null + : pht( + 'Follow naming conventions: class properties should be named '. + 'using lowerCamelCase.'), + ); + } + } + + $superglobal_map = array_fill_keys( + $this->getSuperGlobalNames(), + true); + + + $defs = $root->selectDescendantsOfTypes(array( + 'n_FUNCTION_DECLARATION', + 'n_METHOD_DECLARATION', + )); + + foreach ($defs as $def) { + $globals = $def->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST'); + $globals = $globals->selectDescendantsOfType('n_VARIABLE'); + + $globals_map = array(); + foreach ($globals as $global) { + $global_string = $global->getConcreteString(); + $globals_map[$global_string] = true; + $names[] = array( + 'user', + $global_string, + $global, + + // No advice for globals, but hooks have an option to provide some. + null, + ); + } + + // Exclude access of static properties, since lint will be raised at + // their declaration if they're invalid and they may not conform to + // variable rules. This is slightly overbroad (includes the entire + // RHS of a "Class::..." token) to cover cases like "Class:$x[0]". These + // variables are simply made exempt from naming conventions. + $exclude_tokens = array(); + $statics = $def->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); + foreach ($statics as $static) { + $rhs = $static->getChildByIndex(1); + if ($rhs->getTypeName() == 'n_VARIABLE') { + $exclude_tokens[$rhs->getID()] = true; + } else { + $rhs_vars = $rhs->selectDescendantsOfType('n_VARIABLE'); + foreach ($rhs_vars as $var) { + $exclude_tokens[$var->getID()] = true; + } + } + } + + $vars = $def->selectDescendantsOfType('n_VARIABLE'); + foreach ($vars as $token_id => $var) { + if (isset($member_tokens[$token_id])) { + continue; + } + if (isset($param_tokens[$token_id])) { + continue; + } + if (isset($exclude_tokens[$token_id])) { + continue; + } + + $var_string = $var->getConcreteString(); + + // Awkward artifact of "$o->{$x}". + $var_string = trim($var_string, '{}'); + + if (isset($superglobal_map[$var_string])) { + continue; + } + if (isset($globals_map[$var_string])) { + continue; + } + + $names[] = array( + 'variable', + $var_string, + $var, + ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores( + ArcanistXHPASTLintNamingHook::stripPHPVariable($var_string)) + ? null + : pht( + 'Follow naming conventions: variables should be named using '. + 'lowercase_with_underscores.'), + ); + } + } + + // If a naming hook is configured, give it a chance to override the + // default results for all the symbol names. + $hook_class = $this->naminghook; + if ($hook_class) { + $hook_obj = newv($hook_class, array()); + foreach ($names as $k => $name_attrs) { + list($type, $name, $token, $default) = $name_attrs; + $result = $hook_obj->lintSymbolName($type, $name, $default); + $names[$k][3] = $result; + } + } + + // Raise anything we're left with. + foreach ($names as $k => $name_attrs) { + list($type, $name, $token, $result) = $name_attrs; + if ($result) { + $this->raiseLintAtNode( + $token, + $result); + } + } + + // Lint constant declarations. + $defines = $this + ->getFunctionCalls($root, array('define')) + ->add($root->selectDescendantsOfTypes(array( + 'n_CLASS_CONSTANT_DECLARATION', + 'n_CONSTANT_DECLARATION', + ))); + + foreach ($defines as $define) { + switch ($define->getTypeName()) { + case 'n_CLASS_CONSTANT_DECLARATION': + case 'n_CONSTANT_DECLARATION': + $constant = $define->getChildByIndex(0); + + if ($constant->getTypeName() !== 'n_STRING') { + $constant = null; + } + + break; + + case 'n_FUNCTION_CALL': + $constant = $define + ->getChildOfType(1, 'n_CALL_PARAMETER_LIST') + ->getChildByIndex(0); + + if ($constant->getTypeName() !== 'n_STRING_SCALAR') { + $constant = null; + } + + break; + + default: + $constant = null; + break; + } + + if (!$constant) { + continue; + } + $constant_name = $constant->getConcreteString(); + + if ($constant_name !== strtoupper($constant_name)) { + $this->raiseLintAtNode( + $constant, + pht('Constants should be uppercase.')); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistNoParentScopeXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistNoParentScopeXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistNoParentScopeXHPASTLinterRule.php @@ -0,0 +1,45 @@ +selectDescendantsOfType('n_CLASS_DECLARATION'); + + foreach ($classes as $class) { + $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); + + if ($class->getChildByIndex(2)->getTypeName() == 'n_EXTENDS_LIST') { + continue; + } + + foreach ($methods as $method) { + $static_accesses = $method + ->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); + + foreach ($static_accesses as $static_access) { + $called_class = $static_access->getChildByIndex(0); + + if ($called_class->getTypeName() != 'n_CLASS_NAME') { + continue; + } + + if ($called_class->getConcreteString() == 'parent') { + $this->raiseLintAtNode( + $static_access, + pht( + 'Cannot access %s when current class scope has no parent.', + 'parent::')); + } + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistPHPCloseTagXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistPHPCloseTagXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistPHPCloseTagXHPASTLinterRule.php @@ -0,0 +1,20 @@ +'); + } + + public function process(XHPASTNode $root) { + foreach ($root->selectTokensOfType('T_CLOSE_TAG') as $token) { + $this->raiseLintAtToken( + $token, + pht('Do not use the PHP closing tag, "%s".', '?>')); + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php @@ -0,0 +1,435 @@ + array( + 'type' => 'optional string', + 'help' => pht('PHP version to target.'), + ), + 'xhpast.php-version.windows' => array( + 'type' => 'optional string', + 'help' => pht('PHP version to target on Windows.'), + ), + ); + } + + public function setLinterConfigurationValue($key, $value) { + switch ($key) { + case 'xhpast.php-version': + $this->version = $value; + return; + + case 'xhpast.php-version.windows': + $this->windowsVersion = $value; + return; + + default: + return parent::setLinterConfigurationValue($key, $value); + } + } + + public function process(XHPASTNode $root) { + static $compat_info; + + if (!$this->version) { + return; + } + + if ($compat_info === null) { + $target = phutil_get_library_root('phutil'). + '/../resources/php_compat_info.json'; + $compat_info = phutil_json_decode(Filesystem::readFile($target)); + } + + // Create a whitelist for symbols which are being used conditionally. + $whitelist = array( + 'class' => array(), + 'function' => array(), + ); + + $conditionals = $root->selectDescendantsOfType('n_IF'); + foreach ($conditionals as $conditional) { + $condition = $conditional->getChildOfType(0, 'n_CONTROL_CONDITION'); + $function = $condition->getChildByIndex(0); + + if ($function->getTypeName() != 'n_FUNCTION_CALL') { + continue; + } + + $function_token = $function + ->getChildByIndex(0); + + if ($function_token->getTypeName() != 'n_SYMBOL_NAME') { + // This may be `Class::method(...)` or `$var(...)`. + continue; + } + + $function_name = $function_token->getConcreteString(); + + switch ($function_name) { + case 'class_exists': + case 'function_exists': + case 'interface_exists': + $type = null; + switch ($function_name) { + case 'class_exists': + $type = 'class'; + break; + + case 'function_exists': + $type = 'function'; + break; + + case 'interface_exists': + $type = 'interface'; + break; + } + + $params = $function->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); + $symbol = $params->getChildByIndex(0); + + if (!$symbol->isStaticScalar()) { + continue; + } + + $symbol_name = $symbol->evalStatic(); + if (!idx($whitelist[$type], $symbol_name)) { + $whitelist[$type][$symbol_name] = array(); + } + + $span = $conditional + ->getChildByIndex(1) + ->getTokens(); + + $whitelist[$type][$symbol_name][] = range( + head_key($span), + last_key($span)); + break; + } + } + + $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); + foreach ($calls as $call) { + $node = $call->getChildByIndex(0); + $name = $node->getConcreteString(); + + $version = idx($compat_info['functions'], $name, array()); + $min = idx($version, 'php.min'); + $max = idx($version, 'php.max'); + + // Check if whitelisted. + $whitelisted = false; + foreach (idx($whitelist['function'], $name, array()) as $range) { + if (array_intersect($range, array_keys($node->getTokens()))) { + $whitelisted = true; + break; + } + } + + if ($whitelisted) { + continue; + } + + if ($min && version_compare($min, $this->version, '>')) { + $this->raiseLintAtNode( + $node, + pht( + 'This codebase targets PHP %s, but `%s()` was not '. + 'introduced until PHP %s.', + $this->version, + $name, + $min)); + } else if ($max && version_compare($max, $this->version, '<')) { + $this->raiseLintAtNode( + $node, + pht( + 'This codebase targets PHP %s, but `%s()` was '. + 'removed in PHP %s.', + $this->version, + $name, + $max)); + } else if (array_key_exists($name, $compat_info['params'])) { + $params = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); + foreach (array_values($params->getChildren()) as $i => $param) { + $version = idx($compat_info['params'][$name], $i); + if ($version && version_compare($version, $this->version, '>')) { + $this->raiseLintAtNode( + $param, + pht( + 'This codebase targets PHP %s, but parameter %d '. + 'of `%s()` was not introduced until PHP %s.', + $this->version, + $i + 1, + $name, + $version)); + } + } + } + + if ($this->windowsVersion) { + $windows = idx($compat_info['functions_windows'], $name); + + if ($windows === false) { + $this->raiseLintAtNode( + $node, + pht( + 'This codebase targets PHP %s on Windows, '. + 'but `%s()` is not available there.', + $this->windowsVersion, + $name)); + } else if (version_compare($windows, $this->windowsVersion, '>')) { + $this->raiseLintAtNode( + $node, + pht( + 'This codebase targets PHP %s on Windows, '. + 'but `%s()` is not available there until PHP %s.', + $this->windowsVersion, + $name, + $windows)); + } + } + } + + $classes = $root->selectDescendantsOfType('n_CLASS_NAME'); + foreach ($classes as $node) { + $name = $node->getConcreteString(); + $version = idx($compat_info['interfaces'], $name, array()); + $version = idx($compat_info['classes'], $name, $version); + $min = idx($version, 'php.min'); + $max = idx($version, 'php.max'); + // Check if whitelisted. + $whitelisted = false; + foreach (idx($whitelist['class'], $name, array()) as $range) { + if (array_intersect($range, array_keys($node->getTokens()))) { + $whitelisted = true; + break; + } + } + + if ($whitelisted) { + continue; + } + + if ($min && version_compare($min, $this->version, '>')) { + $this->raiseLintAtNode( + $node, + pht( + 'This codebase targets PHP %s, but `%s` was not '. + 'introduced until PHP %s.', + $this->version, + $name, + $min)); + } else if ($max && version_compare($max, $this->version, '<')) { + $this->raiseLintAtNode( + $node, + pht( + 'This codebase targets PHP %s, but `%s` was '. + 'removed in PHP %s.', + $this->version, + $name, + $max)); + } + } + + // TODO: Technically, this will include function names. This is unlikely to + // cause any issues (unless, of course, there existed a function that had + // the same name as some constant). + $constants = $root->selectDescendantsOfTypes(array( + 'n_SYMBOL_NAME', + 'n_MAGIC_SCALAR', + )); + foreach ($constants as $node) { + $name = $node->getConcreteString(); + $version = idx($compat_info['constants'], $name, array()); + $min = idx($version, 'php.min'); + $max = idx($version, 'php.max'); + + if ($min && version_compare($min, $this->version, '>')) { + $this->raiseLintAtNode( + $node, + pht( + 'This codebase targets PHP %s, but `%s` was not '. + 'introduced until PHP %s.', + $this->version, + $name, + $min)); + } else if ($max && version_compare($max, $this->version, '<')) { + $this->raiseLintAtNode( + $node, + pht( + 'This codebase targets PHP %s, but `%s` was '. + 'removed in PHP %s.', + $this->version, + $name, + $max)); + } + } + + if (version_compare($this->version, '5.3.0') < 0) { + $this->lintPHP53Features($root); + } else { + $this->lintPHP53Incompatibilities($root); + } + + if (version_compare($this->version, '5.4.0') < 0) { + $this->lintPHP54Features($root); + } else { + $this->lintPHP54Incompatibilities($root); + } + } + + private function lintPHP53Features(XHPASTNode $root) { + $functions = $root->selectTokensOfType('T_FUNCTION'); + foreach ($functions as $function) { + $next = $function->getNextToken(); + while ($next) { + if ($next->isSemantic()) { + break; + } + $next = $next->getNextToken(); + } + + if ($next) { + if ($next->getTypeName() === '(') { + $this->raiseLintAtToken( + $function, + pht( + 'This codebase targets PHP %s, but anonymous '. + 'functions were not introduced until PHP 5.3.', + $this->version)); + } + } + } + + $namespaces = $root->selectTokensOfType('T_NAMESPACE'); + foreach ($namespaces as $namespace) { + $this->raiseLintAtToken( + $namespace, + pht( + 'This codebase targets PHP %s, but namespaces were not '. + 'introduced until PHP 5.3.', + $this->version)); + } + + // NOTE: This is only "use x;", in anonymous functions the node type is + // n_LEXICAL_VARIABLE_LIST even though both tokens are T_USE. + + // TODO: We parse n_USE in a slightly crazy way right now; that would be + // a better selector once it's fixed. + + $uses = $root->selectDescendantsOfType('n_USE_LIST'); + foreach ($uses as $use) { + $this->raiseLintAtNode( + $use, + pht( + 'This codebase targets PHP %s, but namespaces were not '. + 'introduced until PHP 5.3.', + $this->version)); + } + + $statics = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); + foreach ($statics as $static) { + $name = $static->getChildByIndex(0); + if ($name->getTypeName() != 'n_CLASS_NAME') { + continue; + } + if ($name->getConcreteString() === 'static') { + $this->raiseLintAtNode( + $name, + pht( + 'This codebase targets PHP %s, but `%s` was not '. + 'introduced until PHP 5.3.', + $this->version, + 'static::')); + } + } + + $ternaries = $root->selectDescendantsOfType('n_TERNARY_EXPRESSION'); + foreach ($ternaries as $ternary) { + $yes = $ternary->getChildByIndex(1); + if ($yes->getTypeName() === 'n_EMPTY') { + $this->raiseLintAtNode( + $ternary, + pht( + 'This codebase targets PHP %s, but short ternary was '. + 'not introduced until PHP 5.3.', + $this->version)); + } + } + + $heredocs = $root->selectDescendantsOfType('n_HEREDOC'); + foreach ($heredocs as $heredoc) { + if (preg_match('/^<<<[\'"]/', $heredoc->getConcreteString())) { + $this->raiseLintAtNode( + $heredoc, + pht( + 'This codebase targets PHP %s, but nowdoc was not '. + 'introduced until PHP 5.3.', + $this->version)); + } + } + } + + private function lintPHP53Incompatibilities(XHPASTNode $root) {} + + private function lintPHP54Features(XHPASTNode $root) { + $indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS'); + + foreach ($indexes as $index) { + switch ($index->getChildByIndex(0)->getTypeName()) { + case 'n_FUNCTION_CALL': + case 'n_METHOD_CALL': + $this->raiseLintAtNode( + $index->getChildByIndex(1), + pht( + 'The `%s` syntax was not introduced until PHP 5.4, but this '. + 'codebase targets an earlier version of PHP. You can rewrite '. + 'this expression using `%s`.', + 'f()[...]', + 'idx()')); + break; + } + } + } + + private function lintPHP54Incompatibilities(XHPASTNode $root) { + $breaks = $root->selectDescendantsOfTypes(array('n_BREAK', 'n_CONTINUE')); + + foreach ($breaks as $break) { + $arg = $break->getChildByIndex(0); + + switch ($arg->getTypeName()) { + case 'n_EMPTY': + break; + + case 'n_NUMERIC_SCALAR': + if ($arg->getConcreteString() != '0') { + break; + } + + default: + $this->raiseLintAtNode( + $break->getChildByIndex(0), + pht( + 'The `%s` and `%s` statements no longer accept '. + 'variable arguments.', + 'break', + 'continue')); + break; + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistPHPEchoTagXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistPHPEchoTagXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistPHPEchoTagXHPASTLinterRule.php @@ -0,0 +1,24 @@ +getTokens(); + + foreach ($tokens as $token) { + if ($token->getTypeName() === 'T_OPEN_TAG_WITH_ECHO') { + $this->raiseLintAtToken( + $token, + pht('Avoid the PHP echo short form, "%s".', 'getTokens(); + + foreach ($tokens as $token) { + if ($token->getTypeName() === 'T_OPEN_TAG') { + break; + } else if ($token->getTypeName() === 'T_OPEN_TAG_WITH_ECHO') { + break; + } else { + if (!preg_match('/^#!/', $token->getValue())) { + $this->raiseLintAtToken( + $token, + pht( + 'PHP files should start with "%s", which may be preceded by '. + 'a "%s" line for scripts.', + 'getTokens(); + + foreach ($tokens as $token) { + if ($token->getTypeName() === 'T_OPEN_TAG') { + if (trim($token->getValue()) === 'raiseLintAtToken( + $token, + pht( + 'Use the full form of the PHP open tag, "%s".', + 'selectDescendantsOfTypes(array( + 'n_CALL_PARAMETER_LIST', + 'n_CONTROL_CONDITION', + 'n_FOR_EXPRESSION', + 'n_FOREACH_EXPRESSION', + 'n_DECLARATION_PARAMETER_LIST', + )); + + foreach ($all_paren_groups as $group) { + $tokens = $group->getTokens(); + + $token_o = array_shift($tokens); + $token_c = array_pop($tokens); + if ($token_o->getTypeName() !== '(') { + throw new Exception(pht('Expected open parentheses.')); + } + if ($token_c->getTypeName() !== ')') { + throw new Exception(pht('Expected close parentheses.')); + } + + $nonsem_o = $token_o->getNonsemanticTokensAfter(); + $nonsem_c = $token_c->getNonsemanticTokensBefore(); + + if (!$nonsem_o) { + continue; + } + + $raise = array(); + + $string_o = implode('', mpull($nonsem_o, 'getValue')); + if (preg_match('/^[ ]+$/', $string_o)) { + $raise[] = array($nonsem_o, $string_o); + } + + if ($nonsem_o !== $nonsem_c) { + $string_c = implode('', mpull($nonsem_c, 'getValue')); + if (preg_match('/^[ ]+$/', $string_c)) { + $raise[] = array($nonsem_c, $string_c); + } + } + + foreach ($raise as $warning) { + list($tokens, $string) = $warning; + $this->raiseLintAtOffset( + reset($tokens)->getOffset(), + pht('Parentheses should hug their contents.'), + $string, + ''); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistPlusOperatorOnStringsXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistPlusOperatorOnStringsXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistPlusOperatorOnStringsXHPASTLinterRule.php @@ -0,0 +1,37 @@ +selectDescendantsOfType('n_BINARY_EXPRESSION'); + + foreach ($binops as $binop) { + $op = $binop->getChildByIndex(1); + if ($op->getConcreteString() !== '+') { + continue; + } + + $left = $binop->getChildByIndex(0); + $right = $binop->getChildByIndex(2); + + if ($left->getTypeName() === 'n_STRING_SCALAR' || + $right->getTypeName() === 'n_STRING_SCALAR') { + $this->raiseLintAtNode( + $binop, + pht( + "In PHP, '%s' is the string concatenation operator, not '%s'. ". + "This expression uses '+' with a string literal as an operand.", + '.', + '+')); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistPregQuoteMisuseXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistPregQuoteMisuseXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistPregQuoteMisuseXHPASTLinterRule.php @@ -0,0 +1,42 @@ +getFunctionCalls($root, array('preg_quote')); + + foreach ($function_calls as $call) { + $parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); + if (count($parameter_list->getChildren()) !== 2) { + $this->raiseLintAtNode( + $call, + pht( + 'If you use pattern delimiters that require escaping '. + '(such as `%s`, but not `%s`) then you should pass two '. + 'arguments to %s, so that %s knows which delimiter to escape.', + '//', + '()', + 'preg_quote()', + 'preg_quote()')); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php @@ -0,0 +1,277 @@ +selectDescendantsOfTypes(array( + 'n_FUNCTION_DECLARATION', + 'n_METHOD_DECLARATION', + )); + + foreach ($defs as $def) { + + // We keep track of the first offset where scope becomes unknowable, and + // silence any warnings after that. Default it to INT_MAX so we can min() + // it later to keep track of the first problem we encounter. + $scope_destroyed_at = PHP_INT_MAX; + + $declarations = array( + '$this' => 0, + ) + array_fill_keys($this->getSuperGlobalNames(), 0); + $declaration_tokens = array(); + $exclude_tokens = array(); + $vars = array(); + + // First up, find all the different kinds of declarations, as explained + // above. Put the tokens into the $vars array. + + $param_list = $def->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST'); + $param_vars = $param_list->selectDescendantsOfType('n_VARIABLE'); + foreach ($param_vars as $var) { + $vars[] = $var; + } + + // This is PHP5.3 closure syntax: function () use ($x) {}; + $lexical_vars = $def + ->getChildByIndex(4) + ->selectDescendantsOfType('n_VARIABLE'); + foreach ($lexical_vars as $var) { + $vars[] = $var; + } + + $body = $def->getChildByIndex(5); + if ($body->getTypeName() === 'n_EMPTY') { + // Abstract method declaration. + continue; + } + + $static_vars = $body + ->selectDescendantsOfType('n_STATIC_DECLARATION') + ->selectDescendantsOfType('n_VARIABLE'); + foreach ($static_vars as $var) { + $vars[] = $var; + } + + + $global_vars = $body + ->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST'); + foreach ($global_vars as $var_list) { + foreach ($var_list->getChildren() as $var) { + if ($var->getTypeName() === 'n_VARIABLE') { + $vars[] = $var; + } else { + // Dynamic global variable, i.e. "global $$x;". + $scope_destroyed_at = min($scope_destroyed_at, $var->getOffset()); + // An error is raised elsewhere, no need to raise here. + } + } + } + + // Include "catch (Exception $ex)", but not variables in the body of the + // catch block. + $catches = $body->selectDescendantsOfType('n_CATCH'); + foreach ($catches as $catch) { + $vars[] = $catch->getChildOfType(1, 'n_VARIABLE'); + } + + $binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION'); + foreach ($binary as $expr) { + if ($expr->getChildByIndex(1)->getConcreteString() !== '=') { + continue; + } + $lval = $expr->getChildByIndex(0); + if ($lval->getTypeName() === 'n_VARIABLE') { + $vars[] = $lval; + } else if ($lval->getTypeName() === 'n_LIST') { + // Recursivey grab everything out of list(), since the grammar + // permits list() to be nested. Also note that list() is ONLY valid + // as an lval assignments, so we could safely lift this out of the + // n_BINARY_EXPRESSION branch. + $assign_vars = $lval->selectDescendantsOfType('n_VARIABLE'); + foreach ($assign_vars as $var) { + $vars[] = $var; + } + } + + if ($lval->getTypeName() === 'n_VARIABLE_VARIABLE') { + $scope_destroyed_at = min($scope_destroyed_at, $lval->getOffset()); + // No need to raise here since we raise an error elsewhere. + } + } + + $calls = $body->selectDescendantsOfType('n_FUNCTION_CALL'); + foreach ($calls as $call) { + $name = strtolower($call->getChildByIndex(0)->getConcreteString()); + + if ($name === 'empty' || $name === 'isset') { + $params = $call + ->getChildOfType(1, 'n_CALL_PARAMETER_LIST') + ->selectDescendantsOfType('n_VARIABLE'); + foreach ($params as $var) { + $exclude_tokens[$var->getID()] = true; + } + continue; + } + if ($name !== 'extract') { + continue; + } + $scope_destroyed_at = min($scope_destroyed_at, $call->getOffset()); + } + + // Now we have every declaration except foreach(), handled below. Build + // two maps, one which just keeps track of which tokens are part of + // declarations ($declaration_tokens) and one which has the first offset + // where a variable is declared ($declarations). + + foreach ($vars as $var) { + $concrete = $this->getConcreteVariableString($var); + $declarations[$concrete] = min( + idx($declarations, $concrete, PHP_INT_MAX), + $var->getOffset()); + $declaration_tokens[$var->getID()] = true; + } + + // Excluded tokens are ones we don't "count" as being used, described + // above. Put them into $exclude_tokens. + + $class_statics = $body + ->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); + $class_static_vars = $class_statics + ->selectDescendantsOfType('n_VARIABLE'); + foreach ($class_static_vars as $var) { + $exclude_tokens[$var->getID()] = true; + } + + + // Find all the variables in scope, and figure out where they are used. + // We want to find foreach() iterators which are both declared before and + // used after the foreach() loop. + + $uses = array(); + + $all_vars = $body->selectDescendantsOfType('n_VARIABLE'); + $all = array(); + + // NOTE: $all_vars is not a real array so we can't unset() it. + foreach ($all_vars as $var) { + + // Be strict since it's easier; we don't let you reuse an iterator you + // declared before a loop after the loop, even if you're just assigning + // to it. + + $concrete = $this->getConcreteVariableString($var); + $uses[$concrete][$var->getID()] = $var->getOffset(); + + if (isset($declaration_tokens[$var->getID()])) { + // We know this is part of a declaration, so it's fine. + continue; + } + if (isset($exclude_tokens[$var->getID()])) { + // We know this is part of isset() or similar, so it's fine. + continue; + } + + $all[$var->getOffset()] = $concrete; + } + + + // Do foreach() last, we want to handle implicit redeclaration of a + // variable already in scope since this probably means we're ovewriting a + // local. + + // NOTE: Processing foreach expressions in order allows programs which + // reuse iterator variables in other foreach() loops -- this is fine. We + // have a separate warning to prevent nested loops from reusing the same + // iterators. + + $foreaches = $body->selectDescendantsOfType('n_FOREACH'); + $all_foreach_vars = array(); + foreach ($foreaches as $foreach) { + $foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION'); + + $foreach_vars = array(); + + // Determine the end of the foreach() loop. + $foreach_tokens = $foreach->getTokens(); + $last_token = end($foreach_tokens); + $foreach_end = $last_token->getOffset(); + + $key_var = $foreach_expr->getChildByIndex(1); + if ($key_var->getTypeName() === 'n_VARIABLE') { + $foreach_vars[] = $key_var; + } + + $value_var = $foreach_expr->getChildByIndex(2); + if ($value_var->getTypeName() === 'n_VARIABLE') { + $foreach_vars[] = $value_var; + } else { + // The root-level token may be a reference, as in: + // foreach ($a as $b => &$c) { ... } + // Reach into the n_VARIABLE_REFERENCE node to grab the n_VARIABLE + // node. + $var = $value_var->getChildByIndex(0); + if ($var->getTypeName() === 'n_VARIABLE_VARIABLE') { + $var = $var->getChildByIndex(0); + } + $foreach_vars[] = $var; + } + + // Remove all uses of the iterators inside of the foreach() loop from + // the $uses map. + + foreach ($foreach_vars as $var) { + $concrete = $this->getConcreteVariableString($var); + $offset = $var->getOffset(); + + foreach ($uses[$concrete] as $id => $use_offset) { + if (($use_offset >= $offset) && ($use_offset < $foreach_end)) { + unset($uses[$concrete][$id]); + } + } + + $all_foreach_vars[] = $var; + } + } + + foreach ($all_foreach_vars as $var) { + $concrete = $this->getConcreteVariableString($var); + $offset = $var->getOffset(); + + // If a variable was declared before a foreach() and is used after + // it, raise a message. + + if (isset($declarations[$concrete])) { + if ($declarations[$concrete] < $offset) { + if (!empty($uses[$concrete]) && + max($uses[$concrete]) > $offset) { + $message = $this->raiseLintAtNode( + $var, + pht( + 'This iterator variable is a previously declared local '. + 'variable. To avoid overwriting locals, do not reuse them '. + 'as iterator variables.')); + $message->setOtherLocations(array( + $this->getOtherLocation($declarations[$concrete]), + $this->getOtherLocation(max($uses[$concrete])), + )); + } + } + } + + // This is a declaration, exclude it from the "declare variables prior + // to use" check below. + unset($all[$var->getOffset()]); + + $vars[] = $var; + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php @@ -0,0 +1,185 @@ +selectDescendantsOfTypes(array( + 'n_FUNCTION_DECLARATION', + 'n_METHOD_DECLARATION', + )); + + foreach ($defs as $def) { + $body = $def->getChildByIndex(5); + if ($body->getTypeName() === 'n_EMPTY') { + // Abstract method declaration. + continue; + } + + $exclude = array(); + + // Exclude uses of variables, unsets, and foreach loops + // within closures - they are checked on their own + $func_defs = $body->selectDescendantsOfType('n_FUNCTION_DECLARATION'); + foreach ($func_defs as $func_def) { + $vars = $func_def->selectDescendantsOfType('n_VARIABLE'); + foreach ($vars as $var) { + $exclude[$var->getID()] = true; + } + + $unset_lists = $func_def->selectDescendantsOfType('n_UNSET_LIST'); + foreach ($unset_lists as $unset_list) { + $exclude[$unset_list->getID()] = true; + } + + $foreaches = $func_def->selectDescendantsOfType('n_FOREACH'); + foreach ($foreaches as $foreach) { + $exclude[$foreach->getID()] = true; + } + } + + // Find all variables that are unset within the scope + $unset_vars = array(); + $unset_lists = $body->selectDescendantsOfType('n_UNSET_LIST'); + foreach ($unset_lists as $unset_list) { + if (isset($exclude[$unset_list->getID()])) { + continue; + } + + $unset_list_vars = $unset_list->selectDescendantsOfType('n_VARIABLE'); + foreach ($unset_list_vars as $var) { + $concrete = $this->getConcreteVariableString($var); + $unset_vars[$concrete][] = $var->getOffset(); + $exclude[$var->getID()] = true; + } + } + + // Find all reference variables in foreach expressions + $reference_vars = array(); + $foreaches = $body->selectDescendantsOfType('n_FOREACH'); + foreach ($foreaches as $foreach) { + if (isset($exclude[$foreach->getID()])) { + continue; + } + + $foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION'); + $var = $foreach_expr->getChildByIndex(2); + if ($var->getTypeName() !== 'n_VARIABLE_REFERENCE') { + continue; + } + + $reference = $var->getChildByIndex(0); + if ($reference->getTypeName() !== 'n_VARIABLE') { + continue; + } + + $reference_name = $this->getConcreteVariableString($reference); + $reference_vars[$reference_name][] = $reference->getOffset(); + $exclude[$reference->getID()] = true; + + // Exclude uses of the reference variable within the foreach loop + $foreach_vars = $foreach->selectDescendantsOfType('n_VARIABLE'); + foreach ($foreach_vars as $var) { + $name = $this->getConcreteVariableString($var); + if ($name === $reference_name) { + $exclude[$var->getID()] = true; + } + } + } + + // Allow usage if the reference variable is assigned to another + // reference variable + $binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION'); + foreach ($binary as $expr) { + if ($expr->getChildByIndex(1)->getConcreteString() !== '=') { + continue; + } + $lval = $expr->getChildByIndex(0); + if ($lval->getTypeName() !== 'n_VARIABLE') { + continue; + } + $rval = $expr->getChildByIndex(2); + if ($rval->getTypeName() !== 'n_VARIABLE_REFERENCE') { + continue; + } + + // Counts as unsetting a variable + $concrete = $this->getConcreteVariableString($lval); + $unset_vars[$concrete][] = $lval->getOffset(); + $exclude[$lval->getID()] = true; + } + + $all_vars = array(); + $all = $body->selectDescendantsOfType('n_VARIABLE'); + foreach ($all as $var) { + if (isset($exclude[$var->getID()])) { + continue; + } + + $name = $this->getConcreteVariableString($var); + + if (!isset($reference_vars[$name])) { + continue; + } + + // Find the closest reference offset to this variable + $reference_offset = null; + foreach ($reference_vars[$name] as $offset) { + if ($offset < $var->getOffset()) { + $reference_offset = $offset; + } else { + break; + } + } + if (!$reference_offset) { + continue; + } + + // Check if an unset exists between reference and usage of this + // variable + $warn = true; + if (isset($unset_vars[$name])) { + foreach ($unset_vars[$name] as $unset_offset) { + if ($unset_offset > $reference_offset && + $unset_offset < $var->getOffset()) { + $warn = false; + break; + } + } + } + if ($warn) { + $this->raiseLintAtNode( + $var, + pht( + 'This variable was used already as a by-reference iterator '. + 'variable. Such variables survive outside the %s loop, '. + 'do not reuse.', + 'foreach')); + } + } + + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistReusedIteratorXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistReusedIteratorXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistReusedIteratorXHPASTLinterRule.php @@ -0,0 +1,98 @@ +selectDescendantsOfType('n_FOR'); + foreach ($for_loops as $for_loop) { + $var_map = array(); + + // Find all the variables that are assigned to in the for() expression. + $for_expr = $for_loop->getChildOfType(0, 'n_FOR_EXPRESSION'); + $bin_exprs = $for_expr->selectDescendantsOfType('n_BINARY_EXPRESSION'); + foreach ($bin_exprs as $bin_expr) { + if ($bin_expr->getChildByIndex(1)->getConcreteString() === '=') { + $var = $bin_expr->getChildByIndex(0); + $var_map[$var->getConcreteString()] = $var; + } + } + + $used_vars[$for_loop->getID()] = $var_map; + } + + $foreach_loops = $root->selectDescendantsOfType('n_FOREACH'); + foreach ($foreach_loops as $foreach_loop) { + $var_map = array(); + + $foreach_expr = $foreach_loop->getChildOfType(0, 'n_FOREACH_EXPRESSION'); + + // We might use one or two vars, i.e. "foreach ($x as $y => $z)" or + // "foreach ($x as $y)". + $possible_used_vars = array( + $foreach_expr->getChildByIndex(1), + $foreach_expr->getChildByIndex(2), + ); + foreach ($possible_used_vars as $var) { + if ($var->getTypeName() === 'n_EMPTY') { + continue; + } + $name = $var->getConcreteString(); + $name = trim($name, '&'); // Get rid of ref silliness. + $var_map[$name] = $var; + } + + $used_vars[$foreach_loop->getID()] = $var_map; + } + + $all_loops = $for_loops->add($foreach_loops); + foreach ($all_loops as $loop) { + $child_loops = $loop->selectDescendantsOfTypes(array( + 'n_FOR', + 'n_FOREACH', + )); + + $outer_vars = $used_vars[$loop->getID()]; + foreach ($child_loops as $inner_loop) { + $inner_vars = $used_vars[$inner_loop->getID()]; + $shared = array_intersect_key($outer_vars, $inner_vars); + if ($shared) { + $shared_desc = implode(', ', array_keys($shared)); + $message = $this->raiseLintAtNode( + $inner_loop->getChildByIndex(0), + pht( + 'This loop reuses iterator variables (%s) from an '. + 'outer loop. You might be clobbering the outer iterator. '. + 'Change the inner loop to use a different iterator name.', + $shared_desc)); + + $locations = array(); + foreach ($shared as $var) { + $locations[] = $this->getOtherLocation($var->getOffset()); + } + $message->setOtherLocations($locations); + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistSelfMemberReferenceXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistSelfMemberReferenceXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistSelfMemberReferenceXHPASTLinterRule.php @@ -0,0 +1,86 @@ +selectDescendantsOfType('n_CLASS_DECLARATION'); + + foreach ($class_declarations as $class_declaration) { + $class_name = $class_declaration + ->getChildOfType(1, 'n_CLASS_NAME') + ->getConcreteString(); + + $class_static_accesses = $class_declaration + ->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); + + foreach ($class_static_accesses as $class_static_access) { + $double_colons = $class_static_access + ->selectTokensOfType('T_PAAMAYIM_NEKUDOTAYIM'); + $class_ref = $class_static_access->getChildByIndex(0); + + if ($class_ref->getTypeName() != 'n_CLASS_NAME') { + continue; + } + $class_ref_name = $class_ref->getConcreteString(); + + if (strtolower($class_name) == strtolower($class_ref_name)) { + $this->raiseLintAtNode( + $class_ref, + pht( + 'Use `%s` for local static member references.', + 'self::'), + 'self'); + } + + static $self_refs = array( + 'parent', + 'self', + 'static', + ); + + if (!in_array(strtolower($class_ref_name), $self_refs)) { + continue; + } + + if ($class_ref_name != strtolower($class_ref_name)) { + $this->raiseLintAtNode( + $class_ref, + pht('PHP keywords should be lowercase.'), + strtolower($class_ref_name)); + } + } + } + + $double_colons = $root->selectTokensOfType('T_PAAMAYIM_NEKUDOTAYIM'); + + foreach ($double_colons as $double_colon) { + $tokens = $double_colon->getNonsemanticTokensBefore() + + $double_colon->getNonsemanticTokensAfter(); + + foreach ($tokens as $token) { + if ($token->isAnyWhitespace()) { + if (strpos($token->getValue(), "\n") !== false) { + continue; + } + + $this->raiseLintAtToken( + $token, + pht('Unnecessary whitespace around double colon operator.'), + ''); + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistSemicolonSpacingXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistSemicolonSpacingXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistSemicolonSpacingXHPASTLinterRule.php @@ -0,0 +1,31 @@ +selectTokensOfType(';'); + + foreach ($tokens as $token) { + $prev = $token->getPrevToken(); + + if ($prev->isAnyWhitespace()) { + $this->raiseLintAtToken( + $prev, + pht('Space found before semicolon.'), + ''); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistSlownessXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistSlownessXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistSlownessXHPASTLinterRule.php @@ -0,0 +1,111 @@ +lintStrstrUsedForCheck($root); + $this->lintStrposUsedForStart($root); + } + + private function lintStrstrUsedForCheck(XHPASTNode $root) { + $expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); + + foreach ($expressions as $expression) { + $operator = $expression->getChildOfType(1, 'n_OPERATOR'); + $operator = $operator->getConcreteString(); + + if ($operator !== '===' && $operator !== '!==') { + continue; + } + + $false = $expression->getChildByIndex(0); + if ($false->getTypeName() === 'n_SYMBOL_NAME' && + $false->getConcreteString() === 'false') { + $strstr = $expression->getChildByIndex(2); + } else { + $strstr = $false; + $false = $expression->getChildByIndex(2); + if ($false->getTypeName() !== 'n_SYMBOL_NAME' || + $false->getConcreteString() !== 'false') { + continue; + } + } + + if ($strstr->getTypeName() !== 'n_FUNCTION_CALL') { + continue; + } + + $name = strtolower($strstr->getChildByIndex(0)->getConcreteString()); + if ($name === 'strstr' || $name === 'strchr') { + $this->raiseLintAtNode( + $strstr, + pht( + 'Use %s for checking if the string contains something.', + 'strpos()')); + } else if ($name === 'stristr') { + $this->raiseLintAtNode( + $strstr, + pht( + 'Use %s for checking if the string contains something.', + 'stripos()')); + } + } + } + + private function lintStrposUsedForStart(XHPASTNode $root) { + $expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); + + foreach ($expressions as $expression) { + $operator = $expression->getChildOfType(1, 'n_OPERATOR'); + $operator = $operator->getConcreteString(); + + if ($operator !== '===' && $operator !== '!==') { + continue; + } + + $zero = $expression->getChildByIndex(0); + if ($zero->getTypeName() === 'n_NUMERIC_SCALAR' && + $zero->getConcreteString() === '0') { + $strpos = $expression->getChildByIndex(2); + } else { + $strpos = $zero; + $zero = $expression->getChildByIndex(2); + if ($zero->getTypeName() !== 'n_NUMERIC_SCALAR' || + $zero->getConcreteString() !== '0') { + continue; + } + } + + if ($strpos->getTypeName() !== 'n_FUNCTION_CALL') { + continue; + } + + $name = strtolower($strpos->getChildByIndex(0)->getConcreteString()); + if ($name === 'strpos') { + $this->raiseLintAtNode( + $strpos, + pht( + 'Use %s for checking if the string starts with something.', + 'strncmp()')); + } else if ($name === 'stripos') { + $this->raiseLintAtNode( + $strpos, + pht( + 'Use %s for checking if the string starts with something.', + 'strncasecmp()')); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistStaticThisXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistStaticThisXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistStaticThisXHPASTLinterRule.php @@ -0,0 +1,60 @@ +selectDescendantsOfType('n_CLASS_DECLARATION'); + + foreach ($classes as $class) { + $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); + + foreach ($methods as $method) { + $attributes = $method + ->getChildByIndex(0, 'n_METHOD_MODIFIER_LIST') + ->selectDescendantsOfType('n_STRING'); + + $method_is_static = false; + $method_is_abstract = false; + + foreach ($attributes as $attribute) { + if (strtolower($attribute->getConcreteString()) === 'static') { + $method_is_static = true; + } + if (strtolower($attribute->getConcreteString()) === 'abstract') { + $method_is_abstract = true; + } + } + + if ($method_is_abstract) { + continue; + } + + if (!$method_is_static) { + continue; + } + + $body = $method->getChildOfType(5, 'n_STATEMENT_LIST'); + $variables = $body->selectDescendantsOfType('n_VARIABLE'); + + foreach ($variables as $variable) { + if ($method_is_static && + strtolower($variable->getConcreteString()) === '$this') { + $this->raiseLintAtNode( + $variable, + pht( + 'You can not reference `%s` inside a static method.', + '$this')); + } + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistSyntaxErrorXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistSyntaxErrorXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistSyntaxErrorXHPASTLinterRule.php @@ -0,0 +1,16 @@ +selectDescendantsOfType('n_BINARY_EXPRESSION'); + + static $operators = array( + '-' => true, + '/' => true, + '-=' => true, + '/=' => true, + '<=' => true, + '<' => true, + '==' => true, + '===' => true, + '!=' => true, + '!==' => true, + '>=' => true, + '>' => true, + ); + + static $logical = array( + '||' => true, + '&&' => true, + ); + + foreach ($expressions as $expr) { + $operator = $expr->getChildByIndex(1)->getConcreteString(); + if (!empty($operators[$operator])) { + $left = $expr->getChildByIndex(0)->getSemanticString(); + $right = $expr->getChildByIndex(2)->getSemanticString(); + + if ($left === $right) { + $this->raiseLintAtNode( + $expr, + pht( + 'Both sides of this expression are identical, so it always '. + 'evaluates to a constant.')); + } + } + + if (!empty($logical[$operator])) { + $left = $expr->getChildByIndex(0)->getSemanticString(); + $right = $expr->getChildByIndex(2)->getSemanticString(); + + // NOTE: These will be null to indicate "could not evaluate". + $left = $this->evaluateStaticBoolean($left); + $right = $this->evaluateStaticBoolean($right); + + if (($operator === '||' && ($left === true || $right === true)) || + ($operator === '&&' && ($left === false || $right === false))) { + $this->raiseLintAtNode( + $expr, + pht( + 'The logical value of this expression is static. '. + 'Did you forget to remove some debugging code?')); + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistToStringExceptionXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistToStringExceptionXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistToStringExceptionXHPASTLinterRule.php @@ -0,0 +1,43 @@ +selectDescendantsOfType('n_METHOD_DECLARATION'); + + foreach ($methods as $method) { + $name = $method + ->getChildOfType(2, 'n_STRING') + ->getConcreteString(); + + if ($name != '__toString') { + continue; + } + + $statements = $method->getChildByIndex(5); + + if ($statements->getTypeName() != 'n_STATEMENT_LIST') { + continue; + } + + $throws = $statements->selectDescendantsOfType('n_THROW'); + + foreach ($throws as $throw) { + $this->raiseLintAtNode( + $throw, + pht( + 'It is not possible to throw an %s from within the %s method.', + 'Exception', + '__toString')); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistTodoCommentXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistTodoCommentXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistTodoCommentXHPASTLinterRule.php @@ -0,0 +1,47 @@ +selectTokensOfTypes(array( + 'T_COMMENT', + 'T_DOC_COMMENT', + )); + + foreach ($comments as $token) { + $value = $token->getValue(); + if ($token->getTypeName() === 'T_DOC_COMMENT') { + $regex = '/(TODO|@todo)/'; + } else { + $regex = '/TODO/'; + } + + $matches = null; + $preg = preg_match_all( + $regex, + $value, + $matches, + PREG_OFFSET_CAPTURE); + + foreach ($matches[0] as $match) { + list($string, $offset) = $match; + $this->raiseLintAtOffset( + $token->getOffset() + $offset, + pht('This comment has a TODO.'), + $string); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistUnableToParseXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistUnableToParseXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistUnableToParseXHPASTLinterRule.php @@ -0,0 +1,20 @@ +selectDescendantsOfTypes(array( + 'n_FUNCTION_DECLARATION', + 'n_METHOD_DECLARATION', + )); + + foreach ($defs as $def) { + + // We keep track of the first offset where scope becomes unknowable, and + // silence any warnings after that. Default it to INT_MAX so we can min() + // it later to keep track of the first problem we encounter. + $scope_destroyed_at = PHP_INT_MAX; + + $declarations = array( + '$this' => 0, + ) + array_fill_keys($this->getSuperGlobalNames(), 0); + $declaration_tokens = array(); + $exclude_tokens = array(); + $vars = array(); + + // First up, find all the different kinds of declarations, as explained + // above. Put the tokens into the $vars array. + + $param_list = $def->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST'); + $param_vars = $param_list->selectDescendantsOfType('n_VARIABLE'); + foreach ($param_vars as $var) { + $vars[] = $var; + } + + // This is PHP5.3 closure syntax: function () use ($x) {}; + $lexical_vars = $def + ->getChildByIndex(4) + ->selectDescendantsOfType('n_VARIABLE'); + foreach ($lexical_vars as $var) { + $vars[] = $var; + } + + $body = $def->getChildByIndex(5); + if ($body->getTypeName() === 'n_EMPTY') { + // Abstract method declaration. + continue; + } + + $static_vars = $body + ->selectDescendantsOfType('n_STATIC_DECLARATION') + ->selectDescendantsOfType('n_VARIABLE'); + foreach ($static_vars as $var) { + $vars[] = $var; + } + + + $global_vars = $body + ->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST'); + foreach ($global_vars as $var_list) { + foreach ($var_list->getChildren() as $var) { + if ($var->getTypeName() === 'n_VARIABLE') { + $vars[] = $var; + } else { + // Dynamic global variable, i.e. "global $$x;". + $scope_destroyed_at = min($scope_destroyed_at, $var->getOffset()); + // An error is raised elsewhere, no need to raise here. + } + } + } + + // Include "catch (Exception $ex)", but not variables in the body of the + // catch block. + $catches = $body->selectDescendantsOfType('n_CATCH'); + foreach ($catches as $catch) { + $vars[] = $catch->getChildOfType(1, 'n_VARIABLE'); + } + + $binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION'); + foreach ($binary as $expr) { + if ($expr->getChildByIndex(1)->getConcreteString() !== '=') { + continue; + } + $lval = $expr->getChildByIndex(0); + if ($lval->getTypeName() === 'n_VARIABLE') { + $vars[] = $lval; + } else if ($lval->getTypeName() === 'n_LIST') { + // Recursively grab everything out of list(), since the grammar + // permits list() to be nested. Also note that list() is ONLY valid + // as an lval assignments, so we could safely lift this out of the + // n_BINARY_EXPRESSION branch. + $assign_vars = $lval->selectDescendantsOfType('n_VARIABLE'); + foreach ($assign_vars as $var) { + $vars[] = $var; + } + } + + if ($lval->getTypeName() === 'n_VARIABLE_VARIABLE') { + $scope_destroyed_at = min($scope_destroyed_at, $lval->getOffset()); + // No need to raise here since we raise an error elsewhere. + } + } + + $calls = $body->selectDescendantsOfType('n_FUNCTION_CALL'); + foreach ($calls as $call) { + $name = strtolower($call->getChildByIndex(0)->getConcreteString()); + + if ($name === 'empty' || $name === 'isset') { + $params = $call + ->getChildOfType(1, 'n_CALL_PARAMETER_LIST') + ->selectDescendantsOfType('n_VARIABLE'); + foreach ($params as $var) { + $exclude_tokens[$var->getID()] = true; + } + continue; + } + if ($name !== 'extract') { + continue; + } + $scope_destroyed_at = min($scope_destroyed_at, $call->getOffset()); + } + + // Now we have every declaration except foreach(), handled below. Build + // two maps, one which just keeps track of which tokens are part of + // declarations ($declaration_tokens) and one which has the first offset + // where a variable is declared ($declarations). + + foreach ($vars as $var) { + $concrete = $this->getConcreteVariableString($var); + $declarations[$concrete] = min( + idx($declarations, $concrete, PHP_INT_MAX), + $var->getOffset()); + $declaration_tokens[$var->getID()] = true; + } + + // Excluded tokens are ones we don't "count" as being used, described + // above. Put them into $exclude_tokens. + + $class_statics = $body + ->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); + $class_static_vars = $class_statics + ->selectDescendantsOfType('n_VARIABLE'); + foreach ($class_static_vars as $var) { + $exclude_tokens[$var->getID()] = true; + } + + + // Find all the variables in scope, and figure out where they are used. + // We want to find foreach() iterators which are both declared before and + // used after the foreach() loop. + + $uses = array(); + + $all_vars = $body->selectDescendantsOfType('n_VARIABLE'); + $all = array(); + + // NOTE: $all_vars is not a real array so we can't unset() it. + foreach ($all_vars as $var) { + + // Be strict since it's easier; we don't let you reuse an iterator you + // declared before a loop after the loop, even if you're just assigning + // to it. + + $concrete = $this->getConcreteVariableString($var); + $uses[$concrete][$var->getID()] = $var->getOffset(); + + if (isset($declaration_tokens[$var->getID()])) { + // We know this is part of a declaration, so it's fine. + continue; + } + if (isset($exclude_tokens[$var->getID()])) { + // We know this is part of isset() or similar, so it's fine. + continue; + } + + $all[$var->getOffset()] = $concrete; + } + + + // Do foreach() last, we want to handle implicit redeclaration of a + // variable already in scope since this probably means we're ovewriting a + // local. + + // NOTE: Processing foreach expressions in order allows programs which + // reuse iterator variables in other foreach() loops -- this is fine. We + // have a separate warning to prevent nested loops from reusing the same + // iterators. + + $foreaches = $body->selectDescendantsOfType('n_FOREACH'); + $all_foreach_vars = array(); + foreach ($foreaches as $foreach) { + $foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION'); + + $foreach_vars = array(); + + // Determine the end of the foreach() loop. + $foreach_tokens = $foreach->getTokens(); + $last_token = end($foreach_tokens); + $foreach_end = $last_token->getOffset(); + + $key_var = $foreach_expr->getChildByIndex(1); + if ($key_var->getTypeName() === 'n_VARIABLE') { + $foreach_vars[] = $key_var; + } + + $value_var = $foreach_expr->getChildByIndex(2); + if ($value_var->getTypeName() === 'n_VARIABLE') { + $foreach_vars[] = $value_var; + } else { + // The root-level token may be a reference, as in: + // foreach ($a as $b => &$c) { ... } + // Reach into the n_VARIABLE_REFERENCE node to grab the n_VARIABLE + // node. + $var = $value_var->getChildByIndex(0); + if ($var->getTypeName() === 'n_VARIABLE_VARIABLE') { + $var = $var->getChildByIndex(0); + } + $foreach_vars[] = $var; + } + + // Remove all uses of the iterators inside of the foreach() loop from + // the $uses map. + + foreach ($foreach_vars as $var) { + $concrete = $this->getConcreteVariableString($var); + $offset = $var->getOffset(); + + foreach ($uses[$concrete] as $id => $use_offset) { + if (($use_offset >= $offset) && ($use_offset < $foreach_end)) { + unset($uses[$concrete][$id]); + } + } + + $all_foreach_vars[] = $var; + } + } + + foreach ($all_foreach_vars as $var) { + $concrete = $this->getConcreteVariableString($var); + $offset = $var->getOffset(); + + // This is a declaration, exclude it from the "declare variables prior + // to use" check below. + unset($all[$var->getOffset()]); + + $vars[] = $var; + } + + // Now rebuild declarations to include foreach(). + + foreach ($vars as $var) { + $concrete = $this->getConcreteVariableString($var); + $declarations[$concrete] = min( + idx($declarations, $concrete, PHP_INT_MAX), + $var->getOffset()); + $declaration_tokens[$var->getID()] = true; + } + + foreach (array('n_STRING_SCALAR', 'n_HEREDOC') as $type) { + foreach ($body->selectDescendantsOfType($type) as $string) { + foreach ($string->getStringVariables() as $offset => $var) { + $all[$string->getOffset() + $offset - 1] = '$'.$var; + } + } + } + + // Issue a warning for every variable token, unless it appears in a + // declaration, we know about a prior declaration, we have explicitly + // excluded it, or scope has been made unknowable before it appears. + + $issued_warnings = array(); + foreach ($all as $offset => $concrete) { + if ($offset >= $scope_destroyed_at) { + // This appears after an extract() or $$var so we have no idea + // whether it's legitimate or not. We raised a harshly-worded warning + // when scope was made unknowable, so just ignore anything we can't + // figure out. + continue; + } + if ($offset >= idx($declarations, $concrete, PHP_INT_MAX)) { + // The use appears after the variable is declared, so it's fine. + continue; + } + if (!empty($issued_warnings[$concrete])) { + // We've already issued a warning for this variable so we don't need + // to issue another one. + continue; + } + $this->raiseLintAtOffset( + $offset, + pht( + 'Declare variables prior to use (even if you are passing them '. + 'as reference parameters). You may have misspelled this '. + 'variable name.'), + $concrete); + $issued_warnings[$concrete] = true; + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistUnnecessaryFinalModifierXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistUnnecessaryFinalModifierXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistUnnecessaryFinalModifierXHPASTLinterRule.php @@ -0,0 +1,52 @@ +selectDescendantsOfType('n_CLASS_DECLARATION'); + + foreach ($classes as $class) { + $attributes = $class->getChildOfType(0, 'n_CLASS_ATTRIBUTES'); + $is_final = false; + + foreach ($attributes->getChildren() as $attribute) { + if ($attribute->getConcreteString() == 'final') { + $is_final = true; + break; + } + } + + if (!$is_final) { + continue; + } + + $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); + foreach ($methods as $method) { + $attributes = $method->getChildOfType(0, 'n_METHOD_MODIFIER_LIST'); + + foreach ($attributes->getChildren() as $attribute) { + if ($attribute->getConcreteString() == 'final') { + $this->raiseLintAtNode( + $attribute, + pht( + 'Unnecessary %s modifier in %s class.', + 'final', + 'final')); + } + } + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistUnnecessarySemicolonXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistUnnecessarySemicolonXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistUnnecessarySemicolonXHPASTLinterRule.php @@ -0,0 +1,39 @@ +selectDescendantsOfType('n_STATEMENT'); + + foreach ($statements as $statement) { + if ($statement->getParentNode()->getTypeName() == 'n_DECLARE') { + continue; + } + + if (count($statement->getChildren()) > 1) { + continue; + } else if ($statement->getChildByIndex(0)->getTypeName() != 'n_EMPTY') { + continue; + } + + if ($statement->getConcreteString() == ';') { + $this->raiseLintAtNode( + $statement, + pht('Unnecessary semicolons after statement.'), + ''); + } + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistUselessOverridingMethodXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistUselessOverridingMethodXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistUselessOverridingMethodXHPASTLinterRule.php @@ -0,0 +1,97 @@ +selectDescendantsOfType('n_METHOD_DECLARATION'); + + foreach ($methods as $method) { + $method_name = $method + ->getChildOfType(2, 'n_STRING') + ->getConcreteString(); + + $parameter_list = $method + ->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST'); + $parameters = array(); + + foreach ($parameter_list->getChildren() as $parameter) { + $parameter = $parameter->getChildByIndex(1); + + if ($parameter->getTypeName() == 'n_VARIABLE_REFERENCE') { + $parameter = $parameter->getChildOfType(0, 'n_VARIABLE'); + } + + $parameters[] = $parameter->getConcreteString(); + } + + $statements = $method->getChildByIndex(5); + + if ($statements->getTypeName() != 'n_STATEMENT_LIST') { + continue; + } + + if (count($statements->getChildren()) != 1) { + continue; + } + + $statement = $statements + ->getChildOfType(0, 'n_STATEMENT') + ->getChildByIndex(0); + + if ($statement->getTypeName() == 'n_RETURN') { + $statement = $statement->getChildByIndex(0); + } + + if ($statement->getTypeName() != 'n_FUNCTION_CALL') { + continue; + } + + $function = $statement->getChildByIndex(0); + + if ($function->getTypeName() != 'n_CLASS_STATIC_ACCESS') { + continue; + } + + $called_class = $function->getChildOfType(0, 'n_CLASS_NAME'); + $called_method = $function->getChildOfType(1, 'n_STRING'); + + if ($called_class->getConcreteString() != 'parent') { + continue; + } else if ($called_method->getConcreteString() != $method_name) { + continue; + } + + $params = $statement + ->getChildOfType(1, 'n_CALL_PARAMETER_LIST') + ->getChildren(); + + foreach ($params as $param) { + if ($param->getTypeName() != 'n_VARIABLE') { + continue 2; + } + + $expected = array_shift($parameters); + + if ($param->getConcreteString() != $expected) { + continue 2; + } + } + + $this->raiseLintAtNode( + $method, + pht('Useless overriding method.')); + } + } + +} diff --git a/src/lint/linter/xhpast/rules/ArcanistVariableVariableXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistVariableVariableXHPASTLinterRule.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistVariableVariableXHPASTLinterRule.php @@ -0,0 +1,24 @@ +selectDescendantsOfType('n_VARIABLE_VARIABLE'); + + foreach ($vvars as $vvar) { + $this->raiseLintAtNode( + $vvar, + pht( + 'Rewrite this code to use an array. Variable variables are unclear '. + 'and hinder static analysis.')); + } + } + +}