Page MenuHomePhabricator

D10541.id31269.diff
No OneTemporary

D10541.id31269.diff

This file is larger than 256 KB, so syntax highlighting was skipped.
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,17 +81,26 @@
'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',
'ArcanistFilenameLinter' => 'lint/linter/ArcanistFilenameLinter.php',
'ArcanistFilenameLinterTestCase' => 'lint/linter/__tests__/ArcanistFilenameLinterTestCase.php',
'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',
@@ -90,7 +117,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',
@@ -100,7 +134,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',
@@ -116,19 +153,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',
@@ -140,6 +188,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',
@@ -149,40 +199,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',
@@ -191,6 +258,7 @@
'ArcanistXHPASTLintNamingHookTestCase' => 'lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php',
'ArcanistXHPASTLintSwitchHook' => 'lint/linter/xhpast/ArcanistXHPASTLintSwitchHook.php',
'ArcanistXHPASTLinter' => 'lint/linter/ArcanistXHPASTLinter.php',
+ 'ArcanistXHPASTLinterRule' => 'lint/linter/ArcanistXHPASTLinterRule.php',
'ArcanistXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php',
'ArcanistXMLLinter' => 'lint/linter/ArcanistXMLLinter.php',
'ArcanistXMLLinterTestCase' => 'lint/linter/__tests__/ArcanistXMLLinterTestCase.php',
@@ -211,13 +279,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',
@@ -226,43 +300,64 @@
'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',
'ArcanistFilenameLinter' => 'ArcanistLinter',
'ArcanistFilenameLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistFlagWorkflow' => 'ArcanistWorkflow',
'ArcanistFlake8Linter' => 'ArcanistExternalLinter',
'ArcanistFlake8LinterTestCase' => 'ArcanistExternalLinterTestCase',
+ 'ArcanistFormattedStringXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistFutureLinter' => 'ArcanistLinter',
'ArcanistGeneratedLinter' => 'ArcanistLinter',
'ArcanistGeneratedLinterTestCase' => 'ArcanistLinterTestCase',
@@ -277,7 +372,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',
@@ -287,7 +389,10 @@
'ArcanistJSONLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistJscsLinter' => 'ArcanistExternalLinter',
'ArcanistJscsLinterTestCase' => 'ArcanistExternalLinterTestCase',
+ 'ArcanistKeywordCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
+ 'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistLandWorkflow' => 'ArcanistWorkflow',
+ 'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistLesscLinter' => 'ArcanistExternalLinter',
'ArcanistLesscLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistLiberateWorkflow' => 'ArcanistWorkflow',
@@ -296,18 +401,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',
@@ -319,6 +435,8 @@
'ArcanistPhutilLibraryLinter' => 'ArcanistLinter',
'ArcanistPhutilXHPASTLinter' => 'ArcanistBaseXHPASTLinter',
'ArcanistPhutilXHPASTLinterTestCase' => 'ArcanistLinterTestCase',
+ 'ArcanistPlusOperatorOnStringsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
+ 'ArcanistPregQuoteMisuseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistPuppetLintLinter' => 'ArcanistExternalLinter',
'ArcanistPuppetLintLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistPyFlakesLinter' => 'ArcanistExternalLinter',
@@ -327,35 +445,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
@@ -23,7 +23,7 @@
return implode('-', $parts);
}
- final protected function raiseLintAtToken(
+ final public function raiseLintAtToken(
XHPASTToken $token,
$code,
$desc,
@@ -36,7 +36,7 @@
$replace);
}
- final protected function raiseLintAtNode(
+ final public function raiseLintAtNode(
XHPASTNode $node,
$code,
$desc,
@@ -201,7 +201,7 @@
}
-/* -( Utility )------------------------------------------------------------ */
+/* -( Deprecated )--------------------------------------------------------- */
/**
* Retrieve all calls to some specified function(s).
diff --git a/src/lint/linter/ArcanistLinter.php b/src/lint/linter/ArcanistLinter.php
--- a/src/lint/linter/ArcanistLinter.php
+++ b/src/lint/linter/ArcanistLinter.php
@@ -416,7 +416,7 @@
return $this->messages;
}
- final protected function raiseLintAtLine(
+ final public function raiseLintAtLine(
$line,
$char,
$code,
@@ -438,11 +438,11 @@
return $this->addLintMessage($message);
}
- final protected function raiseLintAtPath($code, $desc) {
+ final public function raiseLintAtPath($code, $desc) {
return $this->raiseLintAtLine(null, null, $code, $desc, null, null);
}
- final protected function raiseLintAtOffset(
+ final public function raiseLintAtOffset(
$offset,
$code,
$desc,
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"', '<?'),
- self::LINT_PHP_ECHO_TAG
- => pht('Use of Echo Tag "%s"', '<?='),
- self::LINT_PHP_CLOSE_TAG
- => 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<string, string>',
- '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<string, int>',
- '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()) === '<?') {
- $this->raiseLintAtToken(
- $token,
- self::LINT_PHP_SHORT_TAG,
- pht(
- 'Use the full form of the PHP open tag, "%s".',
- '<?php'),
- "<?php\n");
- }
- break;
- } else if ($token->getTypeName() === 'T_OPEN_TAG_WITH_ECHO') {
- $this->raiseLintAtToken(
- $token,
- self::LINT_PHP_ECHO_TAG,
- pht('Avoid the PHP echo short form, "%s".', '<?='));
- break;
- } else {
- if (!preg_match('/^#!/', $token->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.',
- '<?php',
- '#!'));
- }
- break;
- }
- }
-
- foreach ($root->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 <type, name, token, error> 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/ArcanistXHPASTLinterRule.php b/src/lint/linter/ArcanistXHPASTLinterRule.php
new file mode 100644
--- /dev/null
+++ b/src/lint/linter/ArcanistXHPASTLinterRule.php
@@ -0,0 +1,164 @@
+<?php
+
+abstract class ArcanistXHPASTLinterRule {
+
+ private $linter = null;
+
+ final public function getLintID() {
+ $class = new ReflectionClass($this);
+
+ $const = $class->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();
+ abstract public function getLintSeverity();
+
+ 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<string> 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/__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/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 @@
+<?php
+
+final class ArcanistAliasFunctionXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 65;
+
+ public function getLintName() {
+ return pht('Alias Functions');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(XHPASTNode $root) {
+ $aliases = $this->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 @@
+<?php
+
+final class ArcanistArrayIndexSpacingXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 28;
+
+ public function getLintName() {
+ return pht('Spacing Before Array Index');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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()),
+ 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 @@
+<?php
+
+final class ArcanistArraySeparatorXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 48;
+
+ public function getLintName() {
+ return pht('Array Separator');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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(),
+ 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 @@
+<?php
+
+final class ArcanistBinaryExpressionSpacingXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 27;
+
+ public function getLintName() {
+ return pht('Space Around Binary Operator');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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,
+ 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,53 @@
+<?php
+
+final class ArcanistBlacklistedFunctionXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 51;
+
+ private $blacklistedFunctions = array();
+
+ public function getLintName() {
+ return pht('Use of Blacklisted Function');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function getLinterConfigurationOptions() {
+ return parent::getLinterConfigurationOptions() + array(
+ 'xhpast.blacklisted.function' => array(
+ 'type' => 'optional map<string, string>',
+ '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 @@
+<?php
+
+final class ArcanistBraceFormattingXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 24;
+
+ public function getLintName() {
+ return pht('Brace Placement');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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,
+ 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,30 @@
+<?php
+
+final class ArcanistCallTimePassByReferenceXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 53;
+
+ public function getLintName() {
+ return pht('Call-Time Pass-By-Reference');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistCastSpacingXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 66;
+
+ public function getLintName() {
+ return pht('Cast Spacing');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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,58 @@
+<?php
+
+/**
+ * Lint that if the file declares exactly one interface or class, the name of
+ * the file matches the name of the class, unless the class name is funky like
+ * an XHP element.
+ */
+final class ArcanistClassFilenameMismatchXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 19;
+
+ public function getLintName() {
+ return pht('Class-Filename Mismatch');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistClassNameLiteralXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 62;
+
+ public function getLintName() {
+ return pht('Class Name Literal');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistClosingCallParenthesesXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 37;
+
+ public function getLintName() {
+ return pht('Call Formatting');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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),
+ 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 @@
+<?php
+
+final class ArcanistClosingDeclarationParenthesesXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 38;
+
+ public function getLintName() {
+ return pht('Declaration Formatting');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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),
+ 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 @@
+<?php
+
+final class ArcanistCommentSpacingXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 34;
+
+ public function getLintName() {
+ return pht('Comment Spaces');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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(),
+ 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,32 @@
+<?php
+
+final class ArcanistCommentStyleXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 18;
+
+ public function getLintName() {
+ return pht('Comment Style');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(XHPASTNode $root) {
+ foreach ($root->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 @@
+<?php
+
+final class ArcanistConcatenationOperatorXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 44;
+
+ public function getLintName() {
+ return pht('Concatenation Spacing');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistConstructorParenthesesXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 49;
+
+ public function getLintName() {
+ return pht('Constructor Parentheses');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistControlStatementSpacingXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 26;
+
+ public function getLintName() {
+ return pht('Space After Control Statement');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistDefaultParametersXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 60;
+
+ public function getLintName() {
+ return pht('Default Parameters');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistDoubleQuoteXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 41;
+
+ public function getLintName() {
+ return pht('Unnecessary Double Quotes');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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,78 @@
+<?php
+
+/**
+ * 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.
+ */
+final class ArcanistDuplicateKeysInArrayXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 22;
+
+ public function getLintName() {
+ return pht('Duplicate Keys in Array');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ 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,53 @@
+<?php
+
+final class ArcanistDuplicateSwitchCaseXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 50;
+
+ public function getLintName() {
+ return pht('Duplicate Case Statements');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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,33 @@
+<?php
+
+final class ArcanistDynamicDefineXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 12;
+
+ public function getLintName() {
+ return pht('Dynamic %s', 'define()');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistElseIfUsageXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 42;
+
+ public function getLintName() {
+ return pht('ElseIf Usage');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(XHPASTNode $root) {
+ $tokens = $root->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 @@
+<?php
+
+final class ArcanistEmptyStatementXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 47;
+
+ public function getLintName() {
+ return pht('Empty Block Statement');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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,47 @@
+<?php
+
+/**
+ * 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!
+ */
+final class ArcanistExitExpressionXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 17;
+
+ public function getLintName() {
+ return pht('Exit Used as Expression');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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,28 @@
+<?php
+
+final class ArcanistExtractUseXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 4;
+
+ public function getLintName() {
+ return pht('Use of %s', 'extract()');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(XHPASTNode $root) {
+ $calls = $this->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,107 @@
+<?php
+
+final class ArcanistFormattedStringXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 54;
+
+ private $printfFunctions = array();
+
+ public function getLintName() {
+ return pht('Formatted String');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function getLinterConfigurationOptions() {
+ return parent::getLinterConfigurationOptions() + array(
+ 'xhpast.printf-functions' => array(
+ 'type' => 'optional map<string, int>',
+ '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,39 @@
+<?php
+
+final class ArcanistImplicitConstructorXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 10;
+
+ public function getLintName() {
+ return pht('Implicit Constructor');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistImplicitFallthroughXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 30;
+
+ private $switchhook;
+
+ public function getLintName() {
+ return pht('Implicit Fallthrough');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function getLinterConfigurationOptions() {
+ return parent::getLinterConfigurationOptions() + array(
+ 'xhpast.switchhook' => 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 @@
+<?php
+
+final class ArcanistImplicitVisibilityXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 52;
+
+ public function getLintName() {
+ return pht('Implicit Method Visibility');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(XHPASTNode $root) {
+ $this->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 @@
+<?php
+
+final class ArcanistInnerFunctionXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 59;
+
+ public function getLintName() {
+ return pht('Inner Functions');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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,
+ 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,39 @@
+<?php
+
+final class ArcanistInstanceOfOperatorXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 69;
+
+ public function getLintName() {
+ return pht('%s Operator', 'instanceof');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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,83 @@
+<?php
+
+final class ArcanistInvalidDefaultParameterXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 70;
+
+ public function getLintName() {
+ return pht('Invalid Default Parameter');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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,107 @@
+<?php
+
+final class ArcanistInvalidModifiersXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 72;
+
+ public function getLintName() {
+ return pht('Invalid Modifiers');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistKeywordCasingXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 40;
+
+ public function getLintName() {
+ return pht('Keyword Conventions');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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,
+ 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,47 @@
+<?php
+
+final class ArcanistLambdaFuncFunctionXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 68;
+
+ public function getLintName() {
+ return pht('%s Function', '__lambda_func');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistLanguageConstructParenthesesXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 46;
+
+ public function getLintName() {
+ return pht('Language Construct Parentheses');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistLogicalOperatorsXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 58;
+
+ public function getLintName() {
+ return pht('Logical Operators');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistLowercaseFunctionsXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 61;
+
+ public function getLintName() {
+ return pht('Lowercase Functions');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistModifierOrderingXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 71;
+
+ public function getLintName() {
+ return pht('Modifier Ordering');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(XHPASTNode $root) {
+ $this->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 @@
+<?php
+
+final class ArcanistNamingConventionsXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 9;
+
+ private $naminghook;
+
+ public function getLintName() {
+ return pht('Naming Conventions');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function getLinterConfigurationOptions() {
+ return parent::getLinterConfigurationOptions() + array(
+ 'xhpast.naminghook' => 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 <type, name, token, error> 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,49 @@
+<?php
+
+final class ArcanistNoParentScopeXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 64;
+
+ public function getLintName() {
+ return pht('No Parent Scope');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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,24 @@
+<?php
+
+final class ArcanistPHPCloseTagXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 8;
+
+ public function getLintName() {
+ return pht('Use of Close Tag "%s"', '?>');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ 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,439 @@
+<?php
+
+final class ArcanistPHPCompatibilityXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 45;
+
+ private $version;
+ private $windowsVersion;
+
+ public function getLintName() {
+ return pht('PHP Compatibility');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function getLinterConfigurationOptions() {
+ return parent::getLinterConfigurationOptions() + array(
+ '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.'),
+ ),
+ );
+ }
+
+ 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,28 @@
+<?php
+
+final class ArcanistPHPEchoTagXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 7;
+
+ public function getLintName() {
+ return pht('Use of Echo Tag "%s"', '<?=');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(XHPASTNode $root) {
+ $tokens = $root->getTokens();
+
+ foreach ($tokens as $token) {
+ if ($token->getTypeName() === 'T_OPEN_TAG_WITH_ECHO') {
+ $this->raiseLintAtToken(
+ $token,
+ pht('Avoid the PHP echo short form, "%s".', '<?='));
+ }
+ }
+ }
+
+}
diff --git a/src/lint/linter/xhpast/rules/ArcanistPHPOpenTagXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistPHPOpenTagXHPASTLinterRule.php
new file mode 100644
--- /dev/null
+++ b/src/lint/linter/xhpast/rules/ArcanistPHPOpenTagXHPASTLinterRule.php
@@ -0,0 +1,39 @@
+<?php
+
+final class ArcanistPHPOpenTagXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 15;
+
+ public function getLintName() {
+ return pht('Expected Open Tag');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(XHPASTNode $root) {
+ $tokens = $root->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.',
+ '<?php',
+ '#!'));
+ }
+ break;
+ }
+ }
+ }
+
+}
diff --git a/src/lint/linter/xhpast/rules/ArcanistPHPShortTagXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistPHPShortTagXHPASTLinterRule.php
new file mode 100644
--- /dev/null
+++ b/src/lint/linter/xhpast/rules/ArcanistPHPShortTagXHPASTLinterRule.php
@@ -0,0 +1,34 @@
+<?php
+
+final class ArcanistPHPShortTagXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 6;
+
+ public function getLintName() {
+ return pht('Use of Short Tag "%s"', '<?');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(XHPASTNode $root) {
+ $tokens = $root->getTokens();
+
+ foreach ($tokens as $token) {
+ if ($token->getTypeName() === 'T_OPEN_TAG') {
+ if (trim($token->getValue()) === '<?') {
+ $this->raiseLintAtToken(
+ $token,
+ pht(
+ 'Use the full form of the PHP open tag, "%s".',
+ '<?php'),
+ "<?php\n");
+ }
+ break;
+ }
+ }
+ }
+
+}
diff --git a/src/lint/linter/xhpast/rules/ArcanistParenthesesSpacingXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistParenthesesSpacingXHPASTLinterRule.php
new file mode 100644
--- /dev/null
+++ b/src/lint/linter/xhpast/rules/ArcanistParenthesesSpacingXHPASTLinterRule.php
@@ -0,0 +1,69 @@
+<?php
+
+final class ArcanistParenthesesSpacingXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 25;
+
+ public function getLintName() {
+ return pht('Spaces Inside Parentheses');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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(),
+ 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,41 @@
+<?php
+
+final class ArcanistPlusOperatorOnStringsXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 21;
+
+ public function getLintName() {
+ return pht('Not String Concatenation');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+/**
+ * @{function: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.
+ */
+final class ArcanistPregQuoteMisuseXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 14;
+
+ public function getLintName() {
+ return pht('Misuse of %s', 'preg_quote()');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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,281 @@
+<?php
+
+final class ArcanistReusedAsIteratorXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 32;
+
+ public function getLintName() {
+ return pht('Variable Reused As Iterator');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(XHPASTNode $root) {
+ $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());
+ }
+
+ // 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 @@
+<?php
+
+/**
+ * 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
+ */
+final class ArcanistReusedIteratorReferenceXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 39;
+
+ public function getLintName() {
+ return pht('Reuse of Iterator References');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(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,
+ 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,102 @@
+<?php
+
+/**
+ * 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
+ * // ...
+ * }
+ * }
+ */
+final class ArcanistReusedIteratorXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 23;
+
+ public function getLintName() {
+ return pht('Reuse of Iterator Variable');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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),
+ 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 @@
+<?php
+
+final class ArcanistSelfMemberReferenceXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 57;
+
+ public function getLintName() {
+ return pht('Self Member Reference');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistSemicolonSpacingXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 43;
+
+ public function getLintName() {
+ return pht('Semicolon Spacing');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(XHPASTNode $root) {
+ $tokens = $root->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 @@
+<?php
+
+final class ArcanistSlownessXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 36;
+
+ public function getLintName() {
+ return pht('Slow Construct');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(XHPASTNode $root) {
+ $this->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,64 @@
+<?php
+
+final class ArcanistStaticThisXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 13;
+
+ public function getLintName() {
+ return pht('Use of %s in Static Context', '$this');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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,20 @@
+<?php
+
+final class ArcanistSyntaxErrorXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 1;
+
+ public function getLintName() {
+ return pht('PHP Syntax Error!');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(XHPASTNode $root) {
+ // This linter rule isn't used explicitly.
+ }
+
+}
diff --git a/src/lint/linter/xhpast/rules/ArcanistTautologicalExpressionXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistTautologicalExpressionXHPASTLinterRule.php
new file mode 100644
--- /dev/null
+++ b/src/lint/linter/xhpast/rules/ArcanistTautologicalExpressionXHPASTLinterRule.php
@@ -0,0 +1,74 @@
+<?php
+
+final class ArcanistTautologicalExpressionXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 20;
+
+ public function getLintName() {
+ return pht('Tautological Expression');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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,47 @@
+<?php
+
+final class ArcanistToStringExceptionXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 67;
+
+ public function getLintName() {
+ return pht('Throwing Exception in %s Method', '__toString');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistTodoCommentXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 16;
+
+ public function getLintName() {
+ return pht('TODO Comment');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_DISABLED;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistUnableToParseXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 2;
+
+ public function getLintName() {
+ return pht('Unable to Parse');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_WARNING;
+ }
+
+ public function process(XHPASTNode $root) {
+ // This linter rule isn't used explicitly.
+ }
+
+}
diff --git a/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php
new file mode 100644
--- /dev/null
+++ b/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php
@@ -0,0 +1,349 @@
+<?php
+
+final class ArcanistUndeclaredVariableXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 5;
+
+ public function getLintName() {
+ return pht('Use of Undeclared Variable');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(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') {
+ // 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 @@
+<?php
+
+final class ArcanistUnnecessaryFinalModifierXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 55;
+
+ public function getLintName() {
+ return pht('Unnecessary Final Modifier');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistUnnecessarySemicolonXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 56;
+
+ public function getLintName() {
+ return pht('Unnecessary Semicolon');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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 @@
+<?php
+
+final class ArcanistUselessOverridingMethodXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 63;
+
+ public function getLintName() {
+ return pht('Useless Overriding Method');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ADVICE;
+ }
+
+ public function process(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,
+ 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,28 @@
+<?php
+
+final class ArcanistVariableVariableXHPASTLinterRule
+ extends ArcanistXHPASTLinterRule {
+
+ const ID = 3;
+
+ public function getLintName() {
+ return pht('Use of Variable Variable');
+ }
+
+ public function getLintSeverity() {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+
+ public function process(XHPASTNode $root) {
+ $vvars = $root->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.'));
+ }
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Fri, May 10, 12:23 PM (3 w, 3 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/fl/gg/6l6o2kdvftp77dgm
Default Alt Text
D10541.id31269.diff (363 KB)

Event Timeline